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,
|
||||
): ApiChat {
|
||||
const {
|
||||
peer, folderId, unreadMark, unreadCount, unreadMentionsCount, notifySettings: { silent, muteUntil },
|
||||
peer, folderId, unreadMark, unreadCount, unreadMentionsCount, unreadReactionsCount,
|
||||
notifySettings: { silent, muteUntil },
|
||||
readOutboxMaxId, readInboxMaxId, draft,
|
||||
} = dialog;
|
||||
const isMuted = silent || (typeof muteUntil === 'number' && getServerTime(serverTimeOffset) < muteUntil);
|
||||
@ -86,6 +87,7 @@ export function buildApiChatFromDialog(
|
||||
lastReadInboxMessageId: readInboxMaxId,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
unreadReactionsCount,
|
||||
isMuted,
|
||||
...(unreadMark && { hasUnreadMark: true }),
|
||||
...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }),
|
||||
|
||||
@ -221,11 +221,15 @@ function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCou
|
||||
}
|
||||
|
||||
export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiUserReaction {
|
||||
const { peerId, reaction } = userReaction;
|
||||
const {
|
||||
peerId, reaction, big, unread,
|
||||
} = userReaction;
|
||||
|
||||
return {
|
||||
userId: getApiChatIdFromMtpPeer(peerId),
|
||||
reaction,
|
||||
isUnread: unread,
|
||||
isBig: big,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ export {
|
||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
||||
saveDefaultSendAs,
|
||||
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions,
|
||||
} from './messages';
|
||||
|
||||
export {
|
||||
|
||||
@ -23,8 +23,8 @@ import {
|
||||
|
||||
import {
|
||||
ALL_FOLDER_ID,
|
||||
DEBUG,
|
||||
PINNED_MESSAGES_LIMIT,
|
||||
DEBUG, MENTION_UNREAD_SLICE,
|
||||
PINNED_MESSAGES_LIMIT, REACTION_UNREAD_SLICE,
|
||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../../config';
|
||||
@ -1319,3 +1319,95 @@ export async function viewSponsoredMessage({ chat, random }: { chat: ApiChat; ra
|
||||
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;
|
||||
unreadCount?: number;
|
||||
unreadMentionsCount?: number;
|
||||
unreadReactionsCount?: number;
|
||||
isVerified?: boolean;
|
||||
isMuted?: boolean;
|
||||
isSignaturesShown?: boolean;
|
||||
@ -65,6 +66,9 @@ export interface ApiChat {
|
||||
|
||||
joinRequests?: ApiChatInviteImporter[];
|
||||
sendAsIds?: string[];
|
||||
|
||||
unreadReactions?: number[];
|
||||
unreadMentions?: number[];
|
||||
}
|
||||
|
||||
export interface ApiTypingStatus {
|
||||
|
||||
@ -336,6 +336,8 @@ export interface ApiReactions {
|
||||
export interface ApiUserReaction {
|
||||
userId: string;
|
||||
reaction: string;
|
||||
isBig?: boolean;
|
||||
isUnread?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
padding: 0.25rem;
|
||||
|
||||
|
||||
@ -16,7 +16,9 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
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 className = buildClassName(
|
||||
'Badge',
|
||||
@ -26,38 +28,41 @@ const Badge: FC<OwnProps> = ({ chat, isPinned, isMuted }) => {
|
||||
);
|
||||
|
||||
function renderContent() {
|
||||
if (chat.unreadCount) {
|
||||
if (chat.unreadMentionsCount) {
|
||||
return (
|
||||
<div className="Badge-wrapper">
|
||||
<div className="Badge mention">
|
||||
<i className="icon-mention" />
|
||||
</div>
|
||||
<div className={className}>
|
||||
{formatIntegerCompact(chat.unreadCount)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const unreadReactionsElement = chat.unreadReactionsCount && (
|
||||
<div className={buildClassName('Badge reaction', isMuted && 'muted')}>
|
||||
<i className="icon-heart" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{formatIntegerCompact(chat.unreadCount)}
|
||||
</div>
|
||||
);
|
||||
} else if (chat.hasUnreadMark) {
|
||||
return (
|
||||
<div className={className} />
|
||||
);
|
||||
} else if (isPinned) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<i className="icon-pinned-chat" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const unreadMentionsElement = chat.unreadMentionsCount && (
|
||||
<div className="Badge mention">
|
||||
<i className="icon-mention" />
|
||||
</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 (
|
||||
|
||||
@ -13,6 +13,7 @@ import windowSize from '../../../util/windowSize';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||
import { getMessageHtmlId } from '../../../global/helpers';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
|
||||
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) {
|
||||
const messageListElement = document.querySelector<HTMLDivElement>('.Transition__slide--active > .MessageList')!;
|
||||
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 MiddleHeader from './MiddleHeader';
|
||||
import MessageList from './MessageList';
|
||||
import ScrollDownButton from './ScrollDownButton';
|
||||
import FloatingActionButtons from './FloatingActionButtons';
|
||||
import Composer from './composer/Composer';
|
||||
import Button from '../ui/Button';
|
||||
import MobileSearch from './MobileSearch.async';
|
||||
@ -505,7 +505,7 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<ScrollDownButton
|
||||
<FloatingActionButtons
|
||||
isShown={renderingIsFabShown}
|
||||
canPost={renderingCanPost}
|
||||
withExtraShift={withExtraShift}
|
||||
|
||||
@ -127,7 +127,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setChosenTab(undefined)}
|
||||
>
|
||||
<i className="icon-reaction-filled" />
|
||||
<i className="icon-heart" />
|
||||
{reactors?.count && formatIntegerCompact(reactors.count)}
|
||||
</Button>
|
||||
{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, {
|
||||
FC, useCallback, memo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import React, { FC, memo, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import { MessageListType } from '../../global/types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import { selectChat, selectCurrentMessageList } from '../../global/selectors';
|
||||
import { formatIntegerCompact } from '../../util/textFormat';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
||||
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 MenuItem from '../ui/MenuItem';
|
||||
|
||||
import './ScrollDownButton.scss';
|
||||
import styles from './ScrollDownButton.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isShown: boolean;
|
||||
canPost?: boolean;
|
||||
withExtraShift?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
messageListType?: MessageListType;
|
||||
icon: string;
|
||||
ariaLabelLang: string;
|
||||
unreadCount?: number;
|
||||
onClick: VoidFunction;
|
||||
onReadAll?: VoidFunction;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const FOCUS_MARGIN = 20;
|
||||
|
||||
const ScrollDownButton: FC<OwnProps & StateProps> = ({
|
||||
isShown,
|
||||
canPost,
|
||||
messageListType,
|
||||
const ScrollDownButton: FC<OwnProps> = ({
|
||||
icon,
|
||||
ariaLabelLang,
|
||||
unreadCount,
|
||||
withExtraShift,
|
||||
onClick,
|
||||
onReadAll,
|
||||
className,
|
||||
}) => {
|
||||
const { focusNextReply } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
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(
|
||||
'ScrollDownButton',
|
||||
isShown && 'revealed',
|
||||
!canPost && 'no-composer',
|
||||
withExtraShift && 'with-extra-shift',
|
||||
);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, !onReadAll);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={fabClassName}>
|
||||
<div className="ScrollDownButton-inner">
|
||||
<Button
|
||||
color="secondary"
|
||||
round
|
||||
onClick={handleClick}
|
||||
ariaLabel={lang('AccDescrPageDown')}
|
||||
<div className={buildClassName(styles.root, className)} ref={ref}>
|
||||
<Button
|
||||
color="secondary"
|
||||
round
|
||||
className={styles.button}
|
||||
onClick={onClick}
|
||||
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" />
|
||||
</Button>
|
||||
{Boolean(unreadCount) && (
|
||||
<div className="unread-count">{formatIntegerCompact(unreadCount!)}</div>
|
||||
)}
|
||||
</div>
|
||||
<MenuItem icon="readchats" onClick={onReadAll}>{lang('MarkAllAsRead')}</MenuItem>
|
||||
</Menu>
|
||||
)}
|
||||
</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);
|
||||
|
||||
return {
|
||||
messageListType,
|
||||
unreadCount: chat && threadId === MAIN_THREAD_ID && messageListType === 'thread' ? chat.unreadCount : undefined,
|
||||
};
|
||||
},
|
||||
)(ScrollDownButton));
|
||||
export default memo(ScrollDownButton);
|
||||
|
||||
@ -16,7 +16,7 @@ export default function useMessageObservers(
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
memoFirstUnreadIdRef: { current: number | undefined },
|
||||
) {
|
||||
const { markMessageListRead, markMessagesRead } = getActions();
|
||||
const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions();
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForMedia,
|
||||
@ -38,6 +38,7 @@ export default function useMessageObservers(
|
||||
|
||||
let maxId = 0;
|
||||
const mentionIds: number[] = [];
|
||||
const reactionIds: number[] = [];
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const { isIntersecting, target } = entry;
|
||||
@ -56,6 +57,10 @@ export default function useMessageObservers(
|
||||
if (dataset.hasUnreadMention) {
|
||||
mentionIds.push(messageId);
|
||||
}
|
||||
|
||||
if (dataset.hasUnreadReaction) {
|
||||
reactionIds.push(messageId);
|
||||
}
|
||||
});
|
||||
|
||||
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
|
||||
@ -63,7 +68,11 @@ export default function useMessageObservers(
|
||||
}
|
||||
|
||||
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 useInnerHandlers from './hooks/useInnerHandlers';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -195,6 +196,7 @@ type StateProps = {
|
||||
defaultReaction?: string;
|
||||
activeReaction?: ActiveReaction;
|
||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||
hasUnreadReaction?: boolean;
|
||||
};
|
||||
|
||||
type MetaPosition =
|
||||
@ -281,11 +283,13 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
shouldLoopStickers,
|
||||
autoLoadFileMaxSizeMb,
|
||||
threadInfo,
|
||||
hasUnreadReaction,
|
||||
}) => {
|
||||
const {
|
||||
toggleMessageSelection,
|
||||
clickBotInlineButton,
|
||||
disableContextMenuHint,
|
||||
animateUnreadReaction,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -521,6 +525,13 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
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 calculatedWidth;
|
||||
let noMediaCorners = false;
|
||||
@ -896,7 +907,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
className="bottom-marker"
|
||||
data-message-id={messageId}
|
||||
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 && (
|
||||
<div className="message-select-control">
|
||||
@ -1065,6 +1077,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const localSticker = singleEmoji ? selectLocalAnimatedEmoji(global, singleEmoji) : undefined;
|
||||
|
||||
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
|
||||
|
||||
return {
|
||||
theme: selectTheme(global),
|
||||
chatUsername,
|
||||
@ -1117,6 +1131,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
||||
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
||||
...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }),
|
||||
hasUnreadReaction,
|
||||
};
|
||||
},
|
||||
)(Message));
|
||||
|
||||
@ -226,7 +226,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
style={menuStyle}
|
||||
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>}
|
||||
{canReschedule && (
|
||||
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
||||
@ -256,7 +256,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
{(canShowSeenBy || canShowReactionsCount) && (
|
||||
<MenuItem
|
||||
className="MessageContextMenu--seen-by"
|
||||
icon={canShowReactionsCount ? 'reactions' : 'group'}
|
||||
icon={canShowReactionsCount ? 'heart-outline' : 'group'}
|
||||
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
||||
disabled={!canShowReactionsCount && !message.seenByUserIds?.length}
|
||||
>
|
||||
|
||||
@ -44,6 +44,8 @@
|
||||
// Fix for weird positioning in Chrome
|
||||
canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
color: var(--accent-color);
|
||||
overflow: visible;
|
||||
|
||||
.ReactionAnimatedEmoji, .icon-reaction-filled {
|
||||
.ReactionAnimatedEmoji, .icon-heart {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
@ -261,7 +261,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem
|
||||
icon="reactions"
|
||||
icon="heart-outline"
|
||||
multiline
|
||||
onClick={handleClickReactions}
|
||||
disabled={!canChangeInfo}
|
||||
|
||||
@ -297,7 +297,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
icon="reactions"
|
||||
icon="heart-outline"
|
||||
multiline
|
||||
onClick={handleClickReactions}
|
||||
disabled={!canChangeInfo}
|
||||
|
||||
@ -66,6 +66,8 @@ export const PROFILE_SENSITIVE_AREA = 500;
|
||||
export const COMMON_CHATS_LIMIT = 100;
|
||||
export const GROUP_CALL_PARTICIPANTS_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;
|
||||
|
||||
|
||||
@ -1082,6 +1082,79 @@ addActionHandler('viewSponsoredMessage', (global, actions, payload) => {
|
||||
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) {
|
||||
let count = 0;
|
||||
|
||||
|
||||
@ -4,13 +4,15 @@ import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { ApiAppConfig, ApiMediaFormat } from '../../../api/types';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectChatMessage, selectCurrentChat,
|
||||
selectDefaultReaction,
|
||||
selectLocalAnimatedEmojiEffectByName,
|
||||
selectMessageIdsByGroupId,
|
||||
} from '../../selectors';
|
||||
import { addMessageReaction, subtractXForEmojiInteraction } from '../../reducers/reactions';
|
||||
import { addUsers, updateChatMessage } from '../../reducers';
|
||||
import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions';
|
||||
import {
|
||||
addChatMessagesById, addChats, addUsers, updateChatMessage,
|
||||
} from '../../reducers';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
||||
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) => {
|
||||
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 { pick } from '../../../util/iteratees';
|
||||
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
|
||||
import { getMessageRecentReaction } from '../../helpers';
|
||||
import {
|
||||
updateChat,
|
||||
updateChatListIds,
|
||||
@ -20,6 +19,7 @@ import {
|
||||
selectChatListType,
|
||||
selectCurrentMessageList,
|
||||
} from '../../selectors';
|
||||
import { updateUnreadReactions } from '../../reducers/reactions';
|
||||
|
||||
const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds
|
||||
// Enough to animate and mark as read in Message List
|
||||
@ -109,15 +109,16 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
setTimeout(() => {
|
||||
actions.requestChatUpdate({ chatId: update.chatId });
|
||||
}, 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({
|
||||
chat,
|
||||
message,
|
||||
@ -126,24 +127,6 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
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 'updateChannelMessages': {
|
||||
const { ids, messageUpdate } = update;
|
||||
@ -154,9 +137,18 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
ids.forEach((id) => {
|
||||
const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!;
|
||||
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) {
|
||||
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 {
|
||||
ApiMessage, ApiPollResult, ApiThreadInfo, MAIN_THREAD_ID,
|
||||
ApiChat,
|
||||
ApiMessage, ApiPollResult, ApiReactions, ApiThreadInfo, MAIN_THREAD_ID,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { unique } from '../../../util/iteratees';
|
||||
@ -45,8 +46,10 @@ import {
|
||||
selectLocalAnimatedEmoji,
|
||||
} from '../../selectors';
|
||||
import {
|
||||
getMessageContent, isUserId, isMessageLocal, getMessageText, checkIfReactionAdded,
|
||||
getMessageContent, isUserId, isMessageLocal, getMessageText, checkIfHasUnreadReactions,
|
||||
} from '../../helpers';
|
||||
import { onTickEnd } from '../../../util/schedulers';
|
||||
import { updateUnreadReactions } from '../../reducers/reactions';
|
||||
|
||||
const ANIMATION_DELAY = 350;
|
||||
|
||||
@ -158,9 +161,8 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
const { chatId, id, message } = update;
|
||||
|
||||
const currentMessage = selectChatMessage(global, chatId, id);
|
||||
if (!currentMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
global = updateWithLocalMedia(global, chatId, id, message);
|
||||
|
||||
@ -173,15 +175,21 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
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);
|
||||
|
||||
// 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.
|
||||
const chat = selectChat(global, chatId);
|
||||
if (
|
||||
chat
|
||||
currentMessage
|
||||
&& chat
|
||||
&& !message.isOutgoing
|
||||
&& chat.lastMessage?.id === message.id
|
||||
&& selectIsChatWithBot(global, chat)
|
||||
@ -482,36 +490,67 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
const { chatId, id, reactions } = update;
|
||||
const message = selectChatMessage(global, chatId, id);
|
||||
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 (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,
|
||||
});
|
||||
}
|
||||
if (!chat || !message) return;
|
||||
|
||||
setGlobal(updateReactions(global, chatId, id, reactions, chat, message.isOutgoing, message));
|
||||
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(
|
||||
global: GlobalState, chatId: string, id: number, message: Partial<ApiMessage>, isScheduled = false,
|
||||
) {
|
||||
|
||||
@ -342,7 +342,7 @@ addActionHandler('focusNextReply', (global, actions) => {
|
||||
addActionHandler('focusMessage', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
||||
replyMessageId, isResizingContainer,
|
||||
replyMessageId, isResizingContainer, shouldReplaceHistory,
|
||||
} = payload!;
|
||||
|
||||
let { messageId } = payload!;
|
||||
@ -387,7 +387,7 @@ addActionHandler('focusMessage', (global, actions, payload) => {
|
||||
const viewportIds = selectViewportIds(global, chatId, threadId);
|
||||
if (viewportIds && viewportIds.includes(messageId)) {
|
||||
setGlobal(global);
|
||||
actions.openChat({ id: chatId, threadId });
|
||||
actions.openChat({ id: chatId, threadId, shouldReplaceHistory });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -404,7 +404,7 @@ addActionHandler('focusMessage', (global, actions, payload) => {
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
actions.openChat({ id: chatId, threadId });
|
||||
actions.openChat({ id: chatId, threadId, shouldReplaceHistory });
|
||||
actions.loadViewportMessages();
|
||||
return undefined;
|
||||
});
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
import { ApiMessage, ApiReactions } from '../../api/types';
|
||||
import { GlobalState } from '../types';
|
||||
|
||||
export function getMessageRecentReaction(message: Partial<ApiMessage>) {
|
||||
return message.isOutgoing ? message.reactions?.recentReactions?.[0] : undefined;
|
||||
}
|
||||
|
||||
export function checkIfReactionAdded(oldReactions?: ApiReactions, newReactions?: ApiReactions, currentUserId?: string) {
|
||||
if (!oldReactions || !oldReactions.recentReactions) return true;
|
||||
if (!newReactions || !newReactions.recentReactions) return false;
|
||||
// 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);
|
||||
export function checkIfHasUnreadReactions(global: GlobalState, reactions: ApiReactions) {
|
||||
const { currentUserId } = global;
|
||||
return reactions?.recentReactions?.some(
|
||||
({ isUnread, userId }) => isUnread && userId !== currentUserId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -50,10 +50,11 @@ export function replaceChats(global: GlobalState, newById: Record<string, ApiCha
|
||||
|
||||
export function updateChat(
|
||||
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||
noOmitUnreadReactionCount = false,
|
||||
): GlobalState {
|
||||
const { byId } = global.chats;
|
||||
|
||||
const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo);
|
||||
const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo, noOmitUnreadReactionCount);
|
||||
if (!updatedChat) {
|
||||
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
|
||||
function getUpdatedChat(
|
||||
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||
noOmitUnreadReactionCount = false,
|
||||
) {
|
||||
const { byId } = global.chats;
|
||||
const chat = byId[chatId];
|
||||
const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin;
|
||||
|
||||
chatUpdate = noOmitUnreadReactionCount
|
||||
? chatUpdate : omit(chatUpdate, ['unreadReactionsCount']);
|
||||
const updatedChat: ApiChat = {
|
||||
...chat,
|
||||
...(shouldOmitMinInfo ? omit(chatUpdate, ['isMin', 'accessHash']) : chatUpdate),
|
||||
...(shouldOmitMinInfo
|
||||
? omit(chatUpdate, ['isMin', 'accessHash'])
|
||||
: chatUpdate),
|
||||
...(photo && { photos: [photo, ...(chat.photos || [])] }),
|
||||
};
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
} from '../../components/middle/helpers/calculateMiddleFooterTransforms';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import windowSize from '../../util/windowSize';
|
||||
import { updateChat } from './chats';
|
||||
import { ApiChat } from '../../api/types';
|
||||
|
||||
function getLeftColumnWidth(windowWidth: number) {
|
||||
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;
|
||||
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
|
||||
openMediaViewer: {
|
||||
@ -845,7 +863,7 @@ export type NonTypedActionNames = (
|
||||
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' | 'loadAvailableReactions' |
|
||||
'stopActiveEmojiInteraction' | 'interactWithAnimatedEmoji' | 'loadReactors' | 'setDefaultReaction' |
|
||||
'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' |
|
||||
'stopActiveReaction' | 'startActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' |
|
||||
'stopActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' |
|
||||
'setEditingId' |
|
||||
// scheduled messages
|
||||
'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.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers;
|
||||
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.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;
|
||||
@ -1140,6 +1142,8 @@ messages.getMessageReactionsList#e0ee6b77 flags:# peer:InputPeer id:int reaction
|
||||
messages.setChatAvailableReactions#14050ea6 peer:InputPeer available_reactions:Vector<string> = Updates;
|
||||
messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions;
|
||||
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.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot;
|
||||
messages.toggleBotInAttachMenu#1aee33af bot:InputUser enabled:Bool = Bool;
|
||||
|
||||
@ -233,5 +233,9 @@
|
||||
"messages.prolongWebView",
|
||||
"messages.requestSimpleWebView",
|
||||
"messages.sendWebViewResultMessage",
|
||||
"messages.sendWebViewData"
|
||||
"messages.sendWebViewData",
|
||||
"messages.readReactions",
|
||||
"messages.getUnreadReactions",
|
||||
"messages.readMentions",
|
||||
"messages.getUnreadMentions"
|
||||
]
|
||||
|
||||
@ -30,12 +30,14 @@ type NotificationData = {
|
||||
body: string;
|
||||
icon?: string;
|
||||
reaction?: string;
|
||||
shouldReplaceHistory?: boolean;
|
||||
};
|
||||
|
||||
type FocusMessageData = {
|
||||
chatId?: string;
|
||||
messageId?: number;
|
||||
reaction?: string;
|
||||
shouldReplaceHistory?: boolean;
|
||||
};
|
||||
|
||||
type CloseNotificationData = {
|
||||
@ -111,6 +113,7 @@ function showNotification({
|
||||
title,
|
||||
icon,
|
||||
reaction,
|
||||
shouldReplaceHistory,
|
||||
}: NotificationData) {
|
||||
const isFirstBatch = new Date().valueOf() - lastSyncAt < 1000;
|
||||
const tag = String(isFirstBatch ? 0 : chatId || 0);
|
||||
@ -121,6 +124,7 @@ function showNotification({
|
||||
messageId,
|
||||
reaction,
|
||||
count: 1,
|
||||
shouldReplaceHistory,
|
||||
},
|
||||
icon: icon || 'icon-192x192.png',
|
||||
badge: 'icon-192x192.png',
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"metadata": {
|
||||
"name": "Telegram T",
|
||||
"lastOpened": 0,
|
||||
"created": 1650288938325
|
||||
"created": 1650375482158
|
||||
},
|
||||
"iconSets": [
|
||||
{
|
||||
@ -158,7 +158,23 @@
|
||||
{
|
||||
"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,
|
||||
"name": "reactions",
|
||||
"prevSize": 32,
|
||||
@ -166,7 +182,7 @@
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 709,
|
||||
"order": 0,
|
||||
"id": 57,
|
||||
"name": "reaction-filled",
|
||||
"prevSize": 32,
|
||||
@ -609,6 +625,36 @@
|
||||
"height": 1024,
|
||||
"prevSize": 32,
|
||||
"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,
|
||||
"paths": [
|
||||
|
||||
@ -51,10 +51,10 @@
|
||||
.icon-volume-3:before {
|
||||
content: "\e991";
|
||||
}
|
||||
.icon-reactions:before {
|
||||
.icon-heart:before {
|
||||
content: "\e99a";
|
||||
}
|
||||
.icon-reaction-filled:before {
|
||||
.icon-heart-outline:before {
|
||||
content: "\e99b";
|
||||
}
|
||||
.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,
|
||||
chatId: chat.id,
|
||||
messageId: message.id,
|
||||
shouldReplaceHistory: true,
|
||||
reaction: activeReaction?.reaction,
|
||||
},
|
||||
});
|
||||
@ -431,13 +432,8 @@ export async function notifyAboutMessage({
|
||||
dispatch.focusMessage({
|
||||
chatId: chat.id,
|
||||
messageId: message.id,
|
||||
shouldReplaceHistory: true,
|
||||
});
|
||||
if (activeReaction) {
|
||||
dispatch.startActiveReaction({
|
||||
messageId: message.id,
|
||||
reaction: activeReaction.reaction,
|
||||
});
|
||||
}
|
||||
if (window.focus) {
|
||||
window.focus();
|
||||
}
|
||||
|
||||
@ -22,12 +22,6 @@ function handleWorkerMessage(e: MessageEvent) {
|
||||
if (dispatch.focusMessage) {
|
||||
dispatch.focusMessage(payload);
|
||||
}
|
||||
if (dispatch.startActiveReaction && payload.reaction) {
|
||||
dispatch.startActiveReaction({
|
||||
messageId: payload.messageId,
|
||||
reaction: payload.reaction,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'playNotificationSound':
|
||||
playNotifySoundDebounced(action.payload.id);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user