You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
5.5 KiB
TypeScript
193 lines
5.5 KiB
TypeScript
import classNames from 'classnames';
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
import { contextMenu } from 'react-contexify';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
// tslint:disable-next-line: no-submodule-imports
|
|
import useInterval from 'react-use/lib/useInterval';
|
|
import _ from 'lodash';
|
|
import { removeMessage } from '../../../../data/data';
|
|
import { MessageRenderingProps, QuoteClickOptions } from '../../../../models/messageType';
|
|
import { getConversationController } from '../../../../session/conversations';
|
|
import { messageExpired } from '../../../../state/ducks/conversations';
|
|
import {
|
|
getGenericReadableMessageSelectorProps,
|
|
getIsMessageSelected,
|
|
getQuotedMessageToAnimate,
|
|
isMessageSelectionMode,
|
|
} from '../../../../state/selectors/conversations';
|
|
import { getIncrement } from '../../../../util/timer';
|
|
import { ExpireTimer } from '../../ExpireTimer';
|
|
import { MessageAvatar } from '../message-content/MessageAvatar';
|
|
import { MessageContentWithStatuses } from '../message-content/MessageContentWithStatus';
|
|
import { ReadableMessage } from './ReadableMessage';
|
|
|
|
export type GenericReadableMessageSelectorProps = Pick<
|
|
MessageRenderingProps,
|
|
| 'direction'
|
|
| 'conversationType'
|
|
| 'receivedAt'
|
|
| 'isUnread'
|
|
| 'expirationLength'
|
|
| 'expirationTimestamp'
|
|
| 'isKickedFromGroup'
|
|
| 'isExpired'
|
|
| 'convoId'
|
|
| 'isDeleted'
|
|
>;
|
|
|
|
type ExpiringProps = {
|
|
isExpired?: boolean;
|
|
expirationTimestamp?: number | null;
|
|
expirationLength?: number | null;
|
|
convoId?: string;
|
|
messageId: string;
|
|
};
|
|
const EXPIRATION_CHECK_MINIMUM = 2000;
|
|
|
|
function useIsExpired(props: ExpiringProps) {
|
|
const {
|
|
convoId,
|
|
messageId,
|
|
expirationLength,
|
|
expirationTimestamp,
|
|
isExpired: isExpiredProps,
|
|
} = props;
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
const [isExpired] = useState(isExpiredProps);
|
|
async function checkExpired() {
|
|
const now = Date.now();
|
|
|
|
if (!expirationTimestamp || !expirationLength) {
|
|
return;
|
|
}
|
|
|
|
if (isExpired || now >= expirationTimestamp) {
|
|
await removeMessage(messageId);
|
|
if (convoId) {
|
|
dispatch(
|
|
messageExpired({
|
|
conversationKey: convoId,
|
|
messageId,
|
|
})
|
|
);
|
|
const convo = getConversationController().get(convoId);
|
|
convo?.updateLastMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
const increment = getIncrement(expirationLength || EXPIRATION_CHECK_MINIMUM);
|
|
const checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment);
|
|
|
|
useEffect(() => {
|
|
void checkExpired();
|
|
}, []); // check on mount
|
|
useInterval(checkExpired, checkFrequency); // check every 2sec or sooner if needed
|
|
|
|
return { isExpired };
|
|
}
|
|
|
|
type Props = {
|
|
messageId: string;
|
|
onQuoteClick: (quote: QuoteClickOptions) => void;
|
|
ctxMenuID: string;
|
|
isDetailView?: boolean;
|
|
};
|
|
// tslint:disable: use-simple-attributes
|
|
|
|
export const GenericReadableMessage = (props: Props) => {
|
|
const msgProps = useSelector(state =>
|
|
getGenericReadableMessageSelectorProps(state as any, props.messageId)
|
|
);
|
|
|
|
const expiringProps: ExpiringProps = {
|
|
convoId: msgProps?.convoId,
|
|
expirationLength: msgProps?.expirationLength,
|
|
messageId: props.messageId,
|
|
expirationTimestamp: msgProps?.expirationTimestamp,
|
|
isExpired: msgProps?.isExpired,
|
|
};
|
|
const { isExpired } = useIsExpired(expiringProps);
|
|
|
|
const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
|
|
const isMessageSelected = useSelector(state =>
|
|
getIsMessageSelected(state as any, props.messageId)
|
|
);
|
|
const multiSelectMode = useSelector(isMessageSelectionMode);
|
|
|
|
const handleContextMenu = useCallback(
|
|
(e: React.MouseEvent<HTMLElement>) => {
|
|
const enableContextMenu = !multiSelectMode && !msgProps?.isKickedFromGroup;
|
|
|
|
if (enableContextMenu) {
|
|
contextMenu.hideAll();
|
|
contextMenu.show({
|
|
id: props.ctxMenuID,
|
|
event: e,
|
|
});
|
|
}
|
|
},
|
|
[props.ctxMenuID, multiSelectMode, msgProps?.isKickedFromGroup]
|
|
);
|
|
|
|
const { messageId, isDetailView } = props;
|
|
|
|
if (!msgProps) {
|
|
return null;
|
|
}
|
|
const {
|
|
direction,
|
|
conversationType,
|
|
receivedAt,
|
|
isUnread,
|
|
expirationLength,
|
|
expirationTimestamp,
|
|
} = msgProps;
|
|
|
|
if (isExpired) {
|
|
return null;
|
|
}
|
|
|
|
const selected = isMessageSelected || false;
|
|
const isGroup = conversationType === 'group';
|
|
const isQuotedMessageToAnimate = quotedMessageToAnimate === messageId;
|
|
const isIncoming = direction === 'incoming';
|
|
|
|
return (
|
|
<ReadableMessage
|
|
messageId={messageId}
|
|
className={classNames(
|
|
'session-message-wrapper',
|
|
selected && 'message-selected',
|
|
isGroup && 'public-chat-message-wrapper',
|
|
isQuotedMessageToAnimate && 'flash-green-once',
|
|
isIncoming ? 'session-message-wrapper-incoming' : 'session-message-wrapper-outgoing'
|
|
)}
|
|
onContextMenu={handleContextMenu}
|
|
receivedAt={receivedAt}
|
|
isUnread={!!isUnread}
|
|
key={`readable-message-${messageId}`}
|
|
>
|
|
<MessageAvatar messageId={messageId} />
|
|
<ExpireTimer
|
|
isCorrectSide={!isIncoming}
|
|
expirationLength={expirationLength || 0}
|
|
expirationTimestamp={expirationTimestamp || null}
|
|
/>
|
|
<MessageContentWithStatuses
|
|
ctxMenuID={props.ctxMenuID}
|
|
messageId={messageId}
|
|
onQuoteClick={props.onQuoteClick}
|
|
isDetailView={isDetailView}
|
|
/>
|
|
<ExpireTimer
|
|
isCorrectSide={isIncoming}
|
|
expirationLength={expirationLength || 0}
|
|
expirationTimestamp={expirationTimestamp || null}
|
|
/>
|
|
</ReadableMessage>
|
|
);
|
|
};
|