Chat List, Message List: New designs for empty screens (#1342)

This commit is contained in:
Alexander Zinchuk 2021-08-04 23:54:13 +03:00
parent 2ff25c9113
commit 1038c2ffcf
33 changed files with 619 additions and 54 deletions

View File

@ -585,6 +585,7 @@ function buildAction(
if (action instanceof GramJs.MessageActionChatCreate) {
text = 'Notification.CreatedChatWithTitle';
translationValues.push('%action_origin%', action.title);
type = 'chatCreate';
} else if (action instanceof GramJs.MessageActionChatEditTitle) {
if (isChannelPost) {
text = 'Channel.MessageTitleUpdated';
@ -656,6 +657,7 @@ function buildAction(
} else if (action instanceof GramJs.MessageActionContactSignUp) {
text = 'Notification.Joined';
translationValues.push('%action_origin%');
type = 'contactSignUp';
} else if (action instanceof GramJs.MessageActionPaymentSent) {
const currencySign = getCurrencySign(action.currency);
const amount = (Number(action.totalAmount) / 100).toFixed(2);

View File

@ -147,7 +147,7 @@ export interface ApiAction {
text: string;
targetUserIds?: number[];
targetChatId?: number;
type: 'historyClear' | 'other';
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'other';
photo?: ApiPhoto;
translationValues: string[];
}

View File

@ -4,8 +4,7 @@ import React, {
import { ApiMediaFormat, ApiSticker } from '../../api/types';
import { STICKER_SIZE_TWO_FA } from '../../config';
import { getStickerDimensions, LIKE_STICKER_ID } from './helpers/mediaDimensions';
import { LIKE_STICKER_ID } from './helpers/mediaDimensions';
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useMedia from '../../hooks/useMedia';
import useTransitionForMedia from '../../hooks/useTransitionForMedia';
@ -18,16 +17,24 @@ import './AnimatedEmoji.scss';
type OwnProps = {
sticker: ApiSticker;
observeIntersection?: ObserveFn;
isInline?: boolean;
size?: 'large' | 'medium' | 'small';
lastSyncTime?: number;
forceLoadPreview?: boolean;
};
const QUALITY = 1;
const RESIZE_FACTOR = 0.5;
const WIDTH = {
large: 160,
medium: 128,
small: 104,
};
const AnimatedEmoji: FC<OwnProps> = ({
sticker, isInline = false, observeIntersection, lastSyncTime, forceLoadPreview,
sticker,
size = 'medium',
observeIntersection,
lastSyncTime,
forceLoadPreview,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
@ -54,13 +61,7 @@ const AnimatedEmoji: FC<OwnProps> = ({
setPlayKey(String(Math.random()));
}, []);
let width: number;
if (isInline) {
width = getStickerDimensions(sticker).width * RESIZE_FACTOR;
} else {
width = STICKER_SIZE_TWO_FA;
}
const width = WIDTH[size];
const style = `width: ${width}px; height: ${width}px;`;
return (

View File

@ -20,6 +20,12 @@
margin: 0 0.5rem;
}
&.large {
width: 10rem;
height: 10rem;
margin: 0;
}
.AnimatedSticker, img {
position: absolute;
top: 0;

View File

@ -34,7 +34,7 @@ const ArchivedChats: FC<OwnProps> = ({ isActive, onReset, onContentChange }) =>
</Button>
<h3>{lang('ArchivedChats')}</h3>
</div>
<ChatList folderType="archived" noChatsText="Archive is empty." isActive={isActive} />
<ChatList folderType="archived" isActive={isActive} />
</div>
);
};

View File

@ -9,6 +9,7 @@ import { LeftColumnContent, SettingsScreens } from '../../types';
import { LAYERS_ANIMATION_NAME } from '../../util/environment';
import captureEscKeyListener from '../../util/captureEscKeyListener';
import { pick } from '../../util/iteratees';
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
import Transition from '../ui/Transition';
import LeftMain from './main/LeftMain';
@ -59,6 +60,7 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
const [content, setContent] = useState<LeftColumnContent>(LeftColumnContent.ChatList);
const [settingsScreen, setSettingsScreen] = useState(SettingsScreens.Main);
const [contactsFilter, setContactsFilter] = useState<string>('');
const [foldersState, foldersDispatch] = useFoldersReducer();
// Used to reset child components in background.
const [lastResetTime, setLastResetTime] = useState<number>(0);
@ -193,6 +195,16 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
case SettingsScreens.FoldersEditFolder:
setSettingsScreen(SettingsScreens.Folders);
return;
case SettingsScreens.FoldersIncludedChatsFromChatList:
case SettingsScreens.FoldersExcludedChatsFromChatList:
setSettingsScreen(SettingsScreens.FoldersEditFolderFromChatList);
return;
case SettingsScreens.FoldersEditFolderFromChatList:
setContent(LeftColumnContent.ChatList);
setSettingsScreen(SettingsScreens.Main);
return;
default:
break;
}
@ -274,6 +286,8 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
<Settings
isActive={isActive}
currentScreen={settingsScreen}
foldersState={foldersState}
foldersDispatch={foldersDispatch}
onScreenSelect={handleSettingsScreenSelect}
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
@ -307,8 +321,10 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
searchQuery={searchQuery}
searchDate={searchDate}
contactsFilter={contactsFilter}
foldersDispatch={foldersDispatch}
onContentChange={setContent}
onSearchQuery={handleSearchQuery}
onScreenSelect={handleSettingsScreenSelect}
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
/>

View File

@ -5,7 +5,8 @@ import { withGlobal } from '../../../lib/teact/teactn';
import { ApiChat, ApiChatFolder, ApiUser } from '../../../api/types';
import { GlobalActions } from '../../../global/types';
import { NotifyException, NotifySettings } from '../../../types';
import { NotifyException, NotifySettings, SettingsScreens } from '../../../types';
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { buildCollectionByKey, pick } from '../../../util/iteratees';
@ -23,6 +24,11 @@ import Transition from '../../ui/Transition';
import TabList from '../../ui/TabList';
import ChatList from './ChatList';
type OwnProps = {
onScreenSelect: (screen: SettingsScreens) => void;
foldersDispatch: FolderEditDispatch;
};
type StateProps = {
chatsById: Record<number, ApiChat>;
usersById: Record<number, ApiUser>;
@ -40,7 +46,7 @@ type DispatchProps = Pick<GlobalActions, 'loadChatFolders' | 'setActiveChatFolde
const INFO_THROTTLE = 3000;
const SAVED_MESSAGES_HOTKEY = '0';
const ChatFolders: FC<StateProps & DispatchProps> = ({
const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
chatsById,
usersById,
chatFoldersById,
@ -50,6 +56,8 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
activeChatFolder,
currentUserId,
lastSyncTime,
foldersDispatch,
onScreenSelect,
loadChatFolders,
setActiveChatFolder,
openChat,
@ -182,15 +190,23 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
.find(({ title }) => title === folderTabs![activeChatFolder].title);
if (!activeFolder || activeChatFolder === 0) {
return <ChatList folderType="all" isActive={isActive} />;
return (
<ChatList
folderType="all"
isActive={isActive}
foldersDispatch={foldersDispatch}
onScreenSelect={onScreenSelect}
/>
);
}
return (
<ChatList
folderType="folder"
folderId={activeFolder.id}
noChatsText={lang('FilterNoChatsToDisplay')}
isActive={isActive}
onScreenSelect={onScreenSelect}
foldersDispatch={foldersDispatch}
/>
);
}
@ -214,7 +230,7 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
);
};
export default memo(withGlobal(
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
chats: { byId: chatsById },

View File

@ -7,7 +7,8 @@ import { GlobalActions } from '../../../global/types';
import {
ApiChat, ApiChatFolder, ApiUser,
} from '../../../api/types';
import { NotifyException, NotifySettings } from '../../../types';
import { NotifyException, NotifySettings, SettingsScreens } from '../../../types';
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import { ALL_CHATS_PRELOAD_DISABLED, CHAT_HEIGHT_PX, CHAT_LIST_SLICE } from '../../../config';
import { IS_ANDROID, IS_MAC_OS, IS_PWA } from '../../../util/environment';
@ -23,12 +24,14 @@ import { useChatAnimationType } from './hooks';
import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading';
import Chat from './Chat';
import EmptyFolder from './EmptyFolder';
type OwnProps = {
folderType: 'all' | 'archived' | 'folder';
folderId?: number;
noChatsText?: string;
isActive: boolean;
onScreenSelect?: (screen: SettingsScreens) => void;
foldersDispatch?: FolderEditDispatch;
};
type StateProps = {
@ -52,7 +55,6 @@ enum FolderTypeToListType {
const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
folderType,
folderId,
noChatsText = 'Chat list is empty.',
isActive,
chatFolder,
chatsById,
@ -60,8 +62,10 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
listIds,
orderedPinnedIds,
lastSyncTime,
foldersDispatch,
notifySettings,
notifyExceptions,
onScreenSelect,
loadMoreChats,
preloadTopChatMessages,
openChat,
@ -205,7 +209,14 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
{viewportIds && viewportIds.length && chatArrays ? (
renderChats()
) : viewportIds && !viewportIds.length ? (
<div className="no-results">{noChatsText}</div>
(
<EmptyFolder
folderId={folderId}
folderType={folderType}
foldersDispatch={foldersDispatch}
onScreenSelect={onScreenSelect}
/>
)
) : (
<Loading key="loading" />
)}

View File

@ -0,0 +1,44 @@
.EmptyFolder {
width: 100%;
height: 80%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
@media (max-height: 480px) {
height: 100%;
}
.sticker {
height: 8rem;
margin-bottom: 1.875rem;
}
.title {
font-size: 1.25rem;
margin-bottom: .125rem;
}
.description {
font-size: .875rem;
color: var(--color-text-secondary);
body.is-ios &,
body.is-macos & {
color: var(--color-text-secondary-apple);
}
}
.Button.pill {
margin-top: .625rem;
font-weight: 500;
padding-inline-start: .75rem;
unicode-bidi: plaintext;
i {
margin-inline-end: .625rem;
font-size: 1.5rem;
}
}
}

View File

@ -0,0 +1,70 @@
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn';
import { ApiChatFolder, ApiSticker } from '../../../api/types';
import { SettingsScreens } from '../../../types';
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { selectAnimatedEmoji, selectChatFolder } from '../../../modules/selectors';
import useLang from '../../../hooks/useLang';
import Button from '../../ui/Button';
import AnimatedEmoji from '../../common/AnimatedEmoji';
import './EmptyFolder.scss';
type OwnProps = {
folderId?: number;
folderType: 'all' | 'archived' | 'folder';
foldersDispatch?: FolderEditDispatch;
onScreenSelect?: (screen: SettingsScreens) => void;
};
type StateProps = {
chatFolder?: ApiChatFolder;
animatedEmoji?: ApiSticker;
};
const EmptyFolder: FC<OwnProps & StateProps> = ({
chatFolder, animatedEmoji, foldersDispatch, onScreenSelect,
}) => {
const lang = useLang();
const handleEditFolder = useCallback(() => {
foldersDispatch!({ type: 'editFolder', payload: chatFolder });
onScreenSelect!(SettingsScreens.FoldersEditFolderFromChatList);
}, [chatFolder, foldersDispatch, onScreenSelect]);
return (
<div className="EmptyFolder">
<div className="sticker">{animatedEmoji && <AnimatedEmoji sticker={animatedEmoji} />}</div>
<h3 className="title" dir="auto">{lang('FilterNoChatsToDisplay')}</h3>
<p className="description" dir="auto">
{lang(chatFolder ? 'ChatList.EmptyChatListFilterText' : 'Chat.EmptyChat')}
</p>
{chatFolder && foldersDispatch && onScreenSelect && (
<Button
ripple={!IS_SINGLE_COLUMN_LAYOUT}
fluid
pill
onClick={handleEditFolder}
size="smaller"
isRtl={lang.isRtl}
>
<i className="icon-settings" />
{lang('ChatList.EmptyChatListEditFilter')}
</Button>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>((global, { folderId, folderType }): StateProps => {
const chatFolder = folderId && folderType === 'folder' ? selectChatFolder(global, folderId) : undefined;
return {
chatFolder,
animatedEmoji: selectAnimatedEmoji(global, '📂'),
};
})(EmptyFolder));

View File

@ -4,7 +4,8 @@ import React, {
import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalState } from '../../../global/types';
import { LeftColumnContent } from '../../../types';
import { LeftColumnContent, SettingsScreens } from '../../../types';
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { pick } from '../../../util/iteratees';
@ -32,8 +33,10 @@ type OwnProps = {
searchDate?: number;
contactsFilter: string;
shouldSkipTransition?: boolean;
foldersDispatch: FolderEditDispatch;
onSearchQuery: (query: string) => void;
onContentChange: (content: LeftColumnContent) => void;
onScreenSelect: (screen: SettingsScreens) => void;
onReset: () => void;
};
@ -51,8 +54,10 @@ const LeftMain: FC<OwnProps & StateProps> = ({
searchDate,
contactsFilter,
shouldSkipTransition,
foldersDispatch,
onSearchQuery,
onContentChange,
onScreenSelect,
onReset,
connectionState,
}) => {
@ -158,7 +163,7 @@ const LeftMain: FC<OwnProps & StateProps> = ({
{(isActive) => {
switch (content) {
case LeftColumnContent.ChatList:
return <ChatFolders />;
return <ChatFolders onScreenSelect={onScreenSelect} foldersDispatch={foldersDispatch} />;
case LeftColumnContent.GlobalSearch:
return (
<LeftSearch

View File

@ -1,9 +1,9 @@
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
import { SettingsScreens } from '../../../types';
import { FolderEditDispatch, FoldersState } from '../../../hooks/reducers/useFoldersReducer';
import { LAYERS_ANIMATION_NAME } from '../../../util/environment';
import useFoldersReducer from '../../../hooks/reducers/useFoldersReducer';
import useTwoFaReducer from '../../../hooks/reducers/useTwoFaReducer';
import Transition from '../../ui/Transition';
@ -51,8 +51,11 @@ const FOLDERS_SCREENS = [
SettingsScreens.Folders,
SettingsScreens.FoldersCreateFolder,
SettingsScreens.FoldersEditFolder,
SettingsScreens.FoldersEditFolderFromChatList,
SettingsScreens.FoldersIncludedChats,
SettingsScreens.FoldersIncludedChatsFromChatList,
SettingsScreens.FoldersExcludedChats,
SettingsScreens.FoldersExcludedChatsFromChatList,
];
const PRIVACY_SCREENS = [
@ -88,6 +91,8 @@ const PRIVACY_GROUP_CHATS_SCREENS = [
export type OwnProps = {
isActive: boolean;
currentScreen: SettingsScreens;
foldersState: FoldersState;
foldersDispatch: FolderEditDispatch;
onScreenSelect: (screen: SettingsScreens) => void;
shouldSkipTransition?: boolean;
onReset: () => void;
@ -96,17 +101,19 @@ export type OwnProps = {
const Settings: FC<OwnProps> = ({
isActive,
currentScreen,
foldersState,
foldersDispatch,
onScreenSelect,
onReset,
shouldSkipTransition,
}) => {
const [foldersState, foldersDispatch] = useFoldersReducer();
const [twoFaState, twoFaDispatch] = useTwoFaReducer();
const handleReset = useCallback(() => {
if (
currentScreen === SettingsScreens.FoldersCreateFolder
|| currentScreen === SettingsScreens.FoldersEditFolder
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
) {
setTimeout(() => {
foldersDispatch({ type: 'reset' });
@ -270,8 +277,11 @@ const Settings: FC<OwnProps> = ({
case SettingsScreens.Folders:
case SettingsScreens.FoldersCreateFolder:
case SettingsScreens.FoldersEditFolder:
case SettingsScreens.FoldersEditFolderFromChatList:
case SettingsScreens.FoldersIncludedChats:
case SettingsScreens.FoldersIncludedChatsFromChatList:
case SettingsScreens.FoldersExcludedChats:
case SettingsScreens.FoldersExcludedChatsFromChatList:
return (
<SettingsFolders
currentScreen={currentScreen}

View File

@ -1,5 +1,5 @@
import React, {
FC, useCallback, useMemo, memo, useState,
FC, memo, useCallback, useMemo, useState,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn';
@ -156,6 +156,7 @@ const SettingsHeader: FC<OwnProps & DispatchProps> = ({
case SettingsScreens.FoldersCreateFolder:
return <h3>{lang('FilterNew')}</h3>;
case SettingsScreens.FoldersEditFolder:
case SettingsScreens.FoldersEditFolderFromChatList:
return (
<div className="settings-main-header">
<h3>{lang('FilterEdit')}</h3>
@ -174,14 +175,17 @@ const SettingsHeader: FC<OwnProps & DispatchProps> = ({
</div>
);
case SettingsScreens.FoldersIncludedChats:
case SettingsScreens.FoldersIncludedChatsFromChatList:
case SettingsScreens.FoldersExcludedChats:
case SettingsScreens.FoldersExcludedChatsFromChatList:
return (
<div className="settings-main-header">
{currentScreen === SettingsScreens.FoldersIncludedChats ? (
<h3>{lang('FilterInclude')}</h3>
) : (
<h3>{lang('FilterExclude')}</h3>
)}
{(currentScreen === SettingsScreens.FoldersIncludedChats
|| currentScreen === SettingsScreens.FoldersIncludedChatsFromChatList) ? (
<h3>{lang('FilterInclude')}</h3>
) : (
<h3>{lang('FilterExclude')}</h3>
)}
<Button
round

View File

@ -36,6 +36,7 @@ const SettingsFolders: FC<OwnProps> = ({
if (
currentScreen === SettingsScreens.FoldersCreateFolder
|| currentScreen === SettingsScreens.FoldersEditFolder
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
) {
setTimeout(() => {
dispatch({ type: 'reset' });
@ -72,13 +73,17 @@ const SettingsFolders: FC<OwnProps> = ({
const handleAddIncludedChats = useCallback(() => {
dispatch({ type: 'editIncludeFilters' });
onScreenSelect(SettingsScreens.FoldersIncludedChats);
}, [dispatch, onScreenSelect]);
onScreenSelect(currentScreen === SettingsScreens.FoldersEditFolderFromChatList
? SettingsScreens.FoldersIncludedChatsFromChatList
: SettingsScreens.FoldersIncludedChats);
}, [currentScreen, dispatch, onScreenSelect]);
const handleAddExcludedChats = useCallback(() => {
dispatch({ type: 'editExcludeFilters' });
onScreenSelect(SettingsScreens.FoldersExcludedChats);
}, [dispatch, onScreenSelect]);
onScreenSelect(currentScreen === SettingsScreens.FoldersEditFolderFromChatList
? SettingsScreens.FoldersExcludedChatsFromChatList
: SettingsScreens.FoldersExcludedChats);
}, [currentScreen, dispatch, onScreenSelect]);
switch (currentScreen) {
case SettingsScreens.Folders:
@ -98,6 +103,7 @@ const SettingsFolders: FC<OwnProps> = ({
);
case SettingsScreens.FoldersCreateFolder:
case SettingsScreens.FoldersEditFolder:
case SettingsScreens.FoldersEditFolderFromChatList:
return (
<SettingsFoldersEdit
state={state}
@ -114,6 +120,7 @@ const SettingsFolders: FC<OwnProps> = ({
/>
);
case SettingsScreens.FoldersIncludedChats:
case SettingsScreens.FoldersIncludedChatsFromChatList:
return (
<SettingsFoldersChatFilters
mode="included"
@ -125,6 +132,7 @@ const SettingsFolders: FC<OwnProps> = ({
/>
);
case SettingsScreens.FoldersExcludedChats:
case SettingsScreens.FoldersExcludedChatsFromChatList:
return (
<SettingsFoldersChatFilters
mode="excluded"

View File

@ -52,7 +52,7 @@ const SUBMIT_TIMEOUT = 500;
const INITIAL_CHATS_LIMIT = 5;
const ERROR_NO_TITLE = 'Please provide a title for this folder.';
const ERROR_NO_CHATS = 'Please select at least one chat for this folder.';
const ERROR_NO_CHATS = 'ChatList.Filter.Error.Empty';
const SettingsFoldersEdit: FC<OwnProps & StateProps & DispatchProps> = ({
state,
@ -265,7 +265,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="settings-item no-border pt-3">
{state.error && state.error === ERROR_NO_CHATS && (
<p className="settings-item-description color-danger mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
{state.error}
{lang(state.error)}
</p>
)}

View File

@ -35,7 +35,7 @@ const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">
<AnimatedEmoji sticker={animatedEmoji} />
<AnimatedEmoji sticker={animatedEmoji} size="large" />
<p className="settings-item-description mb-3" dir="auto">
{lang('TwoStepVerificationPasswordSetInfo')}

View File

@ -80,7 +80,7 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">
<AnimatedEmoji sticker={animatedEmoji} />
<AnimatedEmoji sticker={animatedEmoji} size="large" />
</div>
<div className="settings-item pt-0 no-border">

View File

@ -32,7 +32,7 @@ const SettingsTwoFaEnabled: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">
<AnimatedEmoji sticker={animatedEmoji} />
<AnimatedEmoji sticker={animatedEmoji} size="large" />
<p className="settings-item-description mb-3" dir="auto">
{renderText(lang('EnabledPasswordText'), ['br'])}

View File

@ -101,7 +101,7 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">
<AnimatedEmoji sticker={animatedEmoji} />
<AnimatedEmoji sticker={animatedEmoji} size="large" />
</div>
<div className="settings-item pt-0 no-border">

View File

@ -32,7 +32,7 @@ const SettingsTwoFaStart: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">
<AnimatedEmoji sticker={animatedEmoji} />
<AnimatedEmoji sticker={animatedEmoji} size="large" />
<p className="settings-item-description mb-3" dir="auto">
{lang('SetAdditionalPasswordInfo')}

View File

@ -0,0 +1,41 @@
.ContactGreeting {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
.wrapper {
display: inline-flex;
flex-direction: column;
align-items: center;
background: var(--pattern-color);
width: 14.5rem;
padding: .75rem 1rem;
border-radius: 1.5rem;
color: #fff;
}
.title {
font-weight: 500;
margin-bottom: 0;
}
.description {
font-size: .9375rem;
margin-bottom: 0;
}
.sticker {
margin: 2rem 0 1rem;
height: 10rem;
width: 10rem;
cursor: pointer;
.thumbnail {
height: 10rem;
width: 10rem;
}
}
}

View File

@ -0,0 +1,116 @@
import React, {
FC, memo, useCallback, useEffect, useRef,
} from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
import { GlobalActions } from '../../global/types';
import { ApiSticker, ApiUpdateConnectionStateType } from '../../api/types';
import { pick } from '../../util/iteratees';
import { selectChat } from '../../modules/selectors';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import StickerButton from '../common/StickerButton';
import './ContactGreeting.scss';
type OwnProps = {
userId: number;
};
type StateProps = {
sticker?: ApiSticker;
lastUnreadMessageId?: number;
connectionState?: ApiUpdateConnectionStateType;
};
type DispatchProps = Pick<GlobalActions, 'loadGreetingStickers' | 'sendMessage' | 'markMessageListRead'>;
const INTERSECTION_DEBOUNCE_MS = 200;
const ContactGreeting: FC<OwnProps & StateProps & DispatchProps> = ({
sticker,
connectionState,
lastUnreadMessageId,
loadGreetingStickers,
sendMessage,
markMessageListRead,
}) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const {
observe: observeIntersection,
} = useIntersectionObserver({
rootRef: containerRef,
debounceMs: INTERSECTION_DEBOUNCE_MS,
});
useEffect(() => {
if (sticker || connectionState !== 'connectionStateReady') {
return;
}
loadGreetingStickers();
}, [connectionState, loadGreetingStickers, sticker]);
useEffect(() => {
if (connectionState === 'connectionStateReady' && lastUnreadMessageId) {
markMessageListRead({ maxId: lastUnreadMessageId });
}
}, [connectionState, markMessageListRead, lastUnreadMessageId]);
const handleStickerSelect = useCallback((selectedSticker: ApiSticker) => {
selectedSticker = {
...selectedSticker,
isPreloadedGlobally: true,
};
sendMessage({ sticker: selectedSticker });
}, [sendMessage]);
return (
<div className="ContactGreeting" ref={containerRef}>
<div className="wrapper">
<p className="title" dir="auto">{lang('Conversation.EmptyPlaceholder')}</p>
<p className="description" dir="auto">{lang('Conversation.GreetingText')}</p>
<div className="sticker">
{sticker && (
<StickerButton
sticker={sticker}
onClick={handleStickerSelect}
clickArg={sticker}
observeIntersection={observeIntersection}
size={160}
className="large"
/>
)}
</div>
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { userId }): StateProps => {
const { stickers } = global.stickers.greeting;
const sticker = stickers && stickers.length ? stickers[userId % stickers.length] : undefined;
const chat = selectChat(global, userId);
if (!chat) {
return {};
}
return {
sticker,
lastUnreadMessageId: chat.lastMessage && chat.lastMessage.id !== chat.lastReadInboxMessageId
? chat.lastMessage.id
: undefined,
connectionState: global.connectionState,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'loadGreetingStickers', 'sendMessage', 'markMessageListRead',
]),
)(ContactGreeting));

View File

@ -3,7 +3,9 @@ import React, {
} from '../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../lib/teact/teactn';
import { ApiMessage, ApiRestrictionReason, MAIN_THREAD_ID } from '../../api/types';
import {
ApiAction, ApiMessage, ApiRestrictionReason, MAIN_THREAD_ID,
} from '../../api/types';
import { GlobalActions, MessageListType } from '../../global/types';
import { LoadMoreDirection } from '../../types';
@ -24,13 +26,13 @@ import {
selectScheduledMessages,
selectCurrentMessageIds,
} from '../../modules/selectors';
import { isChatChannel, isChatPrivate } from '../../modules/helpers';
import { isChatChannel, isChatGroup, isChatPrivate } from '../../modules/helpers';
import { orderBy, pick } from '../../util/iteratees';
import { fastRaf, debounce, onTickEnd } from '../../util/schedulers';
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import buildClassName from '../../util/buildClassName';
import { groupMessages } from './helpers/groupMessages';
import { groupMessages, MessageDateGroup } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import useOnChange from '../../hooks/useOnChange';
import useStickyDates from './hooks/useStickyDates';
@ -43,6 +45,8 @@ import useWindowSize from '../../hooks/useWindowSize';
import Loading from '../ui/Loading';
import MessageListContent from './MessageListContent';
import ContactGreeting from './ContactGreeting';
import NoMessages from './NoMessages';
import './MessageList.scss';
@ -60,7 +64,10 @@ type OwnProps = {
type StateProps = {
isChatLoaded?: boolean;
isChannelChat?: boolean;
isGroupChat?: boolean;
isChatWithSelf?: boolean;
isCreator?: boolean;
isBot?: boolean;
messageIds?: number[];
messagesById?: Record<number, ApiMessage>;
firstUnreadId?: number;
@ -100,9 +107,12 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
onNotchToggle,
isChatLoaded,
isChannelChat,
isGroupChat,
canPost,
isReady,
isChatWithSelf,
isCreator,
isBot,
messageIds,
messagesById,
firstUnreadId,
@ -424,6 +434,16 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
const isPrivate = Boolean(chatId && isChatPrivate(chatId));
const withUsers = Boolean((!isPrivate && !isChannelChat) || isChatWithSelf);
const noAvatars = Boolean(!withUsers || isChannelChat);
const shouldRenderGreeting = isChatPrivate(chatId) && !isChatWithSelf && !isBot
&& ((
!messageGroups && !lastMessage && messageIds
// Used to avoid flickering when deleting a greeting that has just been sent
&& (!listItemElementsRef.current || listItemElementsRef.current.length === 0))
|| checkSingleMessageActionByType('contactSignUp', messageGroups)
|| (lastMessage && lastMessage.content.action && lastMessage.content.action.type === 'contactSignUp')
);
const isGroupChatJustCreated = isGroupChat && isCreator
&& checkSingleMessageActionByType('chatCreate', messageGroups);
const className = buildClassName(
'MessageList custom-scroll',
@ -451,8 +471,15 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
</div>
) : botDescription ? (
<div className="empty rich"><span>{renderText(lang(botDescription), ['br', 'emoji', 'links'])}</span></div>
) : messageIds && !messageGroups ? (
<div className="empty"><span>{lang('NoMessages')}</span></div>
) : shouldRenderGreeting ? (
<ContactGreeting userId={chatId} />
) : messageIds && (!messageGroups || isGroupChatJustCreated) ? (
<NoMessages
chatId={chatId}
type={type}
isChatWithSelf={isChatWithSelf}
isGroupChatJustCreated={isGroupChatJustCreated}
/>
) : ((messageIds && messageGroups) || lastMessage) ? (
<MessageListContent
messageIds={messageIds || [lastMessage!.id]}
@ -481,6 +508,16 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
);
};
function checkSingleMessageActionByType(type: ApiAction['type'], messageGroups?: MessageDateGroup[]) {
return messageGroups
&& messageGroups.length === 1
&& messageGroups[0].senderGroups.length === 1
&& messageGroups[0].senderGroups[0].length === 1
&& 'content' in messageGroups[0].senderGroups[0][0]
&& messageGroups[0].senderGroups[0][0].content.action
&& messageGroups[0].senderGroups[0][0].content.action.type === type;
}
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, type }): StateProps => {
const chat = selectChat(global, chatId);
@ -510,6 +547,7 @@ export default memo(withGlobal<OwnProps>(
&& !messageIds && !chat.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId
);
const bot = selectChatBot(global, chatId);
let botDescription: string | undefined;
if (selectIsChatBotNotStarted(global, chatId)) {
const chatBot = selectChatBot(global, chatId)!;
@ -525,7 +563,10 @@ export default memo(withGlobal<OwnProps>(
isRestricted,
restrictionReason,
isChannelChat: isChatChannel(chat),
isGroupChat: isChatGroup(chat),
isCreator: chat.isCreator,
isChatWithSelf: selectIsChatWithSelf(global, chatId),
isBot: Boolean(bot),
messageIds,
messagesById,
firstUnreadId: selectFirstUnreadId(global, chatId, threadId),

View File

@ -330,7 +330,7 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
<>
{renderBackButton()}
<h3>
{isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount)}
{isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i')}
</h3>
</>
) : undefined

View File

@ -0,0 +1,56 @@
.NoMessages {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.icon {
font-size: 5rem;
margin: 0 auto 1rem;
}
.wrapper {
display: inline-flex;
flex-direction: column;
background: var(--pattern-color);
max-width: 20rem;
padding: .75rem 1rem;
border-radius: 1.5rem;
color: #fff;
&[dir=rtl] {
text-align: right;
}
}
.title {
font-weight: 500;
font-size: 1rem;
margin-bottom: .25rem;
text-align: center;
unicode-bidi: plaintext;
}
.description {
font-size: .9375rem;
margin: 0;
padding: 0;
list-style: none;
unicode-bidi: plaintext;
}
.list-checkmarks {
font-size: .9375rem;
margin: .25rem 0 0;
padding: 0;
list-style: none;
unicode-bidi: plaintext;
line-height: 1.8;
li::before {
content: '';
margin-inline-end: .5rem;
}
}
}

View File

@ -0,0 +1,79 @@
import React, { FC, memo } from '../../lib/teact/teact';
import { MessageListType } from '../../global/types';
import useLang, { LangFn } from '../../hooks/useLang';
import './NoMessages.scss';
type OwnProps = {
chatId: number;
isChatWithSelf?: boolean;
type: MessageListType;
isGroupChatJustCreated?: boolean;
};
const NoMessages: FC<OwnProps> = ({
isChatWithSelf, type, isGroupChatJustCreated,
}) => {
const lang = useLang();
if (type === 'scheduled') {
return renderScheduled(lang);
}
if (isChatWithSelf) {
return renderSavedMessages(lang);
}
if (isGroupChatJustCreated) {
return renderGroup(lang);
}
return (
<div className="empty"><span>{lang('NoMessages')}</span></div>
);
};
function renderScheduled(lang: LangFn) {
return (
<div className="empty"><span>{lang('ScheduledMessages.EmptyPlaceholder')}</span></div>
);
}
function renderSavedMessages(lang: LangFn) {
return (
<div className="NoMessages">
<div className="wrapper">
<i className="icon icon-cloud-download" />
<h3 className="title">{lang('Conversation.CloudStorageInfo.Title')}</h3>
<ul className="description">
<li>{lang('Conversation.ClousStorageInfo.Description1')}</li>
<li>{lang('Conversation.ClousStorageInfo.Description2')}</li>
<li>{lang('Conversation.ClousStorageInfo.Description3')}</li>
<li>{lang('Conversation.ClousStorageInfo.Description4')}</li>
</ul>
</div>
</div>
);
}
function renderGroup(lang: LangFn) {
return (
<div className="NoMessages">
<div className="wrapper" dir={lang.isRtl ? 'rtl' : undefined}>
<h3 className="title">{lang('EmptyGroupInfo.Title')}</h3>
<p className="description">{lang('EmptyGroupInfo.Subtitle')}</p>
<ul className="list-checkmarks">
<li>{lang('EmptyGroupInfo.Line1')}</li>
<li>{lang('EmptyGroupInfo.Line2')}</li>
<li>{lang('EmptyGroupInfo.Line3')}</li>
<li>{lang('EmptyGroupInfo.Line4')}</li>
</ul>
</div>
</div>
);
}
export default memo(NoMessages);

View File

@ -609,7 +609,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
)}
{animatedEmoji && (
<AnimatedEmoji
isInline
size="small"
sticker={animatedEmoji}
observeIntersection={observeIntersectionForMedia}
lastSyncTime={lastSyncTime}

View File

@ -38,7 +38,11 @@ const Tab: FC<OwnProps> = ({
const tab = tabRef.current!;
const indicator = tab.querySelector('i')!;
const currentIndicator = tab.parentElement!.children[previousActiveTab].querySelector('i')!;
const prevTab = tab.parentElement!.children[previousActiveTab];
if (!prevTab) {
return;
}
const currentIndicator = prevTab.querySelector('i')!;
currentIndicator.classList.remove('animate');
indicator.classList.remove('animate');

View File

@ -62,6 +62,9 @@ export const INITIAL_STATE: GlobalState = {
favorite: {
stickers: [],
},
greeting: {
stickers: [],
},
featured: {
setIds: [],
},

View File

@ -198,6 +198,10 @@ export type GlobalState = {
hash?: number;
stickers: ApiSticker[];
};
greeting: {
hash?: number;
stickers: ApiSticker[];
};
featured: {
hash?: number;
setIds?: string[];
@ -486,7 +490,7 @@ export type ActionTypes = (
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' |
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' | 'loadGreetingStickers' |
// bots
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'resetInlineBot' | 'restartBot' |

View File

@ -3,7 +3,7 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
import { GlobalState } from '../../../global/types';
import {
ApiPrivacyKey, PrivacyVisibility, ProfileEditProgress, IInputPrivacyRules, IInputPrivacyContact,
UPLOADING_WALLPAPER_SLUG, LangCode,
UPLOADING_WALLPAPER_SLUG,
} from '../../../types';
import { callApi } from '../../../api/gramjs';

View File

@ -52,6 +52,31 @@ addReducer('loadFavoriteStickers', (global) => {
void loadFavoriteStickers(hash);
});
addReducer('loadGreetingStickers', (global) => {
const { hash } = global.stickers.greeting || {};
(async () => {
const greeting = await callApi('fetchStickersForEmoji', { emoji: '👋⭐️', hash });
if (!greeting) {
return;
}
const newGlobal = getGlobal();
setGlobal({
...newGlobal,
stickers: {
...newGlobal.stickers,
greeting: {
hash: greeting.hash,
stickers: greeting.stickers.filter((sticker) => sticker.emoji === '👋'),
},
},
});
})();
});
addReducer('loadFeaturedStickers', (global) => {
const { hash } = global.stickers.featured || {};
void loadFeaturedStickers(hash);

View File

@ -163,8 +163,11 @@ export enum SettingsScreens {
Folders,
FoldersCreateFolder,
FoldersEditFolder,
FoldersEditFolderFromChatList,
FoldersIncludedChats,
FoldersIncludedChatsFromChatList,
FoldersExcludedChats,
FoldersExcludedChatsFromChatList,
TwoFaDisabled,
TwoFaNewPassword,
TwoFaNewPasswordConfirm,