mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 05:11:55 +01:00
Profile: New Design (#1051)
This commit is contained in:
parent
852d195842
commit
20d2656261
@ -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'),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,10 @@ interface LocalDb {
|
||||
// Used for loading avatars and media through in-memory Gram JS instances.
|
||||
chats: Record<number, GramJs.Chat | GramJs.Channel>;
|
||||
users: Record<number, GramJs.User>;
|
||||
messages: Record<string, GramJs.Message>;
|
||||
messages: Record<string, GramJs.Message | GramJs.MessageService>;
|
||||
documents: Record<string, GramJs.Document>;
|
||||
stickerSets: Record<string, GramJs.StickerSet>;
|
||||
photos: Record<string, GramJs.Photo>;
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -18,4 +19,5 @@ export default {
|
||||
messages: {},
|
||||
documents: {},
|
||||
stickerSets: {},
|
||||
photos: {},
|
||||
} as LocalDb;
|
||||
|
@ -26,7 +26,7 @@ export {
|
||||
export {
|
||||
fetchFullUser, fetchNearestCountry,
|
||||
fetchTopUsers, fetchContactList, fetchUsers,
|
||||
updateContact, deleteUser,
|
||||
updateContact, deleteUser, fetchProfilePhotos,
|
||||
} from './users';
|
||||
|
||||
export {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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<ApiPhoto>(Boolean as any),
|
||||
users,
|
||||
};
|
||||
}
|
||||
|
||||
function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice)) {
|
||||
result.photos.forEach(addPhotoToLocalDb);
|
||||
}
|
||||
|
@ -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<string>(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(() => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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<ApiChat>;
|
||||
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 |
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -156,12 +156,11 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
<ListItem
|
||||
className="mb-2"
|
||||
narrow
|
||||
multiline
|
||||
onClick={() => onEditFolder(foldersById[folder.id])}
|
||||
>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{folder.title}</span>
|
||||
<span className="subtitle">{folder.subtitle}</span>
|
||||
</div>
|
||||
<span className="title">{folder.title}</span>
|
||||
<span className="subtitle">{folder.subtitle}</span>
|
||||
</ListItem>
|
||||
)) : userFolders && !userFolders.length ? (
|
||||
<p className="settings-item-description my-4">
|
||||
|
@ -75,6 +75,7 @@ type StateProps = {
|
||||
senderId?: number;
|
||||
origin?: MediaViewerOrigin;
|
||||
avatarOwner?: ApiChat | ApiUser;
|
||||
profilePhotoIndex?: number;
|
||||
message?: ApiMessage;
|
||||
chatMessages?: Record<number, ApiMessage>;
|
||||
collectionIds?: number[];
|
||||
@ -92,6 +93,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
|
||||
senderId,
|
||||
origin,
|
||||
avatarOwner,
|
||||
profilePhotoIndex,
|
||||
message,
|
||||
chatMessages,
|
||||
collectionIds,
|
||||
@ -116,7 +118,9 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
|
||||
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<number | undefined>(senderId);
|
||||
const [canPanZoomWrap, setCanPanZoomWrap] = useState(false);
|
||||
const [isZoomed, setIsZoomed] = useState<boolean>(false);
|
||||
@ -137,8 +141,11 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
|
||||
}
|
||||
|
||||
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<StateProps & DispatchProps> = ({
|
||||
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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<GlobalState, 'lastSyncTime'>;
|
||||
|
||||
const ChatExtra: FC<OwnProps & StateProps> = ({ chat, canInviteUsers }) => {
|
||||
type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'updateChatMutedState' | 'showNotification'>;
|
||||
|
||||
const ChatExtra: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
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 (
|
||||
<div className="ChatExtra">
|
||||
{description && !!description.length && (
|
||||
<div className="item">
|
||||
<i className="icon-info" />
|
||||
<div>
|
||||
<p className="title">{renderText(description, ['br', 'links', 'emoji'])}</p>
|
||||
<p className="subtitle">{lang('Info')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{formattedNumber && !!formattedNumber.length && (
|
||||
<ListItem icon="phone" multiline narrow ripple onClick={() => handleClick(formattedNumber, lang('Phone'))}>
|
||||
<span className="title">{formattedNumber}</span>
|
||||
<span className="subtitle">{lang('Phone')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{canInviteUsers && !!link.length && (
|
||||
<div className="item">
|
||||
<i className="icon-mention" />
|
||||
<div>
|
||||
{printedUsername && (
|
||||
<ListItem
|
||||
icon="mention"
|
||||
multiline
|
||||
narrow
|
||||
ripple
|
||||
onClick={() => handleClick(`@${printedUsername}`, lang('Username'))}
|
||||
>
|
||||
<span className="title">{renderText(printedUsername)}</span>
|
||||
<span className="subtitle">{lang('Username')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{printedDescription && !!printedDescription.length && (
|
||||
<ListItem
|
||||
icon="info"
|
||||
multiline
|
||||
narrow
|
||||
ripple
|
||||
onClick={() => handleClick(printedDescription, lang(userId ? 'UserBio' : 'Info'))}
|
||||
>
|
||||
<span className="title">{renderText(printedDescription, ['br', 'links', 'emoji'])}</span>
|
||||
<span className="subtitle">{lang(userId ? 'UserBio' : 'Info')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{canInviteUsers && !printedUsername && !!link.length && (
|
||||
<ListItem icon="mention" multiline narrow ripple onClick={() => handleClick(link, lang('SetUrlPlaceholder'))}>
|
||||
<div className="title">
|
||||
<SafeLink url={url} className="title" text={link} />
|
||||
<p className="subtitle">{lang('SetUrlPlaceholder')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="subtitle">{lang('SetUrlPlaceholder')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem icon="unmute" onClick={handleNotificationChange}>
|
||||
<span>{lang('Notifications')}</span>
|
||||
<Switcher
|
||||
id="group-notifications"
|
||||
label={`${userId ? 'Toggle User Notifications' : 'Toggle Chat Notifications'}`}
|
||||
checked={!currentIsMuted}
|
||||
inactive
|
||||
/>
|
||||
</ListItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(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));
|
||||
|
@ -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 {
|
||||
|
@ -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<GlobalActions, (
|
||||
'setLocalMediaSearchType' | 'searchMediaMessagesLocal' | 'openMediaViewer' |
|
||||
'openAudioPlayer' | 'openUserInfo' | 'focusMessage'
|
||||
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos'
|
||||
)>;
|
||||
|
||||
const TABS = [
|
||||
@ -108,6 +107,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
openAudioPlayer,
|
||||
openUserInfo,
|
||||
focusMessage,
|
||||
loadProfilePhotos,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -148,6 +148,12 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
|
||||
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<OwnProps & StateProps & DispatchProps> = ({
|
||||
function renderProfileInfo(chatId: number, resolvedUserId?: number) {
|
||||
return (
|
||||
<div className="profile-info">
|
||||
{resolvedUserId ? (
|
||||
<>
|
||||
<PrivateChatInfo
|
||||
userId={resolvedUserId}
|
||||
avatarSize="jumbo"
|
||||
forceShowSelf={resolvedUserId !== chatId}
|
||||
withMediaViewer
|
||||
withFullInfo
|
||||
/>
|
||||
<UserExtra userId={resolvedUserId} forceShowSelf={resolvedUserId !== chatId} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GroupChatInfo chatId={chatId} avatarSize="jumbo" withMediaViewer withFullInfo />
|
||||
<GroupExtra chatId={chatId} />
|
||||
</>
|
||||
)}
|
||||
<ProfileInfo
|
||||
userId={resolvedUserId || chatId}
|
||||
forceShowSelf={resolvedUserId !== chatId}
|
||||
/>
|
||||
<ChatExtra chatOrUserId={resolvedUserId || chatId} forceShowSelf={resolvedUserId !== chatId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -408,5 +402,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
'openAudioPlayer',
|
||||
'openUserInfo',
|
||||
'focusMessage',
|
||||
'loadProfilePhotos',
|
||||
]),
|
||||
)(Profile));
|
||||
|
133
src/components/right/ProfileInfo.scss
Normal file
133
src/components/right/ProfileInfo.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
235
src/components/right/ProfileInfo.tsx
Normal file
235
src/components/right/ProfileInfo.tsx
Normal file
@ -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<GlobalState, 'lastSyncTime'>;
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>;
|
||||
|
||||
const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
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<HTMLDivElement>(
|
||||
'.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 (
|
||||
<div className="photo-dashes">
|
||||
{photos.map((_, i) => (
|
||||
<span className={`photo-dash ${i === currentPhotoIndex ? 'current' : ''}`} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPhoto() {
|
||||
const photo = !isSavedMessages && photos && photos.length > 0 ? photos[currentPhotoIndex] : undefined;
|
||||
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={currentPhotoIndex}
|
||||
user={user}
|
||||
chat={chat}
|
||||
photo={photo}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isFirstPhoto={isFirst}
|
||||
onClick={handleProfilePhotoClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStatus() {
|
||||
if (user) {
|
||||
return (
|
||||
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}>
|
||||
<span className="user-status">{getUserStatus(user, lang)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="status">{
|
||||
isChatChannel(chat!)
|
||||
? lang('Subscribers', chat!.membersCount, 'i')
|
||||
: lang('Members', chat!.membersCount, 'i')
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const isVerifiedIconShown = (user && user.isVerified) || (chat && chat.isVerified);
|
||||
|
||||
return (
|
||||
<div className="ProfileInfo">
|
||||
<div className="photo-wrapper">
|
||||
{renderPhotoTabs()}
|
||||
<Transition activeKey={currentPhotoIndex} name={slideAnimation} className="profile-slide-container">
|
||||
{renderPhoto}
|
||||
</Transition>
|
||||
|
||||
{!isFirst && (
|
||||
<button
|
||||
type="button"
|
||||
className="navigation prev"
|
||||
aria-label={lang('AccDescrPrevious')}
|
||||
onClick={selectPreviousMedia}
|
||||
/>
|
||||
)}
|
||||
{!isLast && (
|
||||
<button
|
||||
type="button"
|
||||
className="navigation next"
|
||||
aria-label={lang('Next')}
|
||||
onClick={selectNextMedia}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="info">
|
||||
{isSavedMessages ? (
|
||||
<div className="title">
|
||||
<h3>{lang('SavedMessages')}</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div className="title">
|
||||
<h3>{fullName && renderText(fullName)}</h3>
|
||||
{isVerifiedIconShown && <VerifiedIcon />}
|
||||
</div>
|
||||
)}
|
||||
{!isSavedMessages && renderStatus()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(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));
|
44
src/components/right/ProfilePhoto.scss
Normal file
44
src/components/right/ProfilePhoto.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
111
src/components/right/ProfilePhoto.tsx
Normal file
111
src/components/right/ProfilePhoto.tsx
Normal file
@ -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<OwnProps> = ({
|
||||
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 = <i className="icon-avatar-saved-messages" />;
|
||||
} else if (isDeleted) {
|
||||
content = <i className="icon-avatar-deleted-account" />;
|
||||
} else if (imageSrc) {
|
||||
content = <img src={imageSrc} className="avatar-media" alt="" decoding="async" />;
|
||||
} 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 = (
|
||||
<div className="spinner-wrapper">
|
||||
<Spinner color="white" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
'ProfilePhoto',
|
||||
`color-bg-${getUserColorKey(user || chat)}`,
|
||||
isSavedMessages && 'saved-messages',
|
||||
isDeleted && 'deleted-account',
|
||||
(!isSavedMessages && !(imageSrc)) && 'no-photo',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={fullClassName} onClick={imageSrc ? onClick : undefined}>
|
||||
{prevImageSrc && imageSrc && prevImageSrc !== imageSrc && (
|
||||
<img src={prevImageSrc} className="prev-avatar-media" alt="" decoding="async" />
|
||||
)}
|
||||
{typeof content === 'string' ? renderText(content, ['hq_emoji']) : content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ProfilePhoto);
|
@ -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;
|
||||
|
@ -264,7 +264,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<h3>{lang('Info')}</h3>
|
||||
<h3>Profile</h3>
|
||||
<section className="tools">
|
||||
{canManage && (
|
||||
<Button
|
||||
|
@ -1,91 +0,0 @@
|
||||
import React, { FC, useEffect, memo } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
import { ApiUser } from '../../api/types';
|
||||
import { GlobalActions, GlobalState } from '../../global/types';
|
||||
|
||||
import { selectUser } from '../../modules/selectors';
|
||||
import { formatPhoneNumberWithCode } from '../../util/phoneNumber';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
type OwnProps = {
|
||||
userId: number;
|
||||
forceShowSelf?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
user?: ApiUser;
|
||||
} & Pick<GlobalState, 'lastSyncTime'>;
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'loadFullUser'>;
|
||||
|
||||
const UserExtra: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
lastSyncTime, user, forceShowSelf, loadFullUser,
|
||||
}) => {
|
||||
const {
|
||||
id: userId,
|
||||
fullInfo,
|
||||
username,
|
||||
phoneNumber,
|
||||
isSelf,
|
||||
} = user || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
loadFullUser({ userId });
|
||||
}
|
||||
}, [loadFullUser, userId, lastSyncTime]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
if (!user || (isSelf && !forceShowSelf)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const bio = fullInfo && fullInfo.bio;
|
||||
const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneNumber);
|
||||
|
||||
return (
|
||||
<div className="ChatExtra">
|
||||
{bio && !!bio.length && (
|
||||
<div className="item">
|
||||
<i className="icon-info" />
|
||||
<div>
|
||||
<p className="title">{renderText(bio, ['br', 'links', 'emoji'])}</p>
|
||||
<p className="subtitle">{lang('UserBio')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{username && !!username.length && (
|
||||
<div className="item">
|
||||
<i className="icon-mention" />
|
||||
<div>
|
||||
<p className="title">{renderText(username)}</p>
|
||||
<p className="subtitle">{lang('Username')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{formattedNumber && !!formattedNumber.length && (
|
||||
<div className="item">
|
||||
<i className="icon-phone" />
|
||||
<div>
|
||||
<p className="title">{formattedNumber}</p>
|
||||
<p className="subtitle">{lang('Phone')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { userId }): StateProps => {
|
||||
const { lastSyncTime } = global;
|
||||
const user = selectUser(global, userId);
|
||||
|
||||
return { lastSyncTime, user };
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser']),
|
||||
)(UserExtra));
|
22
src/components/right/hooks/usePhotosPreload.ts
Normal file
22
src/components/right/hooks/usePhotosPreload.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {
|
||||
ApiChat, ApiMediaFormat, ApiPhoto, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import { useEffect } from '../../../lib/teact/teact';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
|
||||
const PHOTOS_TO_PRELOAD = 4;
|
||||
|
||||
export default function usePhotosPreload(
|
||||
profile: ApiUser | ApiChat | undefined,
|
||||
photos: ApiPhoto[],
|
||||
currentIndex: number,
|
||||
) {
|
||||
useEffect(() => {
|
||||
photos.slice(currentIndex, currentIndex + PHOTOS_TO_PRELOAD).forEach((photo) => {
|
||||
const mediaData = mediaLoader.getFromMemory(`photo${photo.id}?size=c`);
|
||||
if (!mediaData) {
|
||||
mediaLoader.fetch(`photo${photo.id}?size=c`, ApiMediaFormat.BlobUrl);
|
||||
}
|
||||
});
|
||||
}, [currentIndex, photos]);
|
||||
}
|
@ -2,12 +2,13 @@ import { useCallback, useEffect } from '../../../lib/teact/teact';
|
||||
|
||||
export default function useTransitionFixes(
|
||||
containerRef: { current: HTMLDivElement | null },
|
||||
transitionElSelector = '.Transition.shared-media-transition',
|
||||
) {
|
||||
// Set `min-height` for shared media container to prevent jumping when switching tabs
|
||||
useEffect(() => {
|
||||
function setMinHeight() {
|
||||
const container = containerRef.current!;
|
||||
const transitionEl = container.querySelector<HTMLDivElement>('.Transition');
|
||||
const transitionEl = container.querySelector<HTMLDivElement>(transitionElSelector);
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.TabList');
|
||||
if (transitionEl && tabsEl) {
|
||||
transitionEl.style.minHeight = `${container.offsetHeight - tabsEl.offsetHeight}px`;
|
||||
@ -21,7 +22,7 @@ export default function useTransitionFixes(
|
||||
return () => {
|
||||
window.removeEventListener('resize', setMinHeight, false);
|
||||
};
|
||||
}, [containerRef]);
|
||||
}, [containerRef, transitionElSelector]);
|
||||
|
||||
// Workaround for scrollable content flickering during animation.
|
||||
const applyTransitionFix = useCallback(() => {
|
||||
|
@ -177,24 +177,18 @@ const ManageChannel: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
disabled={!canChangeInfo}
|
||||
/>
|
||||
{chat.isCreator && (
|
||||
<ListItem icon="lock" ripple onClick={handleClickEditType}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('ChannelType')}</span>
|
||||
<span className="subtitle">{chat.username ? lang('TypePublic') : lang('TypePrivate')}</span>
|
||||
</div>
|
||||
<ListItem icon="lock" ripple multiline onClick={handleClickEditType}>
|
||||
<span className="title">{lang('ChannelType')}</span>
|
||||
<span className="subtitle">{chat.username ? lang('TypePublic') : lang('TypePrivate')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem icon="message" ripple onClick={handleClickDiscussion} disabled={!canChangeInfo}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('Discussion')}</span>
|
||||
<span className="subtitle">{hasLinkedChat ? lang('DiscussionUnlink') : lang('Add')}</span>
|
||||
</div>
|
||||
<ListItem icon="message" multiline ripple onClick={handleClickDiscussion} disabled={!canChangeInfo}>
|
||||
<span className="title">{lang('Discussion')}</span>
|
||||
<span className="subtitle">{hasLinkedChat ? lang('DiscussionUnlink') : lang('Add')}</span>
|
||||
</ListItem>
|
||||
<ListItem icon="admin" ripple onClick={handleClickAdministrators}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('ChannelAdministrators')}</span>
|
||||
<span className="subtitle">{adminsCount}</span>
|
||||
</div>
|
||||
<ListItem icon="admin" multiline ripple onClick={handleClickAdministrators}>
|
||||
<span className="title">{lang('ChannelAdministrators')}</span>
|
||||
<span className="subtitle">{adminsCount}</span>
|
||||
</ListItem>
|
||||
<div className="ListItem no-selection narrow">
|
||||
<Checkbox
|
||||
@ -205,11 +199,9 @@ const ManageChannel: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<ListItem icon="group" ripple onClick={handleClickSubscribers}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('ChannelSubscribers')}</span>
|
||||
<span className="subtitle">{lang('Subscribers', chat.membersCount!, 'i')}</span>
|
||||
</div>
|
||||
<ListItem icon="group" multiline ripple onClick={handleClickSubscribers}>
|
||||
<span className="title">{lang('ChannelSubscribers')}</span>
|
||||
<span className="subtitle">{lang('Subscribers', chat.membersCount!, 'i')}</span>
|
||||
</ListItem>
|
||||
</div>
|
||||
<div className="section">
|
||||
|
@ -79,11 +79,9 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
|
||||
<div className="Management">
|
||||
<div className="custom-scroll">
|
||||
<div className="section">
|
||||
<ListItem icon="recent" ripple onClick={handleRecentActionsClick}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('EventLog')}</span>
|
||||
<span className="subtitle">{lang(isChannel ? 'EventLogInfoDetailChannel' : 'EventLogInfoDetail')}</span>
|
||||
</div>
|
||||
<ListItem icon="recent" multiline ripple onClick={handleRecentActionsClick}>
|
||||
<span className="title">{lang('EventLog')}</span>
|
||||
<span className="subtitle">{lang(isChannel ? 'EventLogInfoDetailChannel' : 'EventLogInfoDetail')}</span>
|
||||
</ListItem>
|
||||
</div>
|
||||
|
||||
|
@ -228,40 +228,30 @@ const ManageGroup: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
disabled={!canChangeInfo}
|
||||
/>
|
||||
{chat.isCreator && (
|
||||
<ListItem icon="lock" ripple onClick={handleClickEditType}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('GroupType')}</span>
|
||||
<span className="subtitle">{chat.username ? lang('TypePublic') : lang('TypePrivate')}</span>
|
||||
</div>
|
||||
<ListItem icon="lock" multiline ripple onClick={handleClickEditType}>
|
||||
<span className="title">{lang('GroupType')}</span>
|
||||
<span className="subtitle">{chat.username ? lang('TypePublic') : lang('TypePrivate')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{hasLinkedChannel && (
|
||||
<ListItem icon="message" ripple onClick={handleClickDiscussion}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('LinkedChannel')}</span>
|
||||
<span className="subtitle">{lang('DiscussionUnlink')}</span>
|
||||
</div>
|
||||
<ListItem icon="message" multiline ripple onClick={handleClickDiscussion}>
|
||||
<span className="title">{lang('LinkedChannel')}</span>
|
||||
<span className="subtitle">{lang('DiscussionUnlink')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem icon="permissions" ripple onClick={handleClickPermissions} disabled={!canBanUsers}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('ChannelPermissions')}</span>
|
||||
<span className="subtitle">{enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT}</span>
|
||||
</div>
|
||||
<ListItem icon="permissions" multiline ripple onClick={handleClickPermissions} disabled={!canBanUsers}>
|
||||
<span className="title">{lang('ChannelPermissions')}</span>
|
||||
<span className="subtitle">{enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT}</span>
|
||||
</ListItem>
|
||||
<ListItem icon="admin" ripple onClick={handleClickAdministrators}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('ChannelAdministrators')}</span>
|
||||
<span className="subtitle">{formatInteger(adminsCount)}</span>
|
||||
</div>
|
||||
<ListItem icon="admin" multiline ripple onClick={handleClickAdministrators}>
|
||||
<span className="title">{lang('ChannelAdministrators')}</span>
|
||||
<span className="subtitle">{formatInteger(adminsCount)}</span>
|
||||
</ListItem>
|
||||
</div>
|
||||
<div className="section">
|
||||
<ListItem icon="group" ripple onClick={handleClickMembers}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('GroupMembers')}</span>
|
||||
<span className="subtitle">{formatInteger(chat.membersCount!)}</span>
|
||||
</div>
|
||||
<ListItem icon="group" multiline ripple onClick={handleClickMembers}>
|
||||
<span className="title">{lang('GroupMembers')}</span>
|
||||
<span className="subtitle">{formatInteger(chat.membersCount!)}</span>
|
||||
</ListItem>
|
||||
|
||||
{chat.fullInfo && (
|
||||
|
@ -240,11 +240,9 @@ const ManageGroupPermissions: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<ListItem icon="delete-user" ripple narrow onClick={handleRemovedUsersClick}>
|
||||
<div className="multiline-item">
|
||||
<span className="title">{lang('ChannelBlockedUsers')}</span>
|
||||
<span className="subtitle">{removedUsersCount}</span>
|
||||
</div>
|
||||
<ListItem icon="delete-user" multiline ripple narrow onClick={handleRemovedUsersClick}>
|
||||
<span className="title">{lang('ChannelBlockedUsers')}</span>
|
||||
<span className="subtitle">{removedUsersCount}</span>
|
||||
</ListItem>
|
||||
</div>
|
||||
|
||||
|
@ -144,7 +144,6 @@ const ManageUser: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
userId={user.id}
|
||||
avatarSize="jumbo"
|
||||
status="original name"
|
||||
withMediaViewer
|
||||
withFullInfo
|
||||
/>
|
||||
<InputText
|
||||
|
@ -4,7 +4,6 @@
|
||||
.ListItem-button {
|
||||
width: 100%;
|
||||
background-color: var(--background-color);
|
||||
ackground: none;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
@ -25,6 +24,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.multiline {
|
||||
.ListItem-button > 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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<OwnProps> = (props) => {
|
||||
ripple,
|
||||
narrow,
|
||||
inactive,
|
||||
contextActions,
|
||||
focus,
|
||||
destructive,
|
||||
multiline,
|
||||
contextActions,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
@ -118,6 +120,7 @@ const ListItem: FC<OwnProps> = (props) => {
|
||||
contextMenuPosition && 'has-menu-open',
|
||||
focus && 'focus',
|
||||
destructive && 'destructive',
|
||||
multiline && 'multiline',
|
||||
);
|
||||
|
||||
return (
|
||||
@ -138,7 +141,8 @@ const ListItem: FC<OwnProps> = (props) => {
|
||||
{icon && (
|
||||
<i className={`icon-${icon}`} />
|
||||
)}
|
||||
{children}
|
||||
{multiline && (<div className="multiline-item">{children}</div>)}
|
||||
{!multiline && children}
|
||||
{!disabled && !inactive && ripple && (
|
||||
<RippleEffect />
|
||||
)}
|
||||
|
@ -9,6 +9,10 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 0;
|
||||
width: 0;
|
||||
|
@ -12,6 +12,7 @@ type OwnProps = {
|
||||
label: string;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
inactive?: boolean;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onCheck?: (isChecked: boolean) => void;
|
||||
};
|
||||
@ -23,22 +24,24 @@ const Switcher: FC<OwnProps> = ({
|
||||
label,
|
||||
checked = false,
|
||||
disabled,
|
||||
inactive,
|
||||
onChange,
|
||||
onCheck,
|
||||
}) => {
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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' |
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
})();
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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: {},
|
||||
|
@ -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<number, ApiCha
|
||||
};
|
||||
}
|
||||
|
||||
export function updateChat(global: GlobalState, chatId: number, chatUpdate: Partial<ApiChat>): GlobalState {
|
||||
export function updateChat(
|
||||
global: GlobalState, chatId: number, chatUpdate: Partial<ApiChat>, 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) {
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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"],
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user