diff --git a/src/components/common/PreviewBlock.module.scss b/src/components/common/PreviewBlock.module.scss
new file mode 100644
index 000000000..403b41aa9
--- /dev/null
+++ b/src/components/common/PreviewBlock.module.scss
@@ -0,0 +1,132 @@
+.root {
+ --preview-background-overscan: 50%;
+
+ isolation: isolate;
+ position: relative;
+
+ overflow: hidden;
+
+ border-radius: 1.5rem;
+
+ background-color: var(--theme-background-color);
+}
+
+.background {
+ &::before,
+ &::after {
+ inset: calc(var(--preview-background-overscan) * -1);
+ }
+}
+
+.content {
+ position: relative;
+ z-index: 1;
+
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ min-height: 100%;
+ padding: 0.75rem;
+}
+
+.message,
+.bubble {
+ --preview-message-background: var(--color-background);
+ --preview-message-sender-color: var(--accent-color);
+ --preview-message-shadow: 0 1px 2px var(--color-default-shadow);
+
+ min-width: 0;
+ max-width: 100%;
+ padding: 0.375rem 0.5rem 0.4375rem 0.625rem;
+ border-radius: 1.125rem;
+
+ background: var(--preview-message-background);
+ box-shadow: var(--preview-message-shadow);
+}
+
+.message {
+ display: flex;
+ flex-direction: column;
+}
+
+.messageWithAvatar {
+ display: flex;
+ gap: 0.5rem;
+ align-items: flex-end;
+
+ width: 100%;
+ min-width: 0;
+}
+
+.avatar {
+ flex: 0 0 auto;
+}
+
+.bubble {
+ flex: 1 1 auto;
+}
+
+.header {
+ user-select: none;
+
+ display: flex;
+ align-items: center;
+
+ min-width: 0;
+ height: 1.25rem;
+ margin-bottom: 0.375rem;
+
+ font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
+ line-height: 1;
+}
+
+.sender {
+ overflow: hidden;
+ flex: 0 1 auto;
+
+ font-weight: var(--font-weight-medium);
+ color: var(--preview-message-sender-color);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.spacer {
+ flex: 1 1 auto;
+ min-width: 0.5rem;
+}
+
+.badge {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ margin-inline-start: 0.5rem;
+}
+
+.body {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+
+ min-width: 0;
+
+ font-size: var(--message-text-size, 1rem);
+}
+
+.footer {
+ user-select: none;
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 0.375rem;
+}
+
+.time {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: flex-end;
+
+ font-size: 0.75rem;
+ line-height: 1;
+ color: rgba(var(--color-text-meta-rgb), 0.75);
+ white-space: nowrap;
+}
diff --git a/src/components/common/PreviewBlock.tsx b/src/components/common/PreviewBlock.tsx
new file mode 100644
index 000000000..7a56c06c8
--- /dev/null
+++ b/src/components/common/PreviewBlock.tsx
@@ -0,0 +1,241 @@
+import type { FC, TeactNode } from '../../lib/teact/teact';
+import { memo } from '../../lib/teact/teact';
+import { withGlobal } from '../../global';
+
+import type { ThemeKey } from '../../types';
+
+import { selectTheme, selectThemeValues } from '../../global/selectors';
+import buildClassName from '../../util/buildClassName';
+import buildStyle from '../../util/buildStyle';
+
+import useCustomBackground from '../../hooks/useCustomBackground';
+
+import backgroundStyles from '../../styles/_patternBackground.module.scss';
+import styles from './PreviewBlock.module.scss';
+
+type OwnProps = {
+ children: TeactNode;
+ className?: string;
+ style?: string;
+ contentClassName?: string;
+ backgroundClassName?: string;
+ backgroundStyle?: string;
+ backgroundColor?: string;
+ patternColor?: string;
+ customBackground?: string;
+ isBackgroundBlurred?: boolean;
+};
+
+type StateProps = {
+ theme: ThemeKey;
+ themeBackgroundColor?: string;
+ themePatternColor?: string;
+ themeCustomBackground?: string;
+ themeIsBackgroundBlurred?: boolean;
+};
+
+type MessageProps = {
+ children?: TeactNode;
+ className?: string;
+ style?: string;
+ bubbleClassName?: string;
+ bubbleStyle?: string;
+ headerClassName?: string;
+ bodyClassName?: string;
+ footerClassName?: string;
+ avatar?: TeactNode;
+ sender?: TeactNode;
+ badge?: TeactNode;
+ footer?: TeactNode;
+ time?: TeactNode;
+ senderColor?: string;
+ backgroundColor?: string;
+};
+
+type MessageTimeProps = {
+ children?: TeactNode;
+ className?: string;
+ style?: string;
+};
+
+type PreviewBlockMessageComponent = FC
& {
+ Time: FC;
+};
+
+type PreviewBlockComponent = FC & {
+ Message: PreviewBlockMessageComponent;
+};
+
+const PreviewBlockBase = ({
+ children,
+ className,
+ style,
+ contentClassName,
+ backgroundClassName,
+ backgroundStyle,
+ backgroundColor,
+ patternColor,
+ customBackground,
+ isBackgroundBlurred,
+ theme,
+ themeBackgroundColor,
+ themePatternColor,
+ themeCustomBackground,
+ themeIsBackgroundBlurred,
+}: OwnProps & StateProps) => {
+ const resolvedBackgroundColor = backgroundColor ?? themeBackgroundColor;
+ const resolvedPatternColor = patternColor ?? themePatternColor;
+ const resolvedCustomBackground = customBackground ?? themeCustomBackground;
+ const resolvedIsBackgroundBlurred = isBackgroundBlurred ?? themeIsBackgroundBlurred;
+ const customBackgroundValue = useCustomBackground(theme, resolvedCustomBackground);
+
+ const backgroundClassNames = buildClassName(
+ styles.background,
+ backgroundStyles.background,
+ resolvedCustomBackground && backgroundStyles.customBgImage,
+ resolvedBackgroundColor && backgroundStyles.customBgColor,
+ resolvedCustomBackground && resolvedIsBackgroundBlurred && backgroundStyles.blurred,
+ backgroundClassName,
+ );
+
+ return (
+
+ );
+};
+
+const PreviewBlockMessage: FC = ({
+ children,
+ className,
+ style,
+ bubbleClassName,
+ bubbleStyle,
+ headerClassName,
+ bodyClassName,
+ footerClassName,
+ avatar,
+ sender,
+ badge,
+ footer,
+ time,
+ senderColor,
+ backgroundColor,
+}) => {
+ const hasAvatar = avatar !== undefined;
+ const hasSender = sender !== undefined;
+ const hasBadge = badge !== undefined;
+ const hasChildren = children !== undefined;
+ const hasFooterContent = footer !== undefined;
+ const hasTime = time !== undefined;
+ const hasHeader = hasSender || hasBadge;
+ const hasFooter = hasFooterContent || hasTime;
+ const bubbleStyles = buildStyle(
+ senderColor && `--preview-message-sender-color: ${senderColor}`,
+ backgroundColor && `--preview-message-background: ${backgroundColor}`,
+ bubbleStyle,
+ );
+ const content = (
+ <>
+ {hasHeader ? (
+
+ {hasSender ? {sender} : undefined}
+
+ {hasBadge ? {badge} : undefined}
+
+ ) : undefined}
+ {hasChildren ? (
+
+ {children}
+
+ ) : undefined}
+ {hasFooter ? (
+
+ {hasFooterContent ? footer :
{time}}
+
+ ) : undefined}
+ >
+ );
+
+ if (hasAvatar) {
+ return (
+
+
{avatar}
+
+ {content}
+
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+};
+
+const PreviewBlockMessageTime: FC = ({
+ children,
+ className,
+ style,
+}) => (
+
+ {children}
+
+);
+
+const PreviewBlockMessageMemo = memo(PreviewBlockMessage) as PreviewBlockMessageComponent;
+PreviewBlockMessageMemo.Time = memo(PreviewBlockMessageTime);
+
+const PreviewBlock = memo(withGlobal((global) => {
+ const theme = selectTheme(global);
+ const {
+ isBlurred: themeIsBackgroundBlurred,
+ background: themeCustomBackground,
+ backgroundColor: themeBackgroundColor,
+ patternColor: themePatternColor,
+ } = selectThemeValues(global, theme) || {};
+
+ return {
+ theme,
+ themeBackgroundColor,
+ themePatternColor,
+ themeCustomBackground,
+ themeIsBackgroundBlurred,
+ };
+})(PreviewBlockBase)) as PreviewBlockComponent;
+
+PreviewBlock.Message = PreviewBlockMessageMemo;
+
+export default PreviewBlock;
diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx
index eee7b3857..0902d1891 100644
--- a/src/components/common/PrivateChatInfo.tsx
+++ b/src/components/common/PrivateChatInfo.tsx
@@ -19,6 +19,7 @@ import {
} from '../../global/selectors';
import { selectThreadMessagesCount } from '../../global/selectors/threads';
import buildClassName from '../../util/buildClassName';
+import { hasRank } from './helpers/chatMember';
import { REM } from './helpers/mediaDimensions';
import renderText from './helpers/renderText';
@@ -33,6 +34,7 @@ import Avatar from './Avatar';
import DotAnimation from './DotAnimation';
import FullNameTitle from './FullNameTitle';
import Icon from './icons/Icon';
+import RankBadge from './RankBadge';
import TopicIcon from './TopicIcon';
import TypingStatus from './TypingStatus';
@@ -58,7 +60,8 @@ type BaseOwnProps = {
emojiStatusSize?: number;
noStatusOrTyping?: boolean;
noRtl?: boolean;
- adminMember?: ApiChatMember;
+ chatMemberOriginId?: string;
+ chatMember?: ApiChatMember;
isSavedDialog?: boolean;
noAvatar?: boolean;
className?: string;
@@ -118,7 +121,8 @@ const PrivateChatInfo = ({
isSavedMessages,
isSavedDialog,
areMessagesLoaded,
- adminMember,
+ chatMember,
+ chatMemberOriginId,
ripple,
className,
storyViewerOrigin,
@@ -237,10 +241,6 @@ const PrivateChatInfo = ({
);
}
- const customTitle = adminMember
- ? adminMember.customTitle || oldLang(adminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin')
- : undefined;
-
function renderNameTitle() {
if (isTopic) {
return (
@@ -248,18 +248,27 @@ const PrivateChatInfo = ({
);
}
- if (customTitle) {
+ if (chatMember && hasRank(chatMember)) {
return (
- {customTitle && {customTitle}}
+
);
}
diff --git a/src/components/common/RankBadge.tsx b/src/components/common/RankBadge.tsx
new file mode 100644
index 000000000..2d54e7e5a
--- /dev/null
+++ b/src/components/common/RankBadge.tsx
@@ -0,0 +1,58 @@
+import { memo } from '@teact';
+import { getActions } from '../../global';
+
+import buildClassName from '../../util/buildClassName';
+
+import useLang from '../../hooks/useLang';
+import useLastCallback from '../../hooks/useLastCallback';
+import { getPeerColorClass } from '../../hooks/usePeerColor';
+
+import BadgeButton from './BadgeButton';
+
+type OwnProps = {
+ chatId: string;
+ userId: string;
+ isAdmin?: boolean;
+ isOwner?: boolean;
+ className?: string;
+ rank?: string;
+ isClickable?: boolean;
+};
+
+const OWNER_PEER_COLOR = 2;
+const ADMIN_PEER_COLOR = 3;
+
+const RankBadge = ({
+ chatId, className, userId, isAdmin, isOwner, rank, isClickable,
+}: OwnProps) => {
+ const { openRankModal } = getActions();
+ const lang = useLang();
+ const hasCustomColor = isOwner || isAdmin;
+
+ const rankText = rank || (isOwner && lang('ChannelCreator')) || (isAdmin && lang('ChannelAdmin'));
+
+ const handleClick = useLastCallback(() => {
+ if (!chatId) return;
+ openRankModal({ chatId, userId, isAdmin, isOwner, rank });
+ });
+
+ if (!rankText) {
+ return undefined;
+ }
+
+ return (
+
+ {rankText}
+
+ );
+};
+
+export default memo(RankBadge);
diff --git a/src/components/common/helpers/chatMember.ts b/src/components/common/helpers/chatMember.ts
new file mode 100644
index 000000000..29e2b9da9
--- /dev/null
+++ b/src/components/common/helpers/chatMember.ts
@@ -0,0 +1,5 @@
+import type { ApiChatMember } from '../../../api/types';
+
+export function hasRank(member?: ApiChatMember): boolean {
+ return Boolean(member && (member.rank || member.isOwner || member.isAdmin));
+}
diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx
index 0c76a49a3..af5d69afe 100644
--- a/src/components/common/profile/ChatExtra.tsx
+++ b/src/components/common/profile/ChatExtra.tsx
@@ -201,7 +201,7 @@ const ChatExtra = ({
return
;
}
- return ;
+ return ;
}, [businessLocation, locationBlobUrl]);
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
diff --git a/src/components/main/PermissionCheckboxList.tsx b/src/components/main/PermissionCheckboxList.tsx
index d9d9b9a6d..78fab2f99 100644
--- a/src/components/main/PermissionCheckboxList.tsx
+++ b/src/components/main/PermissionCheckboxList.tsx
@@ -249,6 +249,17 @@ const PermissionCheckboxList = ({
onChange={handlePermissionChange}
/>
+ = ({
width={width}
height={height}
forceAspectRatio
+ animation="pulse"
/>
)}
{isVerifyCodes && (
diff --git a/src/components/middle/message/Game.tsx b/src/components/middle/message/Game.tsx
index 92d26c7f5..9ac31be4a 100644
--- a/src/components/middle/message/Game.tsx
+++ b/src/components/middle/message/Game.tsx
@@ -57,7 +57,7 @@ const Game: FC
= ({
onClick={handleGameClick}
>
{!photoBlobUrl && !videoBlobUrl && (
-
+
)}
{photoBlobUrl && (
= ({
width={width}
height={photo.dimensions?.height}
forceAspectRatio
+ animation="pulse"
/>
)}
diff --git a/src/components/middle/message/LastEditTimeMenuItem.tsx b/src/components/middle/message/LastEditTimeMenuItem.tsx
index d3b696e5b..65871e796 100644
--- a/src/components/middle/message/LastEditTimeMenuItem.tsx
+++ b/src/components/middle/message/LastEditTimeMenuItem.tsx
@@ -24,7 +24,7 @@ function LastEditTimeMenuItem({
return (