Seen By Users: Read Time in Small Groups (#3148)
This commit is contained in:
parent
46c6f8fb9b
commit
c8d64dc929
@ -286,7 +286,7 @@ function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCou
|
|||||||
|
|
||||||
export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiUserReaction | undefined {
|
export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiUserReaction | undefined {
|
||||||
const {
|
const {
|
||||||
peerId, reaction, big, unread,
|
peerId, reaction, big, unread, date,
|
||||||
} = userReaction;
|
} = userReaction;
|
||||||
|
|
||||||
const apiReaction = buildApiReaction(reaction);
|
const apiReaction = buildApiReaction(reaction);
|
||||||
@ -295,6 +295,7 @@ export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReactio
|
|||||||
return {
|
return {
|
||||||
userId: getApiChatIdFromMtpPeer(peerId),
|
userId: getApiChatIdFromMtpPeer(peerId),
|
||||||
reaction: apiReaction,
|
reaction: apiReaction,
|
||||||
|
addedDate: date,
|
||||||
isUnread: unread,
|
isUnread: unread,
|
||||||
isBig: big,
|
isBig: big,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1416,7 +1416,13 @@ export async function fetchSeenBy({ chat, messageId }: { chat: ApiChat; messageI
|
|||||||
msgId: messageId,
|
msgId: messageId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return result ? result.map((readDate) => readDate.userId.toString()) : undefined;
|
return result
|
||||||
|
? result.reduce((acc, readDate) => {
|
||||||
|
acc[readDate.userId.toString()] = readDate.date;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>)
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSendAs({
|
export async function fetchSendAs({
|
||||||
|
|||||||
@ -428,7 +428,7 @@ export interface ApiMessage {
|
|||||||
isFromScheduled?: boolean;
|
isFromScheduled?: boolean;
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
seenByUserIds?: string[];
|
seenByDates?: Record<string, number>;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
isForwardingAllowed?: boolean;
|
isForwardingAllowed?: boolean;
|
||||||
transcriptionId?: string;
|
transcriptionId?: string;
|
||||||
@ -453,6 +453,7 @@ export interface ApiUserReaction {
|
|||||||
reaction: ApiReaction;
|
reaction: ApiReaction;
|
||||||
isBig?: boolean;
|
isBig?: boolean;
|
||||||
isUnread?: boolean;
|
isUnread?: boolean;
|
||||||
|
addedDate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiReactionCount {
|
export interface ApiReactionCount {
|
||||||
|
|||||||
@ -32,6 +32,7 @@ type OwnProps = {
|
|||||||
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||||
forceShowSelf?: boolean;
|
forceShowSelf?: boolean;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
statusIcon?: string;
|
||||||
withDots?: boolean;
|
withDots?: boolean;
|
||||||
withMediaViewer?: boolean;
|
withMediaViewer?: boolean;
|
||||||
withUsername?: boolean;
|
withUsername?: boolean;
|
||||||
@ -56,6 +57,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
typingStatus,
|
typingStatus,
|
||||||
avatarSize = 'medium',
|
avatarSize = 'medium',
|
||||||
status,
|
status,
|
||||||
|
statusIcon,
|
||||||
withDots,
|
withDots,
|
||||||
withMediaViewer,
|
withMediaViewer,
|
||||||
withUsername,
|
withUsername,
|
||||||
@ -109,7 +111,10 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
return withDots ? (
|
return withDots ? (
|
||||||
<DotAnimation className="status" content={status} />
|
<DotAnimation className="status" content={status} />
|
||||||
) : (
|
) : (
|
||||||
<span className="status" dir="auto">{renderText(status)}</span>
|
<span className="status" dir="auto">
|
||||||
|
{statusIcon && <i className={`icon ${statusIcon} status-icon`} />}
|
||||||
|
{renderText(status)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import React, { useCallback, memo, useMemo } from '../../lib/teact/teact';
|
||||||
import React, { useCallback, memo } from '../../lib/teact/teact';
|
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import useLang from '../../hooks/useLang';
|
|
||||||
import { selectChatMessage, selectTabState } from '../../global/selectors';
|
import { selectChatMessage, selectTabState } from '../../global/selectors';
|
||||||
|
import { formatDateAtTime } from '../../util/dateFormat';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
|
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
@ -16,15 +16,15 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type StateProps = {
|
export type StateProps = {
|
||||||
memberIds?: string[];
|
seenByDates?: Record<string, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CLOSE_ANIMATION_DURATION = 100;
|
const CLOSE_ANIMATION_DURATION = 100;
|
||||||
|
|
||||||
const SeenByModal: FC<OwnProps & StateProps> = ({
|
function SeenByModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
memberIds,
|
seenByDates,
|
||||||
}) => {
|
}: OwnProps & StateProps) {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
closeSeenByModal,
|
closeSeenByModal,
|
||||||
@ -32,6 +32,18 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const renderingSeenByDates = useCurrentOrPrev(seenByDates, true);
|
||||||
|
const memberIds = useMemo(() => {
|
||||||
|
if (!renderingSeenByDates) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Object.keys(renderingSeenByDates);
|
||||||
|
result.sort((leftId, rightId) => renderingSeenByDates[rightId] - renderingSeenByDates[leftId]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [renderingSeenByDates]);
|
||||||
|
|
||||||
const handleClick = useCallback((userId: string) => {
|
const handleClick = useCallback((userId: string) => {
|
||||||
closeSeenByModal();
|
closeSeenByModal();
|
||||||
|
|
||||||
@ -44,8 +56,6 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
|
|||||||
closeSeenByModal();
|
closeSeenByModal();
|
||||||
}, [closeSeenByModal]);
|
}, [closeSeenByModal]);
|
||||||
|
|
||||||
const renderingMemberIds = useCurrentOrPrev(memberIds, true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@ -54,14 +64,19 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
|
|||||||
title={`Seen by ${memberIds?.length} users`}
|
title={`Seen by ${memberIds?.length} users`}
|
||||||
>
|
>
|
||||||
<div dir={lang.isRtl ? 'rtl' : undefined}>
|
<div dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{renderingMemberIds && renderingMemberIds.map((userId) => (
|
{memberIds && memberIds.map((userId) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={userId}
|
key={userId}
|
||||||
className="chat-item-clickable scroll-item small-icon"
|
className="chat-item-clickable scroll-item small-icon"
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => handleClick(userId)}
|
onClick={() => handleClick(userId)}
|
||||||
>
|
>
|
||||||
<PrivateChatInfo userId={userId} noStatusOrTyping />
|
<PrivateChatInfo
|
||||||
|
userId={userId}
|
||||||
|
noStatusOrTyping
|
||||||
|
status={formatDateAtTime(lang, renderingSeenByDates![userId] * 1000)}
|
||||||
|
statusIcon="icon-message-read"
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +91,7 @@ const SeenByModal: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
@ -86,7 +101,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
memberIds: selectChatMessage(global, chatId, messageId)?.seenByUserIds,
|
seenByDates: selectChatMessage(global, chatId, messageId)?.seenByDates,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SeenByModal));
|
)(SeenByModal));
|
||||||
|
|||||||
@ -5,6 +5,18 @@
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: min(92vh, 32rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactor-list-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog-button {
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Reactions {
|
.Reactions {
|
||||||
@ -14,15 +26,15 @@
|
|||||||
.icon-heart {
|
.icon-heart {
|
||||||
width: 1.125rem;
|
width: 1.125rem;
|
||||||
height: 1.125rem;
|
height: 1.125rem;
|
||||||
margin-right: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reaction-filter-emoji {
|
.reaction-filter-emoji {
|
||||||
margin-right: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reactor-list {
|
.reactor-list {
|
||||||
max-height: 400px;
|
max-height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
@ -41,4 +53,8 @@
|
|||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import buildClassName from '../../util/buildClassName';
|
|||||||
import { formatIntegerCompact } from '../../util/textFormat';
|
import { formatIntegerCompact } from '../../util/textFormat';
|
||||||
import { unique } from '../../util/iteratees';
|
import { unique } from '../../util/iteratees';
|
||||||
import { isSameReaction, getReactionUniqueKey } from '../../global/helpers';
|
import { isSameReaction, getReactionUniqueKey } from '../../global/helpers';
|
||||||
|
import { formatDateAtTime } from '../../util/dateFormat';
|
||||||
|
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||||
@ -28,6 +29,7 @@ import ListItem from '../ui/ListItem';
|
|||||||
import ReactionStaticEmoji from '../common/ReactionStaticEmoji';
|
import ReactionStaticEmoji from '../common/ReactionStaticEmoji';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import FullNameTitle from '../common/FullNameTitle';
|
import FullNameTitle from '../common/FullNameTitle';
|
||||||
|
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||||
|
|
||||||
import './ReactorListModal.scss';
|
import './ReactorListModal.scss';
|
||||||
|
|
||||||
@ -37,7 +39,7 @@ export type OwnProps = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StateProps = Pick<ApiMessage, 'reactors' | 'reactions' | 'seenByUserIds'> & {
|
export type StateProps = Pick<ApiMessage, 'reactors' | 'reactions' | 'seenByDates'> & {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
availableReactions?: ApiAvailableReaction[];
|
availableReactions?: ApiAvailableReaction[];
|
||||||
@ -49,7 +51,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
reactions,
|
reactions,
|
||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
seenByUserIds,
|
seenByDates,
|
||||||
availableReactions,
|
availableReactions,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -118,8 +120,11 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
.filter(({ reaction }) => isSameReaction(reaction, chosenTab))
|
.filter(({ reaction }) => isSameReaction(reaction, chosenTab))
|
||||||
.map(({ userId }) => userId);
|
.map(({ userId }) => userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seenByUserIds = Object.keys(seenByDates || {});
|
||||||
|
|
||||||
return unique(reactors?.reactions.map(({ userId }) => userId).concat(seenByUserIds || []) || []);
|
return unique(reactors?.reactions.map(({ userId }) => userId).concat(seenByUserIds || []) || []);
|
||||||
}, [chosenTab, reactors, seenByUserIds]);
|
}, [chosenTab, reactors, seenByDates]);
|
||||||
|
|
||||||
const [viewportIds, getMore] = useInfiniteScroll(
|
const [viewportIds, getMore] = useInfiniteScroll(
|
||||||
handleLoadMore, userIds, reactors && reactors.nextOffset === undefined,
|
handleLoadMore, userIds, reactors && reactors.nextOffset === undefined,
|
||||||
@ -138,7 +143,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
onCloseAnimationEnd={handleCloseAnimationEnd}
|
onCloseAnimationEnd={handleCloseAnimationEnd}
|
||||||
>
|
>
|
||||||
{canShowFilters && (
|
{canShowFilters && (
|
||||||
<div className="Reactions">
|
<div className="Reactions" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
<Button
|
<Button
|
||||||
className={buildClassName(!chosenTab && 'chosen')}
|
className={buildClassName(!chosenTab && 'chosen')}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@ -173,7 +178,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div dir={lang.isRtl ? 'rtl' : undefined}>
|
<div dir={lang.isRtl ? 'rtl' : undefined} className="reactor-list-wrapper">
|
||||||
{viewportIds?.length ? (
|
{viewportIds?.length ? (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
className="reactor-list custom-scroll"
|
className="reactor-list custom-scroll"
|
||||||
@ -185,8 +190,11 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
const user = usersById[userId];
|
const user = usersById[userId];
|
||||||
const userReactions = reactors?.reactions.filter((reactor) => reactor.userId === userId);
|
const userReactions = reactors?.reactions.filter((reactor) => reactor.userId === userId);
|
||||||
const items: React.ReactNode[] = [];
|
const items: React.ReactNode[] = [];
|
||||||
|
const seenByUser = seenByDates?.[userId];
|
||||||
|
|
||||||
userReactions?.forEach((r) => {
|
userReactions?.forEach((r) => {
|
||||||
if (chosenTab && !isSameReaction(r.reaction, chosenTab)) return;
|
if (chosenTab && !isSameReaction(r.reaction, chosenTab)) return;
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
<ListItem
|
<ListItem
|
||||||
key={`${userId}-${getReactionUniqueKey(r.reaction)}`}
|
key={`${userId}-${getReactionUniqueKey(r.reaction)}`}
|
||||||
@ -195,7 +203,13 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
onClick={() => handleClick(userId)}
|
onClick={() => handleClick(userId)}
|
||||||
>
|
>
|
||||||
<Avatar user={user} size="small" />
|
<Avatar user={user} size="small" />
|
||||||
<FullNameTitle peer={user} withEmojiStatus />
|
<div className="info">
|
||||||
|
<FullNameTitle peer={user} withEmojiStatus />
|
||||||
|
<span className="status" dir="auto">
|
||||||
|
<i className="icon icon-heart-outline status-icon" />
|
||||||
|
{formatDateAtTime(lang, r.addedDate * 1000)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{r.reaction && (
|
{r.reaction && (
|
||||||
<ReactionStaticEmoji
|
<ReactionStaticEmoji
|
||||||
className="reactors-list-emoji"
|
className="reactors-list-emoji"
|
||||||
@ -206,6 +220,24 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>,
|
</ListItem>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!chosenTab && !userReactions?.length) {
|
||||||
|
items.push(
|
||||||
|
<ListItem
|
||||||
|
key={`${userId}-seen-by`}
|
||||||
|
className="chat-item-clickable scroll-item small-icon"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => handleClick(userId)}
|
||||||
|
>
|
||||||
|
<PrivateChatInfo
|
||||||
|
userId={userId}
|
||||||
|
noStatusOrTyping
|
||||||
|
status={seenByUser ? formatDateAtTime(lang, seenByUser * 1000) : undefined}
|
||||||
|
statusIcon="icon-message-read"
|
||||||
|
/>
|
||||||
|
</ListItem>,
|
||||||
|
);
|
||||||
|
}
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
@ -233,7 +265,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
messageId,
|
messageId,
|
||||||
reactions: message?.reactions,
|
reactions: message?.reactions,
|
||||||
reactors: message?.reactors,
|
reactors: message?.reactors,
|
||||||
seenByUserIds: message?.seenByUserIds,
|
seenByDates: message?.seenByDates,
|
||||||
availableReactions: global.availableReactions,
|
availableReactions: global.availableReactions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -240,14 +240,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
return Array.from(uniqueReactors).filter(Boolean).slice(0, 3);
|
return Array.from(uniqueReactors).filter(Boolean).slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.seenByUserIds) {
|
if (!message.seenByDates) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need for expensive global updates on users, so we avoid them
|
// No need for expensive global updates on users, so we avoid them
|
||||||
const usersById = getGlobal().users.byId;
|
const usersById = getGlobal().users.byId;
|
||||||
return message.seenByUserIds?.slice(0, 3).map((id) => usersById[id]).filter(Boolean);
|
return Object.keys(message.seenByDates).slice(0, 3).map((id) => usersById[id]).filter(Boolean);
|
||||||
}, [message.reactions?.recentReactions, message.seenByUserIds]);
|
}, [message.reactions?.recentReactions, message.seenByDates]);
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef,
|
memo, useCallback, useEffect, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
@ -205,10 +205,12 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
const withReactions = canShowReactionList && !noReactions;
|
const withReactions = canShowReactionList && !noReactions;
|
||||||
const isSponsoredMessage = !('id' in message);
|
const isSponsoredMessage = !('id' in message);
|
||||||
const messageId = !isSponsoredMessage ? message.id : '';
|
const messageId = !isSponsoredMessage ? message.id : '';
|
||||||
|
const seenByDates = !isSponsoredMessage ? message.seenByDates : undefined;
|
||||||
|
|
||||||
const [areItemsHidden, hideItems] = useFlag();
|
const [areItemsHidden, hideItems] = useFlag();
|
||||||
const [isReady, markIsReady, unmarkIsReady] = useFlag();
|
const [isReady, markIsReady, unmarkIsReady] = useFlag();
|
||||||
const { isMobile, isDesktop } = useAppLayout();
|
const { isMobile, isDesktop } = useAppLayout();
|
||||||
|
const seenByDatesCount = useMemo(() => (seenByDates ? Object.keys(seenByDates).length : 0), [seenByDates]);
|
||||||
|
|
||||||
const handleAfterCopy = useCallback(() => {
|
const handleAfterCopy = useCallback(() => {
|
||||||
showNotification({
|
showNotification({
|
||||||
@ -384,21 +386,21 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
className="MessageContextMenu--seen-by"
|
className="MessageContextMenu--seen-by"
|
||||||
icon={canShowReactionsCount ? 'heart-outline' : 'group'}
|
icon={canShowReactionsCount ? 'heart-outline' : 'group'}
|
||||||
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
||||||
disabled={!canShowReactionsCount && !message.seenByUserIds?.length}
|
disabled={!canShowReactionsCount && !seenByDatesCount}
|
||||||
>
|
>
|
||||||
<span className="MessageContextMenu--seen-by-label">
|
<span className="MessageContextMenu--seen-by-label">
|
||||||
{canShowReactionsCount && message.reactors?.count ? (
|
{canShowReactionsCount && message.reactors?.count ? (
|
||||||
canShowSeenBy && message.seenByUserIds?.length
|
canShowSeenBy && seenByDatesCount
|
||||||
? lang(
|
? lang(
|
||||||
'Chat.OutgoingContextMixedReactionCount',
|
'Chat.OutgoingContextMixedReactionCount',
|
||||||
[message.reactors.count, message.seenByUserIds.length],
|
[message.reactors.count, seenByDatesCount],
|
||||||
)
|
)
|
||||||
: lang('Chat.ContextReactionCount', message.reactors.count, 'i')
|
: lang('Chat.ContextReactionCount', message.reactors.count, 'i')
|
||||||
) : (
|
) : (
|
||||||
message.seenByUserIds?.length === 1 && seenByRecentUsers
|
seenByDatesCount === 1 && seenByRecentUsers
|
||||||
? renderText(getUserFullName(seenByRecentUsers[0])!)
|
? renderText(getUserFullName(seenByRecentUsers[0])!)
|
||||||
: (message.seenByUserIds?.length
|
: (seenByDatesCount
|
||||||
? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i')
|
? lang('Conversation.ContextMenuSeen', seenByDatesCount, 'i')
|
||||||
: lang('Conversation.ContextMenuNoViews')
|
: lang('Conversation.ContextMenuNoViews')
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -234,6 +234,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
margin-inline-end: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
.contact-phone,
|
.contact-phone,
|
||||||
.contact-username {
|
.contact-username {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|||||||
@ -1222,7 +1222,7 @@ addActionHandler('loadSeenBy', async (global, actions, payload): Promise<void> =
|
|||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
global = updateChatMessage(global, chatId, messageId, {
|
global = updateChatMessage(global, chatId, messageId, {
|
||||||
seenByUserIds: result,
|
seenByDates: result,
|
||||||
});
|
});
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -80,6 +80,7 @@ export function addMessageReaction<T extends GlobalState>(
|
|||||||
recentReactions.unshift({
|
recentReactions.unshift({
|
||||||
userId: currentUserId!,
|
userId: currentUserId!,
|
||||||
reaction,
|
reaction,
|
||||||
|
addedDate: Math.floor(Date.now() / 1000),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -310,6 +310,31 @@ export function formatDateTimeToString(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDateAtTime(
|
||||||
|
lang: LangFn,
|
||||||
|
datetime: number | Date,
|
||||||
|
) {
|
||||||
|
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
||||||
|
|
||||||
|
const today = getDayStart(new Date());
|
||||||
|
const time = formatTime(lang, date);
|
||||||
|
|
||||||
|
if (toIsoString(date) === toIsoString(today)) {
|
||||||
|
return lang('Time.TodayAt', time);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(today.getDate() - 1);
|
||||||
|
if (toIsoString(date) === toIsoString(yesterday)) {
|
||||||
|
return lang('Time.YesterdayAt', time);
|
||||||
|
}
|
||||||
|
|
||||||
|
const noYear = date.getFullYear() === today.getFullYear();
|
||||||
|
const formattedDate = formatDateToString(date, lang.code, noYear);
|
||||||
|
|
||||||
|
return lang('formatDateAtTime', [formattedDate, time]);
|
||||||
|
}
|
||||||
|
|
||||||
function isValidDate(day: number, month: number, year = 2021): boolean {
|
function isValidDate(day: number, month: number, year = 2021): boolean {
|
||||||
if (month > (MAX_MONTH_IN_YEAR - 1) || day > MAX_DAY_IN_MONTH) {
|
if (month > (MAX_MONTH_IN_YEAR - 1) || day > MAX_DAY_IN_MONTH) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user