Chat, Message List: Navigating with mention and reaction badges; Some fixes (#1836)
This commit is contained in:
parent
7a9a6d16f1
commit
62ed3468b6
@ -72,7 +72,8 @@ export function buildApiChatFromDialog(
|
|||||||
serverTimeOffset: number,
|
serverTimeOffset: number,
|
||||||
): ApiChat {
|
): ApiChat {
|
||||||
const {
|
const {
|
||||||
peer, folderId, unreadMark, unreadCount, unreadMentionsCount, notifySettings: { silent, muteUntil },
|
peer, folderId, unreadMark, unreadCount, unreadMentionsCount, unreadReactionsCount,
|
||||||
|
notifySettings: { silent, muteUntil },
|
||||||
readOutboxMaxId, readInboxMaxId, draft,
|
readOutboxMaxId, readInboxMaxId, draft,
|
||||||
} = dialog;
|
} = dialog;
|
||||||
const isMuted = silent || (typeof muteUntil === 'number' && getServerTime(serverTimeOffset) < muteUntil);
|
const isMuted = silent || (typeof muteUntil === 'number' && getServerTime(serverTimeOffset) < muteUntil);
|
||||||
@ -86,6 +87,7 @@ export function buildApiChatFromDialog(
|
|||||||
lastReadInboxMessageId: readInboxMaxId,
|
lastReadInboxMessageId: readInboxMaxId,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
unreadMentionsCount,
|
unreadMentionsCount,
|
||||||
|
unreadReactionsCount,
|
||||||
isMuted,
|
isMuted,
|
||||||
...(unreadMark && { hasUnreadMark: true }),
|
...(unreadMark && { hasUnreadMark: true }),
|
||||||
...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }),
|
...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }),
|
||||||
|
|||||||
@ -221,11 +221,15 @@ function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCou
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiUserReaction {
|
export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiUserReaction {
|
||||||
const { peerId, reaction } = userReaction;
|
const {
|
||||||
|
peerId, reaction, big, unread,
|
||||||
|
} = userReaction;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: getApiChatIdFromMtpPeer(peerId),
|
userId: getApiChatIdFromMtpPeer(peerId),
|
||||||
reaction,
|
reaction,
|
||||||
|
isUnread: unread,
|
||||||
|
isBig: big,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export {
|
|||||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
||||||
saveDefaultSendAs,
|
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -23,8 +23,8 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ALL_FOLDER_ID,
|
ALL_FOLDER_ID,
|
||||||
DEBUG,
|
DEBUG, MENTION_UNREAD_SLICE,
|
||||||
PINNED_MESSAGES_LIMIT,
|
PINNED_MESSAGES_LIMIT, REACTION_UNREAD_SLICE,
|
||||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
@ -1319,3 +1319,95 @@ export async function viewSponsoredMessage({ chat, random }: { chat: ApiChat; ra
|
|||||||
randomId: deserializeBytes(random),
|
randomId: deserializeBytes(random),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readAllMentions({
|
||||||
|
chat,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.ReadMentions({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readAllReactions({
|
||||||
|
chat,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.ReadReactions({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchUnreadMentions({
|
||||||
|
chat, ...pagination
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
offsetId?: number;
|
||||||
|
addOffset?: number;
|
||||||
|
maxId?: number;
|
||||||
|
minId?: number;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetUnreadMentions({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
limit: MENTION_UNREAD_SLICE,
|
||||||
|
...pagination,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (
|
||||||
|
!result
|
||||||
|
|| result instanceof GramJs.messages.MessagesNotModified
|
||||||
|
|| !result.messages
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalDb(result);
|
||||||
|
|
||||||
|
const messages = result.messages.map(buildApiMessage).filter<ApiMessage>(Boolean as any);
|
||||||
|
const users = result.users.map(buildApiUser).filter<ApiUser>(Boolean as any);
|
||||||
|
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter<ApiChat>(Boolean as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
users,
|
||||||
|
chats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchUnreadReactions({
|
||||||
|
chat, ...pagination
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
offsetId?: number;
|
||||||
|
addOffset?: number;
|
||||||
|
maxId?: number;
|
||||||
|
minId?: number;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetUnreadReactions({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
limit: REACTION_UNREAD_SLICE,
|
||||||
|
...pagination,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (
|
||||||
|
!result
|
||||||
|
|| result instanceof GramJs.messages.MessagesNotModified
|
||||||
|
|| !result.messages
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalDb(result);
|
||||||
|
|
||||||
|
const messages = result.messages.map(buildApiMessage).filter<ApiMessage>(Boolean as any);
|
||||||
|
const users = result.users.map(buildApiUser).filter<ApiUser>(Boolean as any);
|
||||||
|
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter<ApiChat>(Boolean as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
users,
|
||||||
|
chats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export interface ApiChat {
|
|||||||
lastReadInboxMessageId?: number;
|
lastReadInboxMessageId?: number;
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
unreadMentionsCount?: number;
|
unreadMentionsCount?: number;
|
||||||
|
unreadReactionsCount?: number;
|
||||||
isVerified?: boolean;
|
isVerified?: boolean;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
isSignaturesShown?: boolean;
|
isSignaturesShown?: boolean;
|
||||||
@ -65,6 +66,9 @@ export interface ApiChat {
|
|||||||
|
|
||||||
joinRequests?: ApiChatInviteImporter[];
|
joinRequests?: ApiChatInviteImporter[];
|
||||||
sendAsIds?: string[];
|
sendAsIds?: string[];
|
||||||
|
|
||||||
|
unreadReactions?: number[];
|
||||||
|
unreadMentions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiTypingStatus {
|
export interface ApiTypingStatus {
|
||||||
|
|||||||
@ -336,6 +336,8 @@ export interface ApiReactions {
|
|||||||
export interface ApiUserReaction {
|
export interface ApiUserReaction {
|
||||||
userId: string;
|
userId: string;
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
isBig?: boolean;
|
||||||
|
isUnread?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiReactionCount {
|
export interface ApiReactionCount {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -65,7 +65,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mention {
|
&.reaction:not(.muted) {
|
||||||
|
background: #ed504f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mention, &.reaction {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,9 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Badge: FC<OwnProps> = ({ chat, isPinned, isMuted }) => {
|
const Badge: FC<OwnProps> = ({ chat, isPinned, isMuted }) => {
|
||||||
const isShown = Boolean(chat.unreadCount || chat.hasUnreadMark || isPinned);
|
const isShown = Boolean(
|
||||||
|
chat.unreadCount || chat.unreadMentionsCount || chat.hasUnreadMark || isPinned || chat.unreadReactionsCount,
|
||||||
|
);
|
||||||
const isUnread = Boolean(chat.unreadCount || chat.hasUnreadMark);
|
const isUnread = Boolean(chat.unreadCount || chat.hasUnreadMark);
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'Badge',
|
'Badge',
|
||||||
@ -26,38 +28,41 @@ const Badge: FC<OwnProps> = ({ chat, isPinned, isMuted }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
if (chat.unreadCount) {
|
const unreadReactionsElement = chat.unreadReactionsCount && (
|
||||||
if (chat.unreadMentionsCount) {
|
<div className={buildClassName('Badge reaction', isMuted && 'muted')}>
|
||||||
return (
|
<i className="icon-heart" />
|
||||||
<div className="Badge-wrapper">
|
</div>
|
||||||
<div className="Badge mention">
|
);
|
||||||
<i className="icon-mention" />
|
|
||||||
</div>
|
|
||||||
<div className={className}>
|
|
||||||
{formatIntegerCompact(chat.unreadCount)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const unreadMentionsElement = chat.unreadMentionsCount && (
|
||||||
<div className={className}>
|
<div className="Badge mention">
|
||||||
{formatIntegerCompact(chat.unreadCount)}
|
<i className="icon-mention" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (chat.hasUnreadMark) {
|
|
||||||
return (
|
|
||||||
<div className={className} />
|
|
||||||
);
|
|
||||||
} else if (isPinned) {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<i className="icon-pinned-chat" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
const unreadCountElement = (chat.hasUnreadMark || chat.unreadCount) ? (
|
||||||
|
<div className={className}>
|
||||||
|
{!chat.hasUnreadMark && formatIntegerCompact(chat.unreadCount!)}
|
||||||
|
</div>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
const pinnedElement = isPinned && !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement && (
|
||||||
|
<div className={className}>
|
||||||
|
<i className="icon-pinned-chat" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const elements = [unreadReactionsElement, unreadMentionsElement, unreadCountElement, pinnedElement].filter(Boolean);
|
||||||
|
|
||||||
|
if (elements.length === 0) return undefined;
|
||||||
|
|
||||||
|
if (elements.length === 1) return elements[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Badge-wrapper">
|
||||||
|
{elements}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import windowSize from '../../../util/windowSize';
|
|||||||
import stopEvent from '../../../util/stopEvent';
|
import stopEvent from '../../../util/stopEvent';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
import { getMessageHtmlId } from '../../../global/helpers';
|
import { getMessageHtmlId } from '../../../global/helpers';
|
||||||
|
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||||
|
|
||||||
const ANIMATION_DURATION = 200;
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
@ -264,17 +265,6 @@ function uncover(realWidth: number, realHeight: number, top: number, left: numbe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isElementInViewport(el: HTMLElement) {
|
|
||||||
if (el.style.display === 'none') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const { height: windowHeight } = windowSize.get();
|
|
||||||
|
|
||||||
return (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMessageImageFullyVisible(container: HTMLElement, imageEl: HTMLElement) {
|
function isMessageImageFullyVisible(container: HTMLElement, imageEl: HTMLElement) {
|
||||||
const messageListElement = document.querySelector<HTMLDivElement>('.Transition__slide--active > .MessageList')!;
|
const messageListElement = document.querySelector<HTMLDivElement>('.Transition__slide--active > .MessageList')!;
|
||||||
let imgOffsetTop = container.offsetTop + imageEl.closest<HTMLDivElement>('.content-inner, .WebPage')!.offsetTop;
|
let imgOffsetTop = container.offsetTop + imageEl.closest<HTMLDivElement>('.content-inner, .WebPage')!.offsetTop;
|
||||||
|
|||||||
51
src/components/middle/FloatingActionButtons.module.scss
Normal file
51
src/components/middle/FloatingActionButtons.module.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.root {
|
||||||
|
--base-bottom-pos: 6rem;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--base-bottom-pos);
|
||||||
|
right: max(1rem, env(safe-area-inset-right));
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4.5rem);
|
||||||
|
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
|
||||||
|
z-index: var(--z-scroll-down-button);
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
:global(body.animation-level-0) & {
|
||||||
|
transform: none !important;
|
||||||
|
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 4.5rem;
|
||||||
|
|
||||||
|
:global(body:not(.keyboard-visible)) & {
|
||||||
|
bottom: calc(4.5rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.revealed {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
&.no-composer.no-extra-shift {
|
||||||
|
transform: translateY(4rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.only-reactions {
|
||||||
|
transform: translateY(4rem);
|
||||||
|
|
||||||
|
.unread {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
body.is-symbol-menu-open & {
|
||||||
|
bottom: calc(var(--base-bottom-pos) + var(--symbol-menu-height) + var(--symbol-menu-footer-height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/components/middle/FloatingActionButtons.tsx
Normal file
145
src/components/middle/FloatingActionButtons.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import React, {
|
||||||
|
FC, useCallback, memo, useRef, useEffect,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import { MessageListType } from '../../global/types';
|
||||||
|
import { MAIN_THREAD_ID } from '../../api/types';
|
||||||
|
|
||||||
|
import { selectChat, selectCurrentMessageList } from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
||||||
|
|
||||||
|
import ScrollDownButton from './ScrollDownButton';
|
||||||
|
|
||||||
|
import styles from './FloatingActionButtons.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isShown: boolean;
|
||||||
|
canPost?: boolean;
|
||||||
|
withExtraShift?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chatId?: string;
|
||||||
|
messageListType?: MessageListType;
|
||||||
|
unreadCount?: number;
|
||||||
|
reactionsCount?: number;
|
||||||
|
mentionsCount?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FOCUS_MARGIN = 20;
|
||||||
|
|
||||||
|
const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||||
|
isShown,
|
||||||
|
canPost,
|
||||||
|
messageListType,
|
||||||
|
chatId,
|
||||||
|
unreadCount,
|
||||||
|
reactionsCount,
|
||||||
|
mentionsCount,
|
||||||
|
withExtraShift,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
focusNextReply, focusNextReaction, focusNextMention, fetchUnreadReactions,
|
||||||
|
readAllMentions, readAllReactions, fetchUnreadMentions,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const hasUnreadReactions = Boolean(reactionsCount);
|
||||||
|
const hasUnreadMentions = Boolean(mentionsCount);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasUnreadReactions && chatId) {
|
||||||
|
fetchUnreadReactions({ chatId });
|
||||||
|
}
|
||||||
|
}, [chatId, fetchUnreadReactions, hasUnreadReactions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasUnreadMentions && chatId) {
|
||||||
|
fetchUnreadMentions({ chatId });
|
||||||
|
}
|
||||||
|
}, [chatId, fetchUnreadMentions, hasUnreadMentions]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (!isShown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageListType === 'thread') {
|
||||||
|
focusNextReply();
|
||||||
|
} else {
|
||||||
|
const messagesContainer = elementRef.current!.parentElement!.querySelector<HTMLDivElement>('.MessageList')!;
|
||||||
|
const messageElements = messagesContainer.querySelectorAll<HTMLDivElement>('.message-list-item');
|
||||||
|
const lastMessageElement = messageElements[messageElements.length - 1];
|
||||||
|
if (!lastMessageElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fastSmoothScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN);
|
||||||
|
}
|
||||||
|
}, [isShown, messageListType, focusNextReply]);
|
||||||
|
|
||||||
|
const fabClassName = buildClassName(
|
||||||
|
styles.root,
|
||||||
|
(isShown || Boolean(reactionsCount) || Boolean(mentionsCount)) && styles.revealed,
|
||||||
|
(Boolean(reactionsCount) || Boolean(mentionsCount)) && !isShown && styles.onlyReactions,
|
||||||
|
!canPost && styles.noComposer,
|
||||||
|
!withExtraShift && styles.noExtraShift,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={elementRef} className={fabClassName}>
|
||||||
|
{hasUnreadReactions && (
|
||||||
|
<ScrollDownButton
|
||||||
|
icon="heart-outline"
|
||||||
|
ariaLabelLang="AccDescrReactionMentionDown"
|
||||||
|
onClick={focusNextReaction}
|
||||||
|
onReadAll={readAllReactions}
|
||||||
|
unreadCount={reactionsCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{hasUnreadMentions && (
|
||||||
|
<ScrollDownButton
|
||||||
|
icon="mention"
|
||||||
|
ariaLabelLang="AccDescrMentionDown"
|
||||||
|
onClick={focusNextMention}
|
||||||
|
onReadAll={readAllMentions}
|
||||||
|
unreadCount={mentionsCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollDownButton
|
||||||
|
icon="arrow-down"
|
||||||
|
ariaLabelLang="AccDescrPageDown"
|
||||||
|
onClick={handleClick}
|
||||||
|
unreadCount={unreadCount}
|
||||||
|
className={styles.unread}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
|
if (!currentMessageList) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chatId, threadId, type: messageListType } = currentMessageList;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
const shouldShowCount = chat && threadId === MAIN_THREAD_ID && messageListType === 'thread';
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageListType,
|
||||||
|
chatId,
|
||||||
|
reactionsCount: shouldShowCount ? chat.unreadReactionsCount : undefined,
|
||||||
|
mentionsCount: shouldShowCount ? chat.unreadMentionsCount : undefined,
|
||||||
|
unreadCount: shouldShowCount ? chat.unreadCount : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(FloatingActionButtons));
|
||||||
@ -60,7 +60,7 @@ import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTran
|
|||||||
import Transition from '../ui/Transition';
|
import Transition from '../ui/Transition';
|
||||||
import MiddleHeader from './MiddleHeader';
|
import MiddleHeader from './MiddleHeader';
|
||||||
import MessageList from './MessageList';
|
import MessageList from './MessageList';
|
||||||
import ScrollDownButton from './ScrollDownButton';
|
import FloatingActionButtons from './FloatingActionButtons';
|
||||||
import Composer from './composer/Composer';
|
import Composer from './composer/Composer';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import MobileSearch from './MobileSearch.async';
|
import MobileSearch from './MobileSearch.async';
|
||||||
@ -505,7 +505,7 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<ScrollDownButton
|
<FloatingActionButtons
|
||||||
isShown={renderingIsFabShown}
|
isShown={renderingIsFabShown}
|
||||||
canPost={renderingCanPost}
|
canPost={renderingCanPost}
|
||||||
withExtraShift={withExtraShift}
|
withExtraShift={withExtraShift}
|
||||||
|
|||||||
@ -127,7 +127,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
|||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => setChosenTab(undefined)}
|
onClick={() => setChosenTab(undefined)}
|
||||||
>
|
>
|
||||||
<i className="icon-reaction-filled" />
|
<i className="icon-heart" />
|
||||||
{reactors?.count && formatIntegerCompact(reactors.count)}
|
{reactors?.count && formatIntegerCompact(reactors.count)}
|
||||||
</Button>
|
</Button>
|
||||||
{allReactions.map((reaction) => {
|
{allReactions.map((reaction) => {
|
||||||
|
|||||||
65
src/components/middle/ScrollDownButton.module.scss
Normal file
65
src/components/middle/ScrollDownButton.module.scss
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1276px) {
|
||||||
|
transform: translateX(0);
|
||||||
|
|
||||||
|
transition: transform var(--layer-transition), opacity 0.2s ease;
|
||||||
|
|
||||||
|
body.animation-level-0 & {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Main.right-column-open & {
|
||||||
|
transform: translateX(calc(-1 * var(--right-column-width)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
box-shadow: 0 1px 2px var(--color-default-shadow);
|
||||||
|
color: var(--color-composer-button);
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
width: 2.875rem !important;
|
||||||
|
height: 2.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 1.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-count {
|
||||||
|
min-width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0 0.4375rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: -0.3125rem;
|
||||||
|
right: -0.3125rem;
|
||||||
|
|
||||||
|
background: var(--color-green);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
top: -0.6875rem;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,104 +0,0 @@
|
|||||||
.ScrollDownButton {
|
|
||||||
--base-bottom-pos: 6rem;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
bottom: var(--base-bottom-pos);
|
|
||||||
right: max(1rem, env(safe-area-inset-right));
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(4.5rem);
|
|
||||||
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
|
|
||||||
z-index: var(--z-scroll-down-button);
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
body.animation-level-0 & {
|
|
||||||
transform: none !important;
|
|
||||||
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
right: 0.5rem;
|
|
||||||
bottom: 4.5rem;
|
|
||||||
|
|
||||||
body:not(.keyboard-visible) & {
|
|
||||||
bottom: calc(4.5rem + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> .Button {
|
|
||||||
box-shadow: 0 1px 2px var(--color-default-shadow);
|
|
||||||
color: var(--color-composer-button);
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1276px) {
|
|
||||||
transform: translateX(0);
|
|
||||||
|
|
||||||
transition: transform var(--layer-transition);
|
|
||||||
|
|
||||||
body.animation-level-0 & {
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#Main.right-column-open & {
|
|
||||||
transform: translateX(calc(-1 * var(--right-column-width)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
> .Button {
|
|
||||||
width: 2.875rem;
|
|
||||||
height: 2.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.revealed {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: all;
|
|
||||||
|
|
||||||
&.no-composer:not(.with-extra-shift) {
|
|
||||||
transform: translateY(4rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.unread-count {
|
|
||||||
min-width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
padding: 0 0.4375rem;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: -0.3125rem;
|
|
||||||
right: -0.3125rem;
|
|
||||||
|
|
||||||
background: var(--color-green);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
top: -0.6875rem;
|
|
||||||
right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
body.is-symbol-menu-open & {
|
|
||||||
bottom: calc(var(--base-bottom-pos) + var(--symbol-menu-height) + var(--symbol-menu-footer-height));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,105 +1,71 @@
|
|||||||
import React, {
|
import React, { FC, memo, useRef } from '../../lib/teact/teact';
|
||||||
FC, useCallback, memo, useRef,
|
|
||||||
} from '../../lib/teact/teact';
|
|
||||||
import { getActions, withGlobal } from '../../global';
|
|
||||||
|
|
||||||
import { MessageListType } from '../../global/types';
|
|
||||||
import { MAIN_THREAD_ID } from '../../api/types';
|
|
||||||
|
|
||||||
import { selectChat, selectCurrentMessageList } from '../../global/selectors';
|
|
||||||
import { formatIntegerCompact } from '../../util/textFormat';
|
import { formatIntegerCompact } from '../../util/textFormat';
|
||||||
import buildClassName from '../../util/buildClassName';
|
|
||||||
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
import Menu from '../ui/Menu';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
|
import MenuItem from '../ui/MenuItem';
|
||||||
|
|
||||||
import './ScrollDownButton.scss';
|
import styles from './ScrollDownButton.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isShown: boolean;
|
icon: string;
|
||||||
canPost?: boolean;
|
ariaLabelLang: string;
|
||||||
withExtraShift?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateProps = {
|
|
||||||
messageListType?: MessageListType;
|
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
|
onClick: VoidFunction;
|
||||||
|
onReadAll?: VoidFunction;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FOCUS_MARGIN = 20;
|
const ScrollDownButton: FC<OwnProps> = ({
|
||||||
|
icon,
|
||||||
const ScrollDownButton: FC<OwnProps & StateProps> = ({
|
ariaLabelLang,
|
||||||
isShown,
|
|
||||||
canPost,
|
|
||||||
messageListType,
|
|
||||||
unreadCount,
|
unreadCount,
|
||||||
withExtraShift,
|
onClick,
|
||||||
|
onReadAll,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { focusNextReply } = getActions();
|
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const {
|
||||||
const handleClick = useCallback(() => {
|
isContextMenuOpen,
|
||||||
if (!isShown) {
|
handleContextMenu,
|
||||||
return;
|
handleContextMenuClose,
|
||||||
}
|
handleContextMenuHide,
|
||||||
|
} = useContextMenuHandlers(ref, !onReadAll);
|
||||||
if (messageListType === 'thread') {
|
|
||||||
focusNextReply();
|
|
||||||
} else {
|
|
||||||
const messagesContainer = elementRef.current!.parentElement!.querySelector<HTMLDivElement>('.MessageList')!;
|
|
||||||
const messageElements = messagesContainer.querySelectorAll<HTMLDivElement>('.message-list-item');
|
|
||||||
const lastMessageElement = messageElements[messageElements.length - 1];
|
|
||||||
if (!lastMessageElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fastSmoothScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN);
|
|
||||||
}
|
|
||||||
}, [isShown, messageListType, focusNextReply]);
|
|
||||||
|
|
||||||
const fabClassName = buildClassName(
|
|
||||||
'ScrollDownButton',
|
|
||||||
isShown && 'revealed',
|
|
||||||
!canPost && 'no-composer',
|
|
||||||
withExtraShift && 'with-extra-shift',
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={elementRef} className={fabClassName}>
|
<div className={buildClassName(styles.root, className)} ref={ref}>
|
||||||
<div className="ScrollDownButton-inner">
|
<Button
|
||||||
<Button
|
color="secondary"
|
||||||
color="secondary"
|
round
|
||||||
round
|
className={styles.button}
|
||||||
onClick={handleClick}
|
onClick={onClick}
|
||||||
ariaLabel={lang('AccDescrPageDown')}
|
onContextMenu={handleContextMenu}
|
||||||
|
ariaLabel={lang(ariaLabelLang)}
|
||||||
|
>
|
||||||
|
<i className={buildClassName(styles.icon, `icon-${icon}`)} />
|
||||||
|
</Button>
|
||||||
|
{Boolean(unreadCount) && <div className={styles.unreadCount}>{formatIntegerCompact(unreadCount)}</div>}
|
||||||
|
{onReadAll && (
|
||||||
|
<Menu
|
||||||
|
isOpen={isContextMenuOpen}
|
||||||
|
onClose={handleContextMenuClose}
|
||||||
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
|
autoClose
|
||||||
|
positionX="right"
|
||||||
|
positionY="bottom"
|
||||||
>
|
>
|
||||||
<i className="icon-arrow-down" />
|
<MenuItem icon="readchats" onClick={onReadAll}>{lang('MarkAllAsRead')}</MenuItem>
|
||||||
</Button>
|
</Menu>
|
||||||
{Boolean(unreadCount) && (
|
)}
|
||||||
<div className="unread-count">{formatIntegerCompact(unreadCount!)}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(ScrollDownButton);
|
||||||
(global): StateProps => {
|
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
|
||||||
if (!currentMessageList) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { chatId, threadId, type: messageListType } = currentMessageList;
|
|
||||||
const chat = selectChat(global, chatId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageListType,
|
|
||||||
unreadCount: chat && threadId === MAIN_THREAD_ID && messageListType === 'thread' ? chat.unreadCount : undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
)(ScrollDownButton));
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default function useMessageObservers(
|
|||||||
containerRef: RefObject<HTMLDivElement>,
|
containerRef: RefObject<HTMLDivElement>,
|
||||||
memoFirstUnreadIdRef: { current: number | undefined },
|
memoFirstUnreadIdRef: { current: number | undefined },
|
||||||
) {
|
) {
|
||||||
const { markMessageListRead, markMessagesRead } = getActions();
|
const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
observe: observeIntersectionForMedia,
|
observe: observeIntersectionForMedia,
|
||||||
@ -38,6 +38,7 @@ export default function useMessageObservers(
|
|||||||
|
|
||||||
let maxId = 0;
|
let maxId = 0;
|
||||||
const mentionIds: number[] = [];
|
const mentionIds: number[] = [];
|
||||||
|
const reactionIds: number[] = [];
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
const { isIntersecting, target } = entry;
|
const { isIntersecting, target } = entry;
|
||||||
@ -56,6 +57,10 @@ export default function useMessageObservers(
|
|||||||
if (dataset.hasUnreadMention) {
|
if (dataset.hasUnreadMention) {
|
||||||
mentionIds.push(messageId);
|
mentionIds.push(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dataset.hasUnreadReaction) {
|
||||||
|
reactionIds.push(messageId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
|
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
|
||||||
@ -63,7 +68,11 @@ export default function useMessageObservers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mentionIds.length) {
|
if (mentionIds.length) {
|
||||||
markMessagesRead({ messageIds: mentionIds });
|
markMentionsRead({ messageIds: mentionIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactionIds.length) {
|
||||||
|
animateUnreadReaction({ messageIds: reactionIds });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -90,6 +90,7 @@ import useFocusMessage from './hooks/useFocusMessage';
|
|||||||
import useOuterHandlers from './hooks/useOuterHandlers';
|
import useOuterHandlers from './hooks/useOuterHandlers';
|
||||||
import useInnerHandlers from './hooks/useInnerHandlers';
|
import useInnerHandlers from './hooks/useInnerHandlers';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
@ -195,6 +196,7 @@ type StateProps = {
|
|||||||
defaultReaction?: string;
|
defaultReaction?: string;
|
||||||
activeReaction?: ActiveReaction;
|
activeReaction?: ActiveReaction;
|
||||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||||
|
hasUnreadReaction?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MetaPosition =
|
type MetaPosition =
|
||||||
@ -281,11 +283,13 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
shouldLoopStickers,
|
shouldLoopStickers,
|
||||||
autoLoadFileMaxSizeMb,
|
autoLoadFileMaxSizeMb,
|
||||||
threadInfo,
|
threadInfo,
|
||||||
|
hasUnreadReaction,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
toggleMessageSelection,
|
toggleMessageSelection,
|
||||||
clickBotInlineButton,
|
clickBotInlineButton,
|
||||||
disableContextMenuHint,
|
disableContextMenuHint,
|
||||||
|
animateUnreadReaction,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -521,6 +525,13 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
useFocusMessage(ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer);
|
useFocusMessage(ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const bottomMarker = bottomMarkerRef.current;
|
||||||
|
if (hasUnreadReaction && bottomMarker && isElementInViewport(bottomMarker)) {
|
||||||
|
animateUnreadReaction({ messageIds: [messageId] });
|
||||||
|
}
|
||||||
|
}, [hasUnreadReaction, messageId, animateUnreadReaction]);
|
||||||
|
|
||||||
let style = '';
|
let style = '';
|
||||||
let calculatedWidth;
|
let calculatedWidth;
|
||||||
let noMediaCorners = false;
|
let noMediaCorners = false;
|
||||||
@ -896,7 +907,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
className="bottom-marker"
|
className="bottom-marker"
|
||||||
data-message-id={messageId}
|
data-message-id={messageId}
|
||||||
data-last-message-id={album ? album.messages[album.messages.length - 1].id : undefined}
|
data-last-message-id={album ? album.messages[album.messages.length - 1].id : undefined}
|
||||||
data-has-unread-mention={message.hasUnreadMention}
|
data-has-unread-mention={message.hasUnreadMention || undefined}
|
||||||
|
data-has-unread-reaction={hasUnreadReaction || undefined}
|
||||||
/>
|
/>
|
||||||
{!isInDocumentGroup && (
|
{!isInDocumentGroup && (
|
||||||
<div className="message-select-control">
|
<div className="message-select-control">
|
||||||
@ -1065,6 +1077,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
const localSticker = singleEmoji ? selectLocalAnimatedEmoji(global, singleEmoji) : undefined;
|
const localSticker = singleEmoji ? selectLocalAnimatedEmoji(global, singleEmoji) : undefined;
|
||||||
|
|
||||||
|
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
chatUsername,
|
chatUsername,
|
||||||
@ -1117,6 +1131,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
||||||
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
||||||
...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }),
|
...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }),
|
||||||
|
hasUnreadReaction,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Message));
|
)(Message));
|
||||||
|
|||||||
@ -226,7 +226,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
style={menuStyle}
|
style={menuStyle}
|
||||||
ref={scrollableRef}
|
ref={scrollableRef}
|
||||||
>
|
>
|
||||||
{canRemoveReaction && <MenuItem icon="reactions" onClick={handleRemoveReaction}>Remove Reaction</MenuItem>}
|
{canRemoveReaction && <MenuItem icon="heart-outline" onClick={handleRemoveReaction}>Remove Reaction</MenuItem>}
|
||||||
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
|
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
|
||||||
{canReschedule && (
|
{canReschedule && (
|
||||||
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
||||||
@ -256,7 +256,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
{(canShowSeenBy || canShowReactionsCount) && (
|
{(canShowSeenBy || canShowReactionsCount) && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="MessageContextMenu--seen-by"
|
className="MessageContextMenu--seen-by"
|
||||||
icon={canShowReactionsCount ? 'reactions' : 'group'}
|
icon={canShowReactionsCount ? 'heart-outline' : 'group'}
|
||||||
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
||||||
disabled={!canShowReactionsCount && !message.seenByUserIds?.length}
|
disabled={!canShowReactionsCount && !message.seenByUserIds?.length}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -44,6 +44,8 @@
|
|||||||
// Fix for weird positioning in Chrome
|
// Fix for weird positioning in Chrome
|
||||||
canvas {
|
canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
.ReactionAnimatedEmoji, .icon-reaction-filled {
|
.ReactionAnimatedEmoji, .icon-heart {
|
||||||
width: 1.125rem;
|
width: 1.125rem;
|
||||||
height: 1.125rem;
|
height: 1.125rem;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
|
|||||||
@ -261,7 +261,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="reactions"
|
icon="heart-outline"
|
||||||
multiline
|
multiline
|
||||||
onClick={handleClickReactions}
|
onClick={handleClickReactions}
|
||||||
disabled={!canChangeInfo}
|
disabled={!canChangeInfo}
|
||||||
|
|||||||
@ -297,7 +297,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="reactions"
|
icon="heart-outline"
|
||||||
multiline
|
multiline
|
||||||
onClick={handleClickReactions}
|
onClick={handleClickReactions}
|
||||||
disabled={!canChangeInfo}
|
disabled={!canChangeInfo}
|
||||||
|
|||||||
@ -66,6 +66,8 @@ export const PROFILE_SENSITIVE_AREA = 500;
|
|||||||
export const COMMON_CHATS_LIMIT = 100;
|
export const COMMON_CHATS_LIMIT = 100;
|
||||||
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
||||||
export const REACTION_LIST_LIMIT = 100;
|
export const REACTION_LIST_LIMIT = 100;
|
||||||
|
export const REACTION_UNREAD_SLICE = 100;
|
||||||
|
export const MENTION_UNREAD_SLICE = 100;
|
||||||
|
|
||||||
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
||||||
|
|
||||||
|
|||||||
@ -1082,6 +1082,79 @@ addActionHandler('viewSponsoredMessage', (global, actions, payload) => {
|
|||||||
void callApi('viewSponsoredMessage', { chat, random: message.randomId });
|
void callApi('viewSponsoredMessage', { chat, random: message.randomId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('fetchUnreadMentions', async (global, actions, payload) => {
|
||||||
|
const { chatId, offsetId } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
const result = await callApi('fetchUnreadMentions', { chat, offsetId });
|
||||||
|
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const { messages, chats, users } = result;
|
||||||
|
|
||||||
|
const byId = buildCollectionByKey(messages, 'id');
|
||||||
|
const ids = Object.keys(byId).map(Number);
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = addChatMessagesById(global, chat.id, byId);
|
||||||
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||||
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
||||||
|
global = updateChat(global, chatId, {
|
||||||
|
unreadMentions: [...(chat.unreadMentions || []), ...ids],
|
||||||
|
});
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('markMentionsRead', (global, actions, payload) => {
|
||||||
|
const { messageIds } = payload;
|
||||||
|
|
||||||
|
const chat = selectCurrentChat(global);
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
if (!chat.unreadMentionsCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadMentionsCount = chat.unreadMentionsCount - messageIds.length;
|
||||||
|
const unreadMentions = (chat.unreadMentions || []).filter((id) => !messageIds.includes(id));
|
||||||
|
global = updateChat(global, chat.id, {
|
||||||
|
unreadMentions,
|
||||||
|
});
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (!unreadMentions.length && unreadMentionsCount) {
|
||||||
|
actions.fetchUnreadMentions({
|
||||||
|
chatId: chat.id,
|
||||||
|
offsetId: Math.max(...messageIds),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.markMessagesRead({ messageIds });
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('focusNextMention', (global, actions) => {
|
||||||
|
const chat = selectCurrentChat(global);
|
||||||
|
|
||||||
|
if (!chat?.unreadMentions) return;
|
||||||
|
|
||||||
|
actions.focusMessage({ chatId: chat.id, messageId: chat.unreadMentions[0] });
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('readAllMentions', (global) => {
|
||||||
|
const chat = selectCurrentChat(global);
|
||||||
|
if (!chat) return undefined;
|
||||||
|
|
||||||
|
callApi('readAllMentions', { chat });
|
||||||
|
|
||||||
|
return updateChat(global, chat.id, {
|
||||||
|
unreadMentionsCount: undefined,
|
||||||
|
unreadMentions: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function countSortedIds(ids: number[], from: number, to: number) {
|
function countSortedIds(ids: number[], from: number, to: number) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,15 @@ import * as mediaLoader from '../../../util/mediaLoader';
|
|||||||
import { ApiAppConfig, ApiMediaFormat } from '../../../api/types';
|
import { ApiAppConfig, ApiMediaFormat } from '../../../api/types';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
selectChatMessage,
|
selectChatMessage, selectCurrentChat,
|
||||||
selectDefaultReaction,
|
selectDefaultReaction,
|
||||||
selectLocalAnimatedEmojiEffectByName,
|
selectLocalAnimatedEmojiEffectByName,
|
||||||
selectMessageIdsByGroupId,
|
selectMessageIdsByGroupId,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { addMessageReaction, subtractXForEmojiInteraction } from '../../reducers/reactions';
|
import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions';
|
||||||
import { addUsers, updateChatMessage } from '../../reducers';
|
import {
|
||||||
|
addChatMessagesById, addChats, addUsers, updateChatMessage,
|
||||||
|
} from '../../reducers';
|
||||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||||
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
||||||
import { isMessageLocal } from '../../helpers';
|
import { isMessageLocal } from '../../helpers';
|
||||||
@ -156,30 +158,6 @@ addActionHandler('openChat', (global) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('startActiveReaction', (global, actions, payload) => {
|
|
||||||
const { messageId, reaction } = payload;
|
|
||||||
const { animationLevel } = global.settings.byKey;
|
|
||||||
|
|
||||||
if (animationLevel !== ANIMATION_LEVEL_MAX) return global;
|
|
||||||
|
|
||||||
if (global.activeReactions[messageId]?.reaction === reaction) {
|
|
||||||
return global;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...global,
|
|
||||||
activeReactions: {
|
|
||||||
...(reaction ? global.activeReactions : omit(global.activeReactions, [messageId])),
|
|
||||||
...(reaction && {
|
|
||||||
[messageId]: {
|
|
||||||
reaction,
|
|
||||||
messageId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('stopActiveReaction', (global, actions, payload) => {
|
addActionHandler('stopActiveReaction', (global, actions, payload) => {
|
||||||
const { messageId, reaction } = payload;
|
const { messageId, reaction } = payload;
|
||||||
|
|
||||||
@ -300,3 +278,110 @@ addActionHandler('sendWatchingEmojiInteraction', (global, actions, payload) => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('fetchUnreadReactions', async (global, actions, payload) => {
|
||||||
|
const { chatId, offsetId } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
const result = await callApi('fetchUnreadReactions', { chat, offsetId, addOffset: offsetId ? -1 : undefined });
|
||||||
|
|
||||||
|
// Server side bug, when server returns unread reactions count > 0 for deleted messages
|
||||||
|
if (!result || !result.messages.length) {
|
||||||
|
global = getGlobal();
|
||||||
|
global = updateUnreadReactions(global, chatId, {
|
||||||
|
unreadReactionsCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messages, chats, users } = result;
|
||||||
|
|
||||||
|
const byId = buildCollectionByKey(messages, 'id');
|
||||||
|
const ids = Object.keys(byId).map(Number);
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = addChatMessagesById(global, chat.id, byId);
|
||||||
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||||
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
||||||
|
global = updateUnreadReactions(global, chatId, {
|
||||||
|
unreadReactions: [...(chat.unreadReactions || []), ...ids],
|
||||||
|
});
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('animateUnreadReaction', (global, actions, payload) => {
|
||||||
|
const { messageIds } = payload;
|
||||||
|
|
||||||
|
const { animationLevel } = global.settings.byKey;
|
||||||
|
|
||||||
|
const chat = selectCurrentChat(global);
|
||||||
|
if (!chat) return undefined;
|
||||||
|
|
||||||
|
if (chat.unreadReactionsCount) {
|
||||||
|
const unreadReactionsCount = chat.unreadReactionsCount - messageIds.length;
|
||||||
|
const unreadReactions = (chat.unreadReactions || []).filter((id) => !messageIds.includes(id));
|
||||||
|
|
||||||
|
global = updateUnreadReactions(global, chat.id, {
|
||||||
|
unreadReactions,
|
||||||
|
});
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (!unreadReactions.length && unreadReactionsCount) {
|
||||||
|
actions.fetchUnreadReactions({ chatId: chat.id, offsetId: Math.min(...messageIds) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.markMessagesRead({ messageIds });
|
||||||
|
|
||||||
|
if (animationLevel !== ANIMATION_LEVEL_MAX) return undefined;
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
activeReactions: {
|
||||||
|
...global.activeReactions,
|
||||||
|
...Object.fromEntries(messageIds.map((messageId) => {
|
||||||
|
const message = selectChatMessage(global, chat.id, messageId);
|
||||||
|
|
||||||
|
if (!message) return undefined;
|
||||||
|
|
||||||
|
const unread = message.reactions?.recentReactions?.find((l) => l.isUnread);
|
||||||
|
|
||||||
|
if (!unread) return undefined;
|
||||||
|
|
||||||
|
const reaction = unread?.reaction;
|
||||||
|
|
||||||
|
return [messageId, {
|
||||||
|
messageId,
|
||||||
|
reaction,
|
||||||
|
}];
|
||||||
|
}).filter(Boolean)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('focusNextReaction', (global, actions) => {
|
||||||
|
const chat = selectCurrentChat(global);
|
||||||
|
|
||||||
|
if (!chat?.unreadReactions) return;
|
||||||
|
|
||||||
|
actions.focusMessage({ chatId: chat.id, messageId: chat.unreadReactions[0] });
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('readAllReactions', (global) => {
|
||||||
|
const chat = selectCurrentChat(global);
|
||||||
|
if (!chat) return undefined;
|
||||||
|
|
||||||
|
callApi('readAllReactions', { chat });
|
||||||
|
|
||||||
|
return updateUnreadReactions(global, chat.id, {
|
||||||
|
unreadReactionsCount: undefined,
|
||||||
|
unreadReactions: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
|||||||
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
|
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
|
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
|
||||||
import { getMessageRecentReaction } from '../../helpers';
|
|
||||||
import {
|
import {
|
||||||
updateChat,
|
updateChat,
|
||||||
updateChatListIds,
|
updateChatListIds,
|
||||||
@ -20,6 +19,7 @@ import {
|
|||||||
selectChatListType,
|
selectChatListType,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
|
import { updateUnreadReactions } from '../../reducers/reactions';
|
||||||
|
|
||||||
const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds
|
const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds
|
||||||
// Enough to animate and mark as read in Message List
|
// Enough to animate and mark as read in Message List
|
||||||
@ -109,15 +109,16 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
actions.requestChatUpdate({ chatId: update.chatId });
|
actions.requestChatUpdate({ chatId: update.chatId });
|
||||||
}, CURRENT_CHAT_UNREAD_DELAY);
|
}, CURRENT_CHAT_UNREAD_DELAY);
|
||||||
} else {
|
|
||||||
setGlobal(updateChat(global, update.chatId, {
|
|
||||||
unreadCount: chat.unreadCount ? chat.unreadCount + 1 : 1,
|
|
||||||
...(update.message.hasUnreadMention && {
|
|
||||||
unreadMentionsCount: chat.unreadMentionsCount ? chat.unreadMentionsCount + 1 : 1,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGlobal(updateChat(global, update.chatId, {
|
||||||
|
unreadCount: chat.unreadCount ? chat.unreadCount + 1 : 1,
|
||||||
|
...(update.message.id && update.message.hasUnreadMention && {
|
||||||
|
unreadMentionsCount: (chat.unreadMentionsCount || 0) + 1,
|
||||||
|
unreadMentions: [...(chat.unreadMentions || []), update.message.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
notifyAboutMessage({
|
notifyAboutMessage({
|
||||||
chat,
|
chat,
|
||||||
message,
|
message,
|
||||||
@ -126,24 +127,6 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'updateMessage': {
|
|
||||||
const { message } = update;
|
|
||||||
const chat = selectChat(global, update.chatId);
|
|
||||||
if (!chat) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getMessageRecentReaction(message)) {
|
|
||||||
notifyAboutMessage({
|
|
||||||
chat,
|
|
||||||
message,
|
|
||||||
isReaction: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'updateCommonBoxMessages':
|
case 'updateCommonBoxMessages':
|
||||||
case 'updateChannelMessages': {
|
case 'updateChannelMessages': {
|
||||||
const { ids, messageUpdate } = update;
|
const { ids, messageUpdate } = update;
|
||||||
@ -154,9 +137,18 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!;
|
const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!;
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
if (chat?.unreadReactionsCount) {
|
||||||
|
global = updateUnreadReactions(global, chatId, {
|
||||||
|
unreadReactionsCount: (chat.unreadReactionsCount - 1) || undefined,
|
||||||
|
unreadReactions: chat.unreadReactions?.filter((i) => i !== id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (chat?.unreadMentionsCount) {
|
if (chat?.unreadMentionsCount) {
|
||||||
global = updateChat(global, chatId, {
|
global = updateChat(global, chatId, {
|
||||||
unreadMentionsCount: chat.unreadMentionsCount - 1,
|
unreadMentionsCount: (chat.unreadMentionsCount - 1) || undefined,
|
||||||
|
unreadMentions: chat.unreadMentions?.filter((i) => i !== id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiMessage, ApiPollResult, ApiThreadInfo, MAIN_THREAD_ID,
|
ApiChat,
|
||||||
|
ApiMessage, ApiPollResult, ApiReactions, ApiThreadInfo, MAIN_THREAD_ID,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
|
|
||||||
import { unique } from '../../../util/iteratees';
|
import { unique } from '../../../util/iteratees';
|
||||||
@ -45,8 +46,10 @@ import {
|
|||||||
selectLocalAnimatedEmoji,
|
selectLocalAnimatedEmoji,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import {
|
import {
|
||||||
getMessageContent, isUserId, isMessageLocal, getMessageText, checkIfReactionAdded,
|
getMessageContent, isUserId, isMessageLocal, getMessageText, checkIfHasUnreadReactions,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
|
import { onTickEnd } from '../../../util/schedulers';
|
||||||
|
import { updateUnreadReactions } from '../../reducers/reactions';
|
||||||
|
|
||||||
const ANIMATION_DELAY = 350;
|
const ANIMATION_DELAY = 350;
|
||||||
|
|
||||||
@ -158,9 +161,8 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
const { chatId, id, message } = update;
|
const { chatId, id, message } = update;
|
||||||
|
|
||||||
const currentMessage = selectChatMessage(global, chatId, id);
|
const currentMessage = selectChatMessage(global, chatId, id);
|
||||||
if (!currentMessage) {
|
|
||||||
return;
|
const chat = selectChat(global, chatId);
|
||||||
}
|
|
||||||
|
|
||||||
global = updateWithLocalMedia(global, chatId, id, message);
|
global = updateWithLocalMedia(global, chatId, id, message);
|
||||||
|
|
||||||
@ -173,15 +175,21 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
message.threadInfo,
|
message.threadInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
global = updateChatLastMessage(global, chatId, newMessage);
|
if (currentMessage) {
|
||||||
|
global = updateChatLastMessage(global, chatId, newMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.reactions && chat) {
|
||||||
|
global = updateReactions(global, chatId, id, message.reactions, chat, message.isOutgoing, currentMessage);
|
||||||
|
}
|
||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
// Scroll down if bot message height is changed with an updated inline keyboard.
|
// Scroll down if bot message height is changed with an updated inline keyboard.
|
||||||
// A drawback: this will scroll down even if the previous scroll was not at bottom.
|
// A drawback: this will scroll down even if the previous scroll was not at bottom.
|
||||||
const chat = selectChat(global, chatId);
|
|
||||||
if (
|
if (
|
||||||
chat
|
currentMessage
|
||||||
|
&& chat
|
||||||
&& !message.isOutgoing
|
&& !message.isOutgoing
|
||||||
&& chat.lastMessage?.id === message.id
|
&& chat.lastMessage?.id === message.id
|
||||||
&& selectIsChatWithBot(global, chat)
|
&& selectIsChatWithBot(global, chat)
|
||||||
@ -482,36 +490,67 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
const { chatId, id, reactions } = update;
|
const { chatId, id, reactions } = update;
|
||||||
const message = selectChatMessage(global, chatId, id);
|
const message = selectChatMessage(global, chatId, id);
|
||||||
const chat = selectChat(global, update.chatId);
|
const chat = selectChat(global, update.chatId);
|
||||||
const currentReactions = message?.reactions;
|
|
||||||
|
|
||||||
// `updateMessageReactions` happens with an interval, so we try to avoid redundant global state updates
|
if (!chat || !message) return;
|
||||||
if (currentReactions && areDeepEqual(reactions, currentReactions)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only notify about added reactions, not removed ones
|
|
||||||
const shouldNotify = checkIfReactionAdded(currentReactions, reactions, global.currentUserId);
|
|
||||||
|
|
||||||
global = updateChatMessage(global, chatId, id, { reactions: update.reactions });
|
|
||||||
|
|
||||||
setGlobal(global);
|
|
||||||
|
|
||||||
if (shouldNotify) {
|
|
||||||
const newMessage = selectChatMessage(global, chatId, id);
|
|
||||||
if (!chat || !newMessage) return;
|
|
||||||
|
|
||||||
void notifyAboutMessage({
|
|
||||||
chat,
|
|
||||||
message: newMessage,
|
|
||||||
isReaction: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
setGlobal(updateReactions(global, chatId, id, reactions, chat, message.isOutgoing, message));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateReactions(
|
||||||
|
global: GlobalState,
|
||||||
|
chatId: string,
|
||||||
|
id: number,
|
||||||
|
reactions: ApiReactions,
|
||||||
|
chat: ApiChat,
|
||||||
|
isOutgoing?: boolean,
|
||||||
|
message?: ApiMessage,
|
||||||
|
) {
|
||||||
|
const currentReactions = message?.reactions;
|
||||||
|
|
||||||
|
// `updateMessageReactions` happens with an interval, so we try to avoid redundant global state updates
|
||||||
|
if (currentReactions && areDeepEqual(reactions, currentReactions)) {
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = updateChatMessage(global, chatId, id, { reactions });
|
||||||
|
|
||||||
|
if (!isOutgoing) {
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyHasUnreadReaction = chat.unreadReactions?.includes(id);
|
||||||
|
|
||||||
|
// Only notify about added reactions, not removed ones
|
||||||
|
if (checkIfHasUnreadReactions(global, reactions) && !alreadyHasUnreadReaction) {
|
||||||
|
global = updateUnreadReactions(global, chatId, {
|
||||||
|
unreadReactionsCount: (chat?.unreadReactionsCount || 0) + 1,
|
||||||
|
unreadReactions: [...(chat?.unreadReactions || []), id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newMessage = selectChatMessage(global, chatId, id);
|
||||||
|
|
||||||
|
if (!chat || !newMessage) return global;
|
||||||
|
|
||||||
|
onTickEnd(() => {
|
||||||
|
notifyAboutMessage({
|
||||||
|
chat,
|
||||||
|
message: newMessage,
|
||||||
|
isReaction: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (alreadyHasUnreadReaction) {
|
||||||
|
global = updateUnreadReactions(global, chatId, {
|
||||||
|
unreadReactionsCount: (chat?.unreadReactionsCount || 1) - 1,
|
||||||
|
unreadReactions: chat?.unreadReactions?.filter((i) => i !== id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
function updateWithLocalMedia(
|
function updateWithLocalMedia(
|
||||||
global: GlobalState, chatId: string, id: number, message: Partial<ApiMessage>, isScheduled = false,
|
global: GlobalState, chatId: string, id: number, message: Partial<ApiMessage>, isScheduled = false,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -342,7 +342,7 @@ addActionHandler('focusNextReply', (global, actions) => {
|
|||||||
addActionHandler('focusMessage', (global, actions, payload) => {
|
addActionHandler('focusMessage', (global, actions, payload) => {
|
||||||
const {
|
const {
|
||||||
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
||||||
replyMessageId, isResizingContainer,
|
replyMessageId, isResizingContainer, shouldReplaceHistory,
|
||||||
} = payload!;
|
} = payload!;
|
||||||
|
|
||||||
let { messageId } = payload!;
|
let { messageId } = payload!;
|
||||||
@ -387,7 +387,7 @@ addActionHandler('focusMessage', (global, actions, payload) => {
|
|||||||
const viewportIds = selectViewportIds(global, chatId, threadId);
|
const viewportIds = selectViewportIds(global, chatId, threadId);
|
||||||
if (viewportIds && viewportIds.includes(messageId)) {
|
if (viewportIds && viewportIds.includes(messageId)) {
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
actions.openChat({ id: chatId, threadId });
|
actions.openChat({ id: chatId, threadId, shouldReplaceHistory });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,7 +404,7 @@ addActionHandler('focusMessage', (global, actions, payload) => {
|
|||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
actions.openChat({ id: chatId, threadId });
|
actions.openChat({ id: chatId, threadId, shouldReplaceHistory });
|
||||||
actions.loadViewportMessages();
|
actions.loadViewportMessages();
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
import { ApiMessage, ApiReactions } from '../../api/types';
|
import { ApiMessage, ApiReactions } from '../../api/types';
|
||||||
|
import { GlobalState } from '../types';
|
||||||
|
|
||||||
export function getMessageRecentReaction(message: Partial<ApiMessage>) {
|
export function getMessageRecentReaction(message: Partial<ApiMessage>) {
|
||||||
return message.isOutgoing ? message.reactions?.recentReactions?.[0] : undefined;
|
return message.isOutgoing ? message.reactions?.recentReactions?.[0] : undefined;
|
||||||
}
|
}
|
||||||
|
export function checkIfHasUnreadReactions(global: GlobalState, reactions: ApiReactions) {
|
||||||
export function checkIfReactionAdded(oldReactions?: ApiReactions, newReactions?: ApiReactions, currentUserId?: string) {
|
const { currentUserId } = global;
|
||||||
if (!oldReactions || !oldReactions.recentReactions) return true;
|
return reactions?.recentReactions?.some(
|
||||||
if (!newReactions || !newReactions.recentReactions) return false;
|
({ isUnread, userId }) => isUnread && userId !== currentUserId,
|
||||||
// Skip reactions from yourself
|
);
|
||||||
if (newReactions.recentReactions.every((reaction) => reaction.userId === currentUserId)) return false;
|
|
||||||
const oldReactionsMap = oldReactions.results.reduce<Record<string, number>>((acc, reaction) => {
|
|
||||||
acc[reaction.reaction] = reaction.count;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
return newReactions.results.some((r) => !oldReactionsMap[r.reaction] || oldReactionsMap[r.reaction] < r.count);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,10 +50,11 @@ export function replaceChats(global: GlobalState, newById: Record<string, ApiCha
|
|||||||
|
|
||||||
export function updateChat(
|
export function updateChat(
|
||||||
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||||
|
noOmitUnreadReactionCount = false,
|
||||||
): GlobalState {
|
): GlobalState {
|
||||||
const { byId } = global.chats;
|
const { byId } = global.chats;
|
||||||
|
|
||||||
const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo);
|
const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo, noOmitUnreadReactionCount);
|
||||||
if (!updatedChat) {
|
if (!updatedChat) {
|
||||||
return global;
|
return global;
|
||||||
}
|
}
|
||||||
@ -115,13 +116,19 @@ export function addChats(global: GlobalState, newById: Record<string, ApiChat>):
|
|||||||
// @optimization Don't spread/unspread global for each element, do it in a batch
|
// @optimization Don't spread/unspread global for each element, do it in a batch
|
||||||
function getUpdatedChat(
|
function getUpdatedChat(
|
||||||
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||||
|
noOmitUnreadReactionCount = false,
|
||||||
) {
|
) {
|
||||||
const { byId } = global.chats;
|
const { byId } = global.chats;
|
||||||
const chat = byId[chatId];
|
const chat = byId[chatId];
|
||||||
const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin;
|
const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin;
|
||||||
|
|
||||||
|
chatUpdate = noOmitUnreadReactionCount
|
||||||
|
? chatUpdate : omit(chatUpdate, ['unreadReactionsCount']);
|
||||||
const updatedChat: ApiChat = {
|
const updatedChat: ApiChat = {
|
||||||
...chat,
|
...chat,
|
||||||
...(shouldOmitMinInfo ? omit(chatUpdate, ['isMin', 'accessHash']) : chatUpdate),
|
...(shouldOmitMinInfo
|
||||||
|
? omit(chatUpdate, ['isMin', 'accessHash'])
|
||||||
|
: chatUpdate),
|
||||||
...(photo && { photos: [photo, ...(chat.photos || [])] }),
|
...(photo && { photos: [photo, ...(chat.photos || [])] }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
} from '../../components/middle/helpers/calculateMiddleFooterTransforms';
|
} from '../../components/middle/helpers/calculateMiddleFooterTransforms';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
import windowSize from '../../util/windowSize';
|
import windowSize from '../../util/windowSize';
|
||||||
|
import { updateChat } from './chats';
|
||||||
|
import { ApiChat } from '../../api/types';
|
||||||
|
|
||||||
function getLeftColumnWidth(windowWidth: number) {
|
function getLeftColumnWidth(windowWidth: number) {
|
||||||
if (windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN) {
|
if (windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN) {
|
||||||
@ -80,3 +82,9 @@ export function addMessageReaction(global: GlobalState, chatId: string, messageI
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateUnreadReactions(
|
||||||
|
global: GlobalState, chatId: string, update: Pick<ApiChat, 'unreadReactionsCount' | 'unreadReactions'>,
|
||||||
|
) {
|
||||||
|
return updateChat(global, chatId, update, undefined, true);
|
||||||
|
}
|
||||||
|
|||||||
@ -632,6 +632,24 @@ export interface ActionPayloads {
|
|||||||
threadId: number;
|
threadId: number;
|
||||||
type: MessageListType;
|
type: MessageListType;
|
||||||
};
|
};
|
||||||
|
fetchUnreadMentions: {
|
||||||
|
chatId: string;
|
||||||
|
offsetId?: number;
|
||||||
|
};
|
||||||
|
fetchUnreadReactions: {
|
||||||
|
chatId: string;
|
||||||
|
offsetId?: number;
|
||||||
|
};
|
||||||
|
animateUnreadReaction: {
|
||||||
|
messageIds: number[];
|
||||||
|
};
|
||||||
|
focusNextReaction: {};
|
||||||
|
focusNextMention: {};
|
||||||
|
readAllReactions: {};
|
||||||
|
readAllMentions: {};
|
||||||
|
markMentionsRead: {
|
||||||
|
messageIds: number[];
|
||||||
|
};
|
||||||
|
|
||||||
// Media Viewer & Audio Player
|
// Media Viewer & Audio Player
|
||||||
openMediaViewer: {
|
openMediaViewer: {
|
||||||
@ -845,7 +863,7 @@ export type NonTypedActionNames = (
|
|||||||
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' | 'loadAvailableReactions' |
|
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' | 'loadAvailableReactions' |
|
||||||
'stopActiveEmojiInteraction' | 'interactWithAnimatedEmoji' | 'loadReactors' | 'setDefaultReaction' |
|
'stopActiveEmojiInteraction' | 'interactWithAnimatedEmoji' | 'loadReactors' | 'setDefaultReaction' |
|
||||||
'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' |
|
'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' |
|
||||||
'stopActiveReaction' | 'startActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' |
|
'stopActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' |
|
||||||
'setEditingId' |
|
'setEditingId' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
||||||
|
|||||||
@ -1102,6 +1102,8 @@ messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs;
|
|||||||
messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia;
|
messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia;
|
||||||
messages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers;
|
messages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers;
|
||||||
messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;
|
messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;
|
||||||
|
messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||||
|
messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory;
|
||||||
messages.sendMultiMedia#f803138f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
|
messages.sendMultiMedia#f803138f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
|
||||||
messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets;
|
messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets;
|
||||||
messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool;
|
messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool;
|
||||||
@ -1140,6 +1142,8 @@ messages.getMessageReactionsList#e0ee6b77 flags:# peer:InputPeer id:int reaction
|
|||||||
messages.setChatAvailableReactions#14050ea6 peer:InputPeer available_reactions:Vector<string> = Updates;
|
messages.setChatAvailableReactions#14050ea6 peer:InputPeer available_reactions:Vector<string> = Updates;
|
||||||
messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions;
|
messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions;
|
||||||
messages.setDefaultReaction#d960c4d4 reaction:string = Bool;
|
messages.setDefaultReaction#d960c4d4 reaction:string = Bool;
|
||||||
|
messages.getUnreadReactions#e85bae1a peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||||
|
messages.readReactions#82e251d7 peer:InputPeer = messages.AffectedHistory;
|
||||||
messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots;
|
messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots;
|
||||||
messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot;
|
messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot;
|
||||||
messages.toggleBotInAttachMenu#1aee33af bot:InputUser enabled:Bool = Bool;
|
messages.toggleBotInAttachMenu#1aee33af bot:InputUser enabled:Bool = Bool;
|
||||||
|
|||||||
@ -233,5 +233,9 @@
|
|||||||
"messages.prolongWebView",
|
"messages.prolongWebView",
|
||||||
"messages.requestSimpleWebView",
|
"messages.requestSimpleWebView",
|
||||||
"messages.sendWebViewResultMessage",
|
"messages.sendWebViewResultMessage",
|
||||||
"messages.sendWebViewData"
|
"messages.sendWebViewData",
|
||||||
|
"messages.readReactions",
|
||||||
|
"messages.getUnreadReactions",
|
||||||
|
"messages.readMentions",
|
||||||
|
"messages.getUnreadMentions"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -30,12 +30,14 @@ type NotificationData = {
|
|||||||
body: string;
|
body: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
reaction?: string;
|
reaction?: string;
|
||||||
|
shouldReplaceHistory?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FocusMessageData = {
|
type FocusMessageData = {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
reaction?: string;
|
reaction?: string;
|
||||||
|
shouldReplaceHistory?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CloseNotificationData = {
|
type CloseNotificationData = {
|
||||||
@ -111,6 +113,7 @@ function showNotification({
|
|||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
reaction,
|
reaction,
|
||||||
|
shouldReplaceHistory,
|
||||||
}: NotificationData) {
|
}: NotificationData) {
|
||||||
const isFirstBatch = new Date().valueOf() - lastSyncAt < 1000;
|
const isFirstBatch = new Date().valueOf() - lastSyncAt < 1000;
|
||||||
const tag = String(isFirstBatch ? 0 : chatId || 0);
|
const tag = String(isFirstBatch ? 0 : chatId || 0);
|
||||||
@ -121,6 +124,7 @@ function showNotification({
|
|||||||
messageId,
|
messageId,
|
||||||
reaction,
|
reaction,
|
||||||
count: 1,
|
count: 1,
|
||||||
|
shouldReplaceHistory,
|
||||||
},
|
},
|
||||||
icon: icon || 'icon-192x192.png',
|
icon: icon || 'icon-192x192.png',
|
||||||
badge: 'icon-192x192.png',
|
badge: 'icon-192x192.png',
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "Telegram T",
|
"name": "Telegram T",
|
||||||
"lastOpened": 0,
|
"lastOpened": 0,
|
||||||
"created": 1650288938325
|
"created": 1650375482158
|
||||||
},
|
},
|
||||||
"iconSets": [
|
"iconSets": [
|
||||||
{
|
{
|
||||||
@ -158,7 +158,23 @@
|
|||||||
{
|
{
|
||||||
"selection": [
|
"selection": [
|
||||||
{
|
{
|
||||||
"order": 710,
|
"order": 711,
|
||||||
|
"id": 60,
|
||||||
|
"name": "heart",
|
||||||
|
"prevSize": 32,
|
||||||
|
"code": 59802,
|
||||||
|
"tempChar": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 712,
|
||||||
|
"id": 59,
|
||||||
|
"name": "heart-outline",
|
||||||
|
"prevSize": 32,
|
||||||
|
"code": 59803,
|
||||||
|
"tempChar": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 0,
|
||||||
"id": 58,
|
"id": 58,
|
||||||
"name": "reactions",
|
"name": "reactions",
|
||||||
"prevSize": 32,
|
"prevSize": 32,
|
||||||
@ -166,7 +182,7 @@
|
|||||||
"tempChar": ""
|
"tempChar": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"order": 709,
|
"order": 0,
|
||||||
"id": 57,
|
"id": 57,
|
||||||
"name": "reaction-filled",
|
"name": "reaction-filled",
|
||||||
"prevSize": 32,
|
"prevSize": 32,
|
||||||
@ -609,6 +625,36 @@
|
|||||||
"height": 1024,
|
"height": 1024,
|
||||||
"prevSize": 32,
|
"prevSize": 32,
|
||||||
"icons": [
|
"icons": [
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"paths": [
|
||||||
|
"M838.667 599.067c-66 93.333-168.267 183.2-304 267.2-6.933 4.267-14.667 6.4-22.4 6.4-10 0-19.733-3.467-27.6-10-133.467-83.2-234.133-172-299.333-264.267-58.933-83.467-86.933-168-81.067-244.667 6.533-84.133 54.8-153.067 129.2-184.4 44.667-18.8 95.733-22.933 148-11.867 45.067 9.467 88.933 29.467 130.933 59.467 41.733-29.6 85.333-49.333 130-58.8 52.133-10.933 103.333-6.8 148 12 74.4 31.333 122.667 100.267 129.2 184.4 6 76.533-22 161.2-80.933 244.533z"
|
||||||
|
],
|
||||||
|
"attrs": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"isMulticolor": false,
|
||||||
|
"isMulticolor2": false,
|
||||||
|
"grid": 24,
|
||||||
|
"tags": [
|
||||||
|
"heart"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"paths": [
|
||||||
|
"M919.6 354.4c-6.533-84.133-54.8-153.067-129.2-184.4-44.667-18.8-95.733-22.933-148-12-44.667 9.467-88.267 29.2-130 58.8-42-30-85.867-50-130.933-59.467-52.133-10.933-103.333-6.8-148 11.867-74.4 31.333-122.667 100.267-129.2 184.4-6 76.667 22 161.2 81.067 244.667 65.2 92.4 165.867 181.2 299.333 264.4 7.733 6.533 17.6 10 27.6 10 7.6 0 15.333-2 22.4-6.4 135.867-83.867 238.133-173.867 304-267.2 58.933-83.333 86.933-168 80.933-244.667zM768.933 549.867c-55.6 78.8-141.867 155.867-256.4 229.333-115.067-73.733-201.6-151.067-257.467-230.133-59.733-84.667-68.667-149.467-65.6-188.933 4.133-52.533 32.267-93.467 77.2-112.4 28.533-12 62.133-14.4 97.333-7.067 39.067 8.267 79.467 28.667 116.933 59.333 15.333 16.4 41.067 18.267 58.533 3.6 38.533-32.267 80.133-53.733 120.533-62.133 35.067-7.333 68.8-4.933 97.333 7.067 44.933 18.933 73.2 59.867 77.2 112.4 3.067 39.467-5.867 104.267-65.6 188.933z"
|
||||||
|
],
|
||||||
|
"attrs": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"isMulticolor": false,
|
||||||
|
"isMulticolor2": false,
|
||||||
|
"grid": 24,
|
||||||
|
"tags": [
|
||||||
|
"heart-outline"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": 58,
|
"id": 58,
|
||||||
"paths": [
|
"paths": [
|
||||||
|
|||||||
@ -51,10 +51,10 @@
|
|||||||
.icon-volume-3:before {
|
.icon-volume-3:before {
|
||||||
content: "\e991";
|
content: "\e991";
|
||||||
}
|
}
|
||||||
.icon-reactions:before {
|
.icon-heart:before {
|
||||||
content: "\e99a";
|
content: "\e99a";
|
||||||
}
|
}
|
||||||
.icon-reaction-filled:before {
|
.icon-heart-outline:before {
|
||||||
content: "\e99b";
|
content: "\e99b";
|
||||||
}
|
}
|
||||||
.icon-webapp:before {
|
.icon-webapp:before {
|
||||||
|
|||||||
12
src/util/isElementInViewport.ts
Normal file
12
src/util/isElementInViewport.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import windowSize from './windowSize';
|
||||||
|
|
||||||
|
export function isElementInViewport(el: HTMLElement) {
|
||||||
|
if (el.style.display === 'none') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const { height: windowHeight } = windowSize.get();
|
||||||
|
|
||||||
|
return (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
|
||||||
|
}
|
||||||
@ -407,6 +407,7 @@ export async function notifyAboutMessage({
|
|||||||
icon,
|
icon,
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
|
shouldReplaceHistory: true,
|
||||||
reaction: activeReaction?.reaction,
|
reaction: activeReaction?.reaction,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -431,13 +432,8 @@ export async function notifyAboutMessage({
|
|||||||
dispatch.focusMessage({
|
dispatch.focusMessage({
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
|
shouldReplaceHistory: true,
|
||||||
});
|
});
|
||||||
if (activeReaction) {
|
|
||||||
dispatch.startActiveReaction({
|
|
||||||
messageId: message.id,
|
|
||||||
reaction: activeReaction.reaction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (window.focus) {
|
if (window.focus) {
|
||||||
window.focus();
|
window.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,12 +22,6 @@ function handleWorkerMessage(e: MessageEvent) {
|
|||||||
if (dispatch.focusMessage) {
|
if (dispatch.focusMessage) {
|
||||||
dispatch.focusMessage(payload);
|
dispatch.focusMessage(payload);
|
||||||
}
|
}
|
||||||
if (dispatch.startActiveReaction && payload.reaction) {
|
|
||||||
dispatch.startActiveReaction({
|
|
||||||
messageId: payload.messageId,
|
|
||||||
reaction: payload.reaction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'playNotificationSound':
|
case 'playNotificationSound':
|
||||||
playNotifySoundDebounced(action.payload.id);
|
playNotifySoundDebounced(action.payload.id);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user