diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index 1a1752806..b333242cd 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -1,7 +1,9 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils'; -import { ApiThumbnail } from '../../types'; +import { + ApiPhoto, ApiPhotoSize, ApiThumbnail, +} from '../../types'; import { bytesToDataUri } from './helpers'; import { pathBytesToSvg } from './pathBytesToSvg'; @@ -59,3 +61,25 @@ export function buildApiThumbnailFromPath( height: h, }; } + +export function buildApiPhoto(photo: GramJs.Photo): ApiPhoto { + const sizes = photo.sizes + .filter((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize) + .map(buildApiPhotoSize); + + return { + id: String(photo.id), + thumbnail: buildApiThumbnailFromStripped(photo.sizes), + sizes, + }; +} + +export function buildApiPhotoSize(photoSize: GramJs.PhotoSize): ApiPhotoSize { + const { w, h, type } = photoSize; + + return { + width: w, + height: h, + type: type as ('m' | 'x' | 'y'), + }; +} diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index dc1422224..4dbab8a89 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -3,7 +3,6 @@ import { ApiMessage, ApiMessageForwardInfo, ApiPhoto, - ApiPhotoSize, ApiSticker, ApiVideo, ApiVoice, @@ -28,12 +27,14 @@ import { DELETED_COMMENTS_CHANNEL_ID, LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIO import { pick } from '../../../util/iteratees'; import { getApiChatIdFromMtpPeer } from './chats'; import { buildStickerFromDocument } from './symbols'; -import { buildApiThumbnailFromStripped } from './common'; +import { buildApiPhoto, buildApiThumbnailFromStripped, buildApiPhotoSize } from './common'; import { interpolateArray } from '../../../util/waveform'; import { getCurrencySign } from '../../../components/middle/helpers/getCurrencySign'; import { buildPeer } from '../gramjsBuilders'; +import { addPhotoToLocalDb, resolveMessageApiChatId } from '../helpers'; -const LOCAL_VIDEO_TEMP_ID = 'temp'; +const LOCAL_IMAGE_UPLOADING_TEMP_ID = 'temp'; +const LOCAL_VIDEO_UPLOADING_TEMP_ID = 'temp'; const INPUT_WAVEFORM_LENGTH = 63; let localMessageCounter = LOCAL_MESSAGE_ID_BASE; @@ -54,14 +55,6 @@ export function buildApiMessage(mtpMessage: GramJs.TypeMessage): ApiMessage | un return buildApiMessageWithChatId(chatId, mtpMessage); } -export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) { - if (!(mtpMessage instanceof GramJs.Message || mtpMessage instanceof GramJs.MessageService)) { - return undefined; - } - - return getApiChatIdFromMtpPeer(mtpMessage.peerId); -} - export function buildApiMessageFromShort(mtpMessage: GramJs.UpdateShortMessage): ApiMessage { const chatId = getApiChatIdFromMtpPeer({ userId: mtpMessage.userId } as GramJs.TypePeer); @@ -271,24 +264,7 @@ function buildPhoto(media: GramJs.TypeMessageMedia): ApiPhoto | undefined { return undefined; } - const sizes = media.photo.sizes - .filter((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize) - .map(buildApiPhotoSize); - - return { - thumbnail: buildApiThumbnailFromStripped(media.photo.sizes), - sizes, - }; -} - -function buildApiPhotoSize(photoSize: GramJs.PhotoSize): ApiPhotoSize { - const { w, h, type } = photoSize; - - return { - width: w, - height: h, - type: type as ('m' | 'x' | 'y'), - }; + return buildApiPhoto(media.photo); } export function buildVideoFromDocument(document: GramJs.Document): ApiVideo | undefined { @@ -541,6 +517,7 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef ]), photo: photo && photo instanceof GramJs.Photo ? { + id: String(photo.id), thumbnail: buildApiThumbnailFromStripped(photo.sizes), sizes: photo.sizes .filter((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize) @@ -564,6 +541,7 @@ function buildAction( let text = ''; let type: ApiAction['type'] = 'other'; + let photo: ApiPhoto | undefined; const targetUserId = 'users' in action // Api returns array of userIds, but no action currently has multiple users in it @@ -625,11 +603,17 @@ function buildAction( text = '%ACTION_NOT_IMPLEMENTED%'; } + if ('photo' in action && action.photo instanceof GramJs.Photo) { + addPhotoToLocalDb(action.photo); + photo = buildApiPhoto(action.photo); + } + return { text, type, targetUserId, targetChatId, + photo, // TODO Only used internally now, will be used for the UI in future }; } @@ -814,6 +798,7 @@ function buildUploadingMedia( if (mimeType.startsWith('image/')) { return { photo: { + id: LOCAL_IMAGE_UPLOADING_TEMP_ID, sizes: [], thumbnail: { width, height, dataUri: '' }, // Used only for dimensions blobUrl, @@ -822,7 +807,7 @@ function buildUploadingMedia( } else { return { video: { - id: LOCAL_VIDEO_TEMP_ID, + id: LOCAL_VIDEO_UPLOADING_TEMP_ID, mimeType, duration: duration || 0, fileName, diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index c26ea1cf7..5f35b67a8 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -297,6 +297,10 @@ export function isMessageWithMedia(message: GramJs.Message | GramJs.UpdateServic ); } +export function isServiceMessageWithMedia(message: GramJs.MessageService) { + return 'photo' in message.action && message.action.photo instanceof GramJs.Photo; +} + export function buildChatPhotoForLocalDb(photo: GramJs.TypePhoto) { if (photo instanceof GramJs.PhotoEmpty) { return new GramJs.ChatPhotoEmpty(); diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index 2c4cb7861..63a167cba 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -1,14 +1,34 @@ import { Api as GramJs } from '../../lib/gramjs'; import localDb from './localDb'; -import { resolveMessageApiChatId } from './apiBuilders/messages'; +import { getApiChatIdFromMtpPeer } from './apiBuilders/chats'; -export function addMessageToLocalDb(message: GramJs.Message) { +export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) { + if (!(mtpMessage instanceof GramJs.Message || mtpMessage instanceof GramJs.MessageService)) { + return undefined; + } + + return getApiChatIdFromMtpPeer(mtpMessage.peerId); +} + +export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageService) { const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`; localDb.messages[messageFullId] = message; + if ( - message.media instanceof GramJs.MessageMediaDocument + message instanceof GramJs.Message + && message.media instanceof GramJs.MessageMediaDocument && message.media.document instanceof GramJs.Document ) { localDb.documents[String(message.media.document.id)] = message.media.document; } + + if (message instanceof GramJs.MessageService && 'photo' in message.action) { + addPhotoToLocalDb(message.action.photo); + } +} + +export function addPhotoToLocalDb(photo: GramJs.TypePhoto) { + if (photo instanceof GramJs.Photo) { + localDb.photos[String(photo.id)] = photo; + } } diff --git a/src/api/gramjs/localDb.ts b/src/api/gramjs/localDb.ts index d2e7478f8..b4e350810 100644 --- a/src/api/gramjs/localDb.ts +++ b/src/api/gramjs/localDb.ts @@ -6,9 +6,10 @@ interface LocalDb { // Used for loading avatars and media through in-memory Gram JS instances. chats: Record; users: Record; - messages: Record; + messages: Record; documents: Record; stickerSets: Record; + photos: Record; } export default { @@ -18,4 +19,5 @@ export default { messages: {}, documents: {}, stickerSets: {}, + photos: {}, } as LocalDb; diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index f8a80a3ba..493f05305 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -26,7 +26,7 @@ export { export { fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers, - updateContact, deleteUser, + updateContact, deleteUser, fetchProfilePhotos, } from './users'; export { diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index c035d796d..e92eca007 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -17,7 +17,7 @@ import { getEntityTypeById } from '../gramjsBuilders'; import { blobToDataUri } from '../../../util/files'; import * as cacheApi from '../../../util/cacheApi'; -type EntityType = 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'stickerSet'; +type EntityType = 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet'; export default async function downloadMedia( { @@ -70,7 +70,8 @@ async function download( end?: number, mediaFormat?: ApiMediaFormat, ) { - const mediaMatch = url.match(/(avatar|profile|msg|stickerSet|sticker|wallpaper|gif|file)([-\d\w./]+)(\?size=\w+)?/); + // eslint-disable-next-line max-len + const mediaMatch = url.match(/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file)([-\d\w./]+)(\?size=\w+)?/); if (!mediaMatch) { return undefined; } @@ -89,7 +90,7 @@ async function download( let entityId: string | number = mediaMatch[2]; const sizeType = mediaMatch[3] ? mediaMatch[3].replace('?size=', '') : undefined; let entity: ( - GramJs.User | GramJs.Chat | GramJs.Channel | + GramJs.User | GramJs.Chat | GramJs.Channel | GramJs.Photo | GramJs.Message | GramJs.Document | GramJs.StickerSet | undefined ); @@ -97,7 +98,7 @@ async function download( entityType = getEntityTypeById(Number(entityId)); entityId = Math.abs(Number(entityId)); } else { - entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet'; + entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo'; } switch (entityType) { @@ -116,6 +117,9 @@ async function download( case 'wallpaper': entity = localDb.documents[entityId as string]; break; + case 'photo': + entity = localDb.photos[entityId as string]; + break; case 'stickerSet': entity = localDb.stickerSets[entityId as string]; break; @@ -125,7 +129,7 @@ async function download( return undefined; } - if (entityType === 'msg' || entityType === 'sticker' || entityType === 'gif' || entityType === 'wallpaper') { + if (['msg', 'sticker', 'gif', 'wallpaper', 'photo'].includes(entityType)) { if (mediaFormat === ApiMediaFormat.Stream) { onProgress!.acceptsBuffer = true; } @@ -141,6 +145,8 @@ async function download( if (entity.media instanceof GramJs.MessageMediaDocument && entity.media.document instanceof GramJs.Document) { fullSize = entity.media.document.size; } + } else if (entity instanceof GramJs.Photo) { + mimeType = 'image/jpeg'; } else if (entityType === 'sticker' && sizeType) { mimeType = 'image/webp'; } else { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 3ceb6577f..725ca7270 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -24,7 +24,6 @@ import { buildLocalMessage, buildWebPage, buildForwardedMessage, - resolveMessageApiChatId, } from '../apiBuilders/messages'; import { buildApiUser } from '../apiBuilders/users'; import { @@ -36,12 +35,13 @@ import { buildInputPoll, buildMtpMessageEntity, isMessageWithMedia, + isServiceMessageWithMedia, calculateResultHash, } from '../gramjsBuilders'; import localDb from '../localDb'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { fetchFile } from '../../../util/files'; -import { addMessageToLocalDb } from '../helpers'; +import { addMessageToLocalDb, resolveMessageApiChatId } from '../helpers'; import { interpolateArray } from '../../../util/waveform'; import { requestChatUpdate } from './chats'; @@ -766,6 +766,9 @@ export async function searchMessagesLocal({ case 'voice': filter = new GramJs.InputMessagesFilterVoice(); break; + case 'profilePhoto': + filter = new GramJs.InputMessagesFilterChatPhotos(); + break; case 'text': default: { filter = new GramJs.InputMessagesFilterEmpty(); @@ -1085,7 +1088,9 @@ function updateLocalDb(result: ( }); result.messages.forEach((message) => { - if (message instanceof GramJs.Message && isMessageWithMedia(message)) { + if ((message instanceof GramJs.Message && isMessageWithMedia(message)) + || (message instanceof GramJs.MessageService && isServiceMessageWithMedia(message)) + ) { addMessageToLocalDb(message); } }); diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 3e7d29285..53c27ebe4 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -1,7 +1,12 @@ +import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; -import { OnApiUpdate, ApiUser, ApiChat } from '../../types'; +import { + OnApiUpdate, ApiUser, ApiChat, ApiPhoto, +} from '../../types'; +import { PROFILE_PHOTOS_LIMIT } from '../../../config'; import { invokeRequest } from './client'; +import { searchMessagesLocal } from './messages'; import { buildInputEntity, calculateResultHash, @@ -9,8 +14,10 @@ import { buildInputContact, } from '../gramjsBuilders'; import { buildApiUser, buildApiUserFromFull } from '../apiBuilders/users'; -import localDb from '../localDb'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; +import { buildApiPhoto } from '../apiBuilders/common'; +import localDb from '../localDb'; +import { addPhotoToLocalDb } from '../helpers'; let onUpdate: OnApiUpdate; @@ -152,3 +159,49 @@ export async function deleteUser({ id, }); } + +export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) { + if (user) { + const { id, accessHash } = user; + + const result = await invokeRequest(new GramJs.photos.GetUserPhotos({ + userId: buildInputEntity(id, accessHash) as GramJs.InputUser, + limit: PROFILE_PHOTOS_LIMIT, + offset: 0, + maxId: BigInt('0'), + })); + + if (!result) { + return undefined; + } + + updateLocalDb(result); + + return { + photos: result.photos + .filter((photo): photo is GramJs.Photo => photo instanceof GramJs.Photo) + .map(buildApiPhoto), + }; + } + + const result = await searchMessagesLocal({ + chatOrUser: chat!, + type: 'profilePhoto', + limit: PROFILE_PHOTOS_LIMIT, + }); + + if (!result) { + return undefined; + } + + const { messages, users } = result; + + return { + photos: messages.map((message) => message.content.action!.photo).filter(Boolean as any), + users, + }; +} + +function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice)) { + result.photos.forEach(addPhotoToLocalDb); +} diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index e65d9f462..a1fcd6d36 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -8,7 +8,6 @@ import { buildApiMessageFromShortChat, buildMessageMediaContent, buildMessageTextContent, - resolveMessageApiChatId, buildPoll, buildPollResults, buildApiMessageFromNotification, @@ -32,8 +31,9 @@ import { import localDb from './localDb'; import { omitVirtualClassFields } from './apiBuilders/helpers'; import { DEBUG } from '../../config'; -import { addMessageToLocalDb } from './helpers'; +import { addMessageToLocalDb, addPhotoToLocalDb, resolveMessageApiChatId } from './helpers'; import { buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc'; +import { buildApiPhoto } from './apiBuilders/common'; type Update = ( (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } @@ -181,12 +181,16 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { if (localDb.chats[localDbChatId]) { localDb.chats[localDbChatId].photo = photo; } + addPhotoToLocalDb(action.photo); if (avatarHash) { onUpdate({ '@type': 'updateChat', id: message.chatId, - chat: { avatarHash }, + chat: { + avatarHash, + }, + ...(action.photo instanceof GramJs.Photo && { newProfilePhoto: buildApiPhoto(action.photo) }), }); } } else if (action instanceof GramJs.MessageActionChatDeletePhoto) { @@ -264,6 +268,13 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { const ids = update.messages; const existingIds = ids.filter((id) => localDb.messages[`${chatId}-${id}`]); const missingIds = ids.filter((id) => !localDb.messages[`${chatId}-${id}`]); + const profilePhotoIds = ids.map((id) => { + const message = localDb.messages[`${chatId}-${id}`]; + + return message && message instanceof GramJs.MessageService && 'photo' in message.action + ? String(message.action.photo.id) + : undefined; + }).filter(Boolean as any); if (existingIds.length) { onUpdate({ @@ -273,6 +284,14 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { }); } + if (profilePhotoIds.length) { + onUpdate({ + '@type': 'deleteProfilePhotos', + ids: profilePhotoIds, + chatId, + }); + } + // For some reason delete message update sometimes comes before new message update if (missingIds.length) { setTimeout(() => { diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 0d99c068b..07d014632 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -1,4 +1,4 @@ -import { ApiMessage } from './messages'; +import { ApiMessage, ApiPhoto } from './messages'; type ApiChatType = ( 'chatTypePrivate' | 'chatTypeSecret' | @@ -28,6 +28,7 @@ export interface ApiChat { membersCount?: number; joinDate?: number; isSupport?: boolean; + photos?: ApiPhoto[]; // Current user permissions isNotJoined?: boolean; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 3923c8263..d5a860906 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -11,6 +11,7 @@ export interface ApiThumbnail { } export interface ApiPhoto { + id: string; thumbnail?: ApiThumbnail; sizes: ApiPhotoSize[]; blobUrl?: string; @@ -144,6 +145,7 @@ export interface ApiAction { targetUserId?: number; targetChatId?: number; type: 'historyClear' | 'other'; + photo?: ApiPhoto; } export interface ApiWebPage { @@ -264,7 +266,7 @@ export interface ApiKeyboardButton { export type ApiKeyboardButtons = ApiKeyboardButton[][]; -export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio'; +export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'profilePhoto'; export type ApiGlobalMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice'; export const MAIN_THREAD_ID = -1; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 2bee273bc..decd5cc92 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -6,7 +6,7 @@ import { ApiChatFolder, } from './chats'; import { - ApiMessage, ApiPoll, ApiStickerSet, ApiThreadInfo, + ApiMessage, ApiPhoto, ApiPoll, ApiStickerSet, ApiThreadInfo, } from './messages'; import { ApiUser, ApiUserFullInfo, ApiUserStatus } from './users'; @@ -60,6 +60,7 @@ export type ApiUpdateChat = { '@type': 'updateChat'; id: number; chat: Partial; + newProfilePhoto?: ApiPhoto; }; export type ApiUpdateChatJoin = { @@ -240,6 +241,12 @@ export type ApiUpdateDeleteHistory = { chatId: number; }; +export type ApiUpdateDeleteProfilePhotos = { + '@type': 'deleteProfilePhotos'; + ids: string[]; + chatId: number; +}; + export type ApiUpdateResetMessages = { '@type': 'resetMessages'; id: number; @@ -353,7 +360,7 @@ export type ApiUpdate = ( ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdateChannelMessages | ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | - ApiDeleteUser | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | + ApiDeleteUser | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos | ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateError | ApiUpdateResetContacts | ApiUpdateFavoriteStickers | ApiUpdateStickerSet | diff --git a/src/api/types/users.ts b/src/api/types/users.ts index ea7ef8cde..e96ad2aed 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -1,3 +1,5 @@ +import { ApiPhoto } from './messages'; + export interface ApiUser { id: number; isMin: boolean; @@ -12,6 +14,7 @@ export interface ApiUser { phoneNumber: string; accessHash?: string; avatarHash?: string; + photos?: ApiPhoto[]; // Obtained from GetFullUser / UserFullInfo fullInfo?: ApiUserFullInfo; diff --git a/src/components/common/Avatar.scss b/src/components/common/Avatar.scss index 8a1f1465e..50002eaf3 100644 --- a/src/components/common/Avatar.scss +++ b/src/components/common/Avatar.scss @@ -123,40 +123,4 @@ width: 100%; height: 100%; } - - &.color-bg-1 { - --color-user: var(--color-user-1); - } - - &.color-bg-2 { - --color-user: var(--color-user-2); - } - - &.color-bg-4 { - --color-user: var(--color-user-4); - } - - &.color-bg-5 { - --color-user: var(--color-user-5); - } - - &.color-bg-6 { - --color-user: var(--color-user-6); - } - - &.color-bg-7 { - --color-user: var(--color-user-7); - } - - &.color-bg-8 { - --color-user: var(--color-user-8); - } - - &.saved-messages { - --color-user: var(--color-primary); - } - - &.deleted-account { - --color-user: var(--color-gray); - } } diff --git a/src/components/left/main/LeftMain.scss b/src/components/left/main/LeftMain.scss index 01b7b8b04..3b8804b5d 100644 --- a/src/components/left/main/LeftMain.scss +++ b/src/components/left/main/LeftMain.scss @@ -36,13 +36,8 @@ .Tab { flex: 0 0 auto; - padding-left: 0.625rem; - padding-right: 0.625rem; - - > span { - padding-left: 0.5rem; - padding-right: 0.5rem; - } + padding-left: 1rem; + padding-right: 1rem; } > .Transition { diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index f5e3171b4..a3e926f0b 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -156,12 +156,11 @@ const SettingsFoldersMain: FC = ({ onEditFolder(foldersById[folder.id])} > -
- {folder.title} - {folder.subtitle} -
+ {folder.title} + {folder.subtitle}
)) : userFolders && !userFolders.length ? (

diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 903b4c797..34d47fc0a 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -75,6 +75,7 @@ type StateProps = { senderId?: number; origin?: MediaViewerOrigin; avatarOwner?: ApiChat | ApiUser; + profilePhotoIndex?: number; message?: ApiMessage; chatMessages?: Record; collectionIds?: number[]; @@ -92,6 +93,7 @@ const MediaViewer: FC = ({ senderId, origin, avatarOwner, + profilePhotoIndex, message, chatMessages, collectionIds, @@ -116,7 +118,9 @@ const MediaViewer: FC = ({ const slideAnimation = animationLevel >= 1 ? 'mv-slide' : 'none'; const headerAnimation = animationLevel === 2 ? 'slide-fade' : 'none'; const isGhostAnimation = animationLevel === 2; - const fileName = avatarOwner ? `avatar${avatarOwner.id}.jpg` : message && getMessageMediaFilename(message); + const fileName = avatarOwner + ? `avatar${avatarOwner.id}-${profilePhotoIndex}.jpg` + : message && getMessageMediaFilename(message); const prevSenderId = usePrevious(senderId); const [canPanZoomWrap, setCanPanZoomWrap] = useState(false); const [isZoomed, setIsZoomed] = useState(false); @@ -137,8 +141,11 @@ const MediaViewer: FC = ({ } function getMediaHash(full?: boolean) { - if (avatarOwner) { - return getChatAvatarHash(avatarOwner, full ? 'big' : 'normal'); + if (avatarOwner && profilePhotoIndex !== undefined) { + const { photos } = avatarOwner; + return photos && photos[profilePhotoIndex] + ? `photo${photos[profilePhotoIndex].id}?size=c` + : getChatAvatarHash(avatarOwner, full ? 'big' : 'normal'); } return message && getMessageMediaHash(message, full ? 'viewerFull' : 'viewerPreview'); @@ -151,10 +158,13 @@ const MediaViewer: FC = ({ undefined, isGhostAnimation && ANIMATION_DURATION, ); + const previewMediaHash = getMediaHash(); const blobUrlPreview = useMedia( - getMediaHash(), + previewMediaHash, undefined, - avatarOwner ? ApiMediaFormat.DataUri : ApiMediaFormat.BlobUrl, + avatarOwner && previewMediaHash && previewMediaHash.startsWith('profilePhoto') + ? ApiMediaFormat.DataUri + : ApiMediaFormat.BlobUrl, undefined, isGhostAnimation && ANIMATION_DURATION, ); @@ -544,7 +554,7 @@ function renderPhoto(blobUrl?: string, imageSize?: IDimensions) { export default memo(withGlobal( (global): StateProps => { const { - chatId, threadId, messageId, avatarOwnerId, origin, + chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin, } = global.mediaViewer; const { animationLevel, @@ -571,12 +581,13 @@ export default memo(withGlobal( } if (avatarOwnerId) { - const sender = selectChat(global, avatarOwnerId) || selectUser(global, avatarOwnerId); + const sender = selectUser(global, avatarOwnerId) || selectChat(global, avatarOwnerId); return { messageId: -1, senderId: avatarOwnerId, avatarOwner: sender, + profilePhotoIndex: profilePhotoIndex || 0, animationLevel, origin, }; diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts index fb6d7e40c..a667ef23f 100644 --- a/src/components/mediaViewer/helpers/ghostAnimation.ts +++ b/src/components/mediaViewer/helpers/ghostAnimation.ts @@ -148,7 +148,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string, } } - const ghost = createGhost(bestImageData || toImage); + const ghost = createGhost(bestImageData || toImage, origin === MediaViewerOrigin.ProfileAvatar); applyStyles(ghost, { top: `${toTop}px`, left: `${toLeft}px`, @@ -179,7 +179,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string, }); } -function createGhost(source: string | HTMLImageElement | HTMLVideoElement) { +function createGhost(source: string | HTMLImageElement | HTMLVideoElement, shouldAppendProfileInfo = false) { const ghost = document.createElement('div'); ghost.classList.add('ghost'); @@ -195,6 +195,14 @@ function createGhost(source: string | HTMLImageElement | HTMLVideoElement) { ghost.appendChild(img); + if (shouldAppendProfileInfo) { + ghost.classList.add('ProfileInfo'); + const profileInfo = document.querySelector('#RightColumn .ProfileInfo .info'); + if (profileInfo) { + ghost.appendChild(profileInfo.cloneNode(true)); + } + } + return ghost; } @@ -283,7 +291,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) { break; case MediaViewerOrigin.ProfileAvatar: - containerSelector = '#RightColumn .active .profile-info .Avatar'; + containerSelector = '#RightColumn .ProfileInfo .active .ProfilePhoto'; mediaSelector = 'img.avatar-media'; break; @@ -313,12 +321,12 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) { break; case MediaViewerOrigin.SharedMedia: + case MediaViewerOrigin.ProfileAvatar: case MediaViewerOrigin.SearchResult: (ghost.firstChild as HTMLElement).style.objectFit = 'cover'; break; case MediaViewerOrigin.MiddleHeaderAvatar: - case MediaViewerOrigin.ProfileAvatar: ghost.classList.add('circle'); break; } diff --git a/src/components/right/ChatExtra.tsx b/src/components/right/ChatExtra.tsx index 48793d7f1..81ca541ce 100644 --- a/src/components/right/ChatExtra.tsx +++ b/src/components/right/ChatExtra.tsx @@ -1,71 +1,159 @@ -import React, { FC, memo } from '../../lib/teact/teact'; +import React, { + FC, memo, useCallback, useEffect, +} from '../../lib/teact/teact'; import { withGlobal } from '../../lib/teact/teactn'; -import { ApiChat } from '../../api/types'; +import { GlobalActions, GlobalState } from '../../global/types'; +import { ApiChat, ApiUser } from '../../api/types'; -import { selectChat } from '../../modules/selectors'; +import { selectChat, selectUser } from '../../modules/selectors'; import { - getChatDescription, getChatLink, getHasAdminRight, isChatChannel, isUserRightBanned, + getChatDescription, getChatLink, getHasAdminRight, isChatChannel, isChatPrivate, isUserRightBanned, } from '../../modules/helpers'; import renderText from '../common/helpers/renderText'; +import { pick } from '../../util/iteratees'; +import { copyTextToClipboard } from '../../util/clipboard'; +import { formatPhoneNumberWithCode } from '../../util/phoneNumber'; import useLang from '../../hooks/useLang'; import SafeLink from '../common/SafeLink'; +import ListItem from '../ui/ListItem'; +import Switcher from '../ui/Switcher'; type OwnProps = { - chatId: number; + chatOrUserId: number; + forceShowSelf?: boolean; }; type StateProps = { + user?: ApiUser; chat?: ApiChat; canInviteUsers?: boolean; -}; +} & Pick; -const ChatExtra: FC = ({ chat, canInviteUsers }) => { +type DispatchProps = Pick; + +const ChatExtra: FC = ({ + lastSyncTime, + user, + chat, + forceShowSelf, + canInviteUsers, + loadFullUser, + showNotification, + updateChatMutedState, +}) => { + const { + id: userId, + fullInfo, + username, + phoneNumber, + isSelf, + } = user || {}; + const { + id: chatId, + isMuted: currentIsMuted, + username: chatUsername, + } = chat || {}; const lang = useLang(); - if (!chat || chat.isRestricted) { + useEffect(() => { + if (lastSyncTime && userId) { + loadFullUser({ userId }); + } + }, [loadFullUser, userId, lastSyncTime]); + + const handleClick = useCallback((text: string, entity: string) => { + copyTextToClipboard(text); + showNotification({ message: `${entity} was copied` }); + }, [showNotification]); + + const handleNotificationChange = useCallback(() => { + updateChatMutedState({ chatId, isMuted: !currentIsMuted }); + }, [chatId, currentIsMuted, updateChatMutedState]); + + if (!chat || chat.isRestricted || (isSelf && !forceShowSelf)) { return undefined; } + const bio = fullInfo && fullInfo.bio; + const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneNumber); const description = getChatDescription(chat); const link = getChatLink(chat); const url = link.indexOf('http') === 0 ? link : `http://${link}`; + const printedUsername = username || chatUsername; + const printedDescription = bio || description; return (

- {description && !!description.length && ( -
- -
-

{renderText(description, ['br', 'links', 'emoji'])}

-

{lang('Info')}

-
-
+ {formattedNumber && !!formattedNumber.length && ( + handleClick(formattedNumber, lang('Phone'))}> + {formattedNumber} + {lang('Phone')} + )} - {canInviteUsers && !!link.length && ( -
- -
+ {printedUsername && ( + handleClick(`@${printedUsername}`, lang('Username'))} + > + {renderText(printedUsername)} + {lang('Username')} + + )} + {printedDescription && !!printedDescription.length && ( + handleClick(printedDescription, lang(userId ? 'UserBio' : 'Info'))} + > + {renderText(printedDescription, ['br', 'links', 'emoji'])} + {lang(userId ? 'UserBio' : 'Info')} + + )} + {canInviteUsers && !printedUsername && !!link.length && ( + handleClick(link, lang('SetUrlPlaceholder'))}> +
-

{lang('SetUrlPlaceholder')}

-
+ {lang('SetUrlPlaceholder')} + )} + + {lang('Notifications')} + +
); }; export default memo(withGlobal( - (global, { chatId }): StateProps => { - const chat = selectChat(global, chatId); + (global, { chatOrUserId }): StateProps => { + const { lastSyncTime } = global; + const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; + const user = isChatPrivate(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined; const canInviteUsers = chat && ( (!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers')) || getHasAdminRight(chat, 'inviteUsers') ); - return { chat, canInviteUsers }; + return { + lastSyncTime, chat, user, canInviteUsers, + }; }, + (setGlobal, actions): DispatchProps => pick(actions, [ + 'loadFullUser', 'updateChatMutedState', 'showNotification', + ]), )(ChatExtra)); diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index 3a33cf147..552caad76 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -3,6 +3,10 @@ overflow-y: scroll; overflow-x: hidden; + @supports (overflow-y: overlay) { + overflow-y: overlay !important; + } + > .profile-info > .ChatInfo { grid-area: chat_info; @@ -11,37 +15,22 @@ } } - > .profile-info >.ChatExtra { - padding: 0 1.5rem; + > .profile-info > .ChatExtra { + padding: .875rem .5rem .5rem; + box-shadow: inset 0 -.0625rem 0 0 var(--color-background-secondary-accent); + border-bottom: .625rem solid var(--color-background-secondary); - .item { - display: flex; - padding: .75rem 0 1rem; - text-align: left; + .narrow { + margin-bottom: 0; + } - i { - font-size: 1.5rem; - color: var(--color-text-secondary); - margin-right: 2rem; - } + .inactive.no-selection { + user-select: auto; + -webkit-user-select: auto !important; + } - .title { - font-size: 1rem; - line-height: 1.4375rem; - margin-bottom: 0; - font-weight: 400; - word-break: break-word; - } - - a.title { - color: var(--color-text); - } - - .subtitle { - margin-bottom: 0; - font-size: 0.875rem; - color: var(--color-text-secondary); - } + .Switcher { + margin-left: auto; } } } @@ -54,10 +43,9 @@ background: var(--color-background); top: -1px; .Tab { - padding: .6875rem .25rem; + padding: 1rem .25rem; i { - padding-right: 1.5rem; - margin-left: -.75rem; + bottom: -1rem; } } } @@ -81,10 +69,9 @@ &.media-list { display: grid; - padding: .5rem; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; - grid-gap: .25rem; + grid-gap: .0625rem; } &.documents-list { diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 63cb0b1b2..cbc07a021 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -39,11 +39,10 @@ import TabList from '../ui/TabList'; import Spinner from '../ui/Spinner'; import ListItem from '../ui/ListItem'; import PrivateChatInfo from '../common/PrivateChatInfo'; -import GroupChatInfo from '../common/GroupChatInfo'; +import ProfileInfo from './ProfileInfo'; import Document from '../common/Document'; import Audio from '../common/Audio'; -import UserExtra from './UserExtra'; -import GroupExtra from './ChatExtra'; +import ChatExtra from './ChatExtra'; import Media from '../common/Media'; import WebLink from '../common/WebLink'; import NothingFound from '../common/NothingFound'; @@ -74,7 +73,7 @@ type StateProps = { type DispatchProps = Pick; const TABS = [ @@ -108,6 +107,7 @@ const Profile: FC = ({ openAudioPlayer, openUserInfo, focusMessage, + loadProfilePhotos, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); @@ -148,6 +148,12 @@ const Profile: FC = ({ const profileId = resolvedUserId || chatId; + useEffect(() => { + if (lastSyncTime) { + loadProfilePhotos({ profileId }); + } + }, [loadProfilePhotos, profileId, lastSyncTime]); + const handleSelectMedia = useCallback((messageId: number) => { openMediaViewer({ chatId: profileId, @@ -331,23 +337,11 @@ const Profile: FC = ({ function renderProfileInfo(chatId: number, resolvedUserId?: number) { return (
- {resolvedUserId ? ( - <> - - - - ) : ( - <> - - - - )} + +
); } @@ -408,5 +402,6 @@ export default memo(withGlobal( 'openAudioPlayer', 'openUserInfo', 'focusMessage', + 'loadProfilePhotos', ]), )(Profile)); diff --git a/src/components/right/ProfileInfo.scss b/src/components/right/ProfileInfo.scss new file mode 100644 index 000000000..a6772ff98 --- /dev/null +++ b/src/components/right/ProfileInfo.scss @@ -0,0 +1,133 @@ +.ProfileInfo { + aspect-ratio: 1 / 1; + position: relative; + + @supports not (aspect-ratio: 1 / 1) { + &::before { + float: left; + padding-top: 100%; + content: ""; + } + + &::after { + display: block; + content: ""; + clear: both; + } + } + + .photo-wrapper { + width: 100%; + position: absolute; + left: 0; + top: 0; + bottom: 0; + + > .Transition { + width: 100%; + height: 100%; + } + } + + .photo-dashes { + position: absolute; + width: 100%; + height: .125rem; + padding: 0 .375rem; + z-index: 1; + + display: flex; + top: .5rem; + left: 0; + } + + .photo-dash { + flex: 1 1 auto; + background-color: var(--color-white); + opacity: .5; + border-radius: .125rem; + margin: 0 .125rem; + + &.current { + opacity: 1; + } + } + + .navigation { + position: absolute; + top: 0; + bottom: 0; + width: 25%; + border: none; + padding: 0; + margin: 0; + appearance: none; + background: transparent no-repeat; + background-size: 1.25rem; + opacity: .25; + transition: opacity .15s; + outline: none; + cursor: pointer; + z-index: 1; + + &:hover, .is-touch-env & { + opacity: 1; + } + + &.prev { + left: 0; + background-image: url("../../assets/media_navigation_previous.svg"); + background-position: 1.25rem 50%; + } + + &.next { + right: 0; + background-image: url("../../assets/media_navigation_next.svg"); + background-position: calc(100% - 1.25rem) 50%; + } + } + + .info { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + min-height: 100px; + padding: 0 1.5rem .5rem; + background: linear-gradient(0deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 100%); + color: var(--color-white); + display: flex; + flex-direction: column; + justify-content: flex-end; + } + + .title { + display: flex; + align-items: center; + + h3 { + font-weight: 500; + font-size: 1.25rem; + line-height: 1.375rem; + white-space: pre-wrap; + word-break: break-word; + margin-bottom: .25rem; + } + + .VerifiedIcon { + margin-left: 0.25rem; + margin-top: -0.125rem; + } + + .emoji { + width: 1.5rem; + height: 1.5rem; + background-size: 1.5rem; + } + } + + .status { + font-size: 0.875rem; + opacity: .5; + } +} diff --git a/src/components/right/ProfileInfo.tsx b/src/components/right/ProfileInfo.tsx new file mode 100644 index 000000000..d4e54ec5e --- /dev/null +++ b/src/components/right/ProfileInfo.tsx @@ -0,0 +1,235 @@ +import React, { + FC, useEffect, useCallback, memo, useState, +} from '../../lib/teact/teact'; +import { withGlobal } from '../../lib/teact/teactn'; + +import { ApiUser, ApiChat } from '../../api/types'; +import { GlobalActions, GlobalState } from '../../global/types'; +import { MediaViewerOrigin } from '../../types'; + +import { IS_TOUCH_ENV } from '../../util/environment'; +import { selectChat, selectUser } from '../../modules/selectors'; +import { + getUserFullName, getUserStatus, isChatChannel, isUserOnline, +} from '../../modules/helpers'; +import renderText from '../common/helpers/renderText'; +import { pick } from '../../util/iteratees'; +import { captureEvents, SwipeDirection } from '../../util/captureEvents'; +import usePhotosPreload from './hooks/usePhotosPreload'; +import useLang from '../../hooks/useLang'; + +import VerifiedIcon from '../common/VerifiedIcon'; +import ProfilePhoto from './ProfilePhoto'; +import Transition from '../ui/Transition'; + +import './ProfileInfo.scss'; + +type OwnProps = { + userId: number; + forceShowSelf?: boolean; +}; + +type StateProps = { + user?: ApiUser; + chat?: ApiChat; + isSavedMessages?: boolean; + animationLevel: 0 | 1 | 2; +} & Pick; + +type DispatchProps = Pick; + +const PrivateChatInfo: FC = ({ + user, + chat, + isSavedMessages, + lastSyncTime, + animationLevel, + loadFullUser, + openMediaViewer, +}) => { + const { id: userId } = user || {}; + const { id: chatId } = chat || {}; + const fullName = user ? getUserFullName(user) : (chat ? chat.title : ''); + const photos = (user ? user.photos : (chat ? chat.photos : undefined)) || []; + const slideAnimation = animationLevel >= 1 ? 'slide' : 'none'; + + const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); + const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0; + const isLast = isSavedMessages || photos.length <= 1 || currentPhotoIndex === photos.length - 1; + + // Deleting the last profile photo may result in an error + useEffect(() => { + if (currentPhotoIndex > photos.length) { + setCurrentPhotoIndex(Math.max(0, photos.length - 1)); + } + }, [currentPhotoIndex, photos.length]); + + const lang = useLang(); + + useEffect(() => { + if (lastSyncTime && userId) { + loadFullUser({ userId }); + } + }, [userId, loadFullUser, lastSyncTime]); + + usePhotosPreload(user || chat, photos, currentPhotoIndex); + + const handleProfilePhotoClick = useCallback(() => { + openMediaViewer({ + avatarOwnerId: userId || chatId, + profilePhotoIndex: currentPhotoIndex, + origin: MediaViewerOrigin.ProfileAvatar, + }); + }, [openMediaViewer, userId, chatId, currentPhotoIndex]); + + const selectPreviousMedia = useCallback(() => { + if (isFirst) { + return; + } + + setCurrentPhotoIndex(currentPhotoIndex - 1); + }, [currentPhotoIndex, isFirst]); + + const selectNextMedia = useCallback(() => { + if (isLast) { + return; + } + + setCurrentPhotoIndex(currentPhotoIndex + 1); + }, [currentPhotoIndex, isLast]); + + // Support for swipe gestures and closing on click + useEffect(() => { + const element = document.querySelector( + '.profile-slide-container > .active, .profile-slide-container > .to', + ); + if (!element) { + return undefined; + } + + return captureEvents(element, { + excludedClosestSelector: '.navigation', + onSwipe: IS_TOUCH_ENV ? (e, direction) => { + if (direction === SwipeDirection.Right) { + selectPreviousMedia(); + } else if (direction === SwipeDirection.Left) { + selectNextMedia(); + } + } : undefined, + }); + }, [selectNextMedia, selectPreviousMedia]); + + if (!user && !chat) { + return undefined; + } + + function renderPhotoTabs() { + if (isSavedMessages || !photos || photos.length <= 1) { + return undefined; + } + + return ( +
+ {photos.map((_, i) => ( + + ))} +
+ ); + } + + function renderPhoto() { + const photo = !isSavedMessages && photos && photos.length > 0 ? photos[currentPhotoIndex] : undefined; + + return ( + + ); + } + + function renderStatus() { + if (user) { + return ( +
+ {getUserStatus(user, lang)} +
+ ); + } + + return ( + { + isChatChannel(chat!) + ? lang('Subscribers', chat!.membersCount, 'i') + : lang('Members', chat!.membersCount, 'i') + } + + ); + } + + const isVerifiedIconShown = (user && user.isVerified) || (chat && chat.isVerified); + + return ( +
+
+ {renderPhotoTabs()} + + {renderPhoto} + + + {!isFirst && ( +
+ +
+ {isSavedMessages ? ( +
+

{lang('SavedMessages')}

+
+ ) : ( +
+

{fullName && renderText(fullName)}

+ {isVerifiedIconShown && } +
+ )} + {!isSavedMessages && renderStatus()} +
+
+ ); +}; + +export default memo(withGlobal( + (global, { userId, forceShowSelf }): StateProps => { + const { lastSyncTime } = global; + const user = selectUser(global, userId); + const chat = selectChat(global, userId); + const isSavedMessages = !forceShowSelf && user && user.isSelf; + const { + animationLevel, + } = global.settings.byKey; + + return { + lastSyncTime, user, chat, isSavedMessages, animationLevel, + }; + }, + (setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']), +)(PrivateChatInfo)); diff --git a/src/components/right/ProfilePhoto.scss b/src/components/right/ProfilePhoto.scss new file mode 100644 index 000000000..34108f928 --- /dev/null +++ b/src/components/right/ProfilePhoto.scss @@ -0,0 +1,44 @@ +.ProfilePhoto { + width: 100%; + height: 100%; + cursor: pointer; + position: relative; + + img { + width: 100%; + object-fit: cover; + } + + .prev-avatar-media { + position: absolute; + left: 0; + top: 0; + z-index: -1; + } + + .spinner-wrapper { + width: 100%; + height: 100%; + } + + .spinner-wrapper, + &.deleted-account, + &.no-photo, + &.saved-messages { + display: flex; + align-items: center; + justify-content: center; + color: var(--color-white); + background: linear-gradient(var(--color-white) -125%, var(--color-user)); + cursor: default; + } + + &.no-photo { + font-size: 14rem; + } + + &.deleted-account, + &.saved-messages { + font-size: 20rem; + } +} diff --git a/src/components/right/ProfilePhoto.tsx b/src/components/right/ProfilePhoto.tsx new file mode 100644 index 000000000..bf75f8934 --- /dev/null +++ b/src/components/right/ProfilePhoto.tsx @@ -0,0 +1,111 @@ +import React, { FC, memo } from '../../lib/teact/teact'; + +import { + ApiUser, ApiChat, ApiMediaFormat, ApiPhoto, +} from '../../api/types'; + +import { + getChatAvatarHash, isDeletedUser, getUserColorKey, getChatTitle, isChatPrivate, getUserFullName, +} from '../../modules/helpers'; +import renderText from '../common/helpers/renderText'; +import buildClassName from '../../util/buildClassName'; +import { getFirstLetters } from '../../util/textFormat'; +import useMedia from '../../hooks/useMedia'; +import useBlurSync from '../../hooks/useBlurSync'; +import usePrevious from '../../hooks/usePrevious'; + +import Spinner from '../ui/Spinner'; + +import './ProfilePhoto.scss'; + +type OwnProps = { + chat?: ApiChat; + user?: ApiUser; + isFirstPhoto?: boolean; + isSavedMessages?: boolean; + photo?: ApiPhoto; + lastSyncTime?: number; + onClick: NoneToVoidFunction; +}; + +const ProfilePhoto: FC = ({ + chat, + user, + photo, + isFirstPhoto, + isSavedMessages, + lastSyncTime, + onClick, +}) => { + const isDeleted = user && isDeletedUser(user); + + function getMediaHash(size: 'normal' | 'big' = 'big', forceAvatar?: boolean) { + if (photo && !forceAvatar) { + return `photo${photo.id}?size=c`; + } + + let hash: string | undefined; + if (!isSavedMessages && !isDeleted) { + if (user) { + hash = getChatAvatarHash(user, size); + } else if (chat) { + hash = getChatAvatarHash(chat, size); + } + } + + return hash; + } + + const imageHash = getMediaHash(); + const fullMediaData = useMedia(imageHash, false, ApiMediaFormat.BlobUrl, lastSyncTime); + const avatarThumbnailData = useMedia( + !fullMediaData && isFirstPhoto ? getMediaHash('normal', true) : undefined, + false, + ApiMediaFormat.BlobUrl, + lastSyncTime, + ); + const thumbDataUri = useBlurSync(!fullMediaData && photo && photo.thumbnail && photo.thumbnail.dataUri); + const imageSrc = fullMediaData || avatarThumbnailData || thumbDataUri; + const prevImageSrc = usePrevious(imageSrc); + + let content: string | undefined = ''; + + if (isSavedMessages) { + content = ; + } else if (isDeleted) { + content = ; + } else if (imageSrc) { + content = ; + } else if (!imageSrc && user) { + const userFullName = getUserFullName(user); + content = userFullName ? getFirstLetters(userFullName, 2) : undefined; + } else if (!imageSrc && chat) { + const title = getChatTitle(chat); + content = title && getFirstLetters(title, isChatPrivate(chat.id) ? 2 : 1); + } else { + content = ( +
+ +
+ ); + } + + const fullClassName = buildClassName( + 'ProfilePhoto', + `color-bg-${getUserColorKey(user || chat)}`, + isSavedMessages && 'saved-messages', + isDeleted && 'deleted-account', + (!isSavedMessages && !(imageSrc)) && 'no-photo', + ); + + return ( +
+ {prevImageSrc && imageSrc && prevImageSrc !== imageSrc && ( + + )} + {typeof content === 'string' ? renderText(content, ['hq_emoji']) : content} +
+ ); +}; + +export default memo(ProfilePhoto); diff --git a/src/components/right/RightColumn.scss b/src/components/right/RightColumn.scss index fba30c827..0ee6b7540 100644 --- a/src/components/right/RightColumn.scss +++ b/src/components/right/RightColumn.scss @@ -24,6 +24,7 @@ @media (max-width: 1275px) { box-shadow: 0 .25rem .5rem .125rem var(--color-default-shadow); + border-left: none; } @media (max-width: 600px) { @@ -36,8 +37,7 @@ overflow: hidden; } - .Management .section > .ChatInfo, - .profile-info > .ChatInfo { + .Management .section > .ChatInfo { padding: 0 1.5rem; margin: 1rem 0; text-align: center; diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index 5c62ca884..28b880838 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -264,7 +264,7 @@ const RightHeader: FC = ({ default: return ( <> -

{lang('Info')}

+

Profile

{canManage && (
- -
- {lang('ChannelSubscribers')} - {lang('Subscribers', chat.membersCount!, 'i')} -
+ + {lang('ChannelSubscribers')} + {lang('Subscribers', chat.membersCount!, 'i')}
diff --git a/src/components/right/management/ManageChatAdministrators.tsx b/src/components/right/management/ManageChatAdministrators.tsx index ee57b2ded..3a84623c3 100644 --- a/src/components/right/management/ManageChatAdministrators.tsx +++ b/src/components/right/management/ManageChatAdministrators.tsx @@ -79,11 +79,9 @@ const ManageChatAdministrators: FC = ({
- -
- {lang('EventLog')} - {lang(isChannel ? 'EventLogInfoDetailChannel' : 'EventLogInfoDetail')} -
+ + {lang('EventLog')} + {lang(isChannel ? 'EventLogInfoDetailChannel' : 'EventLogInfoDetail')}
diff --git a/src/components/right/management/ManageGroup.tsx b/src/components/right/management/ManageGroup.tsx index c027dd04d..4cb0544f4 100644 --- a/src/components/right/management/ManageGroup.tsx +++ b/src/components/right/management/ManageGroup.tsx @@ -228,40 +228,30 @@ const ManageGroup: FC = ({ disabled={!canChangeInfo} /> {chat.isCreator && ( - -
- {lang('GroupType')} - {chat.username ? lang('TypePublic') : lang('TypePrivate')} -
+ + {lang('GroupType')} + {chat.username ? lang('TypePublic') : lang('TypePrivate')} )} {hasLinkedChannel && ( - -
- {lang('LinkedChannel')} - {lang('DiscussionUnlink')} -
+ + {lang('LinkedChannel')} + {lang('DiscussionUnlink')} )} - -
- {lang('ChannelPermissions')} - {enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT} -
+ + {lang('ChannelPermissions')} + {enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT} - -
- {lang('ChannelAdministrators')} - {formatInteger(adminsCount)} -
+ + {lang('ChannelAdministrators')} + {formatInteger(adminsCount)}
- -
- {lang('GroupMembers')} - {formatInteger(chat.membersCount!)} -
+ + {lang('GroupMembers')} + {formatInteger(chat.membersCount!)} {chat.fullInfo && ( diff --git a/src/components/right/management/ManageGroupPermissions.tsx b/src/components/right/management/ManageGroupPermissions.tsx index 6cd8ff100..c258b5c38 100644 --- a/src/components/right/management/ManageGroupPermissions.tsx +++ b/src/components/right/management/ManageGroupPermissions.tsx @@ -240,11 +240,9 @@ const ManageGroupPermissions: FC = ({
- -
- {lang('ChannelBlockedUsers')} - {removedUsersCount} -
+ + {lang('ChannelBlockedUsers')} + {removedUsersCount}
diff --git a/src/components/right/management/ManageUser.tsx b/src/components/right/management/ManageUser.tsx index 629340791..a7aaa1e58 100644 --- a/src/components/right/management/ManageUser.tsx +++ b/src/components/right/management/ManageUser.tsx @@ -144,7 +144,6 @@ const ManageUser: FC = ({ userId={user.id} avatarSize="jumbo" status="original name" - withMediaViewer withFullInfo /> i { + position: relative; + top: .25rem; + } + } + &.disabled { pointer-events: none; @@ -242,6 +248,7 @@ .multiline-item { white-space: initial; + overflow: hidden; .title, .subtitle { display: block; @@ -250,6 +257,8 @@ .title { line-height: 1.25rem; + overflow: hidden; + text-overflow: ellipsis; } .subtitle { @@ -266,5 +275,4 @@ } } } - } diff --git a/src/components/ui/ListItem.tsx b/src/components/ui/ListItem.tsx index ef1317bce..15d36f05a 100644 --- a/src/components/ui/ListItem.tsx +++ b/src/components/ui/ListItem.tsx @@ -33,6 +33,7 @@ type OwnProps = { inactive?: boolean; focus?: boolean; destructive?: boolean; + multiline?: boolean; contextActions?: MenuItemContextAction[]; onClick?: OnClickHandler; }; @@ -48,9 +49,10 @@ const ListItem: FC = (props) => { ripple, narrow, inactive, - contextActions, focus, destructive, + multiline, + contextActions, onClick, } = props; @@ -118,6 +120,7 @@ const ListItem: FC = (props) => { contextMenuPosition && 'has-menu-open', focus && 'focus', destructive && 'destructive', + multiline && 'multiline', ); return ( @@ -138,7 +141,8 @@ const ListItem: FC = (props) => { {icon && ( )} - {children} + {multiline && (
{children}
)} + {!multiline && children} {!disabled && !inactive && ripple && ( )} diff --git a/src/components/ui/Switcher.scss b/src/components/ui/Switcher.scss index a0b9a0591..2f2f332af 100644 --- a/src/components/ui/Switcher.scss +++ b/src/components/ui/Switcher.scss @@ -9,6 +9,10 @@ opacity: 0.5; } + &.inactive { + pointer-events: none; + } + input { height: 0; width: 0; diff --git a/src/components/ui/Switcher.tsx b/src/components/ui/Switcher.tsx index 705748cea..1af4f94af 100644 --- a/src/components/ui/Switcher.tsx +++ b/src/components/ui/Switcher.tsx @@ -12,6 +12,7 @@ type OwnProps = { label: string; checked?: boolean; disabled?: boolean; + inactive?: boolean; onChange?: (e: ChangeEvent) => void; onCheck?: (isChecked: boolean) => void; }; @@ -23,22 +24,24 @@ const Switcher: FC = ({ label, checked = false, disabled, + inactive, onChange, onCheck, }) => { - const handleChange = useCallback((event: ChangeEvent) => { + const handleChange = useCallback((e: ChangeEvent) => { if (onChange) { - onChange(event); + onChange(e); } if (onCheck) { - onCheck(event.currentTarget.checked); + onCheck(e.currentTarget.checked); } }, [onChange, onCheck]); const className = buildClassName( 'Switcher', disabled && 'disabled', + inactive && 'inactive', ); return ( diff --git a/src/components/ui/Tab.scss b/src/components/ui/Tab.scss index 1d33077b6..51a723b58 100644 --- a/src/components/ui/Tab.scss +++ b/src/components/ui/Tab.scss @@ -67,8 +67,6 @@ width: 100%; border-radius: .1875rem .1875rem 0 0; pointer-events: none; - padding-right: .5rem; - margin-left: -.25rem; box-sizing: content-box; transform-origin: left; diff --git a/src/components/ui/TabList.scss b/src/components/ui/TabList.scss index 73db6fa4d..8333b29c4 100644 --- a/src/components/ui/TabList.scss +++ b/src/components/ui/TabList.scss @@ -5,7 +5,7 @@ display: flex; justify-content: space-between; align-items: flex-end; - font-size: 0.875rem; + font-size: 1rem; flex-wrap: nowrap; box-shadow: 0 2px 2px var(--color-light-shadow); background-color: var(--color-background); diff --git a/src/config.ts b/src/config.ts index bcbffa598..8899f3b53 100644 --- a/src/config.ts +++ b/src/config.ts @@ -53,6 +53,7 @@ export const GLOBAL_SEARCH_SLICE = 20; export const CHANNEL_MEMBERS_LIMIT = 30; export const PINNED_MESSAGES_LIMIT = 50; export const BLOCKED_LIST_LIMIT = 100; +export const PROFILE_PHOTOS_LIMIT = 40; export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 25; export const ALL_CHATS_PRELOAD_DISABLED = false; diff --git a/src/global/types.ts b/src/global/types.ts index 3d6cccf71..a1323571a 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -276,6 +276,7 @@ export type GlobalState = { threadId?: number; messageId?: number; avatarOwnerId?: number; + profilePhotoIndex?: number; origin?: MediaViewerOrigin; }; @@ -392,6 +393,7 @@ export type ActionTypes = ( 'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' | 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' | + 'loadProfilePhotos' | // messages 'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' | 'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' | diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 8bb177d2b..c22f3389c 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -949,6 +949,7 @@ updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; photos.uploadProfilePhoto#89f30f69 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo; +photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool; upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File; upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; diff --git a/src/lib/gramjs/tl/static/api.reduced.tl b/src/lib/gramjs/tl/static/api.reduced.tl index 0e085042b..f78ef168a 100644 --- a/src/lib/gramjs/tl/static/api.reduced.tl +++ b/src/lib/gramjs/tl/static/api.reduced.tl @@ -949,6 +949,7 @@ updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; photos.uploadProfilePhoto#89f30f69 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo; +photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool; upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File; upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; diff --git a/src/modules/actions/api/users.ts b/src/modules/actions/api/users.ts index 9252e47b3..ce7d3a75e 100644 --- a/src/modules/actions/api/users.ts +++ b/src/modules/actions/api/users.ts @@ -7,10 +7,11 @@ import { ManagementProgress } from '../../../types'; import { debounce } from '../../../util/schedulers'; import { buildCollectionByKey } from '../../../util/iteratees'; +import { isChatPrivate } from '../../helpers'; import { callApi } from '../../../api/gramjs'; -import { selectUser } from '../../selectors'; +import { selectChat, selectUser } from '../../selectors'; import { - addChats, addUsers, updateManagementProgress, updateUser, updateUsers, + addChats, addUsers, updateChat, updateManagementProgress, updateUser, updateUsers, } from '../../reducers'; const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true); @@ -170,3 +171,27 @@ async function deleteUser(userId: number) { await callApi('deleteUser', { id, accessHash }); } + +addReducer('loadProfilePhotos', (global, actions, payload) => { + const { profileId } = payload!; + const isPrivate = isChatPrivate(profileId); + const user = isPrivate ? selectUser(global, profileId) : undefined; + const chat = !isPrivate ? selectChat(global, profileId) : undefined; + + (async () => { + const result = await callApi('fetchProfilePhotos', user, chat); + if (!result || !result.photos) { + return; + } + + let newGlobal = getGlobal(); + if (isPrivate) { + newGlobal = updateUser(newGlobal, profileId, { photos: result.photos }); + } else { + newGlobal = addUsers(newGlobal, buildCollectionByKey(result.users!, 'id')); + newGlobal = updateChat(newGlobal, profileId, { photos: result.photos }); + } + + setGlobal(newGlobal); + })(); +}); diff --git a/src/modules/actions/apiUpdaters/chats.ts b/src/modules/actions/apiUpdaters/chats.ts index eb0208bab..6a4c43496 100644 --- a/src/modules/actions/apiUpdaters/chats.ts +++ b/src/modules/actions/apiUpdaters/chats.ts @@ -31,7 +31,7 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { actions.loadTopChats(); } - setGlobal(updateChat(global, update.id, update.chat)); + setGlobal(updateChat(global, update.id, update.chat, update.newProfilePhoto)); break; } @@ -330,5 +330,17 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { break; } + + case 'deleteProfilePhotos': { + const { chatId, ids } = update; + const chat = global.chats.byId[chatId]; + + if (chat && chat.photos) { + setGlobal(updateChat(global, chatId, { + photos: chat.photos.filter((photo) => !ids.includes(photo.id)), + })); + } + break; + } } }); diff --git a/src/modules/actions/ui/messages.ts b/src/modules/actions/ui/messages.ts index b7411682f..2c6488335 100644 --- a/src/modules/actions/ui/messages.ts +++ b/src/modules/actions/ui/messages.ts @@ -85,7 +85,7 @@ addReducer('editLastMessage', (global) => { addReducer('openMediaViewer', (global, actions, payload) => { const { - chatId, threadId, messageId, avatarOwnerId, origin, + chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin, } = payload!; return { @@ -95,6 +95,7 @@ addReducer('openMediaViewer', (global, actions, payload) => { threadId, messageId, avatarOwnerId, + profilePhotoIndex, origin, }, forwardMessages: {}, diff --git a/src/modules/reducers/chats.ts b/src/modules/reducers/chats.ts index 557dc0b65..3982cb18a 100644 --- a/src/modules/reducers/chats.ts +++ b/src/modules/reducers/chats.ts @@ -1,5 +1,5 @@ import { GlobalState } from '../../global/types'; -import { ApiChat } from '../../api/types'; +import { ApiChat, ApiPhoto } from '../../api/types'; import { ARCHIVED_FOLDER_ID } from '../../config'; import { omit } from '../../util/iteratees'; @@ -47,13 +47,16 @@ export function replaceChats(global: GlobalState, newById: Record): GlobalState { +export function updateChat( + global: GlobalState, chatId: number, chatUpdate: Partial, photo?: ApiPhoto, +): GlobalState { const { byId } = global.chats; const chat = byId[chatId]; const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin; const updatedChat = { ...chat, ...(shouldOmitMinInfo ? omit(chatUpdate, ['isMin', 'accessHash']) : chatUpdate), + ...(photo && { photos: [photo, ...(chat.photos || [])] }), }; if (!updatedChat.id || !updatedChat.type) { diff --git a/src/modules/reducers/messages.ts b/src/modules/reducers/messages.ts index 189ed784d..427d4741e 100644 --- a/src/modules/reducers/messages.ts +++ b/src/modules/reducers/messages.ts @@ -173,10 +173,10 @@ export function deleteChatMessages( if (!byId) { return global; } + const newById = omit(byId, messageIds); const deletedForwardedPosts = Object.values(pickTruthy(byId, messageIds)).filter( ({ forwardInfo }) => forwardInfo && forwardInfo.isLinkedChannelPost, ); - const newById = omit(byId, messageIds); const threadIds = Object.keys(global.messages.byChatId[chatId].threadsById).map(Number); threadIds.forEach((threadId) => { diff --git a/src/styles/_common.scss b/src/styles/_common.scss index 67860c358..ee662f3ba 100644 --- a/src/styles/_common.scss +++ b/src/styles/_common.scss @@ -94,3 +94,42 @@ text-align: center; } } + +// Used by Avatar and ProfilePhoto components +div { + &.color-bg-1 { + --color-user: var(--color-user-1); + } + + &.color-bg-2 { + --color-user: var(--color-user-2); + } + + &.color-bg-4 { + --color-user: var(--color-user-4); + } + + &.color-bg-5 { + --color-user: var(--color-user-5); + } + + &.color-bg-6 { + --color-user: var(--color-user-6); + } + + &.color-bg-7 { + --color-user: var(--color-user-7); + } + + &.color-bg-8 { + --color-user: var(--color-user-8); + } + + &.saved-messages { + --color-user: var(--color-primary); + } + + &.deleted-account { + --color-user: var(--color-gray); + } +} diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 7134dd224..53db564ba 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -56,6 +56,8 @@ $color-user-8: #faa774; :root { --color-background: #{$color-white}; --color-background-selected: #f4f4f5; + --color-background-secondary: #f4f4f5; + --color-background-secondary-accent: #E4E4E5; --color-background-own: #{$color-light-green}; --color-background-own-selected: #{darken($color-light-green, 10%)}; --color-background-own-rgb: #{toRGB($color-light-green)}; @@ -144,7 +146,7 @@ $color-user-8: #faa774; --border-radius-messages-small: 0.375rem; --messages-container-width: 45.5rem; --right-column-width: 26.5rem; - --header-height: 3.625rem; + --header-height: 3.5rem; --symbol-menu-width: 26.25rem; --symbol-menu-height: 23.25rem; @@ -156,7 +158,6 @@ $color-user-8: #faa774; @media (max-width: 600px) { --right-column-width: 100vw; - --header-height: 3.5rem; --symbol-menu-width: 100vw; --symbol-menu-height: 14.6875rem; } diff --git a/src/styles/reboot.css b/src/styles/reboot.css index 7a0e44731..f4818d9be 100644 --- a/src/styles/reboot.css +++ b/src/styles/reboot.css @@ -123,7 +123,7 @@ sup { } a { - color: theme-color("primary"); + color: var(--color-links); text-decoration: none; background-color: transparent; -webkit-text-decoration-skip: objects; diff --git a/src/styles/themes.json b/src/styles/themes.json index b39272b5f..75b76b47e 100644 --- a/src/styles/themes.json +++ b/src/styles/themes.json @@ -3,6 +3,8 @@ "--color-primary-opacity": ["#50A2E980", "#8378DB80"], "--color-primary-shade": ["#4a95d6", "#7b71c6"], "--color-background": ["#FFFFFF", "#212121"], + "--color-background-secondary": ["#f4f4f5", "#121212"], + "--color-background-secondary-accent": ["#E4E4E5", "#100f10"], "--color-background-own": ["#EEFEDF", "#8378DB"], "--color-background-selected": ["#F4F4F5", "#2C2C2C"], "--color-background-own-selected": ["#d4fcae", "#7b71c6"], diff --git a/src/util/mediaLoader.ts b/src/util/mediaLoader.ts index 35ff44ca4..071731ff4 100644 --- a/src/util/mediaLoader.ts +++ b/src/util/mediaLoader.ts @@ -157,7 +157,6 @@ async function fetchFromCacheOrRemote(url: string, mediaFormat: ApiMediaFormat, const media = await webpToPng(url, blob); if (media) { prepared = prepareMedia(media); - mimeType = blob.type; } }