Profile: Display user background (#6205)
This commit is contained in:
parent
38b9836e68
commit
7c6f646153
@ -1,5 +0,0 @@
|
||||
{
|
||||
"plugins": {
|
||||
"autoprefixer": {},
|
||||
}
|
||||
}
|
||||
@ -42,13 +42,22 @@
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoClasses": ["global"]
|
||||
"ignorePseudoClasses": ["global", "local"]
|
||||
}
|
||||
],
|
||||
"plugin/selector-tag-no-without-class": ["/^(?!body|html)([^_-]*)$/", { "severity": "warning" }],
|
||||
"plugin/use-baseline": [true, {
|
||||
"severity": "warning",
|
||||
"ignoreSelectors": ["nesting", "dir", "selection", "view-transition-group"],
|
||||
"ignoreSelectors": [
|
||||
"nesting",
|
||||
"dir",
|
||||
"selection",
|
||||
"view-transition",
|
||||
"view-transition-group",
|
||||
"view-transition-image-pair",
|
||||
"view-transition-old",
|
||||
"view-transition-new"
|
||||
],
|
||||
"ignoreProperties": [
|
||||
"text-wrap",
|
||||
"outline",
|
||||
|
||||
13
CLAUDE.md
13
CLAUDE.md
@ -32,7 +32,7 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
|
||||
- Early returns.
|
||||
- Prefix boolean variables with primary or modal auxiliaries (e.q. `isOpen`, `willUpdate`, `shouldRender`).
|
||||
- Functions should start with a verb (e.q. `openModal`, `closeDialog`, `handleClick`).
|
||||
- Prefer checking required parameter before calling a function, avoid making it optinal and checking at the beginning of the function.
|
||||
- Prefer checking required parameter before calling a function, avoid making it optional and checking at the beginning of the function.
|
||||
- Only leave comments for complex logic.
|
||||
- Do not use `null`. There's linter rule to enforce it.
|
||||
- **IMPORTANT: Avoid conditional spread operators** - TypeScript doesn't check if spread fields match the target type.
|
||||
@ -203,8 +203,10 @@ const NewComp = (props: OwnProps & StateProps) => { … }
|
||||
### Example
|
||||
|
||||
```ts
|
||||
import React, { useFlag } from '../../lib/teact/teact';
|
||||
import { memo, useState, useRef } from '../../lib/teact/teact';
|
||||
import { withGlobal, getActions } from '../../global';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
|
||||
@ -236,10 +238,10 @@ const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (!ref.current) return;
|
||||
const el = ref.current;
|
||||
setColor(el.value);
|
||||
setColor(el.dataset.value);
|
||||
close();
|
||||
onClick?.();
|
||||
someAction(el.value);
|
||||
someAction(el.dataset.value);
|
||||
});
|
||||
|
||||
return (
|
||||
@ -251,12 +253,11 @@ const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global, { id }): Complete<StateProps> => {
|
||||
|
||||
const stateValue = selectValue(global, id);
|
||||
return {
|
||||
stateValue,
|
||||
};
|
||||
})(Component);
|
||||
})(Component)
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
46
dev/postcss-remove-global.ts
Normal file
46
dev/postcss-remove-global.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { Plugin } from 'postcss';
|
||||
|
||||
const pluginName = 'postcss-remove-global';
|
||||
|
||||
function removeGlobalPlugin(): Plugin {
|
||||
return {
|
||||
postcssPlugin: pluginName,
|
||||
Once(root, helpers) {
|
||||
const file = helpers.result.opts.from || '';
|
||||
const isModule = /\.module\.(css|scss|sass)$/i.test(file);
|
||||
if (isModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
// :global in rules
|
||||
root.walkRules((rule) => {
|
||||
// :global as nested selector
|
||||
const globalReg = /:global(\s+)/;
|
||||
// :global(.selector) as nested selector
|
||||
const globalWithSelectorReg = /:global\(\s*((?:[a-zA-Z0-9.#:[\]_\-\s>+~]+))\s*\)/;
|
||||
if (rule.selector === ':global') {
|
||||
const parent = rule.parent || root;
|
||||
parent.append(...rule.nodes);
|
||||
rule.remove();
|
||||
} else if (rule.selector.match(globalReg)) {
|
||||
rule.selector = rule.selector.replace(globalReg, '');
|
||||
} else if (rule.selector.match(globalWithSelectorReg)) {
|
||||
rule.selector = rule.selector.replace(globalWithSelectorReg, '$1');
|
||||
}
|
||||
});
|
||||
// :global in AtRules
|
||||
root.walkAtRules((atRule) => {
|
||||
const name = atRule.name;
|
||||
const params = atRule.params;
|
||||
const globalReg = /:global\((\w+)\)/;
|
||||
if (name === 'keyframes' && params.match(globalReg)) {
|
||||
atRule.params = params.replace(globalReg, '$1');
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
removeGlobalPlugin.postcss = true;
|
||||
|
||||
export default removeGlobalPlugin;
|
||||
@ -3,6 +3,8 @@ export default {
|
||||
moduleNameMapper: {
|
||||
'\\.(css|scss|wasm|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|tgs)$':
|
||||
'<rootDir>/tests/staticFileMock.js',
|
||||
'^@teact$': '<rootDir>/src/lib/teact/teact.ts',
|
||||
'^@teact/(.*)$': '<rootDir>/src/lib/teact/$1',
|
||||
},
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/tests/playwright/',
|
||||
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@ -89,6 +89,8 @@
|
||||
"lint-staged": "^16.1.2",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"minimatch": "^10.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"postcss-modules": "^6.0.1",
|
||||
"react": "^19.1.0",
|
||||
@ -16084,6 +16086,49 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-load-config": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lilconfig": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jiti": ">=1.21.0",
|
||||
"postcss": ">=8.0.9",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"postcss": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-loader": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
|
||||
|
||||
@ -99,6 +99,8 @@
|
||||
"lint-staged": "^16.1.2",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"minimatch": "^10.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"postcss-modules": "^6.0.1",
|
||||
"react": "^19.1.0",
|
||||
|
||||
13
postcss.config.ts
Normal file
13
postcss.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import type { Config } from 'postcss-load-config';
|
||||
|
||||
import removeGlobalPlugin from './dev/postcss-remove-global.ts';
|
||||
|
||||
const config: Config = {
|
||||
plugins: [
|
||||
removeGlobalPlugin(),
|
||||
autoprefixer(),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@ -83,6 +83,7 @@ function buildApiChatFieldsFromPeerEntity(
|
||||
const botVerificationIconId = userOrChannel?.botVerificationIcon?.toString();
|
||||
const storiesUnavailable = userOrChannel?.storiesUnavailable;
|
||||
const color = userOrChannel?.color ? buildApiPeerColor(userOrChannel.color) : undefined;
|
||||
const profileColor = userOrChannel?.profileColor ? buildApiPeerColor(userOrChannel.profileColor) : undefined;
|
||||
const emojiStatus = userOrChannel?.emojiStatus ? buildApiEmojiStatus(userOrChannel.emojiStatus) : undefined;
|
||||
const paidMessagesStars = userOrChannel?.sendPaidMessagesStars;
|
||||
const isVerified = userOrChannel?.verified;
|
||||
@ -107,6 +108,7 @@ function buildApiChatFieldsFromPeerEntity(
|
||||
isCreator,
|
||||
fakeType: isScam ? 'scam' : (isFake ? 'fake' : undefined),
|
||||
color,
|
||||
profileColor,
|
||||
isJoinToSend: channel?.joinToSend,
|
||||
isJoinRequest: channel?.joinRequest,
|
||||
isForum: channel?.forum,
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
ApiOldLangString,
|
||||
ApiPeerColors,
|
||||
ApiPeerNotifySettings,
|
||||
ApiPeerProfileColorSet,
|
||||
ApiPrivacyKey,
|
||||
ApiRestrictionReason,
|
||||
ApiSession,
|
||||
@ -293,11 +294,16 @@ export function buildApiLanguage(lang: GramJs.TypeLangPackLanguage): ApiLanguage
|
||||
};
|
||||
}
|
||||
|
||||
function buildApiPeerColorSet(colorSet: GramJs.help.TypePeerColorSet) {
|
||||
if (colorSet instanceof GramJs.help.PeerColorSet) {
|
||||
return colorSet.colors.map((color) => numberToHexColor(color));
|
||||
}
|
||||
return undefined;
|
||||
function buildApiPeerColorSet(colorSet: GramJs.help.PeerColorSet) {
|
||||
return colorSet.colors.map((color) => numberToHexColor(color));
|
||||
}
|
||||
|
||||
function buildApiPeerProfileColorSet(colorSet: GramJs.help.PeerColorProfileSet): ApiPeerProfileColorSet {
|
||||
return {
|
||||
paletteColors: colorSet.paletteColors.map((color) => numberToHexColor(color)),
|
||||
bgColors: colorSet.bgColors.map((color) => numberToHexColor(color)),
|
||||
storyColors: colorSet.storyColors.map((color) => numberToHexColor(color)),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiPeerColors(wrapper: GramJs.help.TypePeerColors): ApiPeerColors['general'] | undefined {
|
||||
@ -306,8 +312,24 @@ export function buildApiPeerColors(wrapper: GramJs.help.TypePeerColors): ApiPeer
|
||||
return buildCollectionByCallback(wrapper.colors, (color) => {
|
||||
return [color.colorId, {
|
||||
isHidden: color.hidden,
|
||||
colors: color.colors && buildApiPeerColorSet(color.colors),
|
||||
darkColors: color.darkColors && buildApiPeerColorSet(color.darkColors),
|
||||
colors: color.colors instanceof GramJs.help.PeerColorSet
|
||||
? buildApiPeerColorSet(color.colors) : undefined,
|
||||
darkColors: color.darkColors instanceof GramJs.help.PeerColorSet
|
||||
? buildApiPeerColorSet(color.darkColors) : undefined,
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
export function buildApiPeerProfileColors(wrapper: GramJs.help.TypePeerColors): ApiPeerColors['profile'] | undefined {
|
||||
if (!(wrapper instanceof GramJs.help.PeerColors)) return undefined;
|
||||
|
||||
return buildCollectionByCallback(wrapper.colors, (color) => {
|
||||
return [color.colorId, {
|
||||
isHidden: color.hidden,
|
||||
colors: color.colors instanceof GramJs.help.PeerColorProfileSet
|
||||
? buildApiPeerProfileColorSet(color.colors) : undefined,
|
||||
darkColors: color.darkColors instanceof GramJs.help.PeerColorProfileSet
|
||||
? buildApiPeerProfileColorSet(color.darkColors) : undefined,
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
const {
|
||||
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId,
|
||||
bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit,
|
||||
sendPaidMessagesStars, username,
|
||||
sendPaidMessagesStars, username, profileColor,
|
||||
} = mtpUser;
|
||||
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
|
||||
const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo);
|
||||
@ -141,6 +141,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
botActiveUsers,
|
||||
botVerificationIconId: botVerificationIcon?.toString(),
|
||||
color: mtpUser.color && buildApiPeerColor(mtpUser.color),
|
||||
profileColor: profileColor && buildApiPeerColor(profileColor),
|
||||
paidMessagesStars: sendPaidMessagesStars?.toJSNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
buildApiLanguage,
|
||||
buildApiPeerColors,
|
||||
buildApiPeerNotifySettings,
|
||||
buildApiPeerProfileColors,
|
||||
buildApiSession,
|
||||
buildApiTimezone,
|
||||
buildApiWallpaper,
|
||||
@ -631,6 +632,23 @@ export async function fetchPeerColors(hash?: number) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchPeerProfileColors(hash?: number) {
|
||||
const result = await invokeRequest(new GramJs.help.GetPeerProfileColors({
|
||||
hash: hash ?? DEFAULT_PRIMITIVES.INT,
|
||||
}));
|
||||
if (!result) return undefined;
|
||||
|
||||
const colors = buildApiPeerProfileColors(result);
|
||||
if (!colors) return undefined;
|
||||
|
||||
const newHash = result instanceof GramJs.help.PeerColors ? result.hash : undefined;
|
||||
|
||||
return {
|
||||
colors,
|
||||
hash: newHash,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchTimezones(hash?: number) {
|
||||
const result = await invokeRequest(new GramJs.help.GetTimezonesList({
|
||||
hash: hash ?? DEFAULT_PRIMITIVES.INT,
|
||||
|
||||
@ -44,6 +44,7 @@ export interface ApiChat {
|
||||
isProtected?: boolean;
|
||||
fakeType?: ApiFakeType;
|
||||
color?: ApiPeerColor;
|
||||
profileColor?: ApiPeerColor;
|
||||
emojiStatus?: ApiEmojiStatusType;
|
||||
isForum?: boolean;
|
||||
isForumAsMessages?: true;
|
||||
|
||||
@ -288,14 +288,23 @@ export interface ApiConfig {
|
||||
}
|
||||
|
||||
export type ApiPeerColorSet = string[];
|
||||
export type ApiPeerProfileColorSet = {
|
||||
paletteColors: string[];
|
||||
bgColors: string[];
|
||||
storyColors: string[];
|
||||
};
|
||||
|
||||
export type ApiPeerColorOption<T extends ApiPeerColorSet | ApiPeerProfileColorSet> = {
|
||||
isHidden?: true;
|
||||
colors?: T;
|
||||
darkColors?: T;
|
||||
};
|
||||
|
||||
export interface ApiPeerColors {
|
||||
general: Record<number, {
|
||||
isHidden?: true;
|
||||
colors?: ApiPeerColorSet;
|
||||
darkColors?: ApiPeerColorSet;
|
||||
}>;
|
||||
general: Record<number, ApiPeerColorOption<ApiPeerColorSet>>;
|
||||
generalHash?: number;
|
||||
profile: Record<number, ApiPeerColorOption<ApiPeerProfileColorSet>>;
|
||||
profileHash?: number;
|
||||
}
|
||||
|
||||
export interface ApiTimezone {
|
||||
|
||||
@ -35,6 +35,7 @@ export interface ApiUser {
|
||||
hasUnreadStories?: boolean;
|
||||
maxStoryId?: number;
|
||||
color?: ApiPeerColor;
|
||||
profileColor?: ApiPeerColor;
|
||||
canEditBot?: boolean;
|
||||
hasMainMiniApp?: boolean;
|
||||
botActiveUsers?: number;
|
||||
|
||||
@ -66,6 +66,7 @@ cn.icon = cn('icon');
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
style?: string;
|
||||
size?: AvatarSize;
|
||||
peer?: ApiPeer | CustomPeer;
|
||||
photo?: ApiPhoto;
|
||||
@ -79,6 +80,7 @@ type OwnProps = {
|
||||
forPremiumPromo?: boolean;
|
||||
withStoryGap?: boolean;
|
||||
withStorySolid?: boolean;
|
||||
storyColors?: string[];
|
||||
forceFriendStorySolid?: boolean;
|
||||
forceUnreadStorySolid?: boolean;
|
||||
storyViewerOrigin?: StoryViewerOrigin;
|
||||
@ -94,6 +96,7 @@ type OwnProps = {
|
||||
|
||||
const Avatar: FC<OwnProps> = ({
|
||||
className,
|
||||
style,
|
||||
size = 'large',
|
||||
peer,
|
||||
photo,
|
||||
@ -107,6 +110,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
forPremiumPromo,
|
||||
withStoryGap,
|
||||
withStorySolid,
|
||||
storyColors,
|
||||
forceFriendStorySolid,
|
||||
forceUnreadStorySolid,
|
||||
storyViewerOrigin,
|
||||
@ -130,6 +134,9 @@ const Avatar: FC<OwnProps> = ({
|
||||
const isReplies = realPeer && isChatWithRepliesBot(realPeer.id);
|
||||
const isAnonymousForwards = realPeer && isAnonymousForwardsChat(realPeer.id);
|
||||
const isForum = chat?.isForum;
|
||||
|
||||
const isStoryClickable = withStory && storyViewerMode !== 'disabled' && realPeer?.hasStories;
|
||||
|
||||
let imageHash: string | undefined;
|
||||
let videoHash: string | undefined;
|
||||
|
||||
@ -271,14 +278,14 @@ const Avatar: FC<OwnProps> = ({
|
||||
withStorySolid && realPeer?.hasStories && 'with-story-solid',
|
||||
withStorySolid && forceFriendStorySolid && 'close-friend',
|
||||
withStorySolid && (realPeer?.hasUnreadStories || forceUnreadStorySolid) && 'has-unread-story',
|
||||
onClick && 'interactive',
|
||||
(onClick || isStoryClickable) && 'interactive',
|
||||
(!isSavedMessages && !imgUrl) && 'no-photo',
|
||||
);
|
||||
|
||||
const hasMedia = Boolean(isSavedMessages || imgUrl);
|
||||
|
||||
const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (withStory && storyViewerMode !== 'disabled' && realPeer?.hasStories) {
|
||||
if (isStoryClickable) {
|
||||
e.stopPropagation();
|
||||
|
||||
openStoryViewer({
|
||||
@ -302,7 +309,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
data-peer-id={realPeer?.id}
|
||||
data-test-sender-id={IS_TEST ? realPeer?.id : undefined}
|
||||
aria-label={typeof content === 'string' ? author : undefined}
|
||||
style={buildStyle(`--_size: ${pxSize}px;`, customColor && `--color-user: ${customColor}`)}
|
||||
style={buildStyle(`--_size: ${pxSize}px;`, customColor && `--color-user: ${customColor}`, style)}
|
||||
onClick={handleClick}
|
||||
onContextMenu={onContextMenu}
|
||||
onMouseDown={handleMouseDown}
|
||||
@ -312,7 +319,12 @@ const Avatar: FC<OwnProps> = ({
|
||||
{typeof content === 'string' ? renderText(content, [isBig ? 'hq_emoji' : 'emoji']) : content}
|
||||
</div>
|
||||
{withStory && realPeer?.hasStories && (
|
||||
<AvatarStoryCircle peerId={realPeer.id} size={pxSize} withExtraGap={withStoryGap} />
|
||||
<AvatarStoryCircle
|
||||
peerId={realPeer.id}
|
||||
size={pxSize}
|
||||
withExtraGap={withStoryGap}
|
||||
colors={storyColors}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -13,11 +13,11 @@ import { REM } from './helpers/mediaDimensions';
|
||||
import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio';
|
||||
|
||||
interface OwnProps {
|
||||
|
||||
peerId: string;
|
||||
className?: string;
|
||||
size: number;
|
||||
withExtraGap?: boolean;
|
||||
colors?: string[];
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
@ -29,14 +29,13 @@ interface StateProps {
|
||||
|
||||
const BLUE = ['#34C578', '#3CA3F3'];
|
||||
const GREEN = ['#C9EB38', '#09C167'];
|
||||
const PURPLE = ['#A667FF', '#55A5FF'];
|
||||
const GRAY = '#C4C9CC';
|
||||
const DARK_GRAY = '#737373';
|
||||
const STROKE_WIDTH = 0.125 * REM;
|
||||
const STROKE_WIDTH_READ = 0.0625 * REM;
|
||||
const STROKE_WIDTH_LARGE = 0.25 * REM;
|
||||
const GAP_PERCENT = 2;
|
||||
const SEGMENTS_MAX = 45; // More than this breaks rendering in Safari and Chrome
|
||||
const LARGE_AVATAR_SIZE = 3.5 * REM;
|
||||
const LARGE_SIZE = 4 * REM;
|
||||
|
||||
const GAP_PERCENT_EXTRA = 10;
|
||||
const EXTRA_GAP_ANGLE = Math.PI / 4;
|
||||
@ -52,12 +51,15 @@ function AvatarStoryCircle({
|
||||
lastReadId,
|
||||
withExtraGap,
|
||||
appTheme,
|
||||
colors,
|
||||
}: OwnProps & StateProps) {
|
||||
const ref = useRef<HTMLCanvasElement>();
|
||||
|
||||
const dpr = useDevicePixelRatio();
|
||||
|
||||
const adaptedSize = size + STROKE_WIDTH;
|
||||
const isLarge = size > LARGE_SIZE;
|
||||
const strokeWidth = isLarge ? STROKE_WIDTH_LARGE : STROKE_WIDTH;
|
||||
const adaptedSize = size + strokeWidth + (isLarge ? 0.25 * REM : 0); // Add extra gap space for large avatars
|
||||
|
||||
const values = useMemo(() => {
|
||||
return (storyIds || []).reduce((acc, id) => {
|
||||
@ -92,15 +94,16 @@ function AvatarStoryCircle({
|
||||
|
||||
drawGradientCircle({
|
||||
canvas: ref.current,
|
||||
size: adaptedSize * dpr,
|
||||
size: adaptedSize,
|
||||
strokeWidth,
|
||||
segmentsCount: values.total,
|
||||
color: isCloseFriend ? 'green' : 'blue',
|
||||
colorStops: colors || (isCloseFriend ? GREEN : BLUE),
|
||||
readSegmentsCount: values.read,
|
||||
withExtraGap,
|
||||
readSegmentColor: appTheme === 'dark' ? DARK_GRAY : GRAY,
|
||||
dpr,
|
||||
});
|
||||
}, [appTheme, isCloseFriend, adaptedSize, values.read, values.total, withExtraGap, dpr]);
|
||||
}, [appTheme, isCloseFriend, adaptedSize, values.read, values.total, withExtraGap, dpr, colors, size, strokeWidth]);
|
||||
|
||||
if (!values.total) {
|
||||
return undefined;
|
||||
@ -130,7 +133,8 @@ export default memo(withGlobal<OwnProps>((global, { peerId }): Complete<StatePro
|
||||
export function drawGradientCircle({
|
||||
canvas,
|
||||
size,
|
||||
color,
|
||||
strokeWidth: strokeWidthPx = STROKE_WIDTH,
|
||||
colorStops,
|
||||
segmentsCount,
|
||||
readSegmentsCount = 0,
|
||||
withExtraGap = false,
|
||||
@ -138,8 +142,9 @@ export function drawGradientCircle({
|
||||
dpr,
|
||||
}: {
|
||||
canvas: HTMLCanvasElement;
|
||||
strokeWidth?: number;
|
||||
size: number;
|
||||
color: string;
|
||||
colorStops: string[];
|
||||
segmentsCount: number;
|
||||
readSegmentsCount?: number;
|
||||
withExtraGap?: boolean;
|
||||
@ -152,33 +157,41 @@ export function drawGradientCircle({
|
||||
segmentsCount = SEGMENTS_MAX;
|
||||
}
|
||||
|
||||
const strokeModifier = Math.max(Math.max(size - LARGE_AVATAR_SIZE * dpr, 0) / dpr / REM / 1.5, 1) * dpr;
|
||||
const sizeModifier = dpr;
|
||||
const strokeModifier = dpr;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const centerCoordinate = size / 2;
|
||||
const radius = (size - STROKE_WIDTH * strokeModifier) / 2;
|
||||
const canvasSize = size * sizeModifier;
|
||||
const strokeWidth = strokeWidthPx * strokeModifier;
|
||||
|
||||
canvas.width = canvasSize;
|
||||
canvas.height = canvasSize;
|
||||
const centerCoordinate = canvasSize / 2;
|
||||
const radius = (canvasSize - strokeWidth) / 2;
|
||||
const segmentAngle = (2 * Math.PI) / segmentsCount;
|
||||
const gapSize = (GAP_PERCENT / 100) * (2 * Math.PI);
|
||||
const gradient = ctx.createLinearGradient(
|
||||
0,
|
||||
0,
|
||||
Math.ceil(size * Math.cos(Math.PI / 2)),
|
||||
Math.ceil(size * Math.sin(Math.PI / 2)),
|
||||
Math.ceil(canvasSize * Math.cos(Math.PI / 2)),
|
||||
Math.ceil(canvasSize * Math.sin(Math.PI / 2)),
|
||||
);
|
||||
|
||||
const colorStops = color === 'purple' ? PURPLE : color === 'green' ? GREEN : BLUE;
|
||||
colorStops.forEach((colorStop, index) => {
|
||||
gradient.addColorStop(index / (colorStops.length - 1), colorStop);
|
||||
});
|
||||
if (colorStops.length === 1) {
|
||||
gradient.addColorStop(0, colorStops[0]);
|
||||
gradient.addColorStop(1, colorStops[0]);
|
||||
} else {
|
||||
colorStops.forEach((colorStop, index) => {
|
||||
gradient.addColorStop(index / (colorStops.length - 1), colorStop);
|
||||
});
|
||||
}
|
||||
|
||||
ctx.lineCap = 'round';
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.clearRect(0, 0, canvasSize, canvasSize);
|
||||
|
||||
Array.from({ length: segmentsCount }).forEach((_, i) => {
|
||||
const isRead = i < readSegmentsCount;
|
||||
@ -186,7 +199,7 @@ export function drawGradientCircle({
|
||||
let endAngle = startAngle + segmentAngle - (segmentsCount > 1 ? gapSize : 0);
|
||||
|
||||
ctx.strokeStyle = isRead ? readSegmentColor : gradient;
|
||||
ctx.lineWidth = (isRead ? STROKE_WIDTH_READ : STROKE_WIDTH) * strokeModifier;
|
||||
ctx.lineWidth = strokeWidth * (isRead ? 0.5 : 1);
|
||||
|
||||
if (withExtraGap) {
|
||||
if (startAngle >= EXTRA_GAP_START && endAngle <= EXTRA_GAP_END) { // Segment is inside extra gap
|
||||
|
||||
@ -16,15 +16,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.withSparkles {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
position: absolute;
|
||||
inset: -0.25rem;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
|
||||
@ -13,7 +13,6 @@ import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListene
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||
|
||||
import Sparkles from './Sparkles';
|
||||
import StickerView from './StickerView';
|
||||
|
||||
import styles from './CustomEmoji.module.scss';
|
||||
@ -42,9 +41,6 @@ type OwnProps = {
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
onAnimationEnd?: NoneToVoidFunction;
|
||||
withSparkles?: boolean;
|
||||
sparklesClassName?: string;
|
||||
sparklesStyle?: string;
|
||||
};
|
||||
|
||||
const STICKER_SIZE = 20;
|
||||
@ -71,9 +67,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
observeIntersectionForPlaying,
|
||||
onClick,
|
||||
onAnimationEnd,
|
||||
withSparkles,
|
||||
sparklesStyle,
|
||||
sparklesClassName,
|
||||
}) => {
|
||||
let containerRef = useRef<HTMLDivElement>();
|
||||
if (ref) {
|
||||
@ -120,7 +113,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
ref={containerRef}
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
withSparkles && styles.withSparkles,
|
||||
className,
|
||||
'custom-emoji',
|
||||
'emoji',
|
||||
@ -132,16 +124,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
data-alt={customEmoji?.emoji}
|
||||
style={style}
|
||||
>
|
||||
{withSparkles && (
|
||||
<Sparkles
|
||||
className={buildClassName(
|
||||
styles.sparkles,
|
||||
sparklesClassName,
|
||||
)}
|
||||
style={sparklesStyle}
|
||||
preset="button"
|
||||
/>
|
||||
)}
|
||||
{isSelectable && (
|
||||
<img
|
||||
className={styles.highlightCatch}
|
||||
|
||||
@ -3,17 +3,6 @@
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
|
||||
:global(.custom-emoji) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
:global(.statusSparkles) {
|
||||
color: var(--accent-color);
|
||||
:global(.selected) & {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.statusTransition {
|
||||
width: 1.5em !important;
|
||||
height: 1.5em !important;
|
||||
@ -50,3 +39,14 @@
|
||||
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
.statusPrimaryColor {
|
||||
color: var(--color-primary);
|
||||
|
||||
:global(.statusSparkles) {
|
||||
color: var(--accent-color);
|
||||
:global(.selected) & {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ import {
|
||||
} from '../../global/helpers';
|
||||
import { isApiPeerUser } from '../../global/helpers/peers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import { copyTextToClipboard } from '../../util/clipboard';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import renderText from './helpers/renderText';
|
||||
@ -31,6 +30,7 @@ import useOldLang from '../../hooks/useOldLang';
|
||||
import Transition from '../ui/Transition';
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
import FakeIcon from './FakeIcon';
|
||||
import GiftEffectWrapper from './gift/GiftEffectWrapper';
|
||||
import StarIcon from './icons/StarIcon';
|
||||
import VerifiedIcon from './VerifiedIcon';
|
||||
|
||||
@ -39,6 +39,7 @@ import styles from './FullNameTitle.module.scss';
|
||||
type OwnProps = {
|
||||
peer: ApiPeer | CustomPeer;
|
||||
className?: string;
|
||||
style?: string;
|
||||
noVerified?: boolean;
|
||||
noFake?: boolean;
|
||||
withEmojiStatus?: boolean;
|
||||
@ -50,13 +51,14 @@ type OwnProps = {
|
||||
noLoopLimit?: boolean;
|
||||
canCopyTitle?: boolean;
|
||||
iconElement?: React.ReactNode;
|
||||
statusSparklesColor?: string;
|
||||
withStatusTextColor?: boolean;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
const FullNameTitle: FC<OwnProps> = ({
|
||||
className,
|
||||
style,
|
||||
peer,
|
||||
noVerified,
|
||||
noFake,
|
||||
@ -67,9 +69,9 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
noLoopLimit,
|
||||
canCopyTitle,
|
||||
iconElement,
|
||||
statusSparklesColor,
|
||||
isMonoforum,
|
||||
monoforumBadgeClassName,
|
||||
withStatusTextColor,
|
||||
onEmojiStatusClick,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
@ -123,7 +125,7 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
const botVerificationIconId = realPeer?.botVerificationIconId;
|
||||
|
||||
return (
|
||||
<div className={buildClassName('title', styles.root, className)}>
|
||||
<div className={buildClassName('title', styles.root, className)} style={style}>
|
||||
{botVerificationIconId && (
|
||||
<CustomEmoji
|
||||
documentId={botVerificationIconId}
|
||||
@ -157,18 +159,22 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
direction={-1}
|
||||
shouldCleanup
|
||||
>
|
||||
<CustomEmoji
|
||||
forceAlways
|
||||
className="no-selection"
|
||||
<GiftEffectWrapper
|
||||
withSparkles={emojiStatus.type === 'collectible'}
|
||||
sparklesClassName="statusSparkles"
|
||||
sparklesStyle={buildStyle(statusSparklesColor && `color: ${statusSparklesColor}`)}
|
||||
documentId={emojiStatus.documentId}
|
||||
size={emojiStatusSize}
|
||||
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
onClick={onEmojiStatusClick}
|
||||
/>
|
||||
sparklesColor={emojiStatus.type === 'collectible' && !withStatusTextColor
|
||||
? emojiStatus.textColor : undefined}
|
||||
>
|
||||
<CustomEmoji
|
||||
forceAlways
|
||||
className={buildClassName('no-selection', !withStatusTextColor && styles.statusPrimaryColor)}
|
||||
documentId={emojiStatus.documentId}
|
||||
size={emojiStatusSize}
|
||||
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
onClick={onEmojiStatusClick}
|
||||
/>
|
||||
</GiftEffectWrapper>
|
||||
</Transition>
|
||||
)}
|
||||
{canShowEmojiStatus && !emojiStatus && isPremium && <StarIcon />}
|
||||
|
||||
@ -1,317 +0,0 @@
|
||||
.fallbackPhoto {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
opacity: 0;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
|
||||
transition: 0.25s ease-in-out opacity;
|
||||
}
|
||||
|
||||
.fallbackPhotoContents {
|
||||
pointer-events: none;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
user-select: none;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 1.5rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-white);
|
||||
|
||||
opacity: 0.5;
|
||||
|
||||
transition: 0.25s ease-in-out opacity;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fallbackPhotoVisible {
|
||||
opacity: 1;
|
||||
|
||||
.fallbackPhotoContents {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.fallbackPhotoAvatar {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.photoWrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
|
||||
> :global(.Transition) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.photoDashes {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0.5rem;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
height: 0.125rem;
|
||||
padding: 0 0.375rem;
|
||||
}
|
||||
|
||||
.photoDash {
|
||||
flex: 1 1 auto;
|
||||
|
||||
margin: 0 0.125rem;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
opacity: 0.25;
|
||||
background-color: var(--color-white);
|
||||
|
||||
transition: opacity 300ms ease;
|
||||
|
||||
&_current {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 25%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
||||
appearance: none;
|
||||
opacity: 0.25;
|
||||
background: transparent no-repeat;
|
||||
background-size: 1.25rem;
|
||||
outline: none !important;
|
||||
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:global(:hover),
|
||||
:global(.is-touch-env) & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&_prev {
|
||||
left: 0;
|
||||
background-image: url("../../assets/media_navigation_previous.svg");
|
||||
background-position: 1.25rem 50%;
|
||||
|
||||
&[dir="rtl"] {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
&_next {
|
||||
right: 0;
|
||||
background-image: url("../../assets/media_navigation_next.svg");
|
||||
background-position: calc(100% - 1.25rem) 50%;
|
||||
|
||||
&[dir="rtl"] {
|
||||
right: auto;
|
||||
left: 0;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 0 1.5rem 0.5rem;
|
||||
|
||||
color: var(--color-white);
|
||||
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
|
||||
:global(.statusSparkles) {
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
.status {
|
||||
unicode-bidi: plaintext;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
.status {
|
||||
unicode-bidi: plaintext;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.userRatingNegativeWrapper,
|
||||
.userRatingWrapper {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-right: 0.25rem;
|
||||
|
||||
font-size: 1rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.userRatingWrapper {
|
||||
width: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.ratingNegativeIcon {
|
||||
pointer-events: none;
|
||||
font-size: 1rem;
|
||||
color: var(--color-white);
|
||||
|
||||
-webkit-text-stroke: 1px #000000;
|
||||
}
|
||||
|
||||
.ratingIcon {
|
||||
pointer-events: none;
|
||||
color: var(--color-white);
|
||||
|
||||
-webkit-text-stroke: 1px #000000;
|
||||
}
|
||||
|
||||
.ratingLevel {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
font-size: 0.625rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 1;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.userStatus {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.getStatus {
|
||||
--blured-background-color: #c4c9cc42;
|
||||
|
||||
pointer-events: all;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
margin-inline-start: 0.375rem;
|
||||
padding: 0.1875rem 0.375rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
opacity: 0.8;
|
||||
background: var(--blured-background-color);
|
||||
backdrop-filter: blur(100px);
|
||||
|
||||
transition: 150ms filter ease-in;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.topicContainer {
|
||||
--custom-emoji-size: 7.5rem;
|
||||
|
||||
padding: 1rem 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.topicTitle {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.topicIcon {
|
||||
display: flex !important;
|
||||
width: 7.5rem !important;
|
||||
height: 7.5rem !important;
|
||||
margin: auto;
|
||||
|
||||
&:global(.general-forum-icon) {
|
||||
font-size: 7.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.topicIconTitle {
|
||||
font-size: 3rem !important;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
||||
:global(.emoji-small) {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.topicMessagesCounter {
|
||||
margin: 0;
|
||||
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
@ -32,6 +32,11 @@
|
||||
animation-delay: var(--_duration-shift);
|
||||
}
|
||||
|
||||
.noAnimation .symbol {
|
||||
opacity: 0.5;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
|
||||
@ -18,6 +18,7 @@ type PresetParameters = ButtonParameters | ProgressParameters;
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
style?: string;
|
||||
noAnimation?: boolean;
|
||||
} & PresetParameters;
|
||||
|
||||
const SYMBOL = '✦';
|
||||
@ -85,11 +86,15 @@ const PROGRESS_POSITIONS = generateRandomProgressPositions(100);
|
||||
const Sparkles = ({
|
||||
className,
|
||||
style,
|
||||
noAnimation,
|
||||
...presetSettings
|
||||
}: OwnProps) => {
|
||||
if (presetSettings.preset === 'button') {
|
||||
return (
|
||||
<div className={buildClassName(styles.root, styles.button, className)} style={style}>
|
||||
<div
|
||||
className={buildClassName(styles.root, styles.button, className, noAnimation && styles.noAnimation)}
|
||||
style={style}
|
||||
>
|
||||
{BUTTON_POSITIONS.map((position) => {
|
||||
const shiftX = Math.cos(Math.atan2(-50 + position.y, -50 + position.x)) * 100;
|
||||
const shiftY = Math.sin(Math.atan2(-50 + position.y, -50 + position.x)) * 100;
|
||||
@ -104,6 +109,7 @@ const Sparkles = ({
|
||||
`--_shift-y: ${shiftY}%`,
|
||||
`scale: ${position.size}%`,
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{SYMBOL}
|
||||
</div>
|
||||
@ -128,6 +134,7 @@ const Sparkles = ({
|
||||
`scale: ${position.scale}%`,
|
||||
`--_duration-shift: ${(-position.durationShift / 100) * ANIMATION_DURATION}s`,
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{SYMBOL}
|
||||
</div>
|
||||
|
||||
@ -25,7 +25,6 @@ import Button from '../ui/Button';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import Icon from './icons/Icon';
|
||||
import Sparkles from './Sparkles';
|
||||
import StickerView from './StickerView';
|
||||
|
||||
import './StickerButton.scss';
|
||||
@ -58,7 +57,6 @@ type OwnProps<T> = {
|
||||
onContextMenuClose?: NoneToVoidFunction;
|
||||
onContextMenuClick?: NoneToVoidFunction;
|
||||
isEffectEmoji?: boolean;
|
||||
withSparkles?: boolean;
|
||||
};
|
||||
|
||||
const contentForStatusMenuContext = [
|
||||
@ -97,7 +95,6 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
onContextMenuClose,
|
||||
onContextMenuClick,
|
||||
isEffectEmoji,
|
||||
withSparkles,
|
||||
}: OwnProps<T>) => {
|
||||
const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions();
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
@ -292,7 +289,6 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{withSparkles && <Sparkles preset="button" />}
|
||||
{isIntesectingForShowing && (
|
||||
<StickerView
|
||||
containerRef={ref}
|
||||
|
||||
@ -37,6 +37,7 @@ import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import GiftEffectWrapper from './gift/GiftEffectWrapper';
|
||||
import Icon from './icons/Icon';
|
||||
import ReactionEmoji from './reactions/ReactionEmoji';
|
||||
import StickerButton from './StickerButton';
|
||||
@ -380,10 +381,10 @@ const StickerSet: FC<OwnProps & StateProps> = ({
|
||||
const reactionId = sticker.isCustomEmoji ? sticker.id : sticker.emoji;
|
||||
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
|
||||
|
||||
const withSparkles = sticker.id === COLLECTIBLE_STATUS_SET_ID
|
||||
const withSparkles = stickerSet.id === COLLECTIBLE_STATUS_SET_ID
|
||||
|| collectibleEmojiIdsSet?.has(sticker.id);
|
||||
|
||||
return (
|
||||
const component = (
|
||||
<StickerButton
|
||||
key={sticker.id}
|
||||
sticker={sticker}
|
||||
@ -412,9 +413,18 @@ const StickerSet: FC<OwnProps & StateProps> = ({
|
||||
isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID}
|
||||
noShowPremium={isCurrentUserPremium
|
||||
&& (stickerSet.id === EFFECT_STICKERS_SET_ID || stickerSet.id === EFFECT_EMOJIS_SET_ID)}
|
||||
withSparkles={withSparkles}
|
||||
/>
|
||||
);
|
||||
|
||||
if (withSparkles) {
|
||||
return (
|
||||
<GiftEffectWrapper className="gift-effect-wrapper" withSparkles>
|
||||
{component}
|
||||
</GiftEffectWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
})}
|
||||
{isCut && totalItemsCount > itemsBeforeCutout && (
|
||||
<Button
|
||||
|
||||
21
src/components/common/gift/GiftEffectWrapper.module.scss
Normal file
21
src/components/common/gift/GiftEffectWrapper.module.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.root {
|
||||
--glow-color: transparent;
|
||||
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: -25%;
|
||||
|
||||
background-image: radial-gradient(circle closest-side, var(--glow-color) 0%, transparent 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
position: absolute;
|
||||
inset: -0.25rem;
|
||||
}
|
||||
58
src/components/common/gift/GiftEffectWrapper.tsx
Normal file
58
src/components/common/gift/GiftEffectWrapper.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import type { ElementRef } from '@teact';
|
||||
import { memo, type TeactNode, useRef } from '@teact';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
|
||||
import Sparkles from '../Sparkles';
|
||||
|
||||
import styles from './GiftEffectWrapper.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
children: TeactNode;
|
||||
ref?: ElementRef<HTMLDivElement>;
|
||||
className?: string;
|
||||
style?: string;
|
||||
withSparkles?: boolean;
|
||||
sparklesClassName?: string;
|
||||
sparklesColor?: string;
|
||||
glowColor?: string;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const GiftEffectWrapper = ({
|
||||
children,
|
||||
ref,
|
||||
className,
|
||||
style,
|
||||
withSparkles,
|
||||
sparklesClassName,
|
||||
sparklesColor,
|
||||
glowColor,
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
let containerRef = useRef<HTMLDivElement>();
|
||||
if (ref) {
|
||||
containerRef = ref;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName(styles.root, className)}
|
||||
style={buildStyle(glowColor && `--glow-color: ${glowColor}`, style)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{withSparkles && (
|
||||
<Sparkles
|
||||
preset="button"
|
||||
className={buildClassName(styles.sparkles, sparklesClassName)}
|
||||
style={buildStyle(sparklesColor && `color: ${sparklesColor}`)}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GiftEffectWrapper);
|
||||
@ -6,7 +6,7 @@ import type { ApiEmojiStatusType, ApiPeer, ApiSavedStarGift } from '../../../api
|
||||
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config';
|
||||
import { getHasAdminRight } from '../../../global/helpers';
|
||||
import { selectChat, selectPeer, selectUser } from '../../../global/selectors';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment.ts';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
|
||||
import { CUSTOM_PEER_HIDDEN } from '../../../util/objects/customPeer';
|
||||
@ -14,7 +14,7 @@ import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { getGiftAttributes, getStickerFromGift, getTotalGiftAvailability } from '../helpers/gifts';
|
||||
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../../hooks/useFlag.ts';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
@ -34,6 +34,7 @@ type OwnProps = {
|
||||
peerId: string;
|
||||
gift: ApiSavedStarGift;
|
||||
style?: string;
|
||||
className?: string;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
@ -56,6 +57,7 @@ const SavedGift = ({
|
||||
hasAdminRights,
|
||||
collectibleEmojiStatuses,
|
||||
currentUserEmojiStatus,
|
||||
className,
|
||||
observeIntersection,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { openGiftInfoModal } = getActions();
|
||||
@ -147,7 +149,7 @@ const SavedGift = ({
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName('interactive-gift scroll-item', styles.root)}
|
||||
className={buildClassName('interactive-gift scroll-item', styles.root, className)}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
@ -186,7 +188,8 @@ const SavedGift = ({
|
||||
className={styles.priceBadge}
|
||||
nonInteractive
|
||||
size="tiny"
|
||||
withSparkleEffect={true}
|
||||
withSparkleEffect
|
||||
noSparkleAnimation
|
||||
pill
|
||||
fluid
|
||||
>
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
@ -69,6 +70,8 @@ type OwnProps = {
|
||||
chatOrUserId: string;
|
||||
isSavedDialog?: boolean;
|
||||
isInSettings?: boolean;
|
||||
className?: string;
|
||||
style?: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -117,6 +120,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
isBotCanManageEmojiStatus,
|
||||
botAppPermissions,
|
||||
botVerification,
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
const {
|
||||
showNotification,
|
||||
@ -338,7 +343,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ChatExtra">
|
||||
<div className={buildClassName('ChatExtra', className)} style={style}>
|
||||
{personalChannel && (
|
||||
<div className={styles.personalChannel}>
|
||||
<h3 className={styles.personalChannelTitle}>{oldLang('ProfileChannel')}</h3>
|
||||
|
||||
549
src/components/common/profile/ProfileInfo.module.scss
Normal file
549
src/components/common/profile/ProfileInfo.module.scss
Normal file
@ -0,0 +1,549 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
--rating-outline-color: #000000;
|
||||
--rating-text-color: #000000;
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
color: var(--color-white);
|
||||
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
|
||||
:global(.VerifiedIcon),
|
||||
:global(.StarIcon) {
|
||||
--color-fill: var(--color-white);
|
||||
--color-checkmark: var(--color-primary);
|
||||
|
||||
z-index: 2;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.plain.minimized {
|
||||
color: var(--color-text);
|
||||
|
||||
.userRatingNegativeWrapper,
|
||||
.userRatingWrapper {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
:global(.VerifiedIcon),
|
||||
:global(.StarIcon) {
|
||||
--color-fill: var(--color-primary);
|
||||
--color-checkmark: var(--color-white);
|
||||
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fallbackPhoto {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
opacity: 0;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
|
||||
transition: 0.25s ease-in-out opacity;
|
||||
}
|
||||
|
||||
.fallbackPhotoContents {
|
||||
pointer-events: none;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
user-select: none;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 1.5rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
|
||||
opacity: 0.5;
|
||||
|
||||
transition: 0.25s ease-in-out opacity;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fallbackPhotoVisible {
|
||||
opacity: 1;
|
||||
|
||||
.fallbackPhotoContents {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.fallbackPhotoAvatar {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.photoWrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
height: 2rem;
|
||||
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
|
||||
> :global(.Transition) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.photoDashes {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0.5rem;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
height: 0.125rem;
|
||||
padding: 0 0.375rem;
|
||||
}
|
||||
|
||||
.photoDash {
|
||||
flex: 1 1 auto;
|
||||
|
||||
margin: 0 0.125rem;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
opacity: 0.25;
|
||||
background-color: var(--color-white);
|
||||
|
||||
transition: opacity 300ms ease;
|
||||
|
||||
&_current {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 25%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
||||
appearance: none;
|
||||
opacity: 0.25;
|
||||
background: transparent no-repeat;
|
||||
background-size: 1.25rem;
|
||||
outline: none !important;
|
||||
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:global(:hover),
|
||||
:global(.is-touch-env) & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&_prev {
|
||||
left: 0;
|
||||
background-image: url("../../../assets/media_navigation_previous.svg");
|
||||
background-position: 1.25rem 50%;
|
||||
|
||||
&[dir="rtl"] {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
&_next {
|
||||
right: 0;
|
||||
background-image: url("../../../assets/media_navigation_next.svg");
|
||||
background-position: calc(100% - 1.25rem) 50%;
|
||||
|
||||
&[dir="rtl"] {
|
||||
right: auto;
|
||||
left: 0;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 0 1.5rem 0.5rem;
|
||||
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
|
||||
&:dir(rtl) {
|
||||
.status {
|
||||
unicode-bidi: plaintext;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
.status {
|
||||
unicode-bidi: plaintext;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
}
|
||||
|
||||
.status,
|
||||
.info,
|
||||
.title {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.userRatingNegativeWrapper,
|
||||
.userRatingWrapper {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-right: 0.25rem;
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.userRatingWrapper {
|
||||
width: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.ratingIcon, .ratingNegativeIcon {
|
||||
pointer-events: none;
|
||||
paint-order: stroke fill;
|
||||
|
||||
-webkit-text-stroke: 2px var(--rating-outline-color);
|
||||
}
|
||||
|
||||
.ratingNegativeIcon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.ratingLevel {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
font-size: 0.625rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 1;
|
||||
color: var(--rating-text-color);
|
||||
}
|
||||
|
||||
.userStatus {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.getStatus {
|
||||
--blured-background-color: #c4c9cc42;
|
||||
|
||||
pointer-events: all;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
margin-inline-start: 0.375rem;
|
||||
padding: 0.1875rem 0.375rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
opacity: 0.8;
|
||||
background: var(--blured-background-color);
|
||||
backdrop-filter: blur(100px);
|
||||
|
||||
transition: 150ms filter ease-in;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.topicContainer {
|
||||
--custom-emoji-size: 7.5rem;
|
||||
|
||||
padding: 1rem 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.topicTitle {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.topicIcon {
|
||||
display: flex !important;
|
||||
width: 7.5rem !important;
|
||||
height: 7.5rem !important;
|
||||
margin: auto;
|
||||
|
||||
&:global(.general-forum-icon) {
|
||||
font-size: 7.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.topicIconTitle {
|
||||
font-size: 3rem !important;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
||||
:global(.emoji-small) {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.topicMessagesCounter {
|
||||
margin: 0;
|
||||
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.radialPatternBackground {
|
||||
pointer-events: none;
|
||||
|
||||
// Hacky way to keep background stable during resizes
|
||||
position: absolute;
|
||||
top: 8rem;
|
||||
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 140%;
|
||||
margin-top: -70%;
|
||||
}
|
||||
|
||||
.standaloneAvatar {
|
||||
grid-area: 1 / -1;
|
||||
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
}
|
||||
|
||||
.activeProfilePhoto {
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
}
|
||||
|
||||
.minimized {
|
||||
aspect-ratio: unset;
|
||||
padding-block: 4.25rem 1rem;
|
||||
|
||||
.status, .title, .info {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
position: relative;
|
||||
|
||||
justify-content: unset;
|
||||
|
||||
min-height: unset;
|
||||
margin-top: 1rem;
|
||||
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
}
|
||||
|
||||
.pinnedGifts {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileAvatar') {
|
||||
&::view-transition-group(.info),
|
||||
&::view-transition-group(.title),
|
||||
&::view-transition-group(.status) {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileAvatar') {
|
||||
&::view-transition-old(.profileInfo),
|
||||
&::view-transition-new(.profileInfo) {
|
||||
animation-name: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Delay before showing buttons & overlay
|
||||
@include mixins.on-active-vt('profileExpand') {
|
||||
&::view-transition-new(.photoWrapper),
|
||||
&::view-transition-new(.info) {
|
||||
animation-delay: 0.25s;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide buttons & overlay immediately
|
||||
@include mixins.on-active-vt('profileCollapse') {
|
||||
&::view-transition-old(.photoWrapper),
|
||||
&::view-transition-old(.info) {
|
||||
animation-duration: 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileExpand') {
|
||||
&::view-transition-old(.avatar) {
|
||||
:local {
|
||||
animation-name: fadeOut;
|
||||
animation-duration: 0.1s;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::view-transition-new(.avatar) {
|
||||
:local {
|
||||
animation-name: profileExpandAvatar;
|
||||
animation-duration: inherit;
|
||||
animation-timing-function: inherit;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes profileExpandAvatar {
|
||||
from {
|
||||
opacity: 1;
|
||||
clip-path: inset(0 0 0 0 round 50%);
|
||||
}
|
||||
|
||||
25% {
|
||||
clip-path: inset(0 0 0 0 round 50%);
|
||||
}
|
||||
|
||||
to {
|
||||
clip-path: inset(0 0 0 0 round 0%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileCollapse') {
|
||||
&::view-transition-new(.avatar) {
|
||||
:local {
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 0.125s;
|
||||
animation-delay: 0.125s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::view-transition-old(.avatar) {
|
||||
:local {
|
||||
animation-name: profileCollapseAvatar;
|
||||
animation-duration: inherit;
|
||||
animation-timing-function: inherit;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes profileCollapseAvatar {
|
||||
from {
|
||||
clip-path: inset(0 0 0 0 round 0%);
|
||||
}
|
||||
|
||||
25% {
|
||||
clip-path: inset(0 0 0 0 round 50%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
clip-path: inset(0 0 0 0 round 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,5 @@
|
||||
// This class is used in `ghostAnimation`, so we need to keep it global
|
||||
.ProfileInfo {
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
@supports not (aspect-ratio: 1 / 1) {
|
||||
&::before {
|
||||
content: "";
|
||||
float: left;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.fullName {
|
||||
margin-bottom: 0;
|
||||
|
||||
@ -27,15 +10,6 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.VerifiedIcon,
|
||||
.StarIcon {
|
||||
--color-fill: var(--color-white);
|
||||
--color-checkmark: var(--color-primary);
|
||||
|
||||
z-index: 2;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.emoji:not(.custom-emoji) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
@ -47,6 +21,5 @@
|
||||
|
||||
pointer-events: auto;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
}
|
||||
@ -1,50 +1,69 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useState } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import { memo, useEffect, useMemo, useState } from '../../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiPeerPhotos, ApiSticker, ApiTopic, ApiUser, ApiUserFullInfo, ApiUserStatus,
|
||||
} from '../../api/types';
|
||||
import type { AnimationLevel } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
ApiChat,
|
||||
ApiEmojiStatusType,
|
||||
ApiPeerColorOption,
|
||||
ApiPeerPhotos,
|
||||
ApiPeerProfileColorSet,
|
||||
ApiSavedGifts,
|
||||
ApiSticker,
|
||||
ApiTopic,
|
||||
ApiUser,
|
||||
ApiUserFullInfo,
|
||||
ApiUserStatus,
|
||||
} from '../../../api/types/index';
|
||||
import type { IconName } from '../../../types/icons/index';
|
||||
import type { AnimationLevel, ThemeKey } from '../../../types/index';
|
||||
import { MediaViewerOrigin } from '../../../types/index';
|
||||
|
||||
import {
|
||||
getUserStatus, isAnonymousForwardsChat, isChatChannel, isSystemBot, isUserOnline,
|
||||
} from '../../global/helpers';
|
||||
} from '../../../global/helpers/index';
|
||||
import { getActions, withGlobal } from '../../../global/index';
|
||||
import {
|
||||
selectChat,
|
||||
selectCurrentMessageList,
|
||||
selectCustomEmoji,
|
||||
selectPeer,
|
||||
selectPeerHasProfileBackground,
|
||||
selectPeerPhotos,
|
||||
selectPeerProfileColor,
|
||||
selectPeerSavedGifts,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectThreadMessagesCount,
|
||||
selectTopic,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
selectUserStatus,
|
||||
} from '../../global/selectors';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState.ts';
|
||||
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { resolveTransitionName } from '../../util/resolveTransitionName.ts';
|
||||
import renderText from './helpers/renderText';
|
||||
} from '../../../global/selectors/index';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
import renderText from '../helpers/renderText.tsx';
|
||||
|
||||
import useIntervalForceUpdate from '../../hooks/schedulers/useIntervalForceUpdate';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
import usePhotosPreload from './hooks/usePhotosPreload';
|
||||
import { useVtn } from '../../../hooks/animations/useVtn';
|
||||
import useIntervalForceUpdate from '../../../hooks/schedulers/useIntervalForceUpdate';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
||||
import useCustomEmoji from '../hooks/useCustomEmoji';
|
||||
import usePhotosPreload from '../hooks/usePhotosPreload';
|
||||
|
||||
import Transition from '../ui/Transition';
|
||||
import Avatar from './Avatar';
|
||||
import FullNameTitle from './FullNameTitle';
|
||||
import Icon from './icons/Icon';
|
||||
import Transition from '../../ui/Transition.tsx';
|
||||
import Avatar from '../Avatar.tsx';
|
||||
import FullNameTitle from '../FullNameTitle.tsx';
|
||||
import Icon from '../icons/Icon.tsx';
|
||||
import TopicIcon from '../TopicIcon.tsx';
|
||||
import ProfilePhoto from './ProfilePhoto';
|
||||
import TopicIcon from './TopicIcon';
|
||||
import ProfilePinnedGifts from './ProfilePinnedGifts.tsx';
|
||||
import RadialPatternBackground from './RadialPatternBackground.tsx';
|
||||
|
||||
import './ProfileInfo.scss';
|
||||
import styles from './ProfileInfo.module.scss';
|
||||
@ -52,36 +71,43 @@ import styles from './ProfileInfo.module.scss';
|
||||
const MAX_LEVEL_ICON = 90;
|
||||
|
||||
type OwnProps = {
|
||||
isExpanded?: boolean;
|
||||
peerId: string;
|
||||
forceShowSelf?: boolean;
|
||||
isForSettings?: boolean;
|
||||
canPlayVideo: boolean;
|
||||
isForMonoforum?: boolean;
|
||||
};
|
||||
|
||||
type StateProps =
|
||||
{
|
||||
user?: ApiUser;
|
||||
userFullInfo?: ApiUserFullInfo;
|
||||
userStatus?: ApiUserStatus;
|
||||
chat?: ApiChat;
|
||||
mediaIndex?: number;
|
||||
avatarOwnerId?: string;
|
||||
topic?: ApiTopic;
|
||||
messagesCount?: number;
|
||||
animationLevel: AnimationLevel;
|
||||
emojiStatusSticker?: ApiSticker;
|
||||
emojiStatusSlug?: string;
|
||||
profilePhotos?: ApiPeerPhotos;
|
||||
};
|
||||
type StateProps = {
|
||||
user?: ApiUser;
|
||||
userFullInfo?: ApiUserFullInfo;
|
||||
userStatus?: ApiUserStatus;
|
||||
chat?: ApiChat;
|
||||
mediaIndex?: number;
|
||||
avatarOwnerId?: string;
|
||||
topic?: ApiTopic;
|
||||
messagesCount?: number;
|
||||
animationLevel: AnimationLevel;
|
||||
emojiStatus?: ApiEmojiStatusType;
|
||||
emojiStatusSticker?: ApiSticker;
|
||||
emojiStatusSlug?: string;
|
||||
profilePhotos?: ApiPeerPhotos;
|
||||
profileColorOption?: ApiPeerColorOption<ApiPeerProfileColorSet>;
|
||||
theme: ThemeKey;
|
||||
isPlain?: boolean;
|
||||
savedGifts?: ApiSavedGifts;
|
||||
};
|
||||
|
||||
const EMOJI_STATUS_SIZE = 24;
|
||||
const EMOJI_TOPIC_SIZE = 120;
|
||||
const LOAD_MORE_THRESHOLD = 3;
|
||||
const MAX_PHOTO_DASH_COUNT = 30;
|
||||
const STATUS_UPDATE_INTERVAL = 1000 * 60; // 1 min
|
||||
const PATTERN_COLOR = '#000000';
|
||||
|
||||
const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
forceShowSelf,
|
||||
const ProfileInfo = ({
|
||||
isExpanded,
|
||||
isForSettings,
|
||||
canPlayVideo,
|
||||
user,
|
||||
userFullInfo,
|
||||
@ -92,12 +118,17 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
topic,
|
||||
messagesCount,
|
||||
animationLevel,
|
||||
emojiStatus,
|
||||
emojiStatusSticker,
|
||||
emojiStatusSlug,
|
||||
profilePhotos,
|
||||
peerId,
|
||||
isForMonoforum,
|
||||
}) => {
|
||||
profileColorOption,
|
||||
theme,
|
||||
isPlain,
|
||||
savedGifts,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
openMediaViewer,
|
||||
openPremiumModal,
|
||||
@ -106,6 +137,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
loadMoreProfilePhotos,
|
||||
openUniqueGiftBySlug,
|
||||
openProfileRatingModal,
|
||||
loadPeerSavedGifts,
|
||||
} = getActions();
|
||||
|
||||
const oldLang = useOldLang();
|
||||
@ -113,6 +145,8 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useIntervalForceUpdate(user ? STATUS_UPDATE_INTERVAL : undefined);
|
||||
|
||||
const { createVtnStyle } = useVtn();
|
||||
|
||||
const photos = profilePhotos?.photos || MEMO_EMPTY_ARRAY;
|
||||
const prevMediaIndex = usePreviousDeprecated(mediaIndex);
|
||||
const prevAvatarOwnerId = usePreviousDeprecated(avatarOwnerId);
|
||||
@ -122,12 +156,46 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
const isFirst = photos.length <= 1 || currentPhotoIndex === 0;
|
||||
const isLast = photos.length <= 1 || currentPhotoIndex === photos.length - 1;
|
||||
|
||||
const collectibleEmojiStatus = emojiStatus?.type === 'collectible' ? emojiStatus : undefined;
|
||||
|
||||
const peer = user || chat;
|
||||
const { customEmoji: backgroundEmoji } = useCustomEmoji(
|
||||
collectibleEmojiStatus?.patternDocumentId || peer?.profileColor?.backgroundEmojiId,
|
||||
);
|
||||
|
||||
const profileColorSet = useMemo(() => {
|
||||
if (collectibleEmojiStatus) {
|
||||
return {
|
||||
bgColors: [collectibleEmojiStatus.centerColor, collectibleEmojiStatus.edgeColor],
|
||||
storyColors: [collectibleEmojiStatus.centerColor, collectibleEmojiStatus.edgeColor],
|
||||
};
|
||||
}
|
||||
|
||||
const colors = profileColorOption
|
||||
&& (theme === 'dark' ? profileColorOption.darkColors : profileColorOption.colors);
|
||||
if (!colors) return undefined;
|
||||
|
||||
// Why are they reversed on the server?
|
||||
return {
|
||||
bgColors: [...colors.bgColors].reverse(),
|
||||
storyColors: [...colors.storyColors].reverse(),
|
||||
};
|
||||
}, [profileColorOption, theme, collectibleEmojiStatus]);
|
||||
|
||||
const pinnedGifts = useMemo(() => {
|
||||
return savedGifts?.gifts.filter((gift) => gift.isPinned);
|
||||
}, [savedGifts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (photos.length - currentPhotoIndex <= LOAD_MORE_THRESHOLD) {
|
||||
loadMoreProfilePhotos({ peerId });
|
||||
}
|
||||
}, [currentPhotoIndex, peerId, photos.length]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPeerSavedGifts({ peerId });
|
||||
}, [peerId]);
|
||||
|
||||
// Set the current avatar photo to the last selected photo in Media Viewer after it is closed
|
||||
useEffect(() => {
|
||||
if (prevAvatarOwnerId && prevMediaIndex !== undefined && mediaIndex === undefined) {
|
||||
@ -144,6 +212,13 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [currentPhotoIndex, photos.length]);
|
||||
|
||||
// Reset photo index when collapsing
|
||||
useEffect(() => {
|
||||
if (!isExpanded) {
|
||||
setCurrentPhotoIndex(0);
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
usePhotosPreload(photos, currentPhotoIndex);
|
||||
|
||||
const handleProfilePhotoClick = useLastCallback(() => {
|
||||
@ -151,7 +226,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
isAvatarView: true,
|
||||
chatId: peerId,
|
||||
mediaIndex: currentPhotoIndex,
|
||||
origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar,
|
||||
origin: isForSettings ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar,
|
||||
});
|
||||
});
|
||||
|
||||
@ -239,7 +314,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
letterClassName={styles.topicIconTitle}
|
||||
noLoopLimit
|
||||
/>
|
||||
<h3 className={styles.topicTitle} dir={oldLang.isRtl ? 'rtl' : undefined}>{renderText(topic!.title)}</h3>
|
||||
<h3 className={styles.topicTitle} dir={lang.isRtl ? 'rtl' : undefined}>{renderText(topic!.title)}</h3>
|
||||
<p className={styles.topicMessagesCounter}>
|
||||
{messagesCount ? oldLang('Chat.Title.Topic', messagesCount, 'i') : oldLang('lng_forum_no_messages')}
|
||||
</p>
|
||||
@ -277,6 +352,8 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
chat={chat}
|
||||
photo={photo}
|
||||
canPlayVideo={Boolean(isActive && canPlayVideo)}
|
||||
className={buildClassName(isActive && styles.activeProfilePhoto)}
|
||||
style={isActive ? createVtnStyle('avatar', true) : undefined}
|
||||
onClick={handleProfilePhotoClick}
|
||||
/>
|
||||
);
|
||||
@ -292,7 +369,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (isNegative) {
|
||||
return (
|
||||
<span className={styles.userRatingNegativeWrapper} onClick={onRatingClick}>
|
||||
<span role="button" tabIndex={0} className={styles.userRatingNegativeWrapper} onClick={onRatingClick}>
|
||||
<Icon
|
||||
name="rating-icons-negative"
|
||||
className={styles.ratingNegativeIcon}
|
||||
@ -309,7 +386,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
: `rating-icons-level${Math.floor(iconLevel / 10) * 10}`) as IconName;
|
||||
|
||||
return (
|
||||
<span className={styles.userRatingWrapper} onClick={onRatingClick}>
|
||||
<span role="button" tabIndex={0} className={styles.userRatingWrapper} onClick={onRatingClick}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
className={styles.ratingIcon}
|
||||
@ -326,7 +403,11 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (isForMonoforum) {
|
||||
return (
|
||||
<span className={buildClassName(styles.status, 'status')} dir="auto">
|
||||
<span
|
||||
className={buildClassName(styles.status, 'status')}
|
||||
dir="auto"
|
||||
style={createVtnStyle('status', true)}
|
||||
>
|
||||
{lang('MonoforumStatus')}
|
||||
</span>
|
||||
);
|
||||
@ -340,6 +421,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
'status',
|
||||
isUserOnline(user, userStatus) && 'online',
|
||||
)}
|
||||
style={createVtnStyle('status', true)}
|
||||
>
|
||||
{renderUserRating()}
|
||||
<span className={styles.userStatus} dir="auto">
|
||||
@ -355,7 +437,11 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={buildClassName(styles.status, 'status')} dir="auto">
|
||||
<span
|
||||
className={buildClassName(styles.status, 'status')}
|
||||
dir="auto"
|
||||
style={createVtnStyle('status', true)}
|
||||
>
|
||||
{
|
||||
isChatChannel(chat!)
|
||||
? oldLang('Subscribers', chat!.membersCount ?? 0, 'i')
|
||||
@ -371,72 +457,121 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('ProfileInfo')}
|
||||
dir={oldLang.isRtl ? 'rtl' : undefined}
|
||||
className={buildClassName(
|
||||
'ProfileInfo',
|
||||
styles.root,
|
||||
!isExpanded && styles.minimized,
|
||||
isPlain && styles.plain,
|
||||
)}
|
||||
style={buildStyle(
|
||||
profileColorSet && `--rating-outline-color: ${isExpanded ? 'transparent' : profileColorSet?.bgColors[1]}`,
|
||||
profileColorSet && !isExpanded && `--rating-text-color: ${profileColorSet?.bgColors[1]}`,
|
||||
createVtnStyle('profileInfo', true),
|
||||
)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
<div className={styles.photoWrapper}>
|
||||
{renderPhotoTabs()}
|
||||
{!forceShowSelf && profilePhotos?.personalPhoto && (
|
||||
<div className={buildClassName(
|
||||
styles.fallbackPhoto,
|
||||
isFirst && styles.fallbackPhotoVisible,
|
||||
)}
|
||||
>
|
||||
<div className={styles.fallbackPhotoContents}>
|
||||
{oldLang(profilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
|
||||
{profileColorSet?.bgColors && (
|
||||
<RadialPatternBackground
|
||||
backgroundColors={profileColorSet.bgColors}
|
||||
patternIcon={backgroundEmoji}
|
||||
patternColor={collectibleEmojiStatus?.patternColor || PATTERN_COLOR}
|
||||
className={styles.radialPatternBackground}
|
||||
patternSize={0.75}
|
||||
/>
|
||||
)}
|
||||
{pinnedGifts && (
|
||||
<ProfilePinnedGifts
|
||||
peerId={peerId}
|
||||
gifts={pinnedGifts}
|
||||
isExpanded={isExpanded}
|
||||
className={styles.pinnedGifts}
|
||||
withGlow={!isPlain}
|
||||
/>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<div className={styles.photoWrapper} style={createVtnStyle('photoWrapper', true)}>
|
||||
{renderPhotoTabs()}
|
||||
{!isForSettings && profilePhotos?.personalPhoto && (
|
||||
<div className={buildClassName(
|
||||
styles.fallbackPhoto,
|
||||
isFirst && styles.fallbackPhotoVisible,
|
||||
)}
|
||||
>
|
||||
<div className={styles.fallbackPhotoContents}>
|
||||
{oldLang(profilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{forceShowSelf && profilePhotos?.fallbackPhoto && (
|
||||
<div className={buildClassName(
|
||||
styles.fallbackPhoto,
|
||||
(isFirst || isLast) && styles.fallbackPhotoVisible,
|
||||
)}
|
||||
>
|
||||
<div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}>
|
||||
{!isLast && (
|
||||
<Avatar
|
||||
photo={profilePhotos.fallbackPhoto}
|
||||
className={styles.fallbackPhotoAvatar}
|
||||
size="mini"
|
||||
/>
|
||||
)}
|
||||
{oldLang(profilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
|
||||
{isForSettings && profilePhotos?.fallbackPhoto && (
|
||||
<div className={buildClassName(
|
||||
styles.fallbackPhoto,
|
||||
(isFirst || isLast) && styles.fallbackPhotoVisible,
|
||||
)}
|
||||
>
|
||||
<div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}>
|
||||
{!isLast && (
|
||||
<Avatar
|
||||
photo={profilePhotos.fallbackPhoto}
|
||||
className={styles.fallbackPhotoAvatar}
|
||||
size="mini"
|
||||
/>
|
||||
)}
|
||||
{oldLang(profilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
activeKey={currentPhotoIndex}
|
||||
name={resolveTransitionName('slide', animationLevel, !hasSlideAnimation, oldLang.isRtl)}
|
||||
>
|
||||
{renderPhoto}
|
||||
</Transition>
|
||||
)}
|
||||
<Transition
|
||||
activeKey={currentPhotoIndex}
|
||||
name={resolveTransitionName('slide', animationLevel, !hasSlideAnimation, lang.isRtl)}
|
||||
>
|
||||
{renderPhoto}
|
||||
</Transition>
|
||||
|
||||
{!isFirst && (
|
||||
<button
|
||||
type="button"
|
||||
dir={oldLang.isRtl ? 'rtl' : undefined}
|
||||
className={buildClassName(styles.navigation, styles.navigation_prev)}
|
||||
aria-label={oldLang('AccDescrPrevious')}
|
||||
onClick={selectPreviousMedia}
|
||||
/>
|
||||
)}
|
||||
{!isLast && (
|
||||
<button
|
||||
type="button"
|
||||
dir={oldLang.isRtl ? 'rtl' : undefined}
|
||||
className={buildClassName(styles.navigation, styles.navigation_next)}
|
||||
aria-label={oldLang('Next')}
|
||||
onClick={selectNextMedia}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isFirst && (
|
||||
<button
|
||||
type="button"
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
className={buildClassName(styles.navigation, styles.navigation_prev)}
|
||||
aria-label={oldLang('AccDescrPrevious')}
|
||||
onClick={selectPreviousMedia}
|
||||
/>
|
||||
)}
|
||||
{!isLast && (
|
||||
<button
|
||||
type="button"
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
className={buildClassName(styles.navigation, styles.navigation_next)}
|
||||
aria-label={oldLang('Next')}
|
||||
onClick={selectNextMedia}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isExpanded && (
|
||||
<Avatar
|
||||
withStory
|
||||
storyColors={profileColorSet?.storyColors}
|
||||
className={styles.standaloneAvatar}
|
||||
key={peer?.id}
|
||||
size="jumbo"
|
||||
peer={peer}
|
||||
style={createVtnStyle('avatar', true)}
|
||||
onClick={isForSettings ? handleProfilePhotoClick : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={styles.info} dir={oldLang.isRtl ? 'rtl' : 'auto'}>
|
||||
<div
|
||||
className={styles.info}
|
||||
dir={lang.isRtl ? 'rtl' : 'auto'}
|
||||
style={createVtnStyle('info', true)}
|
||||
>
|
||||
{(user || chat) && (
|
||||
<FullNameTitle
|
||||
className={styles.title}
|
||||
style={createVtnStyle('title', true)}
|
||||
peer={(user || chat)!}
|
||||
withEmojiStatus
|
||||
withStatusTextColor
|
||||
emojiStatusSize={EMOJI_STATUS_SIZE}
|
||||
onEmojiStatusClick={handleStatusClick}
|
||||
noLoopLimit
|
||||
@ -451,6 +586,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { peerId }): Complete<StateProps> => {
|
||||
const peer = selectPeer(global, peerId);
|
||||
const user = selectUser(global, peerId);
|
||||
const userFullInfo = user ? selectUserFullInfo(global, peerId) : undefined;
|
||||
const userStatus = selectUserStatus(global, peerId);
|
||||
@ -462,10 +598,16 @@ export default memo(withGlobal<OwnProps>(
|
||||
const topic = isForum && currentTopicId ? selectTopic(global, peerId, currentTopicId) : undefined;
|
||||
const { animationLevel } = selectSharedSettings(global);
|
||||
|
||||
const emojiStatus = (user || chat)?.emojiStatus;
|
||||
const emojiStatus = peer?.emojiStatus;
|
||||
const emojiStatusSticker = emojiStatus ? selectCustomEmoji(global, emojiStatus.documentId) : undefined;
|
||||
const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined;
|
||||
|
||||
const profileColor = peer && selectPeerProfileColor(global, peer);
|
||||
const theme = selectTheme(global);
|
||||
|
||||
const hasBackground = selectPeerHasProfileBackground(global, peerId);
|
||||
const savedGifts = selectPeerSavedGifts(global, peerId);
|
||||
|
||||
return {
|
||||
user,
|
||||
userFullInfo,
|
||||
@ -476,9 +618,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
animationLevel,
|
||||
emojiStatusSticker,
|
||||
emojiStatusSlug,
|
||||
emojiStatus,
|
||||
profilePhotos,
|
||||
topic,
|
||||
messagesCount: topic ? selectThreadMessagesCount(global, peerId, currentTopicId!) : undefined,
|
||||
profileColorOption: profileColor,
|
||||
theme,
|
||||
isPlain: !hasBackground,
|
||||
savedGifts,
|
||||
};
|
||||
},
|
||||
)(ProfileInfo));
|
||||
@ -1,9 +1,9 @@
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import type { FC, TeactNode } from '../../../lib/teact/teact';
|
||||
import {
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
|
||||
import type { ApiChat, ApiPhoto, ApiUser } from '../../../api/types';
|
||||
|
||||
import {
|
||||
getChatAvatarHash,
|
||||
@ -15,24 +15,24 @@ import {
|
||||
isAnonymousForwardsChat,
|
||||
isChatWithRepliesBot,
|
||||
isDeletedUser,
|
||||
} from '../../global/helpers';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { isUserId } from '../../util/entities/ids';
|
||||
import { getFirstLetters } from '../../util/textFormat';
|
||||
import { getPeerColorClass } from './helpers/peerColor';
|
||||
import renderText from './helpers/renderText';
|
||||
} from '../../../global/helpers';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
import { getFirstLetters } from '../../../util/textFormat';
|
||||
import { getPeerColorClass } from '../helpers/peerColor';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useCanvasBlur from '../../../hooks/useCanvasBlur';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useMediaTransitionDeprecated from '../../../hooks/useMediaTransitionDeprecated';
|
||||
|
||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import Icon from './icons/Icon';
|
||||
import OptimizedVideo from '../../ui/OptimizedVideo';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import Icon from '../icons/Icon';
|
||||
|
||||
import './ProfilePhoto.scss';
|
||||
|
||||
@ -43,6 +43,8 @@ type OwnProps = {
|
||||
isSavedDialog?: boolean;
|
||||
photo?: ApiPhoto;
|
||||
canPlayVideo: boolean;
|
||||
className?: string;
|
||||
style?: string;
|
||||
onClick: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
@ -53,11 +55,13 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
isSavedMessages,
|
||||
isSavedDialog,
|
||||
canPlayVideo,
|
||||
className,
|
||||
style,
|
||||
onClick,
|
||||
}) => {
|
||||
const videoRef = useRef<HTMLVideoElement>();
|
||||
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const isDeleted = user && isDeletedUser(user);
|
||||
@ -142,6 +146,7 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
) : (
|
||||
<img
|
||||
src={fullMediaData}
|
||||
draggable={false}
|
||||
className={buildClassName('avatar-media', transitionClassNames)}
|
||||
alt=""
|
||||
/>
|
||||
@ -171,10 +176,11 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
isDeleted && 'deleted-account',
|
||||
isRepliesChat && 'replies-bot-account',
|
||||
(!isSavedMessages && !hasMedia) && 'no-photo',
|
||||
className,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={fullClassName} onClick={hasMedia ? onClick : undefined}>
|
||||
<div className={fullClassName} onClick={hasMedia ? onClick : undefined} style={style}>
|
||||
{typeof content === 'string' ? renderText(content, ['hq_emoji']) : content}
|
||||
</div>
|
||||
);
|
||||
57
src/components/common/profile/ProfilePinnedGifts.module.scss
Normal file
57
src/components/common/profile/ProfilePinnedGifts.module.scss
Normal file
@ -0,0 +1,57 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gift {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
position: absolute !important;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
@include mixins.with-vt-type('profileAvatar');
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileExpand') {
|
||||
&::view-transition-new(.profilePinnedGift) {
|
||||
:local {
|
||||
animation-name: fadeOut;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::view-transition-old(.profilePinnedGift) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('profileCollapse') {
|
||||
&::view-transition-old(.profilePinnedGift) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::view-transition-new(.profilePinnedGift) {
|
||||
:local {
|
||||
opacity: 0;
|
||||
animation-name: fadeIn;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/components/common/profile/ProfilePinnedGifts.tsx
Normal file
125
src/components/common/profile/ProfilePinnedGifts.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { memo, useMemo, useRef } from '@teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiSavedStarGift } from '../../../api/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { clamp } from '../../../util/math';
|
||||
import { getGiftAttributes } from '../helpers/gifts';
|
||||
import { REM } from '../helpers/mediaDimensions';
|
||||
|
||||
import { useVtn } from '../../../hooks/animations/useVtn';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import GiftEffectWrapper from '../gift/GiftEffectWrapper';
|
||||
import StickerView from '../StickerView';
|
||||
|
||||
import styles from './ProfilePinnedGifts.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
peerId: string;
|
||||
className?: string;
|
||||
gifts?: ApiSavedStarGift[];
|
||||
isExpanded?: boolean;
|
||||
withGlow?: boolean;
|
||||
};
|
||||
|
||||
const GIFT_STICKER_SIZE = 2 * REM;
|
||||
const POSITIONS = [
|
||||
{ x: -0.2, y: -0.3 },
|
||||
{ x: 0.3, y: 0.1 },
|
||||
{ x: -0.4, y: -0.1 },
|
||||
{ x: 0.4, y: -0.1 },
|
||||
{ x: -0.25, y: 0.1 },
|
||||
{ x: 0.25, y: -0.25 },
|
||||
];
|
||||
|
||||
const CENTER = { x: 0.5, y: 0.5 };
|
||||
|
||||
const ProfilePinnedGifts = ({
|
||||
peerId,
|
||||
gifts,
|
||||
isExpanded,
|
||||
className,
|
||||
withGlow,
|
||||
}: OwnProps) => {
|
||||
const { createVtnStyle } = useVtn();
|
||||
|
||||
if (!gifts) return undefined;
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, className)}>
|
||||
{gifts.slice(0, POSITIONS.length).map((gift, index) => {
|
||||
const position = !isExpanded ? POSITIONS[index] : getExpandedPosition(POSITIONS[index]);
|
||||
const style = buildStyle(
|
||||
`top: ${(CENTER.y + position.y) * 100}%`,
|
||||
`left: ${(CENTER.x + position.x) * 100}%`,
|
||||
);
|
||||
return (
|
||||
<PinnedGift
|
||||
peerId={peerId}
|
||||
className={styles.gift}
|
||||
key={gift.gift.id}
|
||||
gift={gift}
|
||||
style={buildStyle(style, createVtnStyle(`profilePinnedGift${index}`, 'profilePinnedGift'))}
|
||||
withGlow={withGlow}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getExpandedPosition(position: { x: number; y: number }) {
|
||||
return {
|
||||
x: clamp(position.x * 1.5, -0.45, 0.45),
|
||||
y: clamp(position.y * 1.5, -0.45, 0.45),
|
||||
};
|
||||
}
|
||||
|
||||
const PinnedGift = ({
|
||||
gift, style, className, withGlow, peerId,
|
||||
}: {
|
||||
gift: ApiSavedStarGift;
|
||||
style?: string;
|
||||
className?: string;
|
||||
withGlow?: boolean;
|
||||
peerId: string;
|
||||
}) => {
|
||||
const { openGiftInfoModal } = getActions();
|
||||
|
||||
const stickerRef = useRef<HTMLDivElement>();
|
||||
|
||||
const giftAttributes = useMemo(() => {
|
||||
return getGiftAttributes(gift.gift);
|
||||
}, [gift]);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
openGiftInfoModal({ peerId, gift });
|
||||
});
|
||||
|
||||
if (!giftAttributes?.model || !giftAttributes.backdrop) return undefined;
|
||||
|
||||
return (
|
||||
<GiftEffectWrapper
|
||||
withSparkles
|
||||
sparklesColor={giftAttributes.backdrop.textColor}
|
||||
glowColor={withGlow ? giftAttributes.backdrop.edgeColor : undefined}
|
||||
ref={stickerRef}
|
||||
className={className}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={giftAttributes.model.sticker}
|
||||
size={GIFT_STICKER_SIZE}
|
||||
withTranslucentThumb
|
||||
noPlay
|
||||
/>
|
||||
</GiftEffectWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ProfilePinnedGifts);
|
||||
@ -3,11 +3,14 @@
|
||||
border-radius: inherit;
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
background-image:
|
||||
radial-gradient(circle closest-side, #ffffff32, #ffffff00),
|
||||
radial-gradient(circle closest-side, #ffffff32 3rem, #ffffff00 7rem),
|
||||
radial-gradient(closest-side, var(--_bg-1), var(--_bg-2));
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ type OwnProps = {
|
||||
patternIcon?: ApiSticker;
|
||||
className?: string;
|
||||
clearBottomSector?: boolean;
|
||||
patternSize?: number;
|
||||
};
|
||||
|
||||
const RINGS = 3;
|
||||
@ -40,6 +41,7 @@ const RadialPatternBackground = ({
|
||||
patternIcon,
|
||||
clearBottomSector,
|
||||
className,
|
||||
patternSize = 1,
|
||||
}: OwnProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const canvasRef = useRef<HTMLCanvasElement>();
|
||||
@ -78,13 +80,12 @@ const RadialPatternBackground = ({
|
||||
const xOffset = ringRadius * 1.71 * Math.cos(angle);
|
||||
const yOffset = ringRadius * Math.sin(angle);
|
||||
|
||||
const x = 0.5 + xOffset;
|
||||
const y = 0.5 + yOffset;
|
||||
|
||||
const sizeFactor = 1.4 - ringProgress * Math.random();
|
||||
|
||||
coordinates.push({
|
||||
x, y, sizeFactor,
|
||||
x: xOffset,
|
||||
y: yOffset,
|
||||
sizeFactor,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -115,15 +116,16 @@ const RadialPatternBackground = ({
|
||||
const { width, height } = canvas;
|
||||
if (!width || !height) return;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.save();
|
||||
patternPositions.forEach(({
|
||||
x, y, sizeFactor,
|
||||
}) => {
|
||||
const centerShift = (width - Math.max(width, MIN_SIZE * dpr)) / 2; // Shift coords if canvas is smaller than `MIN_SIZE`
|
||||
const renderX = x * Math.max(width, MIN_SIZE * dpr) + centerShift;
|
||||
const renderY = y * Math.max(height, MIN_SIZE * dpr) + centerShift;
|
||||
const renderX = x * patternSize * Math.max(width, MIN_SIZE * dpr) + width / 2;
|
||||
const renderY = y * patternSize * Math.max(height, MIN_SIZE * dpr) + height / 2;
|
||||
|
||||
const size = BASE_ICON_SIZE * dpr * sizeFactor * (centerShift ? 0.8 : 1);
|
||||
const size = BASE_ICON_SIZE * dpr * patternSize * sizeFactor;
|
||||
|
||||
ctx.drawImage(emojiImage, renderX - size / 2, renderY - size / 2, size, size);
|
||||
});
|
||||
|
||||
@ -13,12 +13,12 @@ import { type AnimationLevel, LeftColumnContent, SettingsScreens } from '../../t
|
||||
import {
|
||||
selectCurrentChat, selectIsCurrentUserFrozen, selectIsForumPanelOpen, selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState.ts';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||
import {
|
||||
IS_APP, IS_FIREFOX, IS_MAC_OS, IS_TOUCH_ENV,
|
||||
} from '../../util/browser/windowEnvironment';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { resolveTransitionName } from '../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../util/resolveTransitionName';
|
||||
import { captureControlledSwipe } from '../../util/swipeController';
|
||||
|
||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||
|
||||
@ -430,6 +430,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isSavedMessages={chatId === user?.id && user?.isSelf}
|
||||
isSavedDialog={isSavedDialog}
|
||||
observeIntersection={observeIntersection}
|
||||
withStatusTextColor={isSelected}
|
||||
/>
|
||||
{isMuted && !isSavedDialog && <Icon name="muted" />}
|
||||
<div className="separator" />
|
||||
|
||||
@ -13,13 +13,13 @@ import { SettingsScreens } from '../../../types';
|
||||
import { ALL_FOLDER_ID } from '../../../config';
|
||||
import { selectCanShareFolder, selectIsCurrentUserFrozen, selectTabState } from '../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState.ts';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
|
||||
@ -14,6 +14,7 @@ import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import GiftEffectWrapper from '../../common/gift/GiftEffectWrapper';
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
import CustomEmojiEffect from '../../common/reactions/CustomEmojiEffect';
|
||||
import Button from '../../ui/Button';
|
||||
@ -37,6 +38,8 @@ const StatusButton: FC<StateProps> = ({ emojiStatus, collectibleStatuses, isAcco
|
||||
const [isStatusPickerOpen, openStatusPicker, closeStatusPicker] = useFlag(false);
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const collectibleEmojiStatus = emojiStatus?.type === 'collectible' ? emojiStatus : undefined;
|
||||
|
||||
const delay = emojiStatus?.until ? (emojiStatus.until - getServerTime()) * 1000 : undefined;
|
||||
useTimeout(loadCurrentUser, delay);
|
||||
|
||||
@ -86,13 +89,18 @@ const StatusButton: FC<StateProps> = ({ emojiStatus, collectibleStatuses, isAcco
|
||||
onClick={handleEmojiStatusClick}
|
||||
>
|
||||
{emojiStatus ? (
|
||||
<CustomEmoji
|
||||
key={emojiStatus.documentId}
|
||||
documentId={emojiStatus.documentId}
|
||||
size={EMOJI_STATUS_SIZE}
|
||||
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
|
||||
withSparkles={emojiStatus?.type === 'collectible'}
|
||||
/>
|
||||
<GiftEffectWrapper
|
||||
withSparkles={Boolean(collectibleEmojiStatus)}
|
||||
sparklesClassName="statusSparkles"
|
||||
sparklesColor={collectibleEmojiStatus?.textColor}
|
||||
>
|
||||
<CustomEmoji
|
||||
key={emojiStatus.documentId}
|
||||
documentId={emojiStatus.documentId}
|
||||
size={EMOJI_STATUS_SIZE}
|
||||
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
|
||||
/>
|
||||
</GiftEffectWrapper>
|
||||
) : <StarIcon />}
|
||||
</Button>
|
||||
<StatusPickerMenu
|
||||
|
||||
@ -4,9 +4,9 @@ import { getActions } from '../../../global';
|
||||
|
||||
import { type AnimationLevel, LeftColumnContent } from '../../../types';
|
||||
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback.ts';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Transition from '../../ui/Transition';
|
||||
import NewChatStep1 from './NewChatStep1';
|
||||
|
||||
@ -12,9 +12,9 @@ import type { RegularLangKey } from '../../../types/language';
|
||||
import { type AnimationLevel, GlobalSearchContent } from '../../../types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState.ts';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { parseDateString } from '../../../util/dates/dateFormat';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
|
||||
|
||||
@ -7,10 +7,10 @@ import type { AnimationLevel } from '../../../types';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState.ts';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||
|
||||
|
||||
@ -7,11 +7,11 @@ import type { AnimationLevel } from '../../../types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
|
||||
import useTwoFaReducer from '../../../hooks/reducers/useTwoFaReducer';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useScrollNotch from '../../../hooks/useScrollNotch.ts';
|
||||
import useScrollNotch from '../../../hooks/useScrollNotch';
|
||||
|
||||
import Transition from '../../ui/Transition';
|
||||
import SettingsFolders from './folders/SettingsFolders';
|
||||
|
||||
@ -21,7 +21,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
import ChatExtra from '../../common/profile/ChatExtra';
|
||||
import ProfileInfo from '../../common/ProfileInfo';
|
||||
import ProfileInfo from '../../common/profile/ProfileInfo';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
|
||||
@ -86,7 +86,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
<ProfileInfo
|
||||
peerId={currentUserId}
|
||||
canPlayVideo={Boolean(isActive)}
|
||||
forceShowSelf
|
||||
isForSettings
|
||||
/>
|
||||
)}
|
||||
{currentUserId && (
|
||||
|
||||
@ -6,17 +6,17 @@ import type { ApiPrivacySettings } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import { ACCOUNT_TTL_OPTIONS } from '../../../config.ts';
|
||||
import { ACCOUNT_TTL_OPTIONS } from '../../../config';
|
||||
import {
|
||||
selectCanSetPasscode, selectIsCurrentUserFrozen,
|
||||
selectIsCurrentUserPremium,
|
||||
} from '../../../global/selectors';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { getClosestEntry } from '../../../util/getClosestEntry.ts';
|
||||
import { getClosestEntry } from '../../../util/getClosestEntry';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback.ts';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
|
||||
@ -22,6 +22,8 @@ type StateProps = {
|
||||
currentUser: ApiUser;
|
||||
};
|
||||
|
||||
const STORY_COLORS = ['#A667FF', '#55A5FF'];
|
||||
|
||||
const STORY_FEATURE_TITLES = {
|
||||
stories_order: 'PremiumStoriesPriority',
|
||||
stories_stealth: 'PremiumStoriesStealth',
|
||||
@ -54,7 +56,8 @@ const STORY_FEATURE_ICONS: Record<string, IconName> = {
|
||||
|
||||
const STORY_FEATURE_ORDER = Object.keys(STORY_FEATURE_TITLES) as (keyof typeof STORY_FEATURE_TITLES)[];
|
||||
|
||||
const CIRCLE_SIZE = AVATAR_SIZES.giant + 0.25 * REM;
|
||||
const CIRCLE_STROKE_WIDTH = 0.25 * REM;
|
||||
const CIRCLE_SIZE = AVATAR_SIZES.giant + CIRCLE_STROKE_WIDTH;
|
||||
const CIRCLE_SEGMENTS = 8;
|
||||
const CIRCLE_READ_SEGMENTS = 0;
|
||||
|
||||
@ -74,9 +77,10 @@ const PremiumFeaturePreviewVideo = ({
|
||||
|
||||
drawGradientCircle({
|
||||
canvas: circleRef.current,
|
||||
size: CIRCLE_SIZE * dpr,
|
||||
size: CIRCLE_SIZE,
|
||||
strokeWidth: CIRCLE_STROKE_WIDTH,
|
||||
segmentsCount: CIRCLE_SEGMENTS,
|
||||
color: 'purple',
|
||||
colorStops: STORY_COLORS,
|
||||
readSegmentsCount: CIRCLE_READ_SEGMENTS,
|
||||
readSegmentColor: 'transparent',
|
||||
dpr,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from '@teact';
|
||||
import { beginHeavyAnimation, memo, useEffect, useMemo, useRef } from '@teact';
|
||||
import { addExtraClass, removeExtraClass } from '@teact/teact-dom.ts';
|
||||
import { addExtraClass, removeExtraClass } from '@teact/teact-dom';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChatFullInfo, ApiMessage, ApiRestrictionReason, ApiTopic } from '../../api/types';
|
||||
|
||||
@ -54,7 +54,7 @@ import {
|
||||
selectTopics,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState.ts';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||
import { IS_TAURI } from '../../util/browser/globalEnvironment';
|
||||
import {
|
||||
IS_ANDROID, IS_IOS, IS_MAC_OS, IS_SAFARI, IS_TRANSLATION_SUPPORTED, MASK_IMAGE_DISABLED,
|
||||
@ -63,7 +63,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { isUserId } from '../../util/entities/ids';
|
||||
import { resolveTransitionName } from '../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../util/resolveTransitionName';
|
||||
import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
@ -345,7 +345,7 @@ function MiddleColumn({
|
||||
return () => {
|
||||
visualViewport.removeEventListener('resize', handleResize);
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPrivate) {
|
||||
@ -508,7 +508,7 @@ function MiddleColumn({
|
||||
`--composer-hidden-scale: ${composerHiddenScale}`,
|
||||
`--toolbar-hidden-scale: ${toolbarHiddenScale}`,
|
||||
`--unpin-hidden-scale: ${unpinHiddenScale}`,
|
||||
`--toolbar-unpin-hidden-scale: ${toolbarForUnpinHiddenScale},`,
|
||||
`--toolbar-unpin-hidden-scale: ${toolbarForUnpinHiddenScale}`,
|
||||
`--composer-translate-x: ${composerTranslateX}px`,
|
||||
`--toolbar-translate-x: ${toolbarTranslateX}px`,
|
||||
`--pattern-color: ${patternColor}`,
|
||||
|
||||
@ -295,6 +295,7 @@
|
||||
|
||||
.symbol-set-container {
|
||||
display: grid !important;
|
||||
grid-auto-rows: var(--emoji-size, 4.5rem);
|
||||
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4.5rem));
|
||||
row-gap: 0.25rem;
|
||||
column-gap: var(--symbol-set-gap-size, 0.625rem);
|
||||
@ -311,7 +312,8 @@
|
||||
}
|
||||
|
||||
> .EmojiButton,
|
||||
> .StickerButton {
|
||||
> .StickerButton,
|
||||
> .gift-effect-wrapper > .StickerButton {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -11,10 +11,10 @@ import type { MenuPositionOptions } from '../../ui/Menu';
|
||||
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { selectIsContextMenuTranslucent, selectTabState } from '../../../global/selectors';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState.ts';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName.ts';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
@ -36,7 +36,6 @@ import SymbolMenuFooter, { SYMBOL_MENU_TAB_TITLES, SymbolMenuTabs } from './Symb
|
||||
import './SymbolMenu.scss';
|
||||
|
||||
const ANIMATION_DURATION = 350;
|
||||
const STICKERS_TAB_INDEX = 2;
|
||||
|
||||
export type OwnProps = {
|
||||
chatId: string;
|
||||
@ -123,7 +122,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
// If we can't send plain text, we should always show the stickers tab
|
||||
useEffect(() => {
|
||||
if (canSendPlainText) return;
|
||||
setActiveTab(STICKERS_TAB_INDEX);
|
||||
setActiveTab(SymbolMenuTabs.Stickers);
|
||||
}, [canSendPlainText]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -178,7 +177,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
setRecentEmojis([]);
|
||||
setRecentCustomEmojis([]);
|
||||
}, [isOpen, addRecentCustomEmoji]);
|
||||
|
||||
const handleCustomEmojiSelect = useLastCallback((emoji: ApiSticker) => {
|
||||
|
||||
@ -102,7 +102,6 @@ const GiftStatusInfoModal = ({
|
||||
withEmojiStatus
|
||||
noFake
|
||||
noVerified
|
||||
statusSparklesColor={subtitleColor}
|
||||
/>
|
||||
<p className={styles.status} style={buildStyle(subtitleColor && `color: ${subtitleColor}`)}>
|
||||
{lang('Online')}
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
@include mixins.side-panel-section;
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
.narrow {
|
||||
margin-bottom: 0;
|
||||
@ -84,6 +85,8 @@
|
||||
flex: 1;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
.TabList {
|
||||
z-index: 1;
|
||||
background: var(--color-background);
|
||||
@ -241,4 +244,8 @@
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.saved-gift {
|
||||
@include mixins.with-vt-type('profileGifts');
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import { MEMBERS_SLICE, PROFILE_SENSITIVE_AREA, SHARED_MEDIA_SLICE, SLIDE_TRANSI
|
||||
import { selectActiveGiftsCollectionId } from '../../global/selectors/payments';
|
||||
|
||||
const CONTENT_PANEL_SHOW_DELAY = 300;
|
||||
import { forceMutation } from '../../lib/fasterdom/fasterdom.ts';
|
||||
import {
|
||||
getHasAdminRight,
|
||||
getIsDownloading,
|
||||
@ -60,18 +61,26 @@ import { selectPremiumLimit } from '../../global/selectors/limits';
|
||||
import { selectMessageDownloadableMedia } from '../../global/selectors/media';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||
import { selectActiveStoriesCollectionId } from '../../global/selectors/stories';
|
||||
import {
|
||||
VTT_PROFILE_GIFTS,
|
||||
VTT_RIGHT_PROFILE_COLLAPSE,
|
||||
VTT_RIGHT_PROFILE_EXPAND,
|
||||
} from '../../util/animations/viewTransitionTypes.ts';
|
||||
import { areDeepEqual } from '../../util/areDeepEqual';
|
||||
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
||||
import { isUserId } from '../../util/entities/ids';
|
||||
import { stopScrollInertia } from '../../util/resetScroll.ts';
|
||||
import { resolveTransitionName } from '../../util/resolveTransitionName.ts';
|
||||
import { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import { getSenderName } from '../left/search/helpers/getSenderName';
|
||||
|
||||
import { useViewTransition } from '../../hooks/animations/useViewTransition';
|
||||
import { useVtn } from '../../hooks/animations/useVtn.ts';
|
||||
import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
|
||||
import useTopOverscroll from '../../hooks/scroll/useTopOverscroll.tsx';
|
||||
import useCacheBuster from '../../hooks/useCacheBuster';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
@ -96,7 +105,7 @@ import NothingFound from '../common/NothingFound';
|
||||
import PreviewMedia from '../common/PreviewMedia';
|
||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import ChatExtra from '../common/profile/ChatExtra';
|
||||
import ProfileInfo from '../common/ProfileInfo';
|
||||
import ProfileInfo from '../common/profile/ProfileInfo.tsx';
|
||||
import WebLink from '../common/WebLink';
|
||||
import ChatList from '../left/main/ChatList';
|
||||
import MediaStory from '../story/MediaStory';
|
||||
@ -172,6 +181,7 @@ type StateProps = {
|
||||
isSavedDialog?: boolean;
|
||||
forceScrollProfileTab?: boolean;
|
||||
isSynced?: boolean;
|
||||
hasAvatar?: boolean;
|
||||
};
|
||||
|
||||
type TabProps = {
|
||||
@ -242,6 +252,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
isSavedDialog,
|
||||
forceScrollProfileTab,
|
||||
isSynced,
|
||||
hasAvatar,
|
||||
onProfileStateChange,
|
||||
}) => {
|
||||
const {
|
||||
@ -274,10 +285,11 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
const lang = useLang();
|
||||
|
||||
const [deletingUserId, setDeletingUserId] = useState<string | undefined>();
|
||||
const [isViewTransitionEnabled, enableViewTransition, disableViewTransition] = useFlag();
|
||||
const [isGiftTransitionEnabled, enableGiftTransition, disableGiftTransition] = useFlag();
|
||||
|
||||
const profileId = isSavedDialog ? String(threadId) : chatId;
|
||||
const isSavedMessages = profileId === currentUserId && !isSavedDialog;
|
||||
const [isProfileExpanded, expandProfile, collapseProfile] = useFlag();
|
||||
|
||||
const [restoreContentHeightKey, setRestoreContentHeightKey] = useState(0);
|
||||
|
||||
@ -384,11 +396,11 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
}, [chatId]);
|
||||
|
||||
useSyncEffect(() => {
|
||||
enableViewTransition();
|
||||
enableGiftTransition();
|
||||
}, [giftsFilter]);
|
||||
|
||||
useSyncEffect(() => {
|
||||
disableViewTransition();
|
||||
disableGiftTransition();
|
||||
}, [gifts]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -399,7 +411,8 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
}, [chatId, hasGiftsTab, isSynced]);
|
||||
|
||||
const [renderingGifts, setRenderingGifts] = useState(gifts);
|
||||
const { startViewTransition, shouldApplyVtn } = useViewTransition();
|
||||
const { startViewTransition } = useViewTransition();
|
||||
const { createVtnStyle } = useVtn();
|
||||
|
||||
const giftIds = useMemo(() => renderingGifts?.map((gift) => getSavedGiftKey(gift)), [renderingGifts]);
|
||||
|
||||
@ -427,7 +440,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gifts || !prevGifts || !isViewTransitionEnabled) {
|
||||
if (!gifts || !prevGifts || !isGiftTransitionEnabled) {
|
||||
setRenderingGifts(gifts);
|
||||
return;
|
||||
}
|
||||
@ -436,14 +449,14 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
const newGiftIds = gifts.map((gift) => getSavedGiftKey(gift));
|
||||
const hasOrderChanged = prevGiftIds.some((id, index) => id !== newGiftIds[index]);
|
||||
|
||||
if (hasOrderChanged && animationLevel > 0) {
|
||||
startViewTransition(() => {
|
||||
if (hasOrderChanged) {
|
||||
startViewTransition(VTT_PROFILE_GIFTS, () => {
|
||||
setRenderingGifts(gifts);
|
||||
});
|
||||
} else {
|
||||
setRenderingGifts(gifts);
|
||||
}
|
||||
}, [gifts, startViewTransition, animationLevel, isViewTransitionEnabled]);
|
||||
}, [gifts, startViewTransition, isGiftTransitionEnabled]);
|
||||
|
||||
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds({
|
||||
loadMoreMembers: handleLoadMoreMembers,
|
||||
@ -509,15 +522,34 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
stopAutoScrollToTabs();
|
||||
});
|
||||
|
||||
const { handleScroll } = useProfileState(
|
||||
const handleExpandProfile = useLastCallback(() => {
|
||||
if (isProfileExpanded) return;
|
||||
startViewTransition(VTT_RIGHT_PROFILE_EXPAND, () => {
|
||||
expandProfile();
|
||||
});
|
||||
});
|
||||
|
||||
const handleCollapseProfile = useLastCallback(() => {
|
||||
if (!isProfileExpanded) return;
|
||||
const scrollContainer = containerRef.current;
|
||||
startViewTransition(VTT_RIGHT_PROFILE_COLLAPSE, () => {
|
||||
if (!scrollContainer) return;
|
||||
forceMutation(() => {
|
||||
stopScrollInertia(scrollContainer);
|
||||
}, scrollContainer);
|
||||
collapseProfile();
|
||||
});
|
||||
});
|
||||
|
||||
const { handleScroll } = useProfileState({
|
||||
containerRef,
|
||||
resultType,
|
||||
tabType: resultType,
|
||||
profileState,
|
||||
onProfileStateChange,
|
||||
forceScrollProfileTab,
|
||||
allowAutoScrollToTabs,
|
||||
onProfileStateChange,
|
||||
handleStopAutoScrollToTabs,
|
||||
);
|
||||
});
|
||||
|
||||
const { applyTransitionFix, releaseTransitionFix } = useTransitionFixes(containerRef);
|
||||
|
||||
@ -590,6 +622,10 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
resetGiftProfileFilter({ peerId: chatId });
|
||||
});
|
||||
|
||||
const renderedOverflowTrigger = useTopOverscroll(
|
||||
containerRef, handleExpandProfile, handleCollapseProfile, !hasAvatar,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transitionRef.current || !IS_TOUCH_ENV) {
|
||||
return undefined;
|
||||
@ -977,7 +1013,8 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
<SavedGift
|
||||
peerId={chatId}
|
||||
key={getSavedGiftKey(gift)}
|
||||
style={shouldApplyVtn ? `view-transition-name: vt${getSavedGiftKey(gift)}` : undefined}
|
||||
className="saved-gift"
|
||||
style={createVtnStyle(getSavedGiftKey(gift))}
|
||||
gift={gift}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
/>
|
||||
@ -1003,6 +1040,24 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
setRestoreContentHeightKey(restoreContentHeightKey + 1);
|
||||
});
|
||||
|
||||
function renderProfileInfo(peerId: string, isReady: boolean) {
|
||||
return (
|
||||
<div className="profile-info">
|
||||
<ProfileInfo
|
||||
isExpanded={isProfileExpanded}
|
||||
peerId={peerId}
|
||||
canPlayVideo={isReady}
|
||||
isForMonoforum={Boolean(monoforumChannel)}
|
||||
/>
|
||||
<ChatExtra
|
||||
chatOrUserId={profileId}
|
||||
isSavedDialog={isSavedDialog}
|
||||
style={createVtnStyle('chatExtra')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSpinnerOrContent(noContent: boolean, noSpinner: boolean) {
|
||||
const baseContent = renderSpinnerOrContentBase(noContent, noSpinner);
|
||||
|
||||
@ -1059,6 +1114,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
itemSelector={itemSelector}
|
||||
items={canRenderContent ? viewportIds : undefined}
|
||||
cacheBuster={cacheBuster}
|
||||
beforeChildren={renderedOverflowTrigger}
|
||||
sensitiveArea={PROFILE_SENSITIVE_AREA}
|
||||
preloadBackwards={canRenderContent ? (resultType === 'members' ? MEMBERS_SLICE : SHARED_MEDIA_SLICE) : 0}
|
||||
// To prevent scroll jumps caused by reordering member list
|
||||
@ -1071,13 +1127,12 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
renderProfileInfo(
|
||||
monoforumChannel?.id || profileId,
|
||||
isRightColumnShown && canRenderContent,
|
||||
isSavedDialog,
|
||||
Boolean(monoforumChannel),
|
||||
)
|
||||
)}
|
||||
{!isRestricted && (
|
||||
<div
|
||||
className="shared-media"
|
||||
style={createVtnStyle('sharedMedia')}
|
||||
>
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
@ -1120,15 +1175,6 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean, isForMonoforum?: boolean) {
|
||||
return (
|
||||
<div className="profile-info">
|
||||
<ProfileInfo peerId={profileId} canPlayVideo={isReady} isForMonoforum={isForMonoforum} />
|
||||
<ChatExtra chatOrUserId={profileId} isSavedDialog={isSavedDialog} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, {
|
||||
chatId, threadId, isMobile,
|
||||
@ -1201,6 +1247,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const monoforumChannel = selectMonoforumChannel(global, chatId);
|
||||
const isRestricted = chat && selectIsChatRestricted(global, chat.id);
|
||||
const hasAvatar = Boolean(peer?.avatarPhotoId);
|
||||
|
||||
return {
|
||||
theme: selectTheme(global),
|
||||
@ -1251,6 +1298,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
adminMembersById: hasMembersTab ? adminMembersById : undefined,
|
||||
commonChatIds: commonChats?.ids,
|
||||
monoforumChannel,
|
||||
hasAvatar,
|
||||
};
|
||||
},
|
||||
)(Profile));
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use "../../styles/mixins";
|
||||
|
||||
.RightHeader {
|
||||
pointer-events: auto;
|
||||
|
||||
@ -7,6 +9,10 @@
|
||||
height: var(--header-height);
|
||||
padding: 0.5rem 0.8125rem;
|
||||
|
||||
background: var(--color-background);
|
||||
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
.close-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -69,3 +75,9 @@
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('rightColumn') {
|
||||
&::view-transition-group(.rightHeader) {
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import { IS_MAC_OS } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { isUserId } from '../../util/entities/ids';
|
||||
|
||||
import { useVtn } from '../../hooks/animations/useVtn';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
@ -195,6 +196,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||
const { isMobile } = useAppLayout();
|
||||
const { createVtnStyle } = useVtn();
|
||||
|
||||
const {
|
||||
sortType: giftsSortType,
|
||||
@ -697,7 +699,11 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="RightHeader" data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}>
|
||||
<div
|
||||
className="RightHeader"
|
||||
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
|
||||
style={createVtnStyle('rightHeader', true)}
|
||||
>
|
||||
<Button
|
||||
className="close-button"
|
||||
round
|
||||
|
||||
@ -16,15 +16,23 @@ const runThrottledForScroll = throttle((cb) => cb(), 250, false);
|
||||
|
||||
let isScrollingProgrammatically = false;
|
||||
|
||||
export default function useProfileState(
|
||||
containerRef: ElementRef<HTMLDivElement>,
|
||||
tabType: ProfileTabType,
|
||||
profileState: ProfileState,
|
||||
onProfileStateChange: (state: ProfileState) => void,
|
||||
export default function useProfileState({
|
||||
containerRef,
|
||||
tabType,
|
||||
profileState,
|
||||
onProfileStateChange,
|
||||
forceScrollProfileTab = false,
|
||||
allowAutoScrollToTabs = false,
|
||||
handleStopAutoScrollToTabs: () => void,
|
||||
) {
|
||||
handleStopAutoScrollToTabs,
|
||||
}: {
|
||||
containerRef: ElementRef<HTMLDivElement>;
|
||||
tabType: ProfileTabType;
|
||||
profileState: ProfileState;
|
||||
forceScrollProfileTab?: boolean;
|
||||
allowAutoScrollToTabs?: boolean;
|
||||
onProfileStateChange: (state: ProfileState) => void;
|
||||
handleStopAutoScrollToTabs: NoneToVoidFunction;
|
||||
}) {
|
||||
// Scroll to tabs if needed
|
||||
useEffectWithPrevDeps(([prevTabType]) => {
|
||||
if ((prevTabType && prevTabType !== tabType && allowAutoScrollToTabs) || (tabType && forceScrollProfileTab)) {
|
||||
@ -96,7 +104,9 @@ export default function useProfileState(
|
||||
state = getStateFromTabType(tabType);
|
||||
}
|
||||
|
||||
onProfileStateChange(state);
|
||||
if (state !== profileState) {
|
||||
onProfileStateChange(state);
|
||||
}
|
||||
});
|
||||
|
||||
// Determine profile state when switching tabs
|
||||
|
||||
@ -3,12 +3,12 @@ import type React from '../../../lib/teact/teact.ts';
|
||||
import { useState } from '../../../lib/teact/teact.ts';
|
||||
import { memo } from '../../../lib/teact/teact.ts';
|
||||
|
||||
import type { ApiChat } from '../../../api/types/index.ts';
|
||||
import type { ManagementScreens } from '../../../types/index.ts';
|
||||
import { ChatCreationProgress } from '../../../types/index.ts';
|
||||
import type { ApiChat } from '../../../api/types/index';
|
||||
import type { ManagementScreens } from '../../../types/index';
|
||||
import { ChatCreationProgress } from '../../../types/index';
|
||||
|
||||
import { getActions, withGlobal } from '../../../global/index.ts';
|
||||
import { selectChat, selectTabState } from '../../../global/selectors/index.ts';
|
||||
import { getActions, withGlobal } from '../../../global/index';
|
||||
import { selectChat, selectTabState } from '../../../global/selectors/index';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack.ts';
|
||||
import useLang from '../../../hooks/useLang.ts';
|
||||
|
||||
@ -54,6 +54,7 @@ export type OwnProps = {
|
||||
isRectangular?: boolean;
|
||||
withPremiumGradient?: boolean;
|
||||
withSparkleEffect?: boolean;
|
||||
noSparkleAnimation?: boolean;
|
||||
noPreventDefault?: boolean;
|
||||
noForcedUpperCase?: boolean;
|
||||
shouldStopPropagation?: boolean;
|
||||
@ -78,13 +79,6 @@ const Button: FC<OwnProps> = ({
|
||||
ref,
|
||||
type = 'button',
|
||||
id,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onFocus,
|
||||
children,
|
||||
size = 'default',
|
||||
color = 'primary',
|
||||
@ -99,7 +93,7 @@ const Button: FC<OwnProps> = ({
|
||||
isShiny,
|
||||
withPremiumGradient,
|
||||
withSparkleEffect,
|
||||
onTransitionEnd,
|
||||
noSparkleAnimation,
|
||||
ariaLabel,
|
||||
ariaControls,
|
||||
hasPopup,
|
||||
@ -121,6 +115,14 @@ const Button: FC<OwnProps> = ({
|
||||
iconName,
|
||||
iconAlignment = 'start',
|
||||
iconClassName,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onFocus,
|
||||
onTransitionEnd,
|
||||
}) => {
|
||||
let elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>();
|
||||
if (ref) {
|
||||
@ -214,7 +216,7 @@ const Button: FC<OwnProps> = ({
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{withSparkleEffect && <Sparkles preset="button" />}
|
||||
{withSparkleEffect && <Sparkles preset="button" noAnimation={noSparkleAnimation} />}
|
||||
{renderContent()}
|
||||
{!isNotInteractive && ripple && (
|
||||
<RippleEffect />
|
||||
|
||||
@ -160,7 +160,8 @@ export const EDITABLE_STORY_INPUT_CSS_SELECTOR = `#${EDITABLE_STORY_INPUT_ID}`;
|
||||
export const CUSTOM_APPENDIX_ATTRIBUTE = 'data-has-custom-appendix';
|
||||
export const MESSAGE_CONTENT_CLASS_NAME = 'message-content';
|
||||
export const MESSAGE_CONTENT_SELECTOR = '.message-content';
|
||||
export const VIEW_TRANSITION_CLASS_NAME = 'active-view-transition';
|
||||
export const VT_CLASS_NAME = 'active-view-transition';
|
||||
export const VT_TYPE_CLASS_PREFIX = 'active-vt-';
|
||||
|
||||
export const RESIZE_HANDLE_CLASS_NAME = 'resizeHandle';
|
||||
export const RESIZE_HANDLE_SELECTOR = `.${RESIZE_HANDLE_CLASS_NAME}`;
|
||||
|
||||
@ -678,17 +678,27 @@ addActionHandler('loadConfig', async (global): Promise<void> => {
|
||||
});
|
||||
|
||||
addActionHandler('loadPeerColors', async (global): Promise<void> => {
|
||||
const hash = global.peerColors?.generalHash;
|
||||
const result = await callApi('fetchPeerColors', hash);
|
||||
if (!result) return;
|
||||
const generalHash = global.peerColors?.generalHash;
|
||||
const profileHash = global.peerColors?.profileHash;
|
||||
const [generalResult, profileResult] = await Promise.all([
|
||||
callApi('fetchPeerColors', generalHash),
|
||||
callApi('fetchPeerProfileColors', profileHash),
|
||||
]);
|
||||
|
||||
if (!generalResult && !profileResult) return;
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
const currentPeerColors = global.peerColors! || {};
|
||||
|
||||
global = {
|
||||
...global,
|
||||
peerColors: {
|
||||
...global.peerColors,
|
||||
general: result.colors,
|
||||
generalHash: result.hash,
|
||||
...currentPeerColors,
|
||||
general: generalResult?.colors || currentPeerColors.general,
|
||||
generalHash: generalResult?.hash || currentPeerColors.generalHash,
|
||||
profile: profileResult?.colors || currentPeerColors.profile,
|
||||
profileHash: profileResult?.hash || currentPeerColors.profileHash,
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
|
||||
@ -28,6 +28,7 @@ import {
|
||||
selectActiveGiftsCollectionId,
|
||||
selectGiftProfileFilter,
|
||||
selectPeer,
|
||||
selectPeerCollectionSavedGifts,
|
||||
selectPeerSavedGifts,
|
||||
selectTabState,
|
||||
} from '../../selectors';
|
||||
@ -297,13 +298,13 @@ addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
const currentGifts = selectPeerSavedGifts(global, peerId, tabId);
|
||||
const fetchingCollectionId = selectActiveGiftsCollectionId(global, peerId, tabId);
|
||||
const currentGifts = selectPeerCollectionSavedGifts(global, peerId, fetchingCollectionId, tabId);
|
||||
const localNextOffset = currentGifts?.nextOffset;
|
||||
|
||||
if (!shouldRefresh && currentGifts && !localNextOffset) return; // Already loaded all
|
||||
|
||||
const fetchingFilter = selectGiftProfileFilter(global, peerId, tabId);
|
||||
const fetchingCollectionId = selectActiveGiftsCollectionId(global, peerId, tabId);
|
||||
|
||||
const result = await callApi('fetchSavedStarGifts', {
|
||||
peer,
|
||||
@ -332,7 +333,8 @@ addActionHandler('reloadPeerSavedGifts', (global, actions, payload): ActionRetur
|
||||
} = payload;
|
||||
|
||||
Object.values(global.byTabId).forEach((tabState) => {
|
||||
if (selectPeerSavedGifts(global, peerId, tabState.id)) {
|
||||
const activeCollectionId = selectActiveGiftsCollectionId(global, peerId, tabState.id);
|
||||
if (selectPeerCollectionSavedGifts(global, peerId, activeCollectionId, tabState.id)) {
|
||||
actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id });
|
||||
}
|
||||
});
|
||||
@ -428,12 +430,7 @@ addActionHandler('changeGiftVisibility', async (global, actions, payload): Promi
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload gift list to avoid issues with pagination
|
||||
Object.values(global.byTabId).forEach((tabState) => {
|
||||
if (selectPeerSavedGifts(global, peerId, tabId)) {
|
||||
actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id });
|
||||
}
|
||||
});
|
||||
actions.reloadPeerSavedGifts({ peerId });
|
||||
});
|
||||
|
||||
addActionHandler('convertGiftToStars', async (global, actions, payload): Promise<void> => {
|
||||
@ -451,11 +448,7 @@ addActionHandler('convertGiftToStars', async (global, actions, payload): Promise
|
||||
}
|
||||
|
||||
const peerId = gift.type === 'user' ? global.currentUserId! : gift.chatId;
|
||||
Object.values(global.byTabId).forEach((tabState) => {
|
||||
if (selectPeerSavedGifts(global, peerId, tabState.id)) {
|
||||
actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id });
|
||||
}
|
||||
});
|
||||
actions.reloadPeerSavedGifts({ peerId });
|
||||
actions.openStarsBalanceModal({ tabId });
|
||||
});
|
||||
|
||||
@ -509,11 +502,7 @@ addActionHandler('toggleSavedGiftPinned', async (global, actions, payload): Prom
|
||||
|
||||
if (!result) return;
|
||||
|
||||
Object.values(global.byTabId).forEach((tabState) => {
|
||||
if (selectPeerSavedGifts(global, peerId, tabState.id)) {
|
||||
actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id });
|
||||
}
|
||||
});
|
||||
actions.reloadPeerSavedGifts({ peerId });
|
||||
});
|
||||
|
||||
addActionHandler('updateStarGiftPrice', async (global, actions, payload): Promise<void> => {
|
||||
|
||||
@ -6,7 +6,7 @@ import { isUserId } from '../../util/entities/ids';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { isChatAdmin, isDeletedUser } from '../helpers';
|
||||
import { selectChat, selectChatFullInfo } from './chats';
|
||||
import { selectActiveGiftsCollectionId } from './payments';
|
||||
import { type ProfileCollectionKey } from './payments';
|
||||
import { selectTabState } from './tabs';
|
||||
import { selectBot, selectUser, selectUserFullInfo } from './users';
|
||||
|
||||
@ -29,14 +29,22 @@ export function selectCanGift<T extends GlobalState>(global: T, peerId: string)
|
||||
return selectChatFullInfo(global, peerId)?.areStarGiftsAvailable;
|
||||
}
|
||||
|
||||
export function selectPeerCollectionSavedGifts<T extends GlobalState>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
collectionId: ProfileCollectionKey,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): ApiSavedGifts | undefined {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
return tabState.savedGifts.collectionsByPeerId[peerId]?.[collectionId];
|
||||
}
|
||||
|
||||
export function selectPeerSavedGifts<T extends GlobalState>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): ApiSavedGifts | undefined {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const activeCollectionId = selectActiveGiftsCollectionId(global, peerId, tabId);
|
||||
return tabState.savedGifts.collectionsByPeerId[peerId]?.[activeCollectionId];
|
||||
return selectPeerCollectionSavedGifts(global, peerId, 'all', tabId);
|
||||
}
|
||||
|
||||
export function selectPeerStarGiftCollections<T extends GlobalState>(
|
||||
@ -61,3 +69,8 @@ export function selectPeerPaidMessagesStars<T extends GlobalState>(
|
||||
if (isChatAdmin(chat)) return undefined;
|
||||
return chat.paidMessagesStars;
|
||||
}
|
||||
|
||||
export function selectPeerHasProfileBackground<T extends GlobalState>(global: T, peerId: string) {
|
||||
const peer = selectPeer(global, peerId);
|
||||
return Boolean(peer?.profileColor || peer?.emojiStatus?.type === 'collectible');
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ApiMessage, ApiSponsoredMessage } from '../../api/types';
|
||||
import type { PerformanceTypeKey, ThemeKey } from '../../types';
|
||||
import type { ApiMessage, ApiPeer, ApiSponsoredMessage } from '../../api/types';
|
||||
import type { CustomPeer, PerformanceTypeKey, ThemeKey } from '../../types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
import { NewChatMembersProgress, RightColumnContent } from '../../types';
|
||||
|
||||
@ -186,3 +186,9 @@ export function selectSettingsScreen<T extends GlobalState>(
|
||||
) {
|
||||
return selectTabState(global, tabId).leftColumn.settingsScreen;
|
||||
}
|
||||
|
||||
export function selectPeerProfileColor<T extends GlobalState>(global: T, peer: ApiPeer | CustomPeer) {
|
||||
const key = 'isCustomPeer' in peer ? peer.peerColorId : peer.profileColor?.color;
|
||||
if (!key) return undefined;
|
||||
return global.peerColors?.profile[key];
|
||||
}
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import {
|
||||
beginHeavyAnimation,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getGlobal } from '../../global';
|
||||
|
||||
import { VIEW_TRANSITION_CLASS_NAME } from '../../config';
|
||||
import type { AnimationLevel } from '../../types';
|
||||
import type { VTTypes } from '../../util/animations/viewTransitionTypes';
|
||||
|
||||
import { VT_CLASS_NAME, VT_TYPE_CLASS_PREFIX } from '../../config';
|
||||
import { requestMutation, requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||
import { IS_VIEW_TRANSITION_SUPPORTED } from '../../util/browser/windowEnvironment';
|
||||
import Deferred from '../../util/Deferred';
|
||||
|
||||
@ -14,25 +20,37 @@ type TransitionFunction = () => Promise<void> | void;
|
||||
type TransitionState = 'idle' | 'capturing-old' | 'capturing-new' | 'animating' | 'skipped';
|
||||
interface ViewTransitionController {
|
||||
transitionState: TransitionState;
|
||||
shouldApplyVtn?: boolean;
|
||||
startViewTransition: (domUpdateCallback?: TransitionFunction) => PromiseLike<void> | void;
|
||||
startViewTransition: (
|
||||
types: VTTypes, domUpdateCallback?: TransitionFunction, minimumAnimationLevel?: AnimationLevel,
|
||||
) => PromiseLike<void> | void;
|
||||
}
|
||||
|
||||
type ViewTransitionParameters = {
|
||||
domUpdateCallback?: TransitionFunction;
|
||||
types?: VTTypes;
|
||||
};
|
||||
|
||||
const SKIP_TIMEOUT = 1000;
|
||||
|
||||
let hasActiveTransition = false;
|
||||
export function hasActiveViewTransition(): boolean {
|
||||
return hasActiveTransition;
|
||||
}
|
||||
|
||||
export function useViewTransition(): ViewTransitionController {
|
||||
const domUpdaterFn = useRef<TransitionFunction>();
|
||||
const parameters = useRef<ViewTransitionParameters>();
|
||||
const [transitionState, setTransitionState] = useState<TransitionState>('idle');
|
||||
|
||||
useEffect(() => {
|
||||
if (transitionState !== 'capturing-old') return;
|
||||
const { domUpdateCallback, types } = parameters.current || {};
|
||||
|
||||
const onHeavyAnimationEnd = beginHeavyAnimation();
|
||||
const transition = document.startViewTransition(async () => {
|
||||
setTransitionState('capturing-new');
|
||||
if (domUpdaterFn.current) await domUpdaterFn.current();
|
||||
if (domUpdateCallback) {
|
||||
await domUpdateCallback();
|
||||
}
|
||||
const deferred = new Deferred<void>();
|
||||
requestNextMutation(() => {
|
||||
deferred.resolve();
|
||||
@ -40,46 +58,87 @@ export function useViewTransition(): ViewTransitionController {
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
types?.getTypes().forEach((type) => {
|
||||
transition.types?.add(type);
|
||||
});
|
||||
|
||||
transition.finished.then(() => {
|
||||
onHeavyAnimationEnd();
|
||||
setTransitionState('idle');
|
||||
requestMutation(() => {
|
||||
document.body.classList.remove(VIEW_TRANSITION_CLASS_NAME);
|
||||
cleanUp(types);
|
||||
});
|
||||
|
||||
hasActiveTransition = false;
|
||||
});
|
||||
|
||||
let isReady = false;
|
||||
|
||||
transition.ready.then(() => {
|
||||
isReady = true;
|
||||
setTransitionState('animating');
|
||||
}).catch((e: unknown) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
console.error('View transition error', e, types?.getTypes());
|
||||
setTransitionState('skipped');
|
||||
requestMutation(() => {
|
||||
document.body.classList.remove(VIEW_TRANSITION_CLASS_NAME);
|
||||
cleanUp(types);
|
||||
});
|
||||
|
||||
hasActiveTransition = false;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!isReady) { // Skip transition if it's not prepared in time
|
||||
transition.skipTransition();
|
||||
}
|
||||
}, SKIP_TIMEOUT);
|
||||
}, [transitionState]);
|
||||
|
||||
function startViewTransition(updateCallback?: TransitionFunction): PromiseLike<void> | void {
|
||||
function startViewTransition(
|
||||
types: VTTypes,
|
||||
updateCallback?: TransitionFunction,
|
||||
minimumAnimationLevel: AnimationLevel = 1,
|
||||
): PromiseLike<void> | void {
|
||||
const global = getGlobal();
|
||||
const { animationLevel } = selectSharedSettings(global);
|
||||
// Fallback: simply run the callback immediately if view transitions aren't supported.
|
||||
if (!IS_VIEW_TRANSITION_SUPPORTED) {
|
||||
if (updateCallback) updateCallback();
|
||||
if (!IS_VIEW_TRANSITION_SUPPORTED || animationLevel < minimumAnimationLevel) {
|
||||
updateCallback?.();
|
||||
return;
|
||||
}
|
||||
|
||||
domUpdaterFn.current = updateCallback;
|
||||
if (hasActiveTransition) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('VT skipped because another transition is already active', types.getTypes());
|
||||
updateCallback?.();
|
||||
return;
|
||||
}
|
||||
|
||||
parameters.current = {
|
||||
domUpdateCallback: updateCallback,
|
||||
types,
|
||||
};
|
||||
setTransitionState('capturing-old');
|
||||
requestMutation(() => {
|
||||
document.body.classList.add(VIEW_TRANSITION_CLASS_NAME);
|
||||
document.documentElement.classList.add(VT_CLASS_NAME);
|
||||
types.getTypes().forEach((type) => {
|
||||
document.documentElement.classList.add(`${VT_TYPE_CLASS_PREFIX}${type}`);
|
||||
});
|
||||
});
|
||||
|
||||
hasActiveTransition = true;
|
||||
}
|
||||
|
||||
return {
|
||||
shouldApplyVtn: transitionState === 'capturing-old'
|
||||
|| transitionState === 'capturing-new' || transitionState === 'animating',
|
||||
transitionState,
|
||||
startViewTransition,
|
||||
};
|
||||
}
|
||||
|
||||
function cleanUp(types?: VTTypes) {
|
||||
types?.getTypes().forEach((type) => {
|
||||
document.documentElement.classList.remove(`${VT_TYPE_CLASS_PREFIX}${type}`);
|
||||
});
|
||||
document.documentElement.classList.remove(VT_CLASS_NAME);
|
||||
}
|
||||
|
||||
28
src/hooks/animations/useVtn.ts
Normal file
28
src/hooks/animations/useVtn.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import useLastCallback from '../useLastCallback';
|
||||
import useUniqueId from '../useUniqueId';
|
||||
|
||||
const VTN_PROPERTY_NAME = '--_vtn';
|
||||
|
||||
CSS.registerProperty?.({
|
||||
name: VTN_PROPERTY_NAME,
|
||||
syntax: '*',
|
||||
inherits: false,
|
||||
});
|
||||
|
||||
export function useVtn(uniqueId?: string) {
|
||||
const fallbackId = useUniqueId();
|
||||
|
||||
// Pass `true` to use the same class name as the name parameter
|
||||
const createVtnStyle = useLastCallback((name: string, vtClass?: string | boolean) => {
|
||||
const vtClassString = vtClass === true ? name : (vtClass || undefined);
|
||||
return buildStyle(
|
||||
`${VTN_PROPERTY_NAME}: vtn-${name}-${uniqueId || fallbackId}`,
|
||||
vtClassString && `view-transition-class: ${vtClassString}`,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
createVtnStyle,
|
||||
};
|
||||
}
|
||||
@ -26,6 +26,7 @@ export default function useTopOverscroll(
|
||||
const lastIsOnTopRef = useRef(true);
|
||||
const lastScrollAtRef = useRef(0);
|
||||
const isReturningOverscrollRef = useRef(false);
|
||||
const lastCalledStateRef = useRef<'overscroll' | 'reset' | undefined>(undefined);
|
||||
|
||||
const enableOverscrollTrigger = useLastCallback((noScrollInertiaStop = false) => {
|
||||
if (isTriggerEnabledRef.current) return;
|
||||
@ -77,14 +78,16 @@ export default function useTopOverscroll(
|
||||
forceMutation(disableOverscrollTrigger, overscrollTriggerRef.current);
|
||||
}
|
||||
|
||||
if (
|
||||
isMovingUp && (
|
||||
if (lastCalledStateRef.current !== 'overscroll' && isMovingUp
|
||||
&& (
|
||||
(lastIsOnTopRef.current && lastEventDelay > INERTIA_THRESHOLD)
|
||||
|| (newScrollTop < 0 && isReturningOverscrollRef.current) // Overscroll repeated by the user
|
||||
)) {
|
||||
onOverscroll?.();
|
||||
} else if (isMovingDown && newScrollTop > 0) {
|
||||
lastCalledStateRef.current = 'overscroll';
|
||||
} else if (lastCalledStateRef.current !== 'reset' && isMovingDown && newScrollTop > 0) {
|
||||
onReset?.();
|
||||
lastCalledStateRef.current = 'reset';
|
||||
}
|
||||
|
||||
lastScrollTopRef.current = newScrollTop;
|
||||
@ -101,10 +104,12 @@ export default function useTopOverscroll(
|
||||
const isScrollable = container.scrollHeight > container.offsetHeight;
|
||||
if (isScrollable || event.deltaY === 0) return;
|
||||
|
||||
if (event.deltaY < 0) {
|
||||
if (lastCalledStateRef.current !== 'overscroll' && event.deltaY < 0) {
|
||||
onOverscroll?.();
|
||||
} else {
|
||||
lastCalledStateRef.current = 'overscroll';
|
||||
} else if (lastCalledStateRef.current !== 'reset') {
|
||||
onReset?.();
|
||||
lastCalledStateRef.current = 'reset';
|
||||
}
|
||||
}, [containerRef, onOverscroll, onReset], MOUSE_WHEEL_DEBOUNCE);
|
||||
|
||||
|
||||
@ -1708,6 +1708,7 @@ help.getAppConfig#61e3f854 hash:int = help.AppConfig;
|
||||
help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList;
|
||||
help.getPremiumPromo#b81b93d4 = help.PremiumPromo;
|
||||
help.getPeerColors#da80f42f hash:int = help.PeerColors;
|
||||
help.getPeerProfileColors#abcfa9fd hash:int = help.PeerColors;
|
||||
help.getTimezonesList#49b30240 hash:int = help.TimezonesList;
|
||||
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
|
||||
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
|
||||
|
||||
@ -248,6 +248,7 @@
|
||||
"help.getCountriesList",
|
||||
"help.getAppConfig",
|
||||
"help.getPeerColors",
|
||||
"help.getPeerProfileColors",
|
||||
"help.getTimezonesList",
|
||||
"help.getPremiumPromo",
|
||||
"channels.readHistory",
|
||||
|
||||
@ -178,3 +178,17 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin on-active-vt($type) {
|
||||
:global {
|
||||
.active-vt-#{$type} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin with-vt-type($type) {
|
||||
:global(.active-vt-#{$type}) & {
|
||||
view-transition-name: var(--_vtn);
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,13 +484,18 @@ body:not(.is-ios) {
|
||||
--color-chat-username: rgb(233, 238, 244);
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line scss/at-rule-no-unknown */
|
||||
@view-transition {
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
navigation: auto;
|
||||
types: page;
|
||||
}
|
||||
|
||||
:root {
|
||||
view-transition-name: none;
|
||||
|
||||
@include mixins.with-vt-type('page');
|
||||
}
|
||||
|
||||
@include mixins.on-active-vt('page') {
|
||||
&::view-transition-group(root) {
|
||||
animation-duration: 400ms;
|
||||
}
|
||||
|
||||
23
src/util/animations/viewTransitionTypes.ts
Normal file
23
src/util/animations/viewTransitionTypes.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export class VTTypes {
|
||||
private readonly hierarchy: string[] = [];
|
||||
|
||||
constructor(types: readonly string[]) {
|
||||
this.hierarchy = [...(types || [])];
|
||||
}
|
||||
|
||||
with(type: string): VTTypes {
|
||||
return new VTTypes([...this.hierarchy, type]);
|
||||
}
|
||||
|
||||
getTypes(): readonly string[] {
|
||||
return this.hierarchy;
|
||||
}
|
||||
}
|
||||
|
||||
// View transition types
|
||||
export const VTT_RIGHT_COLUMN = new VTTypes(['rightColumn']);
|
||||
export const VTT_RIGHT_PROFILE_AVATAR = VTT_RIGHT_COLUMN.with('profileAvatar');
|
||||
export const VTT_RIGHT_PROFILE_EXPAND = VTT_RIGHT_PROFILE_AVATAR.with('profileExpand');
|
||||
export const VTT_RIGHT_PROFILE_COLLAPSE = VTT_RIGHT_PROFILE_AVATAR.with('profileCollapse');
|
||||
|
||||
export const VTT_PROFILE_GIFTS = VTT_RIGHT_COLUMN.with('profileGifts');
|
||||
@ -109,7 +109,7 @@ export const IS_BACKDROP_BLUR_SUPPORTED = CSS.supports('backdrop-filter: blur()'
|
||||
export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
|
||||
export const IS_OPEN_IN_NEW_TAB_SUPPORTED = !(IS_PWA && IS_MOBILE);
|
||||
export const IS_TRANSLATION_SUPPORTED = !IS_TEST;
|
||||
export const IS_VIEW_TRANSITION_SUPPORTED = 'ViewTransition' in window;
|
||||
export const IS_VIEW_TRANSITION_SUPPORTED = CSS.supports('view-transition-class: test');
|
||||
|
||||
export const MESSAGE_LIST_SENSITIVE_AREA = 750;
|
||||
|
||||
|
||||
@ -33,12 +33,9 @@
|
||||
"tests",
|
||||
"plugins",
|
||||
"dev",
|
||||
"webpack.config.ts",
|
||||
"babel.config.js",
|
||||
"eslint.config.mjs",
|
||||
"*.config.ts",
|
||||
"*.config.js",
|
||||
".fantasticonrc.cjs",
|
||||
"playwright.config.ts",
|
||||
"jest.config.js",
|
||||
".github/workflows/*.js",
|
||||
"deploy/*.js"
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user