Chat: Implement folder badges (#444)
This commit is contained in:
parent
e169b09d0b
commit
6d6249487e
@ -425,6 +425,7 @@ export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFi
|
||||
hasMyInvites: filter.hasMyInvites,
|
||||
isChatList: true,
|
||||
noTitleAnimations: filter.titleNoanimate,
|
||||
color: filter.color,
|
||||
title: buildApiFormattedText(filter.title),
|
||||
};
|
||||
}
|
||||
@ -438,6 +439,7 @@ export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFi
|
||||
pinnedChatIds: filter.pinnedPeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
includedChatIds: filter.includePeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
excludedChatIds: filter.excludePeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
color: filter.color,
|
||||
title: buildApiFormattedText(filter.title),
|
||||
noTitleAnimations: filter.titleNoanimate,
|
||||
};
|
||||
|
||||
@ -296,6 +296,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
|
||||
groups,
|
||||
channels,
|
||||
bots,
|
||||
color,
|
||||
excludeArchived,
|
||||
excludeMuted,
|
||||
excludeRead,
|
||||
@ -321,6 +322,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
|
||||
return new GramJs.DialogFilterChatlist({
|
||||
id: folder.id,
|
||||
title: buildInputTextWithEntities(folder.title),
|
||||
color,
|
||||
emoticon: emoticon || undefined,
|
||||
pinnedPeers,
|
||||
includePeers,
|
||||
@ -337,6 +339,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
|
||||
nonContacts: nonContacts || undefined,
|
||||
groups: groups || undefined,
|
||||
bots: bots || undefined,
|
||||
color,
|
||||
excludeArchived: excludeArchived || undefined,
|
||||
excludeMuted: excludeMuted || undefined,
|
||||
excludeRead: excludeRead || undefined,
|
||||
|
||||
@ -1076,7 +1076,7 @@ export async function fetchChatFolders() {
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
const { filters } = result;
|
||||
const { filters, tagsEnabled: areTagsEnabled } = result;
|
||||
|
||||
const defaultFolderPosition = filters.findIndex((folder) => folder instanceof GramJs.DialogFilterDefault);
|
||||
const dialogFilters = filters.filter(isChatFolder);
|
||||
@ -1090,6 +1090,7 @@ export async function fetchChatFolders() {
|
||||
.map(buildApiChatFolder), 'id',
|
||||
) as Record<number, ApiChatFolder>,
|
||||
orderedIds,
|
||||
areTagsEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1182,6 +1183,12 @@ export function sortChatFolders(ids: number[]) {
|
||||
}));
|
||||
}
|
||||
|
||||
export function toggleDialogFilterTags(isEnabled: boolean) {
|
||||
return invokeRequest(new GramJs.messages.ToggleDialogFilterTags({
|
||||
enabled: isEnabled,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function toggleDialogUnread({
|
||||
chat, hasUnreadMark,
|
||||
}: {
|
||||
|
||||
@ -223,6 +223,7 @@ export interface ApiChatFolder {
|
||||
groups?: true;
|
||||
channels?: true;
|
||||
bots?: true;
|
||||
color?: number;
|
||||
excludeMuted?: true;
|
||||
excludeRead?: true;
|
||||
excludeArchived?: true;
|
||||
|
||||
@ -364,6 +364,10 @@
|
||||
"BlockedUsersBlockUser" = "Block User...";
|
||||
"FilterChatTypes" = "Chat types";
|
||||
"FilterChats" = "Chats";
|
||||
"FilterColorTitle" = "Folder Color";
|
||||
"FilterColorHint" = "This color will be used for the folder's tag in the chat list";
|
||||
"ShowFolderTags" = "Show Folder Tags";
|
||||
"ShowFolderTagsHint" = "Display folder names for each chat in the chat list.";
|
||||
"FilterIncludeInfo" = "Choose chats or types of chats that will appear in this folder.";
|
||||
"FilterNameHint" = "Folder name";
|
||||
"FilterInclude" = "Included Chats";
|
||||
|
||||
@ -30,6 +30,7 @@ type OwnProps = {
|
||||
topics?: Record<number, ApiTopic>;
|
||||
renderLastMessage: () => React.ReactNode;
|
||||
observeIntersection?: ObserveFn;
|
||||
noForumTitle?: boolean;
|
||||
};
|
||||
|
||||
const NO_CORNER_THRESHOLD = Number(REM);
|
||||
@ -40,6 +41,7 @@ const ChatForumLastMessage: FC<OwnProps> = ({
|
||||
topics,
|
||||
renderLastMessage,
|
||||
observeIntersection,
|
||||
noForumTitle,
|
||||
}) => {
|
||||
const { openThread } = getActions();
|
||||
|
||||
@ -102,53 +104,63 @@ const ChatForumLastMessage: FC<OwnProps> = ({
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
style={overwrittenWidth ? `--overwritten-width: ${overwrittenWidth}px` : undefined}
|
||||
>
|
||||
{lastActiveTopic && (
|
||||
<div className={styles.titleRow}>
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.mainColumn,
|
||||
lastActiveTopic.unreadCount && styles.unread,
|
||||
)}
|
||||
ref={mainColumnRef}
|
||||
onClick={handleOpenTopicClick}
|
||||
onMouseDown={handleOpenTopicMouseDown}
|
||||
>
|
||||
<TopicIcon
|
||||
topic={lastActiveTopic}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
<div className={styles.title}>{renderText(lastActiveTopic.title)}</div>
|
||||
{!overwrittenWidth && isReversedCorner && (
|
||||
<div className={styles.afterWrapper}>
|
||||
<div className={styles.after} />
|
||||
{
|
||||
!noForumTitle && (
|
||||
<>
|
||||
{lastActiveTopic && (
|
||||
<div className={styles.titleRow}>
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.mainColumn,
|
||||
lastActiveTopic.unreadCount && styles.unread,
|
||||
)}
|
||||
ref={mainColumnRef}
|
||||
onClick={handleOpenTopicClick}
|
||||
onMouseDown={handleOpenTopicMouseDown}
|
||||
>
|
||||
<TopicIcon
|
||||
topic={lastActiveTopic}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
<div className={styles.title}>{renderText(lastActiveTopic.title)}</div>
|
||||
{!overwrittenWidth && isReversedCorner && (
|
||||
<div className={styles.afterWrapper}>
|
||||
<div className={styles.after} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.otherColumns}>
|
||||
{otherTopics.map((topic) => (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.otherColumn, topic.unreadCount && styles.unread,
|
||||
)}
|
||||
key={topic.id}
|
||||
>
|
||||
<TopicIcon
|
||||
topic={topic}
|
||||
className={styles.otherColumnIcon}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
<span className={styles.otherColumnTitle}>{renderText(topic.title)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.ellipsis} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.otherColumns}>
|
||||
{otherTopics.map((topic) => (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.otherColumn, topic.unreadCount && styles.unread,
|
||||
)}
|
||||
key={topic.id}
|
||||
>
|
||||
<TopicIcon
|
||||
topic={topic}
|
||||
className={styles.otherColumnIcon}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
<span className={styles.otherColumnTitle}>{renderText(topic.title)}</span>
|
||||
{!lastActiveTopic && (
|
||||
<div className={buildClassName(styles.titleRow, styles.loading)}>
|
||||
{lang('Loading')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.ellipsis} />
|
||||
</div>
|
||||
)}
|
||||
{!lastActiveTopic && <div className={buildClassName(styles.titleRow, styles.loading)}>{lang('Loading')}</div>}
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className={buildClassName(styles.lastMessage, lastActiveTopic?.unreadCount && styles.unread)}
|
||||
className={buildClassName(styles.lastMessage, lastActiveTopic?.unreadCount && !noForumTitle && styles.unread)}
|
||||
ref={lastMessageRef}
|
||||
onClick={handleOpenTopicClick}
|
||||
onMouseDown={handleOpenTopicMouseDown}
|
||||
|
||||
@ -335,6 +335,13 @@
|
||||
line-height: initial;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
&.has-tags {
|
||||
line-height: 1.25rem;
|
||||
.info-row {
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
@ -456,6 +463,11 @@
|
||||
color: var(--color-chat-active);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ChatTags {
|
||||
color: var(--color-chat-active);
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat,
|
||||
ApiChatFolder,
|
||||
ApiDraft,
|
||||
ApiMessage,
|
||||
ApiMessageOutgoingStatus,
|
||||
@ -33,6 +34,7 @@ import {
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
selectIsCurrentUserFrozen,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsForumPanelClosed,
|
||||
selectIsForumPanelOpen,
|
||||
selectMonoforumChannel,
|
||||
@ -52,6 +54,7 @@ import {
|
||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
import { getChatFolderIds } from '../../../util/folderManager';
|
||||
import { createLocationHash } from '../../../util/routing';
|
||||
|
||||
import useSelectorSignal from '../../../hooks/data/useSelectorSignal';
|
||||
@ -75,9 +78,9 @@ import ChatFolderModal from '../ChatFolderModal.async';
|
||||
import MuteChatModal from '../MuteChatModal.async';
|
||||
import ChatBadge from './ChatBadge';
|
||||
import ChatCallStatus from './ChatCallStatus';
|
||||
import ChatTags from './ChatTags';
|
||||
|
||||
import './Chat.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
folderId?: number;
|
||||
@ -91,6 +94,7 @@ type OwnProps = {
|
||||
className?: string;
|
||||
observeIntersection?: ObserveFn;
|
||||
onDragEnter?: (chatId: string) => void;
|
||||
withTags?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -118,6 +122,11 @@ type StateProps = {
|
||||
currentUserId: string;
|
||||
isSynced?: boolean;
|
||||
isAccountFrozen?: boolean;
|
||||
folderIds?: number[];
|
||||
orderedIds?: number[];
|
||||
chatFoldersById?: Record<number, ApiChatFolder>;
|
||||
activeChatFolder?: number;
|
||||
areTagsEnabled?: boolean;
|
||||
};
|
||||
|
||||
const Chat: FC<OwnProps & StateProps> = ({
|
||||
@ -157,6 +166,12 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isSynced,
|
||||
onDragEnter,
|
||||
isAccountFrozen,
|
||||
folderIds,
|
||||
orderedIds,
|
||||
chatFoldersById,
|
||||
activeChatFolder,
|
||||
areTagsEnabled,
|
||||
withTags,
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
@ -184,6 +199,8 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage);
|
||||
|
||||
const shouldRenderTags = areTagsEnabled && withTags && folderIds && folderIds.length > 1;
|
||||
|
||||
const { renderSubtitle, ref } = useChatListEntry({
|
||||
chat,
|
||||
chatId,
|
||||
@ -200,6 +217,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isSavedDialog,
|
||||
isPreview,
|
||||
topics,
|
||||
noForumTitle: shouldRenderTags,
|
||||
});
|
||||
|
||||
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
|
||||
@ -388,7 +406,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
<ChatCallStatus isMobile={isMobile} isSelected={isSelected} isActive={withInterfaceAnimations} />
|
||||
)}
|
||||
</div>
|
||||
<div className="info">
|
||||
<div className={buildClassName('info', shouldRenderTags && 'has-tags')}>
|
||||
<div className="info-row">
|
||||
<FullNameTitle
|
||||
peer={isMonoforum ? monoforumChannel! : peer}
|
||||
@ -423,6 +441,14 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{shouldRenderTags && (
|
||||
<ChatTags
|
||||
folderIds={folderIds}
|
||||
orderedIds={orderedIds}
|
||||
chatFoldersById={chatFoldersById}
|
||||
activeChatFolder={activeChatFolder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{shouldRenderDeleteModal && (
|
||||
<DeleteChatModal
|
||||
@ -465,6 +491,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
};
|
||||
}
|
||||
|
||||
const folderIds = getChatFolderIds(chatId);
|
||||
const { areTagsEnabled } = global.chatFolders;
|
||||
const isPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
const lastMessageId = previewMessageId || selectChatLastMessageId(global, chatId, isSavedDialog ? 'saved' : 'all');
|
||||
const lastMessage = previewMessageId
|
||||
? selectChatMessage(global, chatId, previewMessageId)
|
||||
@ -497,6 +527,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const monoforumChannel = selectMonoforumChannel(global, chatId);
|
||||
|
||||
const activeChatFolder = selectTabState(global).activeChatFolder;
|
||||
|
||||
return {
|
||||
chat,
|
||||
isMuted: getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)),
|
||||
@ -524,6 +556,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
lastMessageStory,
|
||||
isAccountFrozen,
|
||||
monoforumChannel,
|
||||
folderIds,
|
||||
orderedIds: global.chatFolders.orderedIds,
|
||||
activeChatFolder,
|
||||
chatFoldersById: global.chatFolders.byId,
|
||||
areTagsEnabled: areTagsEnabled && isPremium,
|
||||
};
|
||||
},
|
||||
)(Chat));
|
||||
|
||||
@ -368,6 +368,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
archiveSettings={archiveSettings}
|
||||
sessions={sessions}
|
||||
isAccountFrozen={isAccountFrozen}
|
||||
withTags
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ type OwnProps = {
|
||||
isAccountFrozen?: boolean;
|
||||
isMainList?: boolean;
|
||||
foldersDispatch?: FolderEditDispatch;
|
||||
withTags?: boolean;
|
||||
};
|
||||
|
||||
const INTERSECTION_THROTTLE = 200;
|
||||
@ -72,6 +73,7 @@ const ChatList: FC<OwnProps> = ({
|
||||
isAccountFrozen,
|
||||
isMainList,
|
||||
foldersDispatch,
|
||||
withTags,
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
@ -238,6 +240,7 @@ const ChatList: FC<OwnProps> = ({
|
||||
offsetTop={offsetTop}
|
||||
observeIntersection={observe}
|
||||
onDragEnter={handleDragEnter}
|
||||
withTags={withTags}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
33
src/components/left/main/ChatTags.module.scss
Normal file
33
src/components/left/main/ChatTags.module.scss
Normal file
@ -0,0 +1,33 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
position: relative;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
min-width: 1.5rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
font-size: 0.5625rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 0.75rem;
|
||||
color: var(--accent-color);
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
|
||||
background-color: var(--accent-background-color);
|
||||
|
||||
:global(.emoji-small) {
|
||||
width: 0.5625rem;
|
||||
height: 0.5625rem;
|
||||
margin-top: -0.0625rem;
|
||||
}
|
||||
|
||||
&ColorMore {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
74
src/components/left/main/ChatTags.tsx
Normal file
74
src/components/left/main/ChatTags.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiChatFolder } from '../../../api/types';
|
||||
|
||||
import { ALL_FOLDER_ID } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getApiPeerColorClass } from '../../common/helpers/peerColor';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import styles from './ChatTags.module.scss';
|
||||
|
||||
const MAX_VISIBLE_TAGS = 3;
|
||||
|
||||
type OwnProps = {
|
||||
folderIds?: number[];
|
||||
orderedIds?: number[];
|
||||
chatFoldersById?: Record<number, ApiChatFolder>;
|
||||
activeChatFolder?: number;
|
||||
};
|
||||
|
||||
const ChatTags = ({
|
||||
folderIds,
|
||||
orderedIds,
|
||||
chatFoldersById,
|
||||
activeChatFolder,
|
||||
}: OwnProps) => {
|
||||
const activeFolderId = activeChatFolder !== undefined && orderedIds ? orderedIds[activeChatFolder] : undefined;
|
||||
|
||||
const orderedFolderIds = useMemo(() => orderedIds?.filter((id) => {
|
||||
const isFolder = folderIds?.includes(id);
|
||||
const isActive = id === activeFolderId;
|
||||
const isAll = id === ALL_FOLDER_ID;
|
||||
|
||||
const folder = chatFoldersById?.[id];
|
||||
const hasColor = folder?.color !== undefined && folder.color !== -1;
|
||||
|
||||
return isFolder && !isActive && !isAll && hasColor;
|
||||
}) || [], [orderedIds, folderIds, activeFolderId, chatFoldersById]);
|
||||
|
||||
const visibleFolderIds = orderedFolderIds.slice(0, MAX_VISIBLE_TAGS);
|
||||
const remainingCount = orderedFolderIds.length - visibleFolderIds.length;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{visibleFolderIds.map((folderId) => {
|
||||
const folder = chatFoldersById?.[folderId];
|
||||
return folder && (
|
||||
<div
|
||||
key={folder.id}
|
||||
className={buildClassName(
|
||||
'ChatTags',
|
||||
styles.tag,
|
||||
getApiPeerColorClass({ color: folder.color }),
|
||||
)}
|
||||
>
|
||||
{renderTextWithEntities({
|
||||
text: folder.title.text,
|
||||
entities: folder.title.entities,
|
||||
noCustomEmojiPlayback: folder.noTitleAnimations,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{remainingCount > 0 && (
|
||||
<div className={`ChatTags ${styles.tag} ${styles.tagColorMore}`}>
|
||||
+
|
||||
{remainingCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatTags);
|
||||
@ -52,6 +52,7 @@ export default function useChatListEntry({
|
||||
isTopic,
|
||||
isSavedDialog,
|
||||
isPreview,
|
||||
noForumTitle,
|
||||
}: {
|
||||
chat?: ApiChat;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
@ -70,6 +71,7 @@ export default function useChatListEntry({
|
||||
animationType: ChatAnimationTypes;
|
||||
orderDiff: number;
|
||||
withInterfaceAnimations?: boolean;
|
||||
noForumTitle?: boolean;
|
||||
}) {
|
||||
const oldLang = useOldLang();
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
@ -153,6 +155,7 @@ export default function useChatListEntry({
|
||||
renderLastMessage={renderLastMessageOrTyping}
|
||||
observeIntersection={observeIntersection}
|
||||
topics={topics}
|
||||
noForumTitle={noForumTitle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -116,3 +116,82 @@
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
min-height: 3rem;
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.color-picker-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.color-picker-item {
|
||||
cursor: pointer;
|
||||
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 0.125rem solid transparent;
|
||||
border-radius: 50%;
|
||||
|
||||
color: var(--accent-color);
|
||||
|
||||
background-color: currentColor;
|
||||
outline: none;
|
||||
|
||||
transition: border 100ms ease-in-out, outline 100ms ease-in-out;
|
||||
}
|
||||
|
||||
.color-picker-item-disabled {
|
||||
--accent-color: var(--color-topic-grey);
|
||||
}
|
||||
|
||||
.color-picker-selected-color {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.color-picker-item-none {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-topic-grey);
|
||||
|
||||
.color-picker-item-none-icon {
|
||||
color: var(--color-background);
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-item:not(.color-picker-item-hover-disabled) {
|
||||
&.color-picker-item-active,
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:active {
|
||||
border: 0.125rem solid var(--color-background);
|
||||
outline: 0.125rem solid currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-item-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-folders-lock-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
padding: 1.3125rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.settings-folders-title {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
@ -12,13 +12,16 @@ import type {
|
||||
} from '../../../../hooks/reducers/useFoldersReducer';
|
||||
|
||||
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
|
||||
import { selectCanShareFolder } from '../../../../global/selectors';
|
||||
import { selectCanShareFolder, selectIsCurrentUserPremium } from '../../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../../global/selectors/limits';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { isUserId } from '../../../../util/entities/ids';
|
||||
import { findIntersectionWithSet } from '../../../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||
import { CUSTOM_PEER_EXCLUDED_CHAT_TYPES, CUSTOM_PEER_INCLUDED_CHAT_TYPES } from '../../../../util/objects/customPeer';
|
||||
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||
import { getApiPeerColorClass } from '../../../common/helpers/peerColor';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import { selectChatFilters } from '../../../../hooks/reducers/useFoldersReducer';
|
||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||
@ -55,12 +58,15 @@ type StateProps = {
|
||||
maxInviteLinks: number;
|
||||
maxChatLists: number;
|
||||
chatListCount: number;
|
||||
isCurrentUserPremium: boolean;
|
||||
};
|
||||
|
||||
const SUBMIT_TIMEOUT = 500;
|
||||
|
||||
const INITIAL_CHATS_LIMIT = 5;
|
||||
|
||||
const FOLDER_COLORS = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
export const ERROR_NO_TITLE = 'Please provide a title for this folder.';
|
||||
export const ERROR_NO_CHATS = 'ChatList.Filter.Error.Empty';
|
||||
|
||||
@ -83,11 +89,13 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
maxChatLists,
|
||||
chatListCount,
|
||||
onSaveFolder,
|
||||
isCurrentUserPremium,
|
||||
}) => {
|
||||
const {
|
||||
loadChatlistInvites,
|
||||
openLimitReachedModal,
|
||||
showNotification,
|
||||
openPremiumModal,
|
||||
} = getActions();
|
||||
|
||||
const isCreating = state.mode === 'create';
|
||||
@ -346,6 +354,73 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="settings-item pt-3">
|
||||
<h4 className="settings-item-header mb-3 color-picker-title" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FilterColorTitle')}
|
||||
<div className={buildClassName(
|
||||
'color-picker-selected-color',
|
||||
isCurrentUserPremium && state.folder.color !== undefined && state.folder.color !== -1
|
||||
? getApiPeerColorClass({ color: state.folder.color })
|
||||
: 'color-picker-item-disabled',
|
||||
)}
|
||||
>
|
||||
{renderTextWithEntities({
|
||||
text: state.folder.title.text,
|
||||
entities: state.folder.title.entities,
|
||||
noCustomEmojiPlayback: state.folder.noTitleAnimations,
|
||||
})}
|
||||
</div>
|
||||
</h4>
|
||||
<div className="color-picker">
|
||||
{FOLDER_COLORS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isCurrentUserPremium) {
|
||||
openPremiumModal();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'setColor', payload: color });
|
||||
}}
|
||||
className={buildClassName(
|
||||
'color-picker-item',
|
||||
getApiPeerColorClass({ color }),
|
||||
!isCurrentUserPremium && 'color-picker-item-hover-disabled',
|
||||
color === state.folder.color && isCurrentUserPremium && 'color-picker-item-active',
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isCurrentUserPremium) {
|
||||
openPremiumModal();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'setColor', payload: undefined });
|
||||
}}
|
||||
className={buildClassName(
|
||||
'color-picker-item',
|
||||
'color-picker-item-none',
|
||||
(state.folder.color === undefined || state.folder.color === -1 || !isCurrentUserPremium)
|
||||
&& 'color-picker-item-active',
|
||||
)}
|
||||
>
|
||||
{isCurrentUserPremium ? (
|
||||
<Icon name="close" className="color-picker-item-none-icon" />
|
||||
) : (
|
||||
<Icon name="lock-badge" className="color-picker-item-none-icon" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="settings-item-description mb-0" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FilterColorHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-item pt-3">
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FolderLinkScreen.Title')}
|
||||
@ -401,6 +476,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const { byId, invites } = global.chatFolders;
|
||||
const chatListCount = Object.values(byId).reduce((acc, el) => acc + (el.isChatList ? 1 : 0), 0);
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
return {
|
||||
loadedActiveChatIds: listIds.active,
|
||||
loadedArchivedChatIds: listIds.archived,
|
||||
@ -409,6 +486,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
maxInviteLinks: selectCurrentLimit(global, 'chatlistInvites'),
|
||||
maxChatLists: selectCurrentLimit(global, 'chatlistJoined'),
|
||||
chatListCount,
|
||||
isCurrentUserPremium,
|
||||
};
|
||||
},
|
||||
)(SettingsFoldersEdit));
|
||||
|
||||
@ -10,10 +10,12 @@ import { ALL_FOLDER_ID, STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'
|
||||
import { getFolderDescriptionText } from '../../../../global/helpers';
|
||||
import { selectIsCurrentUserPremium } from '../../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../../global/selectors/limits';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { isBetween } from '../../../../util/math';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||
import { throttle } from '../../../../util/schedulers';
|
||||
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||
import { getApiPeerColorClass } from '../../../common/helpers/peerColor';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import { useFolderManagerForChatsCount } from '../../../../hooks/useFolderManager';
|
||||
@ -24,6 +26,7 @@ import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated';
|
||||
import AnimatedIconWithPreview from '../../../common/AnimatedIconWithPreview';
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
import Button from '../../../ui/Button';
|
||||
import Checkbox from '../../../ui/Checkbox';
|
||||
import Draggable from '../../../ui/Draggable';
|
||||
import ListItem from '../../../ui/ListItem';
|
||||
import Loading from '../../../ui/Loading';
|
||||
@ -41,6 +44,7 @@ type StateProps = {
|
||||
recommendedChatFolders?: ApiChatFolder[];
|
||||
maxFolders: number;
|
||||
isPremium?: boolean;
|
||||
areTagsEnabled?: boolean;
|
||||
};
|
||||
|
||||
type SortState = {
|
||||
@ -62,6 +66,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
isPremium,
|
||||
recommendedChatFolders,
|
||||
maxFolders,
|
||||
areTagsEnabled,
|
||||
}) => {
|
||||
const {
|
||||
loadRecommendedChatFolders,
|
||||
@ -69,6 +74,8 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
openLimitReachedModal,
|
||||
openDeleteChatFolderModal,
|
||||
sortChatFolders,
|
||||
toggleDialogFilterTags,
|
||||
openPremiumModal,
|
||||
} = getActions();
|
||||
|
||||
const [state, setState] = useState<SortState>({
|
||||
@ -145,6 +152,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
title: folder.title,
|
||||
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
|
||||
isChatList: folder.isChatList,
|
||||
color: folder.color,
|
||||
noTitleAnimations: folder.noTitleAnimations,
|
||||
};
|
||||
});
|
||||
@ -162,6 +170,14 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
addChatFolder({ folder });
|
||||
}, [foldersById, maxFolders, addChatFolder, openLimitReachedModal]);
|
||||
|
||||
const handleToggleTags = useCallback(() => {
|
||||
if (!isPremium) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleDialogFilterTags({ isEnabled: !areTagsEnabled });
|
||||
}, [areTagsEnabled, isPremium, toggleDialogFilterTags]);
|
||||
|
||||
const handleDrag = useCallback((translation: { x: number; y: number }, id: string | number) => {
|
||||
const delta = Math.round(translation.y / FOLDER_HEIGHT_PX);
|
||||
const index = state.orderedFolderIds?.indexOf(id as number) || 0;
|
||||
@ -303,7 +319,12 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="title">
|
||||
<span className={buildClassName(
|
||||
'title',
|
||||
folder?.color !== undefined && folder.color !== -1 && isPremium
|
||||
&& `${getApiPeerColorClass({ color: folder.color })} settings-folders-title`,
|
||||
)}
|
||||
>
|
||||
{renderTextWithEntities({
|
||||
text: folder.title.text,
|
||||
entities: folder.title.entities,
|
||||
@ -365,6 +386,23 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="settings-item pt-3">
|
||||
<div className="settings-item-relative">
|
||||
<Checkbox
|
||||
label={lang('ShowFolderTags')}
|
||||
subLabel={lang('ShowFolderTagsHint')}
|
||||
checked={isPremium && areTagsEnabled}
|
||||
onChange={handleToggleTags}
|
||||
onClickLabel={(event) => {
|
||||
if (!isPremium) {
|
||||
event.preventDefault();
|
||||
openPremiumModal();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{!isPremium && <Icon name="lock-badge" className="settings-folders-lock-icon" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -375,6 +413,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
orderedIds: folderIds,
|
||||
byId: foldersById,
|
||||
recommended: recommendedChatFolders,
|
||||
areTagsEnabled,
|
||||
} = global.chatFolders;
|
||||
|
||||
return {
|
||||
@ -383,6 +422,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isPremium: selectIsCurrentUserPremium(global),
|
||||
recommendedChatFolders,
|
||||
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
||||
areTagsEnabled,
|
||||
};
|
||||
},
|
||||
)(SettingsFoldersMain));
|
||||
|
||||
@ -1115,6 +1115,24 @@ addActionHandler('loadRecommendedChatFolders', async (global): Promise<void> =>
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('toggleDialogFilterTags', async (global, actions, payload): Promise<void> => {
|
||||
const { isEnabled } = payload;
|
||||
|
||||
const result = await callApi('toggleDialogFilterTags', isEnabled);
|
||||
|
||||
if (result) {
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
chatFolders: {
|
||||
...global.chatFolders,
|
||||
areTagsEnabled: isEnabled,
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('editChatFolders', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, idsToRemove, idsToAdd, tabId = getCurrentTabId(),
|
||||
|
||||
@ -198,6 +198,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
chatFolders: {
|
||||
byId: {},
|
||||
invites: {},
|
||||
areTagsEnabled: false,
|
||||
},
|
||||
|
||||
fileUploads: {
|
||||
|
||||
@ -396,6 +396,9 @@ export interface ActionPayloads {
|
||||
deleteChatFolder: {
|
||||
id: number;
|
||||
};
|
||||
toggleDialogFilterTags: {
|
||||
isEnabled: boolean;
|
||||
};
|
||||
openSupportChat: WithTabId | undefined;
|
||||
openChatByPhoneNumber: {
|
||||
phoneNumber: string;
|
||||
|
||||
@ -276,6 +276,7 @@ export type GlobalState = {
|
||||
byId: Record<number, ApiChatFolder>;
|
||||
invites: Record<number, ApiChatlistExportedInvite[]>;
|
||||
recommended?: ApiChatFolder[];
|
||||
areTagsEnabled?: boolean;
|
||||
};
|
||||
|
||||
phoneCall?: ApiPhoneCall;
|
||||
|
||||
@ -116,7 +116,7 @@ export type FoldersState = {
|
||||
export type FoldersActions = (
|
||||
'setTitle' | 'saveFilters' | 'editFolder' | 'reset' | 'setChatFilter' | 'setIsLoading' | 'setError' |
|
||||
'editIncludeFilters' | 'editExcludeFilters' | 'setIncludeFilters' | 'setExcludeFilters' | 'setIsTouched' |
|
||||
'setFolderId' | 'setIsChatlist'
|
||||
'setFolderId' | 'setIsChatlist' | 'setColor'
|
||||
);
|
||||
export type FolderEditDispatch = Dispatch<FoldersState, FoldersActions>;
|
||||
|
||||
@ -248,6 +248,15 @@ const foldersReducer: StateReducer<FoldersState, FoldersActions> = (
|
||||
isChatList: action.payload,
|
||||
},
|
||||
};
|
||||
case 'setColor':
|
||||
return {
|
||||
...state,
|
||||
folder: {
|
||||
...state.folder,
|
||||
color: action.payload,
|
||||
},
|
||||
isTouched: true,
|
||||
};
|
||||
case 'reset':
|
||||
return INITIAL_STATE;
|
||||
default:
|
||||
|
||||
5
src/lib/gramjs/tl/api.d.ts
vendored
5
src/lib/gramjs/tl/api.d.ts
vendored
@ -24829,6 +24829,11 @@ namespace Api {
|
||||
}, Bool> {
|
||||
order: int[];
|
||||
}
|
||||
export class ToggleDialogFilterTags extends Request<{
|
||||
enabled: Bool;
|
||||
}, Bool> {
|
||||
enabled: Bool;
|
||||
}
|
||||
export class GetOldFeaturedStickers extends Request<{
|
||||
offset: int;
|
||||
limit: int;
|
||||
|
||||
@ -1616,6 +1616,7 @@ messages.getDialogFilters#efd48c89 = messages.DialogFilters;
|
||||
messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;
|
||||
messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;
|
||||
messages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool;
|
||||
messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool;
|
||||
messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;
|
||||
messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;
|
||||
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
||||
@ -1671,6 +1672,7 @@ messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate;
|
||||
messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies;
|
||||
messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages;
|
||||
messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector<int> random_id:Vector<long> = Updates;
|
||||
messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool;
|
||||
messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects;
|
||||
messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;
|
||||
messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult;
|
||||
|
||||
@ -163,6 +163,7 @@
|
||||
"messages.getSuggestedDialogFilters",
|
||||
"messages.updateDialogFilter",
|
||||
"messages.updateDialogFiltersOrder",
|
||||
"messages.toggleDialogFilterTags",
|
||||
"messages.getReplies",
|
||||
"messages.getDiscussionMessage",
|
||||
"messages.readDiscussion",
|
||||
|
||||
@ -2311,6 +2311,7 @@ messages.getDialogFilters#efd48c89 = messages.DialogFilters;
|
||||
messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;
|
||||
messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;
|
||||
messages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool;
|
||||
messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool;
|
||||
messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messages.FeaturedStickers;
|
||||
messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;
|
||||
messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;
|
||||
|
||||
4
src/types/language.d.ts
vendored
4
src/types/language.d.ts
vendored
@ -320,6 +320,10 @@ export interface LangPair {
|
||||
'BlockedUsersBlockUser': undefined;
|
||||
'FilterChatTypes': undefined;
|
||||
'FilterChats': undefined;
|
||||
'FilterColorTitle': undefined;
|
||||
'FilterColorHint': undefined;
|
||||
'ShowFolderTags': undefined;
|
||||
'ShowFolderTagsHint': undefined;
|
||||
'FilterIncludeInfo': undefined;
|
||||
'FilterNameHint': undefined;
|
||||
'FilterInclude': undefined;
|
||||
|
||||
@ -175,6 +175,10 @@ export default {
|
||||
FilterChannels: 'Channels',
|
||||
FilterBots: 'Bots',
|
||||
FilterChats: 'Chats',
|
||||
FilterColorTitle: 'Folder Color',
|
||||
FilterColorHint: 'This color will be used for the folder\'s tag in the chat list',
|
||||
ShowFolderTags: 'Show Folder Tags',
|
||||
ShowFolderTagsHint: 'Display folder names for each chat in the chat list.',
|
||||
AccDescrChannel: 'Channel',
|
||||
AccDescrGroup: 'Group',
|
||||
Bot: 'bot',
|
||||
|
||||
@ -177,6 +177,10 @@ export function getOrderKey(chatId: string, isForSaved?: boolean) {
|
||||
return isForSaved ? summary.orderInSaved : summary.orderInAll;
|
||||
}
|
||||
|
||||
export function getChatFolderIds(chatId: string) {
|
||||
return prepared.folderIdsByChatId[chatId];
|
||||
}
|
||||
|
||||
/* Callback managers */
|
||||
|
||||
export function addOrderedIdsCallback(folderId: number, callback: (orderedIds: string[]) => void) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user