Message Context Menu: Support "Seen By" (#1564)
This commit is contained in:
parent
e81dd51872
commit
559c1c80dd
@ -22,7 +22,7 @@ export {
|
|||||||
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
||||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
reportMessages,
|
reportMessages, fetchSeenBy,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -1172,3 +1172,12 @@ export async function fetchPinnedMessages({ chat }: { chat: ApiChat }) {
|
|||||||
chats,
|
chats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchSeenBy({ chat, messageId }: { chat: ApiChat; messageId: number }) {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetMessageReadParticipants({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
msgId: messageId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return result ? result.map(String) : undefined;
|
||||||
|
}
|
||||||
|
|||||||
@ -262,6 +262,7 @@ export interface ApiMessage {
|
|||||||
isScheduled?: boolean;
|
isScheduled?: boolean;
|
||||||
shouldHideKeyboardButtons?: boolean;
|
shouldHideKeyboardButtons?: boolean;
|
||||||
isFromScheduled?: boolean;
|
isFromScheduled?: boolean;
|
||||||
|
seenByUserIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiThreadInfo {
|
export interface ApiThreadInfo {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export { default as DeleteMessageModal } from '../components/common/DeleteMessag
|
|||||||
export { default as PinMessageModal } from '../components/common/PinMessageModal';
|
export { default as PinMessageModal } from '../components/common/PinMessageModal';
|
||||||
export { default as UnpinAllMessagesModal } from '../components/common/UnpinAllMessagesModal';
|
export { default as UnpinAllMessagesModal } from '../components/common/UnpinAllMessagesModal';
|
||||||
export { default as MessageSelectToolbar } from '../components/middle/MessageSelectToolbar';
|
export { default as MessageSelectToolbar } from '../components/middle/MessageSelectToolbar';
|
||||||
|
export { default as SeenByModal } from '../components/common/SeenByModal';
|
||||||
|
|
||||||
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
||||||
export { default as Settings } from '../components/left/settings/Settings';
|
export { default as Settings } from '../components/left/settings/Settings';
|
||||||
|
|||||||
15
src/components/common/SeenByModal.async.tsx
Normal file
15
src/components/common/SeenByModal.async.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { FC, memo } from '../../lib/teact/teact';
|
||||||
|
import { OwnProps } from './SeenByModal';
|
||||||
|
import { Bundles } from '../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const SeenByModalAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const SeenByModal = useModuleLoader(Bundles.Extra, 'SeenByModal', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return SeenByModal ? <SeenByModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SeenByModalAsync);
|
||||||
87
src/components/common/SeenByModal.tsx
Normal file
87
src/components/common/SeenByModal.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React, { FC, useCallback, memo } from '../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { GlobalActions } from '../../global/types';
|
||||||
|
|
||||||
|
import { pick } from '../../util/iteratees';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import { selectChatMessage } from '../../modules/selectors';
|
||||||
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
|
|
||||||
|
import Modal from '../ui/Modal';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
import PrivateChatInfo from './PrivateChatInfo';
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StateProps = {
|
||||||
|
memberIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DispatchProps = Pick<GlobalActions, 'openChat' | 'closeSeenByModal'>;
|
||||||
|
|
||||||
|
const CLOSE_ANIMATION_DURATION = 100;
|
||||||
|
|
||||||
|
const SeenByModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
|
isOpen,
|
||||||
|
memberIds,
|
||||||
|
openChat,
|
||||||
|
closeSeenByModal,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleClick = useCallback((userId: string) => {
|
||||||
|
closeSeenByModal();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
openChat({ id: userId });
|
||||||
|
}, CLOSE_ANIMATION_DURATION);
|
||||||
|
}, [closeSeenByModal, openChat]);
|
||||||
|
|
||||||
|
const renderingMemberIds = useCurrentOrPrev(memberIds, true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closeSeenByModal}
|
||||||
|
className="narrow"
|
||||||
|
title="Which users read the message"
|
||||||
|
>
|
||||||
|
<div dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
{renderingMemberIds && renderingMemberIds.map((userId) => (
|
||||||
|
<ListItem
|
||||||
|
key={userId}
|
||||||
|
className="chat-item-clickable scroll-item small-icon"
|
||||||
|
onClick={() => handleClick(userId)}
|
||||||
|
>
|
||||||
|
<PrivateChatInfo userId={userId} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="confirm-dialog-button"
|
||||||
|
isText
|
||||||
|
onClick={closeSeenByModal}
|
||||||
|
>
|
||||||
|
{lang('Close')}
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
const { chatId, messageId } = global.seenByModal || {};
|
||||||
|
if (!chatId || !messageId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
memberIds: selectChatMessage(global, chatId, messageId)?.seenByUserIds,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(setGlobal, actions): DispatchProps => pick(actions, ['openChat', 'closeSeenByModal']),
|
||||||
|
)(SeenByModal));
|
||||||
@ -64,6 +64,7 @@ import MessageSelectToolbar from './MessageSelectToolbar.async';
|
|||||||
import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
|
import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
|
||||||
import PaymentModal from '../payment/PaymentModal.async';
|
import PaymentModal from '../payment/PaymentModal.async';
|
||||||
import ReceiptModal from '../payment/ReceiptModal.async';
|
import ReceiptModal from '../payment/ReceiptModal.async';
|
||||||
|
import SeenByModal from '../common/SeenByModal.async';
|
||||||
|
|
||||||
import './MiddleColumn.scss';
|
import './MiddleColumn.scss';
|
||||||
|
|
||||||
@ -90,6 +91,7 @@ type StateProps = {
|
|||||||
isSelectModeActive?: boolean;
|
isSelectModeActive?: boolean;
|
||||||
isPaymentModalOpen?: boolean;
|
isPaymentModalOpen?: boolean;
|
||||||
isReceiptModalOpen?: boolean;
|
isReceiptModalOpen?: boolean;
|
||||||
|
isSeenByModalOpen: boolean;
|
||||||
animationLevel?: number;
|
animationLevel?: number;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
currentTransitionKey: number;
|
currentTransitionKey: number;
|
||||||
@ -134,6 +136,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
isPaymentModalOpen,
|
isPaymentModalOpen,
|
||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
|
isSeenByModalOpen,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
currentTransitionKey,
|
currentTransitionKey,
|
||||||
@ -469,6 +472,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
isOpen={Boolean(isReceiptModalOpen)}
|
isOpen={Boolean(isReceiptModalOpen)}
|
||||||
onClose={clearReceipt}
|
onClose={clearReceipt}
|
||||||
/>
|
/>
|
||||||
|
<SeenByModal isOpen={isSeenByModalOpen} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -519,6 +523,7 @@ export default memo(withGlobal(
|
|||||||
isSelectModeActive: selectIsInSelectMode(global),
|
isSelectModeActive: selectIsInSelectMode(global),
|
||||||
isPaymentModalOpen: global.payment.isPaymentModalOpen,
|
isPaymentModalOpen: global.payment.isPaymentModalOpen,
|
||||||
isReceiptModalOpen: Boolean(global.payment.receipt),
|
isReceiptModalOpen: Boolean(global.payment.receipt),
|
||||||
|
isSeenByModalOpen: Boolean(global.seenByModal),
|
||||||
animationLevel: global.settings.byKey.animationLevel,
|
animationLevel: global.settings.byKey.animationLevel,
|
||||||
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useMemo, useState,
|
FC, memo, useCallback, useEffect, useMemo, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../lib/teact/teactn';
|
import { getGlobal, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { GlobalActions, MessageListType } from '../../../global/types';
|
import { GlobalActions, MessageListType } from '../../../global/types';
|
||||||
import { ApiMessage } from '../../../api/types';
|
import { ApiMessage } from '../../../api/types';
|
||||||
@ -9,9 +9,14 @@ import { IAlbum, IAnchorPosition } from '../../../types';
|
|||||||
import {
|
import {
|
||||||
selectActiveDownloadIds,
|
selectActiveDownloadIds,
|
||||||
selectAllowedMessageActions,
|
selectAllowedMessageActions,
|
||||||
|
selectChat,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
} from '../../../modules/selectors';
|
} from '../../../modules/selectors';
|
||||||
|
import { isChatGroup, isOwnMessage } from '../../../modules/helpers';
|
||||||
|
import { SEEN_BY_MEMBERS_EXPIRE, SEEN_BY_MEMBERS_CHAT_MAX } from '../../../config';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
|
import { getDayStartAt } from '../../../util/dateFormat';
|
||||||
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
|
||||||
@ -20,8 +25,6 @@ import ReportMessageModal from '../../common/ReportMessageModal';
|
|||||||
import PinMessageModal from '../../common/PinMessageModal';
|
import PinMessageModal from '../../common/PinMessageModal';
|
||||||
import MessageContextMenu from './MessageContextMenu';
|
import MessageContextMenu from './MessageContextMenu';
|
||||||
import CalendarModal from '../../common/CalendarModal';
|
import CalendarModal from '../../common/CalendarModal';
|
||||||
import { getDayStartAt } from '../../../util/dateFormat';
|
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -52,12 +55,14 @@ type StateProps = {
|
|||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
activeDownloads: number[];
|
activeDownloads: number[];
|
||||||
|
canShowSeenBy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'setReplyingToId' | 'setEditingId' | 'pinMessage' | 'openForwardMenu' |
|
'setReplyingToId' | 'setEditingId' | 'pinMessage' | 'openForwardMenu' |
|
||||||
'faveSticker' | 'unfaveSticker' | 'toggleMessageSelection' | 'sendScheduledMessages' | 'rescheduleMessage' |
|
'faveSticker' | 'unfaveSticker' | 'toggleMessageSelection' | 'sendScheduledMessages' | 'rescheduleMessage' |
|
||||||
'downloadMessageMedia' | 'cancelMessageMediaDownload'
|
'downloadMessageMedia' | 'cancelMessageMediaDownload' | 'loadSeenBy' |
|
||||||
|
'openSeenByModal'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
@ -86,6 +91,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
activeDownloads,
|
activeDownloads,
|
||||||
|
canShowSeenBy,
|
||||||
setReplyingToId,
|
setReplyingToId,
|
||||||
setEditingId,
|
setEditingId,
|
||||||
pinMessage,
|
pinMessage,
|
||||||
@ -97,6 +103,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
rescheduleMessage,
|
rescheduleMessage,
|
||||||
downloadMessageMedia,
|
downloadMessageMedia,
|
||||||
cancelMessageMediaDownload,
|
cancelMessageMediaDownload,
|
||||||
|
loadSeenBy,
|
||||||
|
openSeenByModal,
|
||||||
}) => {
|
}) => {
|
||||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
@ -105,6 +113,22 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||||
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canShowSeenBy && isOpen) {
|
||||||
|
loadSeenBy({ chatId: message.chatId, messageId: message.id });
|
||||||
|
}
|
||||||
|
}, [loadSeenBy, isOpen, message.chatId, message.id, canShowSeenBy]);
|
||||||
|
|
||||||
|
const seenByRecentUsers = useMemo(() => {
|
||||||
|
if (!message.seenByUserIds) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need for expensive global updates on users, so we avoid them
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
return message.seenByUserIds?.slice(0, 3).map((id) => usersById[id]).filter(Boolean);
|
||||||
|
}, [message.seenByUserIds]);
|
||||||
|
|
||||||
const isDownloading = album ? album.messages.some((msg) => activeDownloads.includes(msg.id))
|
const isDownloading = album ? album.messages.some((msg) => activeDownloads.includes(msg.id))
|
||||||
: activeDownloads.includes(message.id);
|
: activeDownloads.includes(message.id);
|
||||||
|
|
||||||
@ -206,6 +230,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
openCalendar();
|
openCalendar();
|
||||||
}, [openCalendar]);
|
}, [openCalendar]);
|
||||||
|
|
||||||
|
const handleOpenSeenByModal = useCallback(() => {
|
||||||
|
closeMenu();
|
||||||
|
openSeenByModal({ chatId: message.chatId, messageId: message.id });
|
||||||
|
}, [closeMenu, message.chatId, message.id, openSeenByModal]);
|
||||||
|
|
||||||
const handleRescheduleMessage = useCallback((date: Date) => {
|
const handleRescheduleMessage = useCallback((date: Date) => {
|
||||||
rescheduleMessage({
|
rescheduleMessage({
|
||||||
chatId: message.chatId,
|
chatId: message.chatId,
|
||||||
@ -262,7 +291,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canCopyLink={canCopyLink}
|
canCopyLink={canCopyLink}
|
||||||
canSelect={canSelect}
|
canSelect={canSelect}
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
|
canShowSeenBy={canShowSeenBy}
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
|
seenByRecentUsers={seenByRecentUsers}
|
||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onPin={handlePin}
|
onPin={handlePin}
|
||||||
@ -278,6 +309,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
onCopyLink={handleCopyLink}
|
onCopyLink={handleCopyLink}
|
||||||
onDownload={handleDownloadClick}
|
onDownload={handleDownloadClick}
|
||||||
|
onShowSeenBy={handleOpenSeenByModal}
|
||||||
/>
|
/>
|
||||||
<DeleteMessageModal
|
<DeleteMessageModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
@ -314,6 +346,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global, { message, messageListType }): StateProps => {
|
(global, { message, messageListType }): StateProps => {
|
||||||
const { threadId } = selectCurrentMessageList(global) || {};
|
const { threadId } = selectCurrentMessageList(global) || {};
|
||||||
const activeDownloads = selectActiveDownloadIds(global, message.chatId);
|
const activeDownloads = selectActiveDownloadIds(global, message.chatId);
|
||||||
|
const chat = selectChat(global, message.chatId);
|
||||||
const {
|
const {
|
||||||
noOptions,
|
noOptions,
|
||||||
canReply,
|
canReply,
|
||||||
@ -332,6 +365,12 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||||
const isPinned = messageListType === 'pinned';
|
const isPinned = messageListType === 'pinned';
|
||||||
const isScheduled = messageListType === 'scheduled';
|
const isScheduled = messageListType === 'scheduled';
|
||||||
|
const canShowSeenBy = Boolean(chat
|
||||||
|
&& isChatGroup(chat)
|
||||||
|
&& isOwnMessage(message)
|
||||||
|
&& chat.membersCount
|
||||||
|
&& chat.membersCount < SEEN_BY_MEMBERS_CHAT_MAX
|
||||||
|
&& message.date > Date.now() / 1000 - SEEN_BY_MEMBERS_EXPIRE);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
noOptions,
|
noOptions,
|
||||||
@ -351,6 +390,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
activeDownloads,
|
activeDownloads,
|
||||||
|
canShowSeenBy,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
@ -365,5 +405,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
'rescheduleMessage',
|
'rescheduleMessage',
|
||||||
'downloadMessageMedia',
|
'downloadMessageMedia',
|
||||||
'cancelMessageMediaDownload',
|
'cancelMessageMediaDownload',
|
||||||
|
'loadSeenBy',
|
||||||
|
'openSeenByModal',
|
||||||
]),
|
]),
|
||||||
)(ContextMenuContainer));
|
)(ContextMenuContainer));
|
||||||
|
|||||||
@ -12,4 +12,21 @@
|
|||||||
.backdrop {
|
.backdrop {
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatars {
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 1rem;
|
||||||
|
|
||||||
|
.Avatar {
|
||||||
|
border: .0625rem solid var(--color-background);
|
||||||
|
margin-right: 0;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: -0.1875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useCallback, useEffect, useRef,
|
FC, memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMessage } from '../../../api/types';
|
import { ApiMessage, ApiUser } from '../../../api/types';
|
||||||
import { IAnchorPosition } from '../../../types';
|
import { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
import { getMessageCopyOptions } from './helpers/copyOptions';
|
import { getMessageCopyOptions } from './helpers/copyOptions';
|
||||||
@ -12,6 +12,7 @@ import useLang from '../../../hooks/useLang';
|
|||||||
|
|
||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
|
||||||
import './MessageContextMenu.scss';
|
import './MessageContextMenu.scss';
|
||||||
|
|
||||||
@ -35,6 +36,8 @@ type OwnProps = {
|
|||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
|
canShowSeenBy?: boolean;
|
||||||
|
seenByRecentUsers?: ApiUser[];
|
||||||
onReply: () => void;
|
onReply: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onPin: () => void;
|
onPin: () => void;
|
||||||
@ -51,6 +54,7 @@ type OwnProps = {
|
|||||||
onCloseAnimationEnd?: () => void;
|
onCloseAnimationEnd?: () => void;
|
||||||
onCopyLink?: () => void;
|
onCopyLink?: () => void;
|
||||||
onDownload?: () => void;
|
onDownload?: () => void;
|
||||||
|
onShowSeenBy?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCROLLBAR_WIDTH = 10;
|
const SCROLLBAR_WIDTH = 10;
|
||||||
@ -75,6 +79,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
|
canShowSeenBy,
|
||||||
|
seenByRecentUsers,
|
||||||
onReply,
|
onReply,
|
||||||
onEdit,
|
onEdit,
|
||||||
onPin,
|
onPin,
|
||||||
@ -91,6 +97,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
onCopyLink,
|
onCopyLink,
|
||||||
onDownload,
|
onDownload,
|
||||||
|
onShowSeenBy,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
@ -166,9 +173,24 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
||||||
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
|
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
|
||||||
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
|
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
|
||||||
|
{canShowSeenBy && (
|
||||||
|
<MenuItem icon="group" onClick={onShowSeenBy} disabled={!message.seenByUserIds?.length}>
|
||||||
|
{message.seenByUserIds?.length
|
||||||
|
? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i')
|
||||||
|
: lang('Conversation.ContextMenuNoViews')}
|
||||||
|
<div className="avatars">
|
||||||
|
{seenByRecentUsers?.map((user) => (
|
||||||
|
<Avatar
|
||||||
|
size="micro"
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MessageContextMenu;
|
export default memo(MessageContextMenu);
|
||||||
|
|||||||
@ -124,6 +124,7 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
max-height: 90vh;
|
||||||
|
|
||||||
b,
|
b,
|
||||||
strong {
|
strong {
|
||||||
|
|||||||
@ -171,6 +171,9 @@ export const LIGHT_THEME_BG_COLOR = '#A2AF8E';
|
|||||||
export const DARK_THEME_BG_COLOR = '#0F0F0F';
|
export const DARK_THEME_BG_COLOR = '#0F0F0F';
|
||||||
export const DARK_THEME_PATTERN_COLOR = '#0a0a0a8c';
|
export const DARK_THEME_PATTERN_COLOR = '#0a0a0a8c';
|
||||||
export const DEFAULT_PATTERN_COLOR = 'rgba(90, 110, 70, 0.6)';
|
export const DEFAULT_PATTERN_COLOR = 'rgba(90, 110, 70, 0.6)';
|
||||||
|
// TODO Get values from `getConfig` method once it's available
|
||||||
|
export const SEEN_BY_MEMBERS_CHAT_MAX = 50;
|
||||||
|
export const SEEN_BY_MEMBERS_EXPIRE = 604680; // One week - 2 min
|
||||||
|
|
||||||
// Group calls
|
// Group calls
|
||||||
export const GROUP_CALL_VOLUME_MULTIPLIER = 100;
|
export const GROUP_CALL_VOLUME_MULTIPLIER = 100;
|
||||||
|
|||||||
@ -205,6 +205,11 @@ export type GlobalState = {
|
|||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
seenByModal?: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
};
|
||||||
|
|
||||||
fileUploads: {
|
fileUploads: {
|
||||||
byMessageLocalId: Record<string, {
|
byMessageLocalId: Record<string, {
|
||||||
progress: number;
|
progress: number;
|
||||||
@ -476,6 +481,7 @@ export type ActionTypes = (
|
|||||||
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
||||||
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
||||||
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
||||||
|
'openSeenByModal' | 'closeSeenByModal' |
|
||||||
// auth
|
// auth
|
||||||
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
|
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
|
||||||
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
||||||
@ -496,7 +502,7 @@ export type ActionTypes = (
|
|||||||
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
||||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
||||||
'reportMessages' | 'focusNextReply' | 'openChatByInvite' |
|
'reportMessages' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
||||||
// downloads
|
// downloads
|
||||||
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
|
|||||||
@ -1054,6 +1054,7 @@ messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.Disc
|
|||||||
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
||||||
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
||||||
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||||
|
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||||
|
|||||||
@ -1055,6 +1055,7 @@ messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.Disc
|
|||||||
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
||||||
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
||||||
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||||
|
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||||
|
|||||||
@ -932,6 +932,25 @@ addReducer('loadPinnedMessages', (global, actions, payload) => {
|
|||||||
void loadPinnedMessages(chat);
|
void loadPinnedMessages(chat);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('loadSeenBy', (global, actions, payload) => {
|
||||||
|
const { chatId, messageId } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const result = await callApi('fetchSeenBy', { chat, messageId });
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobal(updateChatMessage(getGlobal(), chatId, messageId, {
|
||||||
|
seenByUserIds: result,
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
async function loadPinnedMessages(chat: ApiChat) {
|
async function loadPinnedMessages(chat: ApiChat) {
|
||||||
const result = await callApi('fetchPinnedMessages', { chat });
|
const result = await callApi('fetchPinnedMessages', { chat });
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|||||||
@ -641,3 +641,19 @@ addReducer('createServiceNotification', (global, actions, payload) => {
|
|||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('openSeenByModal', (global, actions, payload) => {
|
||||||
|
const { chatId, messageId } = payload!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
seenByModal: { chatId, messageId },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('closeSeenByModal', (global) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
seenByModal: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user