mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 05:11:55 +01:00
Support local changelogs; Fix marking read for service notifications
This commit is contained in:
parent
aa2617a2fd
commit
691c0c4263
10
.eslintrc
10
.eslintrc
@ -21,7 +21,10 @@
|
||||
"error",
|
||||
120
|
||||
],
|
||||
"array-bracket-newline": [2, "consistent"],
|
||||
"array-bracket-newline": [
|
||||
2,
|
||||
"consistent"
|
||||
],
|
||||
"no-null/no-null": 2,
|
||||
"no-console": "error",
|
||||
"semi": "error",
|
||||
@ -68,5 +71,8 @@
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"webpack.config.js"
|
||||
]
|
||||
}
|
||||
|
@ -12,7 +12,9 @@ import {
|
||||
ApiChatAdminRights,
|
||||
} from '../../types';
|
||||
|
||||
import { DEBUG, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE } from '../../../config';
|
||||
import {
|
||||
DEBUG, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../../config';
|
||||
import { invokeRequest, uploadFile } from './client';
|
||||
import {
|
||||
buildApiChatFromDialog,
|
||||
@ -52,12 +54,14 @@ export async function fetchChats({
|
||||
archived,
|
||||
withPinned,
|
||||
serverTimeOffset,
|
||||
lastLocalServiceMessage,
|
||||
}: {
|
||||
limit: number;
|
||||
offsetDate?: number;
|
||||
archived?: boolean;
|
||||
withPinned?: boolean;
|
||||
serverTimeOffset: number;
|
||||
lastLocalServiceMessage?: ApiMessage;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.GetDialogs({
|
||||
offsetPeer: new GramJs.InputPeerEmpty(),
|
||||
@ -111,7 +115,17 @@ export async function fetchChats({
|
||||
|
||||
const peerEntity = peersByKey[getPeerKey(dialog.peer)];
|
||||
const chat = buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset);
|
||||
|
||||
if (
|
||||
chat.id === SERVICE_NOTIFICATIONS_USER_ID
|
||||
&& lastLocalServiceMessage
|
||||
&& (!lastMessagesByChatId[chat.id] || lastLocalServiceMessage.date > lastMessagesByChatId[chat.id].date)
|
||||
) {
|
||||
chat.lastMessage = lastLocalServiceMessage;
|
||||
} else {
|
||||
chat.lastMessage = lastMessagesByChatId[chat.id];
|
||||
}
|
||||
|
||||
chat.isListed = true;
|
||||
chats.push(chat);
|
||||
|
||||
@ -232,8 +246,10 @@ export async function fetchChat({
|
||||
export async function requestChatUpdate({
|
||||
chat,
|
||||
serverTimeOffset,
|
||||
lastLocalMessage,
|
||||
noLastMessage,
|
||||
}: {
|
||||
chat: ApiChat; serverTimeOffset: number;
|
||||
chat: ApiChat; serverTimeOffset: number; lastLocalMessage?: ApiMessage; noLastMessage?: boolean;
|
||||
}) {
|
||||
const { id, accessHash } = chat;
|
||||
|
||||
@ -260,14 +276,23 @@ export async function requestChatUpdate({
|
||||
|
||||
updateLocalDb(result);
|
||||
|
||||
const lastMessage = buildApiMessage(result.messages[0]);
|
||||
let lastMessage: ApiMessage | undefined;
|
||||
if (!noLastMessage) {
|
||||
const lastRemoteMessage = buildApiMessage(result.messages[0]);
|
||||
|
||||
if (lastLocalMessage && (!lastRemoteMessage || (lastLocalMessage.date > lastRemoteMessage.date))) {
|
||||
lastMessage = lastLocalMessage;
|
||||
} else {
|
||||
lastMessage = lastRemoteMessage;
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateChat',
|
||||
id,
|
||||
chat: {
|
||||
...buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset),
|
||||
lastMessage,
|
||||
...(lastMessage && { lastMessage }),
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -442,6 +467,7 @@ export async function updateChatMutedState({
|
||||
void requestChatUpdate({
|
||||
chat,
|
||||
serverTimeOffset,
|
||||
noLastMessage: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -688,7 +688,7 @@ export async function markMessageListRead({
|
||||
}
|
||||
|
||||
if (threadId === MAIN_THREAD_ID) {
|
||||
void requestChatUpdate({ chat, serverTimeOffset });
|
||||
void requestChatUpdate({ chat, serverTimeOffset, noLastMessage: true });
|
||||
} else {
|
||||
void requestThreadInfoUpdate({ chat, threadId });
|
||||
}
|
||||
|
@ -82,7 +82,6 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
|| update instanceof GramJs.UpdateNewChannelMessage
|
||||
|| update instanceof GramJs.UpdateShortChatMessage
|
||||
|| update instanceof GramJs.UpdateShortMessage
|
||||
|| update instanceof GramJs.UpdateServiceNotification
|
||||
) {
|
||||
let message: ApiMessage | undefined;
|
||||
let shouldForceReply: boolean | undefined;
|
||||
@ -91,13 +90,6 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
message = buildApiMessageFromShortChat(update);
|
||||
} else if (update instanceof GramJs.UpdateShortMessage) {
|
||||
message = buildApiMessageFromShort(update);
|
||||
} else if (update instanceof GramJs.UpdateServiceNotification) {
|
||||
const currentDate = Date.now() / 1000 + serverTimeOffset;
|
||||
message = buildApiMessageFromNotification(update, currentDate);
|
||||
|
||||
if (isMessageWithMedia(update)) {
|
||||
addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update));
|
||||
}
|
||||
} else {
|
||||
// TODO Remove if proven not reproducing
|
||||
if (update.message instanceof GramJs.MessageEmpty) {
|
||||
@ -319,6 +311,18 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
});
|
||||
}, DELETE_MISSING_CHANNEL_MESSAGE_DELAY);
|
||||
}
|
||||
} else if (update instanceof GramJs.UpdateServiceNotification) {
|
||||
const currentDate = Date.now() / 1000 + serverTimeOffset;
|
||||
const message = buildApiMessageFromNotification(update, currentDate);
|
||||
|
||||
if (isMessageWithMedia(update)) {
|
||||
addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update));
|
||||
}
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateServiceNotification',
|
||||
message,
|
||||
});
|
||||
} else if ((
|
||||
originRequest instanceof GramJs.messages.SendMessage
|
||||
|| originRequest instanceof GramJs.messages.SendMedia
|
||||
|
@ -239,6 +239,11 @@ export type ApiUpdateMessagePollVote = {
|
||||
options: string[];
|
||||
};
|
||||
|
||||
export type ApiUpdateServiceNotification = {
|
||||
'@type': 'updateServiceNotification';
|
||||
message: ApiMessage;
|
||||
};
|
||||
|
||||
export type ApiUpdateDeleteMessages = {
|
||||
'@type': 'deleteMessages';
|
||||
ids: number[];
|
||||
@ -381,7 +386,7 @@ export type ApiUpdate = (
|
||||
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
|
||||
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdateChannelMessages |
|
||||
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
|
||||
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed |
|
||||
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification |
|
||||
ApiDeleteUser | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos |
|
||||
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage |
|
||||
ApiUpdateError | ApiUpdateResetContacts |
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
selectIsForwardModalOpen,
|
||||
selectIsMediaViewerOpen,
|
||||
selectIsRightColumnShown,
|
||||
selectIsServiceChatReady,
|
||||
} from '../../modules/selectors';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -62,11 +63,12 @@ type StateProps = {
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
language?: LangCode;
|
||||
openedStickerSetShortName?: string;
|
||||
isServiceChatReady?: boolean;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'loadAnimatedEmojis' | 'loadNotificationSettings' | 'loadNotificationExceptions' | 'updateIsOnline' |
|
||||
'loadTopInlineBots' | 'loadEmojiKeywords' | 'openStickerSetShortName' | 'loadCountryList'
|
||||
'loadTopInlineBots' | 'loadEmojiKeywords' | 'openStickerSetShortName' | 'loadCountryList' | 'checkVersionNotification'
|
||||
)>;
|
||||
|
||||
const NOTIFICATION_INTERVAL = 1000;
|
||||
@ -92,6 +94,7 @@ const Main: FC<StateProps & DispatchProps> = ({
|
||||
shouldSkipHistoryAnimations,
|
||||
language,
|
||||
openedStickerSetShortName,
|
||||
isServiceChatReady,
|
||||
loadAnimatedEmojis,
|
||||
loadNotificationSettings,
|
||||
loadNotificationExceptions,
|
||||
@ -100,6 +103,7 @@ const Main: FC<StateProps & DispatchProps> = ({
|
||||
loadEmojiKeywords,
|
||||
loadCountryList,
|
||||
openStickerSetShortName,
|
||||
checkVersionNotification,
|
||||
}) => {
|
||||
if (DEBUG && !DEBUG_isLogged) {
|
||||
DEBUG_isLogged = true;
|
||||
@ -128,6 +132,12 @@ const Main: FC<StateProps & DispatchProps> = ({
|
||||
loadTopInlineBots, loadEmojiKeywords, loadCountryList, language,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSyncTime && isServiceChatReady) {
|
||||
checkVersionNotification();
|
||||
}
|
||||
}, [lastSyncTime, isServiceChatReady, checkVersionNotification]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSyncTime && LOCATION_HASH.startsWith('#?tgaddr=')) {
|
||||
processDeepLink(decodeURIComponent(LOCATION_HASH.substr('#?tgaddr='.length)));
|
||||
@ -302,10 +312,11 @@ export default memo(withGlobal(
|
||||
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
||||
language: global.settings.byKey.language,
|
||||
openedStickerSetShortName: global.openedStickerSetShortName,
|
||||
isServiceChatReady: selectIsServiceChatReady(global),
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline',
|
||||
'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName', 'loadCountryList',
|
||||
'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName', 'loadCountryList', 'checkVersionNotification',
|
||||
]),
|
||||
)(Main));
|
||||
|
@ -396,7 +396,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
}
|
||||
|
||||
const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight;
|
||||
const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`);
|
||||
const anchor = anchorIdRef.current && document.getElementById(anchorIdRef.current);
|
||||
const unreadDivider = (
|
||||
!anchor
|
||||
&& memoUnreadDividerBeforeIdRef.current
|
||||
|
@ -297,17 +297,13 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const senderPeer = forwardInfo ? originSender : sender;
|
||||
|
||||
const selectMessage = useCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
||||
if (isLocal) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleMessageSelection({
|
||||
messageId,
|
||||
groupedId,
|
||||
...(e?.shiftKey && { withShift: true }),
|
||||
...(isAlbum && { childMessageIds: album!.messages.map(({ id }) => id) }),
|
||||
});
|
||||
}, [isLocal, toggleMessageSelection, messageId, isAlbum, album]);
|
||||
}, [toggleMessageSelection, messageId, isAlbum, album]);
|
||||
|
||||
const {
|
||||
handleMouseDown,
|
||||
@ -320,7 +316,6 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
selectMessage,
|
||||
ref,
|
||||
messageId,
|
||||
isLocal,
|
||||
isAlbum,
|
||||
Boolean(isInSelectMode),
|
||||
Boolean(canReply),
|
||||
@ -705,12 +700,12 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
data-last-message-id={album ? album.messages[album.messages.length - 1].id : undefined}
|
||||
data-has-unread-mention={message.hasUnreadMention}
|
||||
/>
|
||||
{!isLocal && !isInDocumentGroup && (
|
||||
{!isInDocumentGroup && (
|
||||
<div className="message-select-control">
|
||||
{isSelected && <i className="icon-select" />}
|
||||
</div>
|
||||
)}
|
||||
{!isLocal && isLastInDocumentGroup && (
|
||||
{isLastInDocumentGroup && (
|
||||
<div
|
||||
className={buildClassName('message-select-control group-select', isGroupSelected && 'is-selected')}
|
||||
onClick={handleDocumentGroupSelectAll}
|
||||
|
@ -15,7 +15,6 @@ export default function useOuterHandlers(
|
||||
selectMessage: (e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void,
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
messageId: number,
|
||||
isLocal: boolean,
|
||||
isAlbum: boolean,
|
||||
isInSelectMode: boolean,
|
||||
canReply: boolean,
|
||||
@ -28,14 +27,11 @@ export default function useOuterHandlers(
|
||||
|
||||
function handleMouseDown(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
preventMessageInputBlur(e);
|
||||
|
||||
if (!isLocal) {
|
||||
handleBeforeContextMenu(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
if (isInSelectMode && !isLocal) {
|
||||
if (isInSelectMode) {
|
||||
selectMessage(e);
|
||||
} else if (IS_ANDROID) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
@ -111,7 +107,7 @@ export default function useOuterHandlers(
|
||||
return {
|
||||
handleMouseDown: !isInSelectMode ? handleMouseDown : undefined,
|
||||
handleClick,
|
||||
handleContextMenu: !isInSelectMode && !isLocal ? handleContextMenu : undefined,
|
||||
handleContextMenu: !isInSelectMode ? handleContextMenu : undefined,
|
||||
handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined,
|
||||
handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined,
|
||||
isSwiped,
|
||||
|
@ -127,6 +127,10 @@ function readCache(initialState: GlobalState): GlobalState {
|
||||
byChatId: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (!cached.serviceNotifications) {
|
||||
cached.serviceNotifications = [];
|
||||
}
|
||||
}
|
||||
|
||||
const newState = {
|
||||
@ -171,6 +175,7 @@ function updateCache() {
|
||||
'push',
|
||||
'shouldShowContextMenuHint',
|
||||
'leftColumnWidth',
|
||||
'serviceNotifications',
|
||||
]),
|
||||
isChatInfoShown: reduceShowChatInfo(global),
|
||||
users: reduceUsers(global),
|
||||
|
@ -167,4 +167,6 @@ export const INITIAL_STATE: GlobalState = {
|
||||
activeDownloads: {
|
||||
byChatId: {},
|
||||
},
|
||||
|
||||
serviceNotifications: [],
|
||||
};
|
||||
|
@ -72,6 +72,12 @@ export interface Thread {
|
||||
replyStack?: number[];
|
||||
}
|
||||
|
||||
export interface ServiceNotification {
|
||||
id: number;
|
||||
message: ApiMessage;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export type GlobalState = {
|
||||
isChatInfoShown: boolean;
|
||||
isLeftColumnShown: boolean;
|
||||
@ -443,6 +449,8 @@ export type GlobalState = {
|
||||
};
|
||||
|
||||
shouldShowContextMenuHint?: boolean;
|
||||
|
||||
serviceNotifications: ServiceNotification[];
|
||||
};
|
||||
|
||||
export type ActionTypes = (
|
||||
@ -521,7 +529,7 @@ export type ActionTypes = (
|
||||
// misc
|
||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
|
||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
|
||||
'deleteDeviceToken' |
|
||||
'deleteDeviceToken' | 'checkVersionNotification' | 'createServiceNotification' |
|
||||
// payment
|
||||
'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
|
||||
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
RE_TME_INVITE_LINK,
|
||||
RE_TME_LINK,
|
||||
TIPS_USERNAME,
|
||||
LOCALIZED_TIPS, RE_TG_LINK, RE_TME_ADDSTICKERS_LINK,
|
||||
LOCALIZED_TIPS, RE_TG_LINK, RE_TME_ADDSTICKERS_LINK, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../../config';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
@ -38,7 +38,7 @@ import {
|
||||
selectChatByUsername,
|
||||
selectThreadTopMessageId,
|
||||
selectCurrentMessageList,
|
||||
selectThreadInfo, selectCurrentChat,
|
||||
selectThreadInfo, selectCurrentChat, selectLastServiceNotification,
|
||||
} from '../../selectors';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { debounce, pause, throttle } from '../../../util/schedulers';
|
||||
@ -255,6 +255,9 @@ addReducer('requestChatUpdate', (global, actions, payload) => {
|
||||
void callApi('requestChatUpdate', {
|
||||
chat,
|
||||
serverTimeOffset,
|
||||
...(chatId === SERVICE_NOTIFICATIONS_USER_ID && {
|
||||
lastLocalMessage: selectLastServiceNotification(global)?.message,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@ -964,12 +967,15 @@ addReducer('deleteChatMember', (global, actions, payload) => {
|
||||
});
|
||||
|
||||
async function loadChats(listType: 'active' | 'archived', offsetId?: number, offsetDate?: number) {
|
||||
let global = getGlobal();
|
||||
|
||||
const result = await callApi('fetchChats', {
|
||||
limit: CHAT_LIST_LOAD_SLICE,
|
||||
offsetDate,
|
||||
archived: listType === 'archived',
|
||||
withPinned: getGlobal().chats.orderedPinnedIds[listType] === undefined,
|
||||
serverTimeOffset: getGlobal().serverTimeOffset,
|
||||
withPinned: global.chats.orderedPinnedIds[listType] === undefined,
|
||||
serverTimeOffset: global.serverTimeOffset,
|
||||
lastLocalServiceMessage: selectLastServiceNotification(global)?.message,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@ -982,7 +988,7 @@ async function loadChats(listType: 'active' | 'archived', offsetId?: number, off
|
||||
chatIds.shift();
|
||||
}
|
||||
|
||||
let global = getGlobal();
|
||||
global = getGlobal();
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
} from '../../../api/types';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { MAX_MEDIA_FILES_FOR_ALBUM, MESSAGE_LIST_SLICE } from '../../../config';
|
||||
import { MAX_MEDIA_FILES_FOR_ALBUM, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||
import { callApi, cancelApiProgress } from '../../../api/gramjs';
|
||||
import { areSortedArraysIntersecting, buildCollectionByKey, split } from '../../../util/iteratees';
|
||||
import {
|
||||
@ -663,11 +663,15 @@ async function loadViewportMessages(
|
||||
messages, users, chats, threadInfos,
|
||||
} = result;
|
||||
|
||||
const byId = buildCollectionByKey(messages, 'id');
|
||||
const ids = Object.keys(byId).map(Number);
|
||||
|
||||
let global = getGlobal();
|
||||
|
||||
const localMessages = chatId === SERVICE_NOTIFICATIONS_USER_ID
|
||||
? global.serviceNotifications.map(({ message }) => message)
|
||||
: [];
|
||||
const allMessages = ([] as ApiMessage[]).concat(messages, localMessages);
|
||||
const byId = buildCollectionByKey(allMessages, 'id');
|
||||
const ids = Object.keys(byId).map(Number);
|
||||
|
||||
global = addChatMessagesById(global, chatId, byId);
|
||||
global = isOutlying
|
||||
? updateOutlyingIds(global, chatId, threadId, ids)
|
||||
|
@ -3,12 +3,12 @@ import {
|
||||
} from '../../../lib/teact/teactn';
|
||||
|
||||
import {
|
||||
ApiChat, ApiFormattedText, ApiUser, MAIN_THREAD_ID,
|
||||
ApiChat, ApiFormattedText, ApiMessage, ApiUser, MAIN_THREAD_ID,
|
||||
} from '../../../api/types';
|
||||
import { GlobalActions } from '../../../global/types';
|
||||
|
||||
import {
|
||||
CHAT_LIST_LOAD_SLICE, DEBUG, MESSAGE_LIST_SLICE,
|
||||
CHAT_LIST_LOAD_SLICE, DEBUG, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../../config';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
@ -22,6 +22,9 @@ import {
|
||||
updateChatListSecondaryInfo,
|
||||
updateThreadInfos,
|
||||
replaceThreadParam,
|
||||
updateListedIds,
|
||||
safeReplaceViewportIds,
|
||||
addChatMessagesById,
|
||||
} from '../../reducers';
|
||||
import {
|
||||
selectUser,
|
||||
@ -31,6 +34,7 @@ import {
|
||||
selectChatMessage,
|
||||
selectThreadInfo,
|
||||
selectCountNotMutedUnread,
|
||||
selectLastServiceNotification,
|
||||
} from '../../selectors';
|
||||
import { isChatPrivate } from '../../helpers';
|
||||
|
||||
@ -91,16 +95,20 @@ async function afterSync(actions: GlobalActions) {
|
||||
}
|
||||
|
||||
async function loadAndReplaceChats() {
|
||||
let global = getGlobal();
|
||||
|
||||
const result = await callApi('fetchChats', {
|
||||
limit: CHAT_LIST_LOAD_SLICE,
|
||||
withPinned: true,
|
||||
serverTimeOffset: getGlobal().serverTimeOffset,
|
||||
serverTimeOffset: global.serverTimeOffset,
|
||||
lastLocalServiceMessage: selectLastServiceNotification(global)?.message,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let global = getGlobal();
|
||||
global = getGlobal();
|
||||
|
||||
const { recentlyFoundChatIds } = global.globalSearch;
|
||||
const { userIds: contactIds } = global.contactList || {};
|
||||
@ -211,29 +219,25 @@ async function loadAndReplaceMessages(savedUsers?: ApiUser[]) {
|
||||
|
||||
if (result && newCurrentChatId === currentChatId) {
|
||||
const currentMessageListInfo = global.messages.byChatId[currentChatId];
|
||||
const byId = buildCollectionByKey(result.messages, 'id');
|
||||
const localMessages = currentChatId === SERVICE_NOTIFICATIONS_USER_ID
|
||||
? global.serviceNotifications.map(({ message }) => message)
|
||||
: [];
|
||||
const allMessages = ([] as ApiMessage[]).concat(result.messages, localMessages);
|
||||
const byId = buildCollectionByKey(allMessages, 'id');
|
||||
const listedIds = Object.keys(byId).map(Number);
|
||||
|
||||
global = {
|
||||
...global,
|
||||
messages: {
|
||||
...global.messages,
|
||||
byChatId: {
|
||||
[currentChatId]: {
|
||||
byId,
|
||||
threadsById: {
|
||||
[MAIN_THREAD_ID]: {
|
||||
...(currentMessageListInfo?.threadsById[MAIN_THREAD_ID]),
|
||||
listedIds,
|
||||
viewportIds: listedIds,
|
||||
outlyingIds: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
byChatId: {},
|
||||
},
|
||||
};
|
||||
|
||||
global = addChatMessagesById(global, currentChatId, byId);
|
||||
global = updateListedIds(global, currentChatId, MAIN_THREAD_ID, listedIds);
|
||||
global = safeReplaceViewportIds(global, currentChatId, MAIN_THREAD_ID, listedIds);
|
||||
|
||||
if (currentThreadId && threadInfo && threadInfo.originChannelId) {
|
||||
const { originChannelId } = threadInfo;
|
||||
const currentMessageListInfoOrigin = global.messages.byChatId[originChannelId];
|
||||
@ -275,6 +279,7 @@ async function loadAndReplaceMessages(savedUsers?: ApiUser[]) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = updateThreadInfos(global, currentChatId, result.threadInfos);
|
||||
|
||||
|
@ -38,6 +38,7 @@ import {
|
||||
selectFirstUnreadId,
|
||||
selectChat,
|
||||
selectIsChatWithBot,
|
||||
selectIsServiceChatReady,
|
||||
} from '../../selectors';
|
||||
import { getMessageContent, isChatPrivate, isMessageLocal } from '../../helpers';
|
||||
|
||||
@ -433,6 +434,16 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateServiceNotification': {
|
||||
const { message } = update;
|
||||
|
||||
if (selectIsServiceChatReady(global)) {
|
||||
actions.createServiceNotification({ message });
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,14 @@
|
||||
import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { ApiMessage, MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { FocusDirection } from '../../../types';
|
||||
|
||||
import { ANIMATION_END_DELAY, FAST_SMOOTH_MAX_DURATION } from '../../../config';
|
||||
import {
|
||||
ANIMATION_END_DELAY,
|
||||
APP_VERSION,
|
||||
FAST_SMOOTH_MAX_DURATION,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../../config';
|
||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||
import {
|
||||
enterMessageSelectMode,
|
||||
@ -29,10 +34,15 @@ import {
|
||||
selectReplyStack,
|
||||
} from '../../selectors';
|
||||
import { findLast } from '../../../util/iteratees';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
|
||||
// @ts-ignore
|
||||
import versionNotification from '../../../versionNotification.txt';
|
||||
|
||||
const FOCUS_DURATION = 1500;
|
||||
const FOCUS_NO_HIGHLIGHT_DURATION = FAST_SMOOTH_MAX_DURATION + ANIMATION_END_DELAY;
|
||||
const POLL_RESULT_OPEN_DELAY_MS = 450;
|
||||
const SERVICE_NOTIFICATIONS_MAX_AMOUNT = 1e4;
|
||||
|
||||
let blurTimeout: number | undefined;
|
||||
|
||||
@ -506,3 +516,65 @@ addReducer('closePollModal', (global) => {
|
||||
isPollModalOpen: false,
|
||||
};
|
||||
});
|
||||
|
||||
addReducer('checkVersionNotification', (global, actions) => {
|
||||
const currentVersion = APP_VERSION.split('.').slice(0, 2).join('.');
|
||||
const { serviceNotifications } = global;
|
||||
|
||||
if (serviceNotifications.find(({ version }) => version === currentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message: Omit<ApiMessage, 'id'> = {
|
||||
chatId: SERVICE_NOTIFICATIONS_USER_ID,
|
||||
date: getServerTime(global.serverTimeOffset),
|
||||
content: {
|
||||
text: {
|
||||
text: versionNotification,
|
||||
},
|
||||
},
|
||||
isOutgoing: false,
|
||||
};
|
||||
|
||||
actions.createServiceNotification({
|
||||
message,
|
||||
version: currentVersion,
|
||||
});
|
||||
});
|
||||
|
||||
addReducer('createServiceNotification', (global, actions, payload) => {
|
||||
const { message, version } = payload;
|
||||
const { serviceNotifications } = global;
|
||||
const serviceChat = selectChat(global, SERVICE_NOTIFICATIONS_USER_ID)!;
|
||||
|
||||
const maxId = Math.max(
|
||||
serviceChat.lastMessage?.id || 0,
|
||||
...serviceNotifications.map(({ id }) => id),
|
||||
);
|
||||
const fractionalPart = (serviceNotifications.length + 1) / SERVICE_NOTIFICATIONS_MAX_AMOUNT;
|
||||
// The fractional ID is made of the largest integer ID and an incremented fractional part
|
||||
const id = Math.floor(maxId) + fractionalPart;
|
||||
|
||||
message.id = id;
|
||||
|
||||
const serviceNotification = {
|
||||
id,
|
||||
message,
|
||||
version,
|
||||
};
|
||||
|
||||
setGlobal({
|
||||
...global,
|
||||
serviceNotifications: [
|
||||
...serviceNotifications,
|
||||
serviceNotification,
|
||||
],
|
||||
});
|
||||
|
||||
actions.apiUpdate({
|
||||
'@type': 'newMessage',
|
||||
id: message.id,
|
||||
chatId: message.chatId,
|
||||
message,
|
||||
});
|
||||
});
|
||||
|
@ -199,7 +199,7 @@ export function isActionMessage(message: ApiMessage) {
|
||||
}
|
||||
|
||||
export function isServiceNotificationMessage(message: ApiMessage) {
|
||||
return message.chatId === SERVICE_NOTIFICATIONS_USER_ID && isMessageLocal(message);
|
||||
return message.chatId === SERVICE_NOTIFICATIONS_USER_ID && Math.round(message.id) !== message.id;
|
||||
}
|
||||
|
||||
export function isAnonymousOwnMessage(message: ApiMessage) {
|
||||
|
@ -366,14 +366,15 @@ export function safeReplaceViewportIds(
|
||||
threadId: number,
|
||||
newViewportIds: number[],
|
||||
): GlobalState {
|
||||
const viewportIds = selectViewportIds(global, chatId, threadId) || [];
|
||||
const currentIds = selectViewportIds(global, chatId, threadId) || [];
|
||||
const newIds = orderHistoryIds(newViewportIds);
|
||||
|
||||
return replaceThreadParam(
|
||||
global,
|
||||
chatId,
|
||||
threadId,
|
||||
'viewportIds',
|
||||
areSortedArraysEqual(viewportIds, newViewportIds) ? viewportIds : newViewportIds,
|
||||
areSortedArraysEqual(currentIds, newIds) ? currentIds : newIds,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,9 @@ import {
|
||||
getPrivateChatUserId, isChatChannel, isChatPrivate, isHistoryClearMessage, isUserBot, isUserOnline, selectIsChatMuted,
|
||||
} from '../helpers';
|
||||
import { selectUser } from './users';
|
||||
import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE } from '../../config';
|
||||
import {
|
||||
ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../config';
|
||||
import { selectNotifyExceptions, selectNotifySettings } from './settings';
|
||||
|
||||
export function selectChat(global: GlobalState, chatId: number): ApiChat | undefined {
|
||||
@ -175,3 +177,7 @@ export function selectCountNotMutedUnread(global: GlobalState) {
|
||||
return acc;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function selectIsServiceChatReady(global: GlobalState) {
|
||||
return Boolean(selectChat(global, SERVICE_NOTIFICATIONS_USER_ID));
|
||||
}
|
||||
|
@ -348,18 +348,20 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
||||
const isBasicGroup = isChatBasicGroup(chat);
|
||||
const isSuperGroup = isChatSuperGroup(chat);
|
||||
const isChannel = isChatChannel(chat);
|
||||
const isLocal = isMessageLocal(message);
|
||||
const isServiceNotification = isServiceNotificationMessage(message);
|
||||
|
||||
const isOwn = isOwnMessage(message);
|
||||
const isAction = isActionMessage(message);
|
||||
const { content } = message;
|
||||
|
||||
const canEditMessagesIndefinitely = isChatWithSelf
|
||||
|| (isSuperGroup && getHasAdminRight(chat, 'pinMessages'))
|
||||
|| (isChannel && getHasAdminRight(chat, 'editMessages'));
|
||||
const isMessageEditable = (
|
||||
(canEditMessagesIndefinitely
|
||||
|| getServerTime(global.serverTimeOffset) - message.date < MESSAGE_EDIT_ALLOWED_TIME)
|
||||
&& !(
|
||||
(
|
||||
canEditMessagesIndefinitely
|
||||
|| getServerTime(global.serverTimeOffset) - message.date < MESSAGE_EDIT_ALLOWED_TIME
|
||||
) && !(
|
||||
content.sticker || content.contact || content.poll || content.action || content.audio
|
||||
|| (content.video?.isRound)
|
||||
)
|
||||
@ -367,7 +369,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
||||
&& !message.viaBotId
|
||||
);
|
||||
|
||||
const canReply = getCanPostInChat(chat, threadId) && !isServiceNotification;
|
||||
const canReply = !isLocal && !isServiceNotification && getCanPostInChat(chat, threadId);
|
||||
|
||||
const hasPinPermission = isPrivate || (
|
||||
chat.isCreator
|
||||
@ -375,7 +377,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
||||
|| getHasAdminRight(chat, 'pinMessages')
|
||||
);
|
||||
|
||||
let canPin = !isAction && hasPinPermission;
|
||||
let canPin = !isLocal && !isServiceNotification && !isAction && hasPinPermission;
|
||||
let canUnpin = false;
|
||||
|
||||
const pinnedMessageIds = selectPinnedIds(global, chat.id);
|
||||
@ -385,27 +387,29 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
||||
canPin = !canUnpin;
|
||||
}
|
||||
|
||||
const canDelete = isPrivate
|
||||
const canDelete = !isLocal && !isServiceNotification && (
|
||||
isPrivate
|
||||
|| isOwn
|
||||
|| isBasicGroup
|
||||
|| chat.isCreator
|
||||
|| getHasAdminRight(chat, 'deleteMessages');
|
||||
|| getHasAdminRight(chat, 'deleteMessages')
|
||||
);
|
||||
|
||||
const canReport = !isPrivate && !isOwn;
|
||||
|
||||
const canDeleteForAll = canDelete && !isServiceNotification && (
|
||||
const canDeleteForAll = canDelete && (
|
||||
(isPrivate && !isChatWithSelf)
|
||||
|| (isBasicGroup && (
|
||||
isOwn || getHasAdminRight(chat, 'deleteMessages') || chat.isCreator
|
||||
))
|
||||
);
|
||||
|
||||
const canEdit = !isAction && isMessageEditable && (
|
||||
const canEdit = !isLocal && !isAction && isMessageEditable && (
|
||||
isOwn
|
||||
|| (isChannel && (chat.isCreator || getHasAdminRight(chat, 'editMessages')))
|
||||
);
|
||||
|
||||
const canForward = !isAction && !isServiceNotification;
|
||||
const canForward = !isLocal && !isServiceNotification && !isAction;
|
||||
|
||||
const hasSticker = Boolean(message.content.sticker);
|
||||
const hasFavoriteSticker = hasSticker && selectIsStickerFavorite(global, message.content.sticker!);
|
||||
@ -751,3 +755,10 @@ export function selectShouldAutoPlayMedia(global: GlobalState, message: ApiMessa
|
||||
export function selectShouldLoopStickers(global: GlobalState) {
|
||||
return global.settings.byKey.shouldLoopStickers;
|
||||
}
|
||||
|
||||
export function selectLastServiceNotification(global: GlobalState) {
|
||||
const { serviceNotifications } = global;
|
||||
const maxId = Math.max(...serviceNotifications.map(({ id }) => id));
|
||||
|
||||
return serviceNotifications.find(({ id }) => id === maxId);
|
||||
}
|
||||
|
3
src/versionNotification.txt
Normal file
3
src/versionNotification.txt
Normal file
@ -0,0 +1,3 @@
|
||||
Welcome to dev version.
|
||||
|
||||
This is a demo notification.
|
@ -1,12 +1,15 @@
|
||||
const path = require('path');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const { EnvironmentPlugin, ProvidePlugin } = require('webpack');
|
||||
const {
|
||||
EnvironmentPlugin,
|
||||
ProvidePlugin,
|
||||
} = require('webpack');
|
||||
const HtmlPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@ -84,7 +87,7 @@ module.exports = (env = {}, argv = {}) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.tl$/i,
|
||||
test: /\.(txt|tl)$/i,
|
||||
loader: 'raw-loader',
|
||||
},
|
||||
],
|
||||
@ -92,11 +95,11 @@ module.exports = (env = {}, argv = {}) => {
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
fallback: {
|
||||
path: require.resolve("path-browserify"),
|
||||
os: require.resolve("os-browserify/browser"),
|
||||
buffer: require.resolve("buffer/"),
|
||||
path: require.resolve('path-browserify'),
|
||||
os: require.resolve('os-browserify/browser'),
|
||||
buffer: require.resolve('buffer/'),
|
||||
fs: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new HtmlPlugin({
|
||||
|
Loading…
x
Reference in New Issue
Block a user