Support local changelogs; Fix marking read for service notifications

This commit is contained in:
Alexander Zinchuk 2021-10-22 13:49:20 +03:00
parent aa2617a2fd
commit 691c0c4263
23 changed files with 267 additions and 87 deletions

View File

@ -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"
]
}

View File

@ -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);
chat.lastMessage = lastMessagesByChatId[chat.id];
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,
});
}

View File

@ -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 });
}

View File

@ -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

View File

@ -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 |

View File

@ -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));

View File

@ -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

View File

@ -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}

View File

@ -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);
}
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,

View File

@ -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),

View File

@ -167,4 +167,6 @@ export const INITIAL_STATE: GlobalState = {
activeDownloads: {
byChatId: {},
},
serviceNotifications: [],
};

View File

@ -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' |

View File

@ -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'));

View File

@ -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)

View File

@ -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);

View File

@ -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;
}
}
});

View File

@ -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,
});
});

View File

@ -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) {

View File

@ -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,
);
}

View File

@ -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));
}

View File

@ -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);
}

View File

@ -0,0 +1,3 @@
Welcome to dev version.
This is a demo notification.

View File

@ -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({