MiddleColumn: show similar channels after join channel (#4126)
This commit is contained in:
parent
c3c71cbc9e
commit
2bf38d0726
@ -2037,7 +2037,13 @@ export async function fetchChannelRecommendations({ chat }: { chat: ApiChat }) {
|
||||
|
||||
updateLocalDb(result);
|
||||
|
||||
return result?.chats.map((_chat) => buildApiChatFromPreview(_chat)).filter(Boolean);
|
||||
return {
|
||||
similarChannels: result?.chats
|
||||
.map((_chat) => buildApiChatFromPreview(_chat))
|
||||
.filter(Boolean),
|
||||
count:
|
||||
result instanceof GramJs.messages.ChatsSlice ? result.count : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function handleUserPrivacyRestrictedUpdates(updates: GramJs.TypeUpdates) {
|
||||
|
||||
@ -291,7 +291,14 @@ export interface ApiAction {
|
||||
text: string;
|
||||
targetUserIds?: string[];
|
||||
targetChatId?: string;
|
||||
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'suggestProfilePhoto' | 'other';
|
||||
type:
|
||||
| 'historyClear'
|
||||
| 'contactSignUp'
|
||||
| 'chatCreate'
|
||||
| 'topicCreate'
|
||||
| 'suggestProfilePhoto'
|
||||
| 'joinedChannel'
|
||||
| 'other';
|
||||
photo?: ApiPhoto;
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
|
||||
@ -13,7 +13,7 @@ import type { FocusDirection, ThreadId } from '../../types';
|
||||
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
|
||||
|
||||
import {
|
||||
getChatTitle, getMessageHtmlId, isChatChannel,
|
||||
getChatTitle, getMessageHtmlId, isChatChannel, isJoinedChannelMessage,
|
||||
} from '../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../global/helpers/replies';
|
||||
import {
|
||||
@ -42,6 +42,7 @@ import useFocusMessage from './message/hooks/useFocusMessage';
|
||||
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
|
||||
import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar';
|
||||
import ContextMenuContainer from './message/ContextMenuContainer.async';
|
||||
import SimilarChannels from './message/SimilarChannels';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
@ -129,6 +130,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
const isGift = Boolean(message.content.action?.text.startsWith('ActionGift'));
|
||||
const isGiftCode = Boolean(message.content.action?.text.startsWith('BoostingReceivedGift'));
|
||||
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
|
||||
const isJoinedMessage = isJoinedChannelMessage(message);
|
||||
|
||||
useEffect(() => {
|
||||
if (noAppearanceAnimation) {
|
||||
@ -291,15 +293,15 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{!isSuggestedAvatar && !isGiftCode && <span className="action-message-content">{renderContent()}</span>}
|
||||
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && (
|
||||
<span className="action-message-content">{renderContent()}</span>
|
||||
)}
|
||||
{isGift && renderGift()}
|
||||
{isGiftCode && renderGiftCode()}
|
||||
{isSuggestedAvatar && (
|
||||
<ActionMessageSuggestedAvatar
|
||||
message={message}
|
||||
renderContent={renderContent}
|
||||
/>
|
||||
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
|
||||
)}
|
||||
{isJoinedMessage && <SimilarChannels chatId={targetChatId!} />}
|
||||
{contextMenuPosition && (
|
||||
<ContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
|
||||
@ -219,6 +219,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.join-text,
|
||||
.sticky-date,
|
||||
.local-action-message,
|
||||
.ActionMessage,
|
||||
|
||||
247
src/components/middle/message/SimilarChannels.module.scss
Normal file
247
src/components/middle/message/SimilarChannels.module.scss
Normal file
@ -0,0 +1,247 @@
|
||||
.root {
|
||||
max-width: 45rem;
|
||||
margin-top: 0.625rem;
|
||||
position: relative;
|
||||
--more-channel-background: #d8d8d8;
|
||||
--more-channel-background-dark: #8f8f8f;
|
||||
--more-channel-badge: #8f8f8f;
|
||||
--more-channel-badge-overlay: #00000033;
|
||||
}
|
||||
|
||||
.notch {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 0.1875rem;
|
||||
}
|
||||
|
||||
.notch-path {
|
||||
fill: var(--color-background);
|
||||
}
|
||||
|
||||
.join-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0.375rem 0.375rem 0 0.75rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.close {
|
||||
width: auto;
|
||||
height: auto;
|
||||
font-size: 1.25rem;
|
||||
padding: 0.125rem;
|
||||
border-radius: 50%;
|
||||
|
||||
> .icon {
|
||||
margin-left: 0.0625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 8.5rem;
|
||||
border-radius: 0.9375rem;
|
||||
margin-top: 0.625rem;
|
||||
}
|
||||
|
||||
.inner {
|
||||
background: var(--color-background);
|
||||
border-radius: 0.9375rem;
|
||||
}
|
||||
|
||||
.is-appearing {
|
||||
animation: 0.15s ease-out channels-appear forwards;
|
||||
}
|
||||
|
||||
.is-hiding {
|
||||
animation: 0.15s ease-out channels-disappear forwards;
|
||||
}
|
||||
|
||||
@keyframes channels-appear {
|
||||
from {
|
||||
transform: scale(0) translateY(-50%);
|
||||
opacity: 0;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
height: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
height: 8.9375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes channels-disappear {
|
||||
from {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
height: 8.9375rem;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(0) translateY(-50%);
|
||||
opacity: 0;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-list {
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
border-bottom-left-radius: 0.9375rem;
|
||||
border-bottom-right-radius: 0.9375rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 6.375rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
padding: 0.5rem 0.5rem 0.25rem 0.5rem;
|
||||
|
||||
&:not(:last-child) {
|
||||
&:hover {
|
||||
background: var(--color-chat-hover);
|
||||
border-radius: 0.625rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.last-item {
|
||||
margin: 0;
|
||||
padding: 0.5rem 0 0.25rem 0;
|
||||
cursor: pointer;
|
||||
min-width: 5rem;
|
||||
margin-right: 0.75rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 3.75rem;
|
||||
height: 3.75rem;
|
||||
}
|
||||
|
||||
.last-item .avatar {
|
||||
z-index: 3;
|
||||
outline: 0.125rem solid var(--color-background);
|
||||
}
|
||||
|
||||
.last-item .badge {
|
||||
z-index: 4;
|
||||
align-self: center;
|
||||
background: var(--more-channel-background);
|
||||
|
||||
:global(.theme-dark) & {
|
||||
background: var(--more-channel-background-dark);
|
||||
|
||||
&::before {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
max-width: 3.75rem;
|
||||
height: 0.9375rem;
|
||||
margin-top: -0.8125rem;
|
||||
outline: 0.0625rem solid var(--color-background);
|
||||
padding: 0.125rem 0.1875rem 0.125rem 0.25rem;
|
||||
border-radius: 0.625rem;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-white);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
background-color: var(--more-channel-badge-overlay);
|
||||
position: absolute;
|
||||
max-width: 3.75rem;
|
||||
width: 100%;
|
||||
height: 0.9375rem;
|
||||
border-radius: 0.625rem;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 0.4375rem;
|
||||
margin-right: 0.0625rem;
|
||||
}
|
||||
|
||||
.members-count {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 700;
|
||||
line-height: 0.6875rem;
|
||||
}
|
||||
|
||||
.channel-title {
|
||||
text-align: center;
|
||||
height: 1.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 400;
|
||||
line-height: 0.8125rem;
|
||||
max-width: 3.4375rem;
|
||||
margin-top: 0.125rem;
|
||||
white-space: normal;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
.last-item .channel-title {
|
||||
color: var(--color-text-secondary);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.fake-avatar {
|
||||
border-radius: 50%;
|
||||
width: 3.75rem;
|
||||
height: 3.75rem;
|
||||
position: absolute;
|
||||
left: 0.625rem;
|
||||
background: var(--more-channel-background);
|
||||
outline: 0.125rem solid var(--color-background);
|
||||
z-index: 2;
|
||||
|
||||
:global(.theme-dark) & {
|
||||
background: var(--more-channel-background-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.fake-avatar-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.last-fake-avatar {
|
||||
left: 1.25rem;
|
||||
z-index: 1;
|
||||
}
|
||||
266
src/components/middle/message/SimilarChannels.tsx
Normal file
266
src/components/middle/message/SimilarChannels.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat } from '../../../api/types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { getChatAvatarHash } from '../../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectIsCurrentUserPremium,
|
||||
selectSimilarChannelIds,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getAverageColor, rgb2hex } from '../../../util/colors';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useTimeout from '../../../hooks/useTimeout';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Icon from '../../common/Icon';
|
||||
import Button from '../../ui/Button';
|
||||
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||
|
||||
import styles from './SimilarChannels.module.scss';
|
||||
|
||||
const DEFAULT_BADGE_COLOR = '#3C3C4399';
|
||||
const SHOW_CHANNELS_NUMBER = 10;
|
||||
const MIN_SKELETON_DELAY = 300;
|
||||
const MAX_SKELETON_DELAY = 2000;
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
similarChannelIds?: string[];
|
||||
shouldShowInChat?: boolean;
|
||||
count: number;
|
||||
isCurrentUserPremium: boolean;
|
||||
};
|
||||
|
||||
const SimilarChannels = ({
|
||||
chatId,
|
||||
similarChannelIds,
|
||||
shouldShowInChat,
|
||||
count,
|
||||
isCurrentUserPremium,
|
||||
}: StateProps & OwnProps) => {
|
||||
const lang = useLang();
|
||||
const { toggleChannelRecommendations } = getActions();
|
||||
const [isShowing, markShowing, markNotShowing] = useFlag(false);
|
||||
const [isHiding, markHiding, markNotHiding] = useFlag(false);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const similarChannels = useMemo(() => {
|
||||
if (!similarChannelIds) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const global = getGlobal();
|
||||
return similarChannelIds.map((id) => selectChat(global, id)).filter(Boolean);
|
||||
}, [similarChannelIds]);
|
||||
// Show skeleton while loading similar channels
|
||||
const [shoulRenderSkeleton, setShoulRenderSkeleton] = useState(!similarChannelIds);
|
||||
const firstSimilarChannels = useMemo(() => similarChannels?.slice(0, SHOW_CHANNELS_NUMBER), [similarChannels]);
|
||||
const areSimilarChannelsPresent = Boolean(firstSimilarChannels?.length);
|
||||
useHorizontalScroll(ref, !areSimilarChannelsPresent || !shouldShowInChat || shoulRenderSkeleton, true);
|
||||
const isAnimating = isHiding || isShowing;
|
||||
const shouldRenderChannels = Boolean(
|
||||
!shoulRenderSkeleton
|
||||
&& (shouldShowInChat || isAnimating)
|
||||
&& areSimilarChannelsPresent,
|
||||
);
|
||||
|
||||
useTimeout(() => setShoulRenderSkeleton(false), MAX_SKELETON_DELAY, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (shoulRenderSkeleton && similarChannels && shouldShowInChat) {
|
||||
const id = setTimeout(() => {
|
||||
setShoulRenderSkeleton(false);
|
||||
}, MIN_SKELETON_DELAY);
|
||||
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [similarChannels, shouldShowInChat, shoulRenderSkeleton]);
|
||||
|
||||
const handleToggle = () => {
|
||||
toggleChannelRecommendations({ chatId });
|
||||
if (shouldShowInChat) {
|
||||
markNotShowing();
|
||||
markHiding();
|
||||
} else {
|
||||
markShowing();
|
||||
markNotHiding();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root)}>
|
||||
<div className="join-text">
|
||||
<span
|
||||
className={buildClassName(areSimilarChannelsPresent && styles.joinText)}
|
||||
onClick={areSimilarChannelsPresent ? handleToggle : undefined}
|
||||
>
|
||||
{lang('ChannelJoined')}
|
||||
</span>
|
||||
</div>
|
||||
{shoulRenderSkeleton && <Skeleton className={styles.skeleton} />}
|
||||
{shouldRenderChannels && (
|
||||
<div
|
||||
className={buildClassName(
|
||||
isShowing && styles.isAppearing,
|
||||
isHiding && styles.isHiding,
|
||||
)}
|
||||
>
|
||||
<div className={styles.notch}>
|
||||
<svg
|
||||
width="19"
|
||||
height="7"
|
||||
viewBox="0 0 19 7"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className={styles.notchPath}
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M19 7C16.8992 7 13.59 3.88897 11.5003 1.67424C10.7648 0.894688 10.397 0.50491 10.0434 0.385149C9.70568 0.270811 9.4225 0.270474 9.08456 0.38401C8.73059 0.50293 8.36133 0.892443 7.62279 1.67147C5.52303 3.88637 2.18302 7 0 7L19 7Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>{lang('SimilarChannels')}</span>
|
||||
<Button
|
||||
className={styles.close}
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
</div>
|
||||
<div ref={ref} className={buildClassName(styles.channelList, 'no-scrollbar')}>
|
||||
{firstSimilarChannels?.map((channel, i) => {
|
||||
return i === SHOW_CHANNELS_NUMBER - 1 ? (
|
||||
<MoreChannels
|
||||
channel={channel}
|
||||
chatId={chatId}
|
||||
channelsCount={count - SHOW_CHANNELS_NUMBER + 1}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
/>
|
||||
) : (
|
||||
<SimilarChannel channel={channel} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function SimilarChannel({ channel }: { channel: ApiChat }) {
|
||||
const { openChat } = getActions();
|
||||
const color = useAverageColor(channel);
|
||||
|
||||
return (
|
||||
<div className={styles.item} onClick={() => openChat({ id: channel.id })}>
|
||||
<Avatar className={styles.avatar} key={channel.id} size="large" peer={channel} />
|
||||
<div style={`background: ${color}`} className={styles.badge}>
|
||||
<i className={buildClassName(styles.icon, 'icon icon-user-filled')} />
|
||||
<span className={styles.membersCount}>{formatIntegerCompact(channel?.membersCount || 0)}
|
||||
</span>
|
||||
</div>
|
||||
<span className={styles.channelTitle}>{channel.title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MoreChannels({
|
||||
channel,
|
||||
chatId,
|
||||
channelsCount,
|
||||
isCurrentUserPremium,
|
||||
}: {
|
||||
channel: ApiChat;
|
||||
chatId: string;
|
||||
channelsCount: number;
|
||||
isCurrentUserPremium: boolean;
|
||||
}) {
|
||||
const { openPremiumModal, openChatWithInfo } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const handleClickMore = () => {
|
||||
if (isCurrentUserPremium) {
|
||||
openChatWithInfo({
|
||||
id: chatId, shouldReplaceHistory: true, profileTab: 'similarChannels', forceScrollProfileTab: true,
|
||||
});
|
||||
} else {
|
||||
openPremiumModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(styles.item, styles.lastItem)}
|
||||
onClick={() => handleClickMore()}
|
||||
>
|
||||
<Avatar className={styles.avatar} key={channel.id} size="large" peer={channel} />
|
||||
<div className={styles.fakeAvatar}>
|
||||
<div className={styles.fakeAvatarInner} />
|
||||
</div>
|
||||
<div className={buildClassName(styles.fakeAvatar, styles.lastFakeAvatar)}>
|
||||
<div className={styles.fakeAvatarInner} />
|
||||
</div>
|
||||
<div className={styles.badge}>
|
||||
<span className={styles.membersCount}>{`+${channelsCount}`}</span>
|
||||
{!isCurrentUserPremium && <Icon name="lock-badge" className={styles.icon} />}
|
||||
</div>
|
||||
<span className={styles.channelTitle}>{lang('MoreSimilar')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useAverageColor(channel: ApiChat) {
|
||||
const [color, setColor] = useState(DEFAULT_BADGE_COLOR);
|
||||
const imgBlobUrl = useMedia(getChatAvatarHash(channel), false, ApiMediaFormat.BlobUrl);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!imgBlobUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const averageColor = await getAverageColor(imgBlobUrl);
|
||||
setColor(`#${rgb2hex(averageColor)}`);
|
||||
})();
|
||||
}, [imgBlobUrl]);
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
export default memo(
|
||||
withGlobal<OwnProps>((global, { chatId }): StateProps => {
|
||||
const { similarChannelIds, shouldShowInChat, count } = selectSimilarChannelIds(global, chatId) || {};
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
return {
|
||||
similarChannelIds,
|
||||
shouldShowInChat,
|
||||
count,
|
||||
isCurrentUserPremium,
|
||||
};
|
||||
})(SimilarChannels),
|
||||
);
|
||||
@ -125,6 +125,7 @@ type StateProps = {
|
||||
limitSimilarChannels: number;
|
||||
isTopicInfo?: boolean;
|
||||
isSavedDialog?: boolean;
|
||||
forceScrollProfileTab?: boolean;
|
||||
};
|
||||
|
||||
type TabProps = {
|
||||
@ -180,6 +181,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
limitSimilarChannels,
|
||||
isTopicInfo,
|
||||
isSavedDialog,
|
||||
forceScrollProfileTab,
|
||||
}) => {
|
||||
const {
|
||||
setLocalMediaSearchType,
|
||||
@ -254,10 +256,10 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
}, [nextProfileTab, tabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isChannel) {
|
||||
if (isChannel && !similarChannels) {
|
||||
fetchChannelRecommendations({ chatId });
|
||||
}
|
||||
}, [chatId, isChannel]);
|
||||
}, [chatId, isChannel, similarChannels]);
|
||||
|
||||
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
|
||||
const tabType = tabs[renderingActiveTab].type as ProfileTabType;
|
||||
@ -296,7 +298,13 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
|
||||
usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined);
|
||||
|
||||
const { handleScroll } = useProfileState(containerRef, resultType, profileState, onProfileStateChange);
|
||||
const { handleScroll } = useProfileState(
|
||||
containerRef,
|
||||
resultType,
|
||||
profileState,
|
||||
onProfileStateChange,
|
||||
forceScrollProfileTab,
|
||||
);
|
||||
|
||||
const { applyTransitionFix, releaseTransitionFix } = useTransitionFixes(containerRef);
|
||||
|
||||
@ -693,7 +701,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
&& (getHasAdminRight(chat, 'inviteUsers') || !isUserRightBanned(chat, 'inviteUsers') || chat.isCreator);
|
||||
const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
|
||||
const activeDownloads = selectActiveDownloads(global, chatId);
|
||||
const similarChannels = selectSimilarChannelIds(global, chatId);
|
||||
const { similarChannelIds } = selectSimilarChannelIds(global, chatId) || {};
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
let hasCommonChatsTab;
|
||||
@ -739,8 +747,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
storyByIds,
|
||||
isChatProtected: chat?.isProtected,
|
||||
nextProfileTab: selectTabState(global).nextProfileTab,
|
||||
forceScrollProfileTab: selectTabState(global).forceScrollProfileTab,
|
||||
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
|
||||
similarChannels,
|
||||
similarChannels: similarChannelIds,
|
||||
isCurrentUserPremium,
|
||||
isTopicInfo,
|
||||
isSavedDialog,
|
||||
|
||||
@ -20,10 +20,11 @@ export default function useProfileState(
|
||||
tabType: ProfileTabType,
|
||||
profileState: ProfileState,
|
||||
onProfileStateChange: (state: ProfileState) => void,
|
||||
forceScrollProfileTab = false,
|
||||
) {
|
||||
// Scroll to tabs if needed
|
||||
useEffectWithPrevDeps(([prevTabType]) => {
|
||||
if (prevTabType && prevTabType !== tabType) {
|
||||
if ((prevTabType && prevTabType !== tabType) || (tabType && forceScrollProfileTab)) {
|
||||
const container = containerRef.current!;
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.TabList')!;
|
||||
if (container.scrollTop < tabsEl.offsetTop) {
|
||||
@ -35,7 +36,7 @@ export default function useProfileState(
|
||||
}, PROGRAMMATIC_SCROLL_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
}, [tabType, onProfileStateChange, containerRef]);
|
||||
}, [tabType, onProfileStateChange, containerRef, forceScrollProfileTab]);
|
||||
|
||||
// Scroll to top
|
||||
useEffectWithPrevDeps(([prevProfileState]) => {
|
||||
|
||||
@ -44,6 +44,7 @@ import {
|
||||
isChatBasicGroup,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isLocalMessageId,
|
||||
isUserBot,
|
||||
toChannelId,
|
||||
} from '../../helpers';
|
||||
@ -58,6 +59,7 @@ import {
|
||||
addUsers,
|
||||
addUserStatuses,
|
||||
addUsersToRestrictedInviteList,
|
||||
deleteChatMessages,
|
||||
deleteTopic,
|
||||
leaveChat,
|
||||
removeChatFromChatLists,
|
||||
@ -67,6 +69,7 @@ import {
|
||||
replaceThreadParam,
|
||||
replaceUsers,
|
||||
replaceUserStatuses,
|
||||
toggleSimilarChannels,
|
||||
updateChat,
|
||||
updateChatFullInfo,
|
||||
updateChatLastMessageId,
|
||||
@ -93,6 +96,7 @@ import {
|
||||
selectChatLastMessage,
|
||||
selectChatLastMessageId,
|
||||
selectChatListType,
|
||||
selectChatMessages,
|
||||
selectCurrentChat,
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
@ -798,7 +802,7 @@ addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
|
||||
void callApi('deleteChat', { chatId: chat.id });
|
||||
});
|
||||
|
||||
addActionHandler('leaveChannel', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload!;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
@ -814,7 +818,12 @@ addActionHandler('leaveChannel', (global, actions, payload): ActionReturnType =>
|
||||
|
||||
const { id: channelId, accessHash } = chat;
|
||||
if (channelId && accessHash) {
|
||||
void callApi('leaveChannel', { channelId, accessHash });
|
||||
await callApi('leaveChannel', { channelId, accessHash });
|
||||
global = getGlobal();
|
||||
const chatMessages = selectChatMessages(global, chatId);
|
||||
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
||||
global = deleteChatMessages(global, chatId, localMessageIds);
|
||||
setGlobal(global);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2615,9 +2624,9 @@ addActionHandler('fetchChannelRecommendations', async (global, actions, payload)
|
||||
return;
|
||||
}
|
||||
|
||||
const similarChannels = await callApi('fetchChannelRecommendations', {
|
||||
const { similarChannels, count } = await callApi('fetchChannelRecommendations', {
|
||||
chat,
|
||||
});
|
||||
}) || {};
|
||||
|
||||
if (!similarChannels) {
|
||||
return;
|
||||
@ -2625,8 +2634,19 @@ addActionHandler('fetchChannelRecommendations', async (global, actions, payload)
|
||||
|
||||
global = getGlobal();
|
||||
global = addChats(global, buildCollectionByKey(similarChannels, 'id'));
|
||||
global = addSimilarChannels(global, chatId, similarChannels.map((channel) => channel.id));
|
||||
global = addSimilarChannels(global, chatId, similarChannels.map((channel) => channel.id), count);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('toggleChannelRecommendations', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = toggleSimilarChannels(global, chatId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
|
||||
@ -5,10 +5,13 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
|
||||
import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages';
|
||||
import { isChatChannel, isLocalMessageId } from '../../helpers';
|
||||
import {
|
||||
addActionHandler, getGlobal, setGlobal,
|
||||
} from '../../index';
|
||||
import {
|
||||
deleteChatMessages,
|
||||
leaveChat,
|
||||
replaceThreadParam,
|
||||
updateChat,
|
||||
@ -23,7 +26,9 @@ import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectChatLastMessageId,
|
||||
selectChatListType,
|
||||
selectChatMessages,
|
||||
selectCommonBoxChatId,
|
||||
selectCurrentMessageList,
|
||||
selectIsChatListed,
|
||||
@ -87,6 +92,26 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
case 'updateChatJoin': {
|
||||
const listType = selectChatListType(global, update.id);
|
||||
const chat = selectChat(global, update.id);
|
||||
if (chat && isChatChannel(chat)) {
|
||||
actions.fetchChannelRecommendations({ chatId: chat.id });
|
||||
const lastMessageId = selectChatLastMessageId(global, chat.id);
|
||||
const localMessage = buildLocalMessage(chat, lastMessageId);
|
||||
localMessage.content.action = {
|
||||
text: 'you joined this channel',
|
||||
translationValues: ['ChannelJoined'],
|
||||
type: 'joinedChannel',
|
||||
targetChatId: chat.id,
|
||||
};
|
||||
|
||||
actions.apiUpdate({
|
||||
'@type': 'newMessage',
|
||||
id: localMessage.id,
|
||||
chatId: chat.id,
|
||||
message: localMessage,
|
||||
});
|
||||
}
|
||||
|
||||
if (!listType) {
|
||||
return undefined;
|
||||
}
|
||||
@ -95,7 +120,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
global = updateChat(global, update.id, { isNotJoined: false });
|
||||
setGlobal(global);
|
||||
|
||||
const chat = selectChat(global, update.id);
|
||||
if (chat) {
|
||||
actions.requestChatUpdate({ chatId: chat.id });
|
||||
}
|
||||
@ -104,7 +128,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
case 'updateChatLeave': {
|
||||
return leaveChat(global, update.id);
|
||||
global = leaveChat(global, update.id);
|
||||
const chat = selectChat(global, update.id);
|
||||
if (chat && isChatChannel(chat)) {
|
||||
const chatMessages = selectChatMessages(global, update.id);
|
||||
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
||||
global = deleteChatMessages(global, chat.id, localMessageIds);
|
||||
}
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
case 'updateChatInbox': {
|
||||
|
||||
@ -97,12 +97,13 @@ addActionHandler('openPreviousChat', (global, actions, payload): ActionReturnTyp
|
||||
});
|
||||
|
||||
addActionHandler('openChatWithInfo', (global, actions, payload): ActionReturnType => {
|
||||
const { profileTab, tabId = getCurrentTabId() } = payload;
|
||||
const { profileTab, forceScrollProfileTab = false, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
global = updateTabState(global, {
|
||||
...selectTabState(global, tabId),
|
||||
isChatInfoShown: true,
|
||||
nextProfileTab: profileTab,
|
||||
forceScrollProfileTab,
|
||||
}, tabId);
|
||||
global = { ...global, lastIsChatInfoShown: true };
|
||||
setGlobal(global);
|
||||
|
||||
@ -21,7 +21,7 @@ import { getServerTime } from '../../../util/serverTime';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import versionNotification from '../../../versionNotification.txt';
|
||||
import {
|
||||
getIsSavedDialog, getMessageSummaryText, getSenderTitle, isChatChannel,
|
||||
getIsSavedDialog, getMessageSummaryText, getSenderTitle, isChatChannel, isJoinedChannelMessage,
|
||||
} from '../../helpers';
|
||||
import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
@ -330,6 +330,13 @@ addActionHandler('focusLastMessage', (global, actions, payload): ActionReturnTyp
|
||||
lastMessageId = pinnedMessageIds[pinnedMessageIds.length - 1];
|
||||
} else {
|
||||
lastMessageId = selectChatLastMessageId(global, chatId);
|
||||
|
||||
const chatMessages = selectChatMessages(global, chatId);
|
||||
// Workaround for scroll to local message 'you joined this channel'
|
||||
const lastChatMessage = Object.values(chatMessages).reverse()[0];
|
||||
if (lastMessageId && isJoinedChannelMessage(lastChatMessage) && lastChatMessage.id > lastMessageId) {
|
||||
lastMessageId = lastChatMessage.id;
|
||||
}
|
||||
}
|
||||
} else if (isSavedDialog) {
|
||||
lastMessageId = selectChatLastMessageId(global, String(threadId), 'saved');
|
||||
|
||||
@ -151,7 +151,7 @@ addActionHandler('resetNextProfileTab', (global, actions, payload): ActionReturn
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateTabState(global, { nextProfileTab: undefined }, tabId);
|
||||
return updateTabState(global, { nextProfileTab: undefined, forceScrollProfileTab: false }, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('toggleStatistics', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
@ -354,3 +354,7 @@ export function isExpiredMessage(message: ApiMessage) {
|
||||
export function hasMessageTtl(message: ApiMessage) {
|
||||
return message.content?.ttlSeconds !== undefined;
|
||||
}
|
||||
|
||||
export function isJoinedChannelMessage(message: ApiMessage) {
|
||||
return message.content.action && message.content.action.type === 'joinedChannel';
|
||||
}
|
||||
|
||||
@ -446,7 +446,9 @@ export function deleteTopic<T extends GlobalState>(
|
||||
export function addSimilarChannels<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
similarChannels: string[],
|
||||
similarChannelIds: string[],
|
||||
count?: number,
|
||||
shouldShowInChat = true,
|
||||
) {
|
||||
return {
|
||||
...global,
|
||||
@ -454,7 +456,33 @@ export function addSimilarChannels<T extends GlobalState>(
|
||||
...global.chats,
|
||||
similarChannelsById: {
|
||||
...global.chats.similarChannelsById,
|
||||
[chatId]: similarChannels,
|
||||
[chatId]: {
|
||||
similarChannelIds,
|
||||
count: count || similarChannelIds.length,
|
||||
shouldShowInChat,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleSimilarChannels<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
) {
|
||||
const similarChannels = global.chats.similarChannelsById[chatId];
|
||||
const shouldShowInChat = !global.chats.similarChannelsById[chatId].shouldShowInChat;
|
||||
|
||||
return {
|
||||
...global,
|
||||
chats: {
|
||||
...global.chats,
|
||||
similarChannelsById: {
|
||||
...global.chats.similarChannelsById,
|
||||
[chatId]: {
|
||||
...similarChannels,
|
||||
shouldShowInChat,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -325,7 +325,7 @@ export function selectRequestedChatTranslationLanguage<T extends GlobalState>(
|
||||
export function selectSimilarChannelIds<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
): string[] | undefined {
|
||||
) {
|
||||
return global.chats.similarChannelsById[chatId];
|
||||
}
|
||||
|
||||
|
||||
@ -234,6 +234,7 @@ export type TabState = {
|
||||
};
|
||||
|
||||
nextProfileTab?: ProfileTabType;
|
||||
forceScrollProfileTab?: boolean;
|
||||
nextSettingsScreen?: SettingsScreens;
|
||||
nextFoldersAction?: ReducerAction<FoldersActions>;
|
||||
shareFolderScreen?: {
|
||||
@ -807,7 +808,14 @@ export type GlobalState = {
|
||||
forDiscussionIds?: string[];
|
||||
// Obtained from GetFullChat / GetFullChannel
|
||||
fullInfoById: Record<string, ApiChatFullInfo>;
|
||||
similarChannelsById: Record<string, string[]>;
|
||||
similarChannelsById: Record<
|
||||
string,
|
||||
{
|
||||
shouldShowInChat: boolean;
|
||||
similarChannelIds: string[];
|
||||
count: number;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
messages: {
|
||||
@ -1223,7 +1231,10 @@ export interface ActionPayloads {
|
||||
onReplace?: VoidFunction;
|
||||
shouldReplace?: boolean;
|
||||
};
|
||||
openChatWithInfo: ActionPayloads['openChat'] & { profileTab?: ProfileTabType } & WithTabId;
|
||||
openChatWithInfo: ActionPayloads['openChat'] & {
|
||||
profileTab?: ProfileTabType;
|
||||
forceScrollProfileTab?: boolean;
|
||||
} & WithTabId;
|
||||
openThreadWithInfo: ActionPayloads['openThread'] & WithTabId;
|
||||
openLinkedChat: { id: string } & WithTabId;
|
||||
loadMoreMembers: WithTabId | undefined;
|
||||
@ -1809,6 +1820,9 @@ export interface ActionPayloads {
|
||||
fetchChannelRecommendations: {
|
||||
chatId: string;
|
||||
};
|
||||
toggleChannelRecommendations: {
|
||||
chatId: string;
|
||||
};
|
||||
updateChatMutedState: {
|
||||
chatId: string;
|
||||
isMuted?: boolean;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user