Management: Introduce Exported Invites (#1645)
This commit is contained in:
parent
8f1b32cdb4
commit
bfc958484c
@ -9,3 +9,4 @@ src/lib/gramjs/tl/apiTl.js
|
|||||||
src/lib/gramjs/tl/schemaTl.js
|
src/lib/gramjs/tl/schemaTl.js
|
||||||
src/lib/gramjs/tl/apiTl.full.js
|
src/lib/gramjs/tl/apiTl.full.js
|
||||||
src/lib/gramjs/tl/schemaTl.full.js
|
src/lib/gramjs/tl/schemaTl.full.js
|
||||||
|
src/lib/gramjs/tl/generateModules.js
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
ApiChatFolder,
|
ApiChatFolder,
|
||||||
ApiChatMember,
|
ApiChatMember,
|
||||||
ApiRestrictionReason,
|
ApiRestrictionReason,
|
||||||
|
ApiExportedInvite,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { pick, pickTruthy } from '../../../util/iteratees';
|
import { pick, pickTruthy } from '../../../util/iteratees';
|
||||||
import {
|
import {
|
||||||
@ -386,3 +387,32 @@ export function buildApiChatBotCommands(botInfos: GramJs.BotInfo[]) {
|
|||||||
return botCommands;
|
return botCommands;
|
||||||
}, [] as ApiBotCommand[]);
|
}, [] as ApiBotCommand[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiExportedInvite(invite: GramJs.ChatInviteExported) : ApiExportedInvite {
|
||||||
|
const {
|
||||||
|
revoked,
|
||||||
|
date,
|
||||||
|
expireDate,
|
||||||
|
link,
|
||||||
|
permanent,
|
||||||
|
startDate,
|
||||||
|
usage,
|
||||||
|
usageLimit,
|
||||||
|
requested,
|
||||||
|
requestNeeded,
|
||||||
|
title,
|
||||||
|
} = invite;
|
||||||
|
return {
|
||||||
|
isRevoked: revoked,
|
||||||
|
date,
|
||||||
|
expireDate,
|
||||||
|
link,
|
||||||
|
isPermanent: permanent,
|
||||||
|
startDate,
|
||||||
|
usage,
|
||||||
|
usageLimit,
|
||||||
|
isRequestNeeded: requestNeeded,
|
||||||
|
requested,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
buildApiChatFolder,
|
buildApiChatFolder,
|
||||||
buildApiChatFolderFromSuggested,
|
buildApiChatFolderFromSuggested,
|
||||||
buildApiChatBotCommands,
|
buildApiChatBotCommands,
|
||||||
|
buildApiExportedInvite,
|
||||||
} from '../apiBuilders/chats';
|
} from '../apiBuilders/chats';
|
||||||
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
||||||
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
||||||
@ -1138,6 +1139,84 @@ function updateLocalDb(result: (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchExportedChatInvites({
|
||||||
|
peer, admin, limit = 0, isRevoked,
|
||||||
|
}: { peer: ApiChat; admin: ApiUser; limit: number; isRevoked?: boolean }) {
|
||||||
|
const exportedInvites = await invokeRequest(new GramJs.messages.GetExportedChatInvites({
|
||||||
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
|
adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser,
|
||||||
|
limit,
|
||||||
|
revoked: isRevoked || undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!exportedInvites) return undefined;
|
||||||
|
|
||||||
|
return exportedInvites.invites.map(buildApiExportedInvite);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editExportedChatInvite({
|
||||||
|
peer, isRevoked, link, expireDate, usageLimit, isRequestNeeded, title,
|
||||||
|
}: {
|
||||||
|
peer: ApiChat;
|
||||||
|
isRevoked?: boolean;
|
||||||
|
link: string;
|
||||||
|
expireDate?: number;
|
||||||
|
usageLimit?: number;
|
||||||
|
isRequestNeeded?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}) {
|
||||||
|
const invite = await invokeRequest(new GramJs.messages.EditExportedChatInvite({
|
||||||
|
link,
|
||||||
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
|
expireDate,
|
||||||
|
usageLimit: !isRequestNeeded ? usageLimit : undefined,
|
||||||
|
requestNeeded: isRequestNeeded,
|
||||||
|
title,
|
||||||
|
revoked: isRevoked || undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!invite) return undefined;
|
||||||
|
|
||||||
|
if (invite instanceof GramJs.messages.ExportedChatInvite) {
|
||||||
|
const replaceInvite = buildApiExportedInvite(invite.invite);
|
||||||
|
return {
|
||||||
|
oldInvite: replaceInvite,
|
||||||
|
newInvite: replaceInvite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invite instanceof GramJs.messages.ExportedChatInviteReplaced) {
|
||||||
|
const oldInvite = buildApiExportedInvite(invite.invite);
|
||||||
|
const newInvite = buildApiExportedInvite(invite.newInvite);
|
||||||
|
return {
|
||||||
|
oldInvite,
|
||||||
|
newInvite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportChatInvite({
|
||||||
|
peer, expireDate, usageLimit, isRequestNeeded, title,
|
||||||
|
}: {
|
||||||
|
peer: ApiChat;
|
||||||
|
expireDate?: number;
|
||||||
|
usageLimit?: number;
|
||||||
|
isRequestNeeded?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}) {
|
||||||
|
const invite = await invokeRequest(new GramJs.messages.ExportChatInvite({
|
||||||
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
|
expireDate,
|
||||||
|
usageLimit: !isRequestNeeded ? usageLimit : undefined,
|
||||||
|
requestNeeded: isRequestNeeded || undefined,
|
||||||
|
title,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!invite) return undefined;
|
||||||
|
return buildApiExportedInvite(invite);
|
||||||
|
}
|
||||||
|
|
||||||
export async function importChatInvite({ hash }: { hash: string }) {
|
export async function importChatInvite({ hash }: { hash: string }) {
|
||||||
const updates = await invokeRequest(new GramJs.messages.ImportChatInvite({ hash }));
|
const updates = await invokeRequest(new GramJs.messages.ImportChatInvite({ hash }));
|
||||||
if (!(updates instanceof GramJs.Updates) || !updates.chats.length) {
|
if (!(updates instanceof GramJs.Updates) || !updates.chats.length) {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export {
|
|||||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||||
|
fetchExportedChatInvites, editExportedChatInvite, exportChatInvite,
|
||||||
} from './chats';
|
} from './chats';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -97,6 +97,20 @@ export type ApiInviteInfo = {
|
|||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiExportedInvite = {
|
||||||
|
isRevoked?: boolean;
|
||||||
|
isPermanent?: boolean;
|
||||||
|
link: string;
|
||||||
|
date: number;
|
||||||
|
startDate?: number;
|
||||||
|
expireDate?: number;
|
||||||
|
usageLimit?: number;
|
||||||
|
usage?: number;
|
||||||
|
isRequestNeeded?: boolean;
|
||||||
|
requested?: number;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ApiCountry {
|
export interface ApiCountry {
|
||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
iso2: string;
|
iso2: string;
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import Button from '../ui/Button';
|
|||||||
|
|
||||||
import './CalendarModal.scss';
|
import './CalendarModal.scss';
|
||||||
|
|
||||||
|
const MAX_SAFE_DATE = 2147483647 * 1000; // API has int for dates
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
selectedAt?: number;
|
selectedAt?: number;
|
||||||
maxAt?: number;
|
maxAt?: number;
|
||||||
@ -53,7 +55,7 @@ const CalendarModal: FC<OwnProps> = ({
|
|||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const defaultSelectedDate = useMemo(() => (selectedAt ? new Date(selectedAt) : new Date()), [selectedAt]);
|
const defaultSelectedDate = useMemo(() => (selectedAt ? new Date(selectedAt) : new Date()), [selectedAt]);
|
||||||
const maxDate = maxAt ? new Date(maxAt) : undefined;
|
const maxDate = new Date(Math.min(maxAt || MAX_SAFE_DATE, MAX_SAFE_DATE));
|
||||||
const prevIsOpen = usePrevious(isOpen);
|
const prevIsOpen = usePrevious(isOpen);
|
||||||
const [isTimeInputFocused, markTimeInputAsFocused, unmarkTimeInputAsFocused] = useFlag(false);
|
const [isTimeInputFocused, markTimeInputAsFocused, unmarkTimeInputAsFocused] = useFlag(false);
|
||||||
|
|
||||||
@ -76,8 +78,12 @@ const CalendarModal: FC<OwnProps> = ({
|
|||||||
if (!prevIsOpen && isOpen) {
|
if (!prevIsOpen && isOpen) {
|
||||||
setSelectedDate(defaultSelectedDate);
|
setSelectedDate(defaultSelectedDate);
|
||||||
setCurrentMonthAndYear(new Date(defaultSelectedDate.getFullYear(), defaultSelectedDate.getMonth(), 1));
|
setCurrentMonthAndYear(new Date(defaultSelectedDate.getFullYear(), defaultSelectedDate.getMonth(), 1));
|
||||||
|
if (withTimePicker) {
|
||||||
|
setSelectedHours(defaultSelectedDate.getHours().toString());
|
||||||
|
setSelectedMinutes(defaultSelectedDate.getMinutes().toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [defaultSelectedDate, isOpen, prevIsOpen]);
|
}, [defaultSelectedDate, isOpen, prevIsOpen, withTimePicker]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFutureMode && !isTimeInputFocused && selectedDate.getTime() < defaultSelectedDate.getTime()) {
|
if (isFutureMode && !isTimeInputFocused && selectedDate.getTime() < defaultSelectedDate.getTime()) {
|
||||||
@ -277,7 +283,7 @@ const CalendarModal: FC<OwnProps> = ({
|
|||||||
|
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
<Button onClick={handleSubmit}>
|
<Button onClick={handleSubmit}>
|
||||||
{withTimePicker ? formatSubmitLabel(lang, selectedDate) : submitButtonLabel}
|
{submitButtonLabel || formatSubmitLabel(lang, selectedDate)}
|
||||||
</Button>
|
</Button>
|
||||||
{secondButtonLabel && (
|
{secondButtonLabel && (
|
||||||
<Button onClick={onSecondButtonClick} isText>
|
<Button onClick={onSecondButtonClick} isText>
|
||||||
@ -348,10 +354,10 @@ function formatSubmitLabel(lang: LangFn, date: Date) {
|
|||||||
const today = formatDateToString(new Date(), lang.code);
|
const today = formatDateToString(new Date(), lang.code);
|
||||||
|
|
||||||
if (day === today) {
|
if (day === today) {
|
||||||
return lang('Conversation.ScheduleMessage.SendToday', formatTime(date, lang));
|
return lang('Conversation.ScheduleMessage.SendToday', formatTime(lang, date));
|
||||||
}
|
}
|
||||||
|
|
||||||
return lang('Conversation.ScheduleMessage.SendOn', [day, formatTime(date, lang)]);
|
return lang('Conversation.ScheduleMessage.SendOn', [day, formatTime(lang, date)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(CalendarModal);
|
export default memo(CalendarModal);
|
||||||
|
|||||||
@ -117,7 +117,13 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{(canInviteUsers || !username) && link && (
|
{(canInviteUsers || !username) && link && (
|
||||||
<ListItem icon="mention" multiline narrow ripple onClick={() => copy(link, lang('SetUrlPlaceholder'))}>
|
<ListItem
|
||||||
|
icon={chat.username ? 'mention' : 'link'}
|
||||||
|
multiline
|
||||||
|
narrow
|
||||||
|
ripple
|
||||||
|
onClick={() => copy(link, lang('SetUrlPlaceholder'))}
|
||||||
|
>
|
||||||
<div className="title">{link}</div>
|
<div className="title">{link}</div>
|
||||||
<span className="subtitle">{lang('SetUrlPlaceholder')}</span>
|
<span className="subtitle">{lang('SetUrlPlaceholder')}</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@ -84,7 +84,7 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
<span className="message-time" title={title} onMouseEnter={markActivated}>
|
<span className="message-time" title={title} onMouseEnter={markActivated}>
|
||||||
{message.isEdited && `${lang('EditedMessage')} `}
|
{message.isEdited && `${lang('EditedMessage')} `}
|
||||||
{formatTime(message.date * 1000, lang)}
|
{formatTime(lang, message.date * 1000)}
|
||||||
</span>
|
</span>
|
||||||
{outgoingStatus && (
|
{outgoingStatus && (
|
||||||
<MessageOutgoingStatus status={outgoingStatus} />
|
<MessageOutgoingStatus status={outgoingStatus} />
|
||||||
|
|||||||
@ -69,6 +69,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
closePollResults,
|
closePollResults,
|
||||||
addChatMembers,
|
addChatMembers,
|
||||||
setNewChatMembersDialogState,
|
setNewChatMembersDialogState,
|
||||||
|
setEditingExportedInvite,
|
||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
|
|
||||||
const { width: windowWidth } = useWindowSize();
|
const { width: windowWidth } = useWindowSize();
|
||||||
@ -123,6 +124,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
case ManagementScreens.ChatAdministrators:
|
case ManagementScreens.ChatAdministrators:
|
||||||
case ManagementScreens.ChannelSubscribers:
|
case ManagementScreens.ChannelSubscribers:
|
||||||
case ManagementScreens.GroupMembers:
|
case ManagementScreens.GroupMembers:
|
||||||
|
case ManagementScreens.Invites:
|
||||||
case ManagementScreens.Reactions:
|
case ManagementScreens.Reactions:
|
||||||
setManagementScreen(ManagementScreens.Initial);
|
setManagementScreen(ManagementScreens.Initial);
|
||||||
break;
|
break;
|
||||||
@ -139,6 +141,10 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
case ManagementScreens.GroupRecentActions:
|
case ManagementScreens.GroupRecentActions:
|
||||||
setManagementScreen(ManagementScreens.ChatAdministrators);
|
setManagementScreen(ManagementScreens.ChatAdministrators);
|
||||||
break;
|
break;
|
||||||
|
case ManagementScreens.EditInvite:
|
||||||
|
setManagementScreen(ManagementScreens.Invites);
|
||||||
|
setEditingExportedInvite({ chatId, invite: undefined });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -164,6 +170,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
}, [
|
}, [
|
||||||
contentKey, isScrolledDown, toggleChatInfo, openUserInfo, closePollResults, setNewChatMembersDialogState,
|
contentKey, isScrolledDown, toggleChatInfo, openUserInfo, closePollResults, setNewChatMembersDialogState,
|
||||||
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
||||||
|
setEditingExportedInvite, chatId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleSelectChatMember = useCallback((memberId, isPromoted) => {
|
const handleSelectChatMember = useCallback((memberId, isPromoted) => {
|
||||||
|
|||||||
@ -52,6 +52,7 @@ type StateProps = {
|
|||||||
messageSearchQuery?: string;
|
messageSearchQuery?: string;
|
||||||
stickerSearchQuery?: string;
|
stickerSearchQuery?: string;
|
||||||
gifSearchQuery?: string;
|
gifSearchQuery?: string;
|
||||||
|
isEditingInvite?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_CLOSE_DELAY_MS = 300;
|
const COLUMN_CLOSE_DELAY_MS = 300;
|
||||||
@ -81,6 +82,8 @@ enum HeaderContent {
|
|||||||
GifSearch,
|
GifSearch,
|
||||||
PollResults,
|
PollResults,
|
||||||
AddingMembers,
|
AddingMembers,
|
||||||
|
ManageInvites,
|
||||||
|
ManageEditInvite,
|
||||||
ManageReactions,
|
ManageReactions,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +107,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
stickerSearchQuery,
|
stickerSearchQuery,
|
||||||
gifSearchQuery,
|
gifSearchQuery,
|
||||||
shouldSkipAnimation,
|
shouldSkipAnimation,
|
||||||
|
isEditingInvite,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setLocalTextSearchQuery,
|
setLocalTextSearchQuery,
|
||||||
@ -191,6 +195,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
HeaderContent.ManageGroupNewAdminRights
|
HeaderContent.ManageGroupNewAdminRights
|
||||||
) : managementScreen === ManagementScreens.GroupMembers ? (
|
) : managementScreen === ManagementScreens.GroupMembers ? (
|
||||||
HeaderContent.ManageGroupMembers
|
HeaderContent.ManageGroupMembers
|
||||||
|
) : managementScreen === ManagementScreens.Invites ? (
|
||||||
|
HeaderContent.ManageInvites
|
||||||
|
) : managementScreen === ManagementScreens.EditInvite ? (
|
||||||
|
HeaderContent.ManageEditInvite
|
||||||
) : managementScreen === ManagementScreens.GroupAddAdmins ? (
|
) : managementScreen === ManagementScreens.GroupAddAdmins ? (
|
||||||
HeaderContent.ManageGroupAddAdmins
|
HeaderContent.ManageGroupAddAdmins
|
||||||
) : managementScreen === ManagementScreens.Reactions ? (
|
) : managementScreen === ManagementScreens.Reactions ? (
|
||||||
@ -251,6 +259,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
return <h3>{lang('ChannelAddException')}</h3>;
|
return <h3>{lang('ChannelAddException')}</h3>;
|
||||||
case HeaderContent.ManageGroupUserPermissions:
|
case HeaderContent.ManageGroupUserPermissions:
|
||||||
return <h3>{lang('UserRestrictions')}</h3>;
|
return <h3>{lang('UserRestrictions')}</h3>;
|
||||||
|
case HeaderContent.ManageInvites:
|
||||||
|
return <h3>{lang('lng_group_invite_title')}</h3>;
|
||||||
|
case HeaderContent.ManageEditInvite:
|
||||||
|
return <h3>{isEditingInvite ? lang('EditLink') : lang('NewLink')}</h3>;
|
||||||
case HeaderContent.ManageGroupAddAdmins:
|
case HeaderContent.ManageGroupAddAdmins:
|
||||||
return <h3>{lang('Channel.Management.AddModerator')}</h3>;
|
return <h3>{lang('Channel.Management.AddModerator')}</h3>;
|
||||||
case HeaderContent.StickerSearch:
|
case HeaderContent.StickerSearch:
|
||||||
@ -368,6 +380,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
// chat.isCreator is for Basic Groups
|
// chat.isCreator is for Basic Groups
|
||||||
&& (isUserId(chat.id) || ((isChatAdmin(chat) || chat.isCreator) && !chat.isNotJoined)),
|
&& (isUserId(chat.id) || ((isChatAdmin(chat) || chat.isCreator) && !chat.isNotJoined)),
|
||||||
);
|
);
|
||||||
|
const isEditingInvite = Boolean(chatId && global.management.byChatId[chatId]?.editingInvite);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canManage,
|
canManage,
|
||||||
@ -377,6 +390,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
messageSearchQuery,
|
messageSearchQuery,
|
||||||
stickerSearchQuery,
|
stickerSearchQuery,
|
||||||
gifSearchQuery,
|
gifSearchQuery,
|
||||||
|
isEditingInvite,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(RightHeader));
|
)(RightHeader));
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import React, {
|
|||||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ManagementScreens, ManagementProgress } from '../../../types';
|
import { ManagementScreens, ManagementProgress } from '../../../types';
|
||||||
import { ApiChat, ApiMediaFormat } from '../../../api/types';
|
import { ApiChat, ApiExportedInvite, ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
import { getChatAvatarHash, getHasAdminRight } from '../../../modules/helpers';
|
import { getChatAvatarHash, getHasAdminRight } from '../../../modules/helpers';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import { selectChat } from '../../../modules/selectors';
|
import { selectChat } from '../../../modules/selectors';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import { formatInteger } from '../../../util/textFormat';
|
||||||
|
|
||||||
import AvatarEditable from '../../ui/AvatarEditable';
|
import AvatarEditable from '../../ui/AvatarEditable';
|
||||||
import InputText from '../../ui/InputText';
|
import InputText from '../../ui/InputText';
|
||||||
@ -19,8 +22,6 @@ import Checkbox from '../../ui/Checkbox';
|
|||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
|
||||||
|
|
||||||
import './Management.scss';
|
import './Management.scss';
|
||||||
|
|
||||||
@ -36,6 +37,9 @@ type StateProps = {
|
|||||||
progress?: ManagementProgress;
|
progress?: ManagementProgress;
|
||||||
isSignaturesShown: boolean;
|
isSignaturesShown: boolean;
|
||||||
canChangeInfo?: boolean;
|
canChangeInfo?: boolean;
|
||||||
|
canInvite?: boolean;
|
||||||
|
exportedInvites?: ApiExportedInvite[];
|
||||||
|
lastSyncTime?: number;
|
||||||
availableReactionsCount?: number;
|
availableReactionsCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -47,6 +51,9 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
progress,
|
progress,
|
||||||
isSignaturesShown,
|
isSignaturesShown,
|
||||||
canChangeInfo,
|
canChangeInfo,
|
||||||
|
canInvite,
|
||||||
|
exportedInvites,
|
||||||
|
lastSyncTime,
|
||||||
availableReactionsCount,
|
availableReactionsCount,
|
||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onClose,
|
onClose,
|
||||||
@ -59,6 +66,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
leaveChannel,
|
leaveChannel,
|
||||||
deleteChannel,
|
deleteChannel,
|
||||||
openChat,
|
openChat,
|
||||||
|
loadExportedChatInvites,
|
||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
|
|
||||||
const currentTitle = chat ? (chat.title || '') : '';
|
const currentTitle = chat ? (chat.title || '') : '';
|
||||||
@ -77,6 +85,12 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
useHistoryBack(isActive, onClose);
|
useHistoryBack(isActive, onClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastSyncTime) {
|
||||||
|
loadExportedChatInvites({ chatId });
|
||||||
|
}
|
||||||
|
}, [chatId, loadExportedChatInvites, lastSyncTime]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (progress === ManagementProgress.Complete) {
|
if (progress === ManagementProgress.Complete) {
|
||||||
setIsProfileFieldsTouched(false);
|
setIsProfileFieldsTouched(false);
|
||||||
@ -102,6 +116,10 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
onScreenSelect(ManagementScreens.ChatAdministrators);
|
onScreenSelect(ManagementScreens.ChatAdministrators);
|
||||||
}, [onScreenSelect]);
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
|
const handleClickInvites = useCallback(() => {
|
||||||
|
onScreenSelect(ManagementScreens.Invites);
|
||||||
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
const handleSetPhoto = useCallback((file: File) => {
|
const handleSetPhoto = useCallback((file: File) => {
|
||||||
setPhoto(file);
|
setPhoto(file);
|
||||||
setIsProfileFieldsTouched(true);
|
setIsProfileFieldsTouched(true);
|
||||||
@ -210,6 +228,19 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
<span className="title">{lang('ChannelAdministrators')}</span>
|
<span className="title">{lang('ChannelAdministrators')}</span>
|
||||||
<span className="subtitle">{adminsCount}</span>
|
<span className="subtitle">{adminsCount}</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
{canInvite && (
|
||||||
|
<ListItem
|
||||||
|
icon="link"
|
||||||
|
onClick={handleClickInvites}
|
||||||
|
multiline
|
||||||
|
disabled={!exportedInvites}
|
||||||
|
>
|
||||||
|
<span className="title">{lang('GroupInfo.InviteLinks')}</span>
|
||||||
|
<span className="subtitle">
|
||||||
|
{exportedInvites ? formatInteger(exportedInvites.length) : lang('Loading')}
|
||||||
|
</span>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="reactions"
|
icon="reactions"
|
||||||
multiline
|
multiline
|
||||||
@ -274,12 +305,16 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const chat = selectChat(global, chatId)!;
|
const chat = selectChat(global, chatId)!;
|
||||||
const { progress } = global.management;
|
const { progress } = global.management;
|
||||||
const isSignaturesShown = Boolean(chat?.isSignaturesShown);
|
const isSignaturesShown = Boolean(chat?.isSignaturesShown);
|
||||||
|
const { invites } = global.management.byChatId[chatId] || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat,
|
chat,
|
||||||
progress,
|
progress,
|
||||||
isSignaturesShown,
|
isSignaturesShown,
|
||||||
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
|
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
|
||||||
|
canInvite: getHasAdminRight(chat, 'inviteUsers'),
|
||||||
|
lastSyncTime: global.lastSyncTime,
|
||||||
|
exportedInvites: invites,
|
||||||
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import React, {
|
|||||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ManagementScreens, ManagementProgress } from '../../../types';
|
import { ManagementScreens, ManagementProgress } from '../../../types';
|
||||||
import { ApiChat, ApiChatBannedRights, ApiMediaFormat } from '../../../api/types';
|
import {
|
||||||
|
ApiChat, ApiChatBannedRights, ApiExportedInvite, ApiMediaFormat,
|
||||||
|
} from '../../../api/types';
|
||||||
|
|
||||||
import { getChatAvatarHash, getHasAdminRight, isChatBasicGroup } from '../../../modules/helpers';
|
import { getChatAvatarHash, getHasAdminRight, isChatBasicGroup } from '../../../modules/helpers';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
@ -40,6 +42,9 @@ type StateProps = {
|
|||||||
hasLinkedChannel: boolean;
|
hasLinkedChannel: boolean;
|
||||||
canChangeInfo?: boolean;
|
canChangeInfo?: boolean;
|
||||||
canBanUsers?: boolean;
|
canBanUsers?: boolean;
|
||||||
|
canInvite?: boolean;
|
||||||
|
exportedInvites?: ApiExportedInvite[];
|
||||||
|
lastSyncTime?: number;
|
||||||
availableReactionsCount?: number;
|
availableReactionsCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,9 +62,12 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
hasLinkedChannel,
|
hasLinkedChannel,
|
||||||
canChangeInfo,
|
canChangeInfo,
|
||||||
canBanUsers,
|
canBanUsers,
|
||||||
|
canInvite,
|
||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onClose,
|
onClose,
|
||||||
isActive,
|
isActive,
|
||||||
|
exportedInvites,
|
||||||
|
lastSyncTime,
|
||||||
availableReactionsCount,
|
availableReactionsCount,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -70,6 +78,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
deleteChannel,
|
deleteChannel,
|
||||||
closeManagement,
|
closeManagement,
|
||||||
openChat,
|
openChat,
|
||||||
|
loadExportedChatInvites,
|
||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
|
|
||||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||||
@ -87,6 +96,12 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
useHistoryBack(isActive, onClose);
|
useHistoryBack(isActive, onClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastSyncTime && canInvite) {
|
||||||
|
loadExportedChatInvites({ chatId });
|
||||||
|
}
|
||||||
|
}, [chatId, loadExportedChatInvites, lastSyncTime, canInvite]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (progress === ManagementProgress.Complete) {
|
if (progress === ManagementProgress.Complete) {
|
||||||
setIsProfileFieldsTouched(false);
|
setIsProfileFieldsTouched(false);
|
||||||
@ -114,6 +129,10 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
onScreenSelect(ManagementScreens.ChatAdministrators);
|
onScreenSelect(ManagementScreens.ChatAdministrators);
|
||||||
}, [onScreenSelect]);
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
|
const handleClickInvites = useCallback(() => {
|
||||||
|
onScreenSelect(ManagementScreens.Invites);
|
||||||
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
const handleSetPhoto = useCallback((file: File) => {
|
const handleSetPhoto = useCallback((file: File) => {
|
||||||
setPhoto(file);
|
setPhoto(file);
|
||||||
setIsProfileFieldsTouched(true);
|
setIsProfileFieldsTouched(true);
|
||||||
@ -285,6 +304,19 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
<span className="title">{lang('ChannelAdministrators')}</span>
|
<span className="title">{lang('ChannelAdministrators')}</span>
|
||||||
<span className="subtitle">{formatInteger(adminsCount)}</span>
|
<span className="subtitle">{formatInteger(adminsCount)}</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
{canInvite && (
|
||||||
|
<ListItem
|
||||||
|
icon="link"
|
||||||
|
onClick={handleClickInvites}
|
||||||
|
multiline
|
||||||
|
disabled={!exportedInvites}
|
||||||
|
>
|
||||||
|
<span className="title">{lang('GroupInfo.InviteLinks')}</span>
|
||||||
|
<span className="subtitle">
|
||||||
|
{exportedInvites ? formatInteger(exportedInvites.length) : lang('Loading')}
|
||||||
|
</span>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<ListItem icon="group" multiline onClick={handleClickMembers}>
|
<ListItem icon="group" multiline onClick={handleClickMembers}>
|
||||||
@ -344,6 +376,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const { progress } = global.management;
|
const { progress } = global.management;
|
||||||
const hasLinkedChannel = Boolean(chat.fullInfo?.linkedChatId);
|
const hasLinkedChannel = Boolean(chat.fullInfo?.linkedChatId);
|
||||||
const isBasicGroup = isChatBasicGroup(chat);
|
const isBasicGroup = isChatBasicGroup(chat);
|
||||||
|
const { invites } = global.management.byChatId[chatId] || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat,
|
chat,
|
||||||
@ -352,6 +385,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
hasLinkedChannel,
|
hasLinkedChannel,
|
||||||
canChangeInfo: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'changeInfo'),
|
canChangeInfo: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'changeInfo'),
|
||||||
canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'),
|
canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'),
|
||||||
|
canInvite: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'inviteUsers'),
|
||||||
|
exportedInvites: invites,
|
||||||
|
lastSyncTime: global.lastSyncTime,
|
||||||
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
268
src/components/right/management/ManageInvite.tsx
Normal file
268
src/components/right/management/ManageInvite.tsx
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useEffect, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ApiExportedInvite } from '../../../api/types';
|
||||||
|
import { ManagementScreens } from '../../../types';
|
||||||
|
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import { formatFullDate, formatTime } from '../../../util/dateFormat';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
import InputText from '../../ui/InputText';
|
||||||
|
import RadioGroup from '../../ui/RadioGroup';
|
||||||
|
import CalendarModalAsync from '../../common/CalendarModal.async';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
|
|
||||||
|
const DEFAULT_USAGE_LIMITS = [1, 10, 100];
|
||||||
|
const DEFAULT_EXPIRE_DATE = {
|
||||||
|
hour: 3600000,
|
||||||
|
day: 86400000,
|
||||||
|
week: 604800000,
|
||||||
|
};
|
||||||
|
const DEFAULT_CUSTOM_EXPIRE_DATE = DEFAULT_EXPIRE_DATE.hour;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
onScreenSelect: (screen: ManagementScreens) => void;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
editingInvite?: ApiExportedInvite;
|
||||||
|
serverTimeOffset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ManageInvite: FC<OwnProps & StateProps> = ({
|
||||||
|
chatId,
|
||||||
|
editingInvite,
|
||||||
|
isActive,
|
||||||
|
serverTimeOffset,
|
||||||
|
onClose,
|
||||||
|
onScreenSelect,
|
||||||
|
}) => {
|
||||||
|
const { editExportedChatInvite, exportChatInvite } = getDispatch();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
|
||||||
|
const [isRequestNeeded, setIsRequestNeeded] = useState(false);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [customExpireDate, setCustomExpireDate] = useState<number>(Date.now() + DEFAULT_CUSTOM_EXPIRE_DATE);
|
||||||
|
const [selectedExpireOption, setSelectedExpireOption] = useState('unlimited');
|
||||||
|
const [customUsageLimit, setCustomUsageLimit] = useState<number | undefined>(10);
|
||||||
|
const [selectedUsageOption, setSelectedUsageOption] = useState('0');
|
||||||
|
|
||||||
|
useHistoryBack(isActive, onClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editingInvite) {
|
||||||
|
setTitle('');
|
||||||
|
setSelectedExpireOption('unlimited');
|
||||||
|
setSelectedUsageOption('0');
|
||||||
|
setCustomExpireDate(getServerTime(serverTimeOffset) * 1000 + DEFAULT_CUSTOM_EXPIRE_DATE);
|
||||||
|
setCustomUsageLimit(10);
|
||||||
|
setIsRequestNeeded(false);
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
title: editingTitle, usageLimit, expireDate, isRequestNeeded: editingIsRequestNeeded,
|
||||||
|
} = editingInvite;
|
||||||
|
if (editingTitle) setTitle(editingTitle);
|
||||||
|
if (usageLimit) {
|
||||||
|
setSelectedUsageOption(DEFAULT_USAGE_LIMITS.includes(usageLimit) ? usageLimit.toString() : 'custom');
|
||||||
|
setCustomUsageLimit(usageLimit);
|
||||||
|
}
|
||||||
|
if (expireDate) {
|
||||||
|
setSelectedExpireOption('custom');
|
||||||
|
setCustomExpireDate(expireDate * 1000);
|
||||||
|
}
|
||||||
|
if (editingIsRequestNeeded) {
|
||||||
|
setIsRequestNeeded(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editingInvite, serverTimeOffset]);
|
||||||
|
|
||||||
|
const handleIsRequestChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsRequestNeeded(e.target.checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTitleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTitle(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCustomUsageLimitChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setCustomUsageLimit(Number.parseInt(e.target.value, 10));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleExpireDateChange = useCallback((date: Date) => {
|
||||||
|
setCustomExpireDate(date.getTime());
|
||||||
|
closeCalendar();
|
||||||
|
}, [closeCalendar]);
|
||||||
|
|
||||||
|
const handleSaveClick = useCallback(() => {
|
||||||
|
const usageLimit = selectedUsageOption === 'custom' ? customUsageLimit : selectedUsageOption;
|
||||||
|
let expireDate;
|
||||||
|
switch (selectedExpireOption) {
|
||||||
|
case 'custom':
|
||||||
|
expireDate = getServerTime(serverTimeOffset) + (customExpireDate - Date.now()) / 1000;
|
||||||
|
break;
|
||||||
|
case 'hour':
|
||||||
|
case 'day':
|
||||||
|
case 'week':
|
||||||
|
expireDate = getServerTime(serverTimeOffset) + DEFAULT_EXPIRE_DATE[selectedExpireOption] / 1000;
|
||||||
|
break;
|
||||||
|
case 'unlimited':
|
||||||
|
default:
|
||||||
|
expireDate = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingInvite) {
|
||||||
|
editExportedChatInvite({
|
||||||
|
link: editingInvite.link,
|
||||||
|
chatId,
|
||||||
|
title,
|
||||||
|
isRequestNeeded,
|
||||||
|
expireDate,
|
||||||
|
usageLimit,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
exportChatInvite({
|
||||||
|
chatId,
|
||||||
|
title,
|
||||||
|
isRequestNeeded,
|
||||||
|
expireDate,
|
||||||
|
usageLimit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onScreenSelect(ManagementScreens.Invites);
|
||||||
|
}, [
|
||||||
|
chatId, customExpireDate, customUsageLimit, editExportedChatInvite, editingInvite,
|
||||||
|
exportChatInvite, isRequestNeeded, selectedExpireOption, selectedUsageOption, title, onScreenSelect,
|
||||||
|
serverTimeOffset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Management ManageInvite">
|
||||||
|
<div className="custom-scroll">
|
||||||
|
<div className="section">
|
||||||
|
<Checkbox
|
||||||
|
label={lang('ApproveNewMembers')}
|
||||||
|
subLabel={lang('ApproveNewMembersDescription')}
|
||||||
|
checked={isRequestNeeded}
|
||||||
|
onChange={handleIsRequestChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="section">
|
||||||
|
<InputText
|
||||||
|
className="link-name"
|
||||||
|
placeholder={lang('LinkNameHint')}
|
||||||
|
value={title}
|
||||||
|
onChange={handleTitleChange}
|
||||||
|
/>
|
||||||
|
<p className="text-muted hint">{lang('LinkNameHelp')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="section">
|
||||||
|
<div className="section-header">{lang('LimitByPeriod')}</div>
|
||||||
|
<RadioGroup
|
||||||
|
name="expireOptions"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'hour',
|
||||||
|
label: lang('Hours', 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'day',
|
||||||
|
label: lang('Days', 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'week',
|
||||||
|
label: lang('Weeks', 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'unlimited',
|
||||||
|
label: lang('NoLimit'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'custom',
|
||||||
|
label: lang('lng_group_invite_expire_custom'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={setSelectedExpireOption}
|
||||||
|
selected={selectedExpireOption}
|
||||||
|
/>
|
||||||
|
{selectedExpireOption === 'custom' && (
|
||||||
|
<Button className="expire-limit" isText onClick={openCalendar}>
|
||||||
|
{formatFullDate(lang, customExpireDate)} {formatTime(lang, customExpireDate)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<p className="text-muted hint">{lang('TimeLimitHelp')}</p>
|
||||||
|
</div>
|
||||||
|
{!isRequestNeeded && (
|
||||||
|
<div className="section">
|
||||||
|
<div className="section-header">{lang('LimitNumberOfUses')}</div>
|
||||||
|
<RadioGroup
|
||||||
|
name="usageOptions"
|
||||||
|
options={[
|
||||||
|
...DEFAULT_USAGE_LIMITS.map((n) => ({ value: n.toString(), label: n })),
|
||||||
|
{
|
||||||
|
value: '0',
|
||||||
|
label: lang('NoLimit'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'custom',
|
||||||
|
label: lang('lng_group_invite_usage_custom'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={setSelectedUsageOption}
|
||||||
|
selected={selectedUsageOption}
|
||||||
|
/>
|
||||||
|
{selectedUsageOption === 'custom' && (
|
||||||
|
<input
|
||||||
|
className="form-control usage-limit"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="99999"
|
||||||
|
value={customUsageLimit}
|
||||||
|
onChange={handleCustomUsageLimitChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="text-muted hint">{lang('UsesLimitHelp')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FloatingActionButton
|
||||||
|
isShown
|
||||||
|
onClick={handleSaveClick}
|
||||||
|
ariaLabel={editingInvite ? lang('SaveLink') : lang('CreateLink')}
|
||||||
|
>
|
||||||
|
<i className="icon-check" />
|
||||||
|
</FloatingActionButton>
|
||||||
|
</div>
|
||||||
|
<CalendarModalAsync
|
||||||
|
isOpen={isCalendarOpened}
|
||||||
|
isFutureMode
|
||||||
|
withTimePicker
|
||||||
|
onClose={closeCalendar}
|
||||||
|
onSubmit={handleExpireDateChange}
|
||||||
|
selectedAt={customExpireDate}
|
||||||
|
submitButtonLabel={lang('Save')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const { editingInvite } = global.management.byChatId[chatId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
editingInvite,
|
||||||
|
serverTimeOffset: global.serverTimeOffset,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(ManageInvite));
|
||||||
262
src/components/right/management/ManageInvites.tsx
Normal file
262
src/components/right/management/ManageInvites.tsx
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useMemo,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ApiChat, ApiExportedInvite } from '../../../api/types';
|
||||||
|
import { ManagementScreens } from '../../../types';
|
||||||
|
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import { formatCountdown, MILLISECONDS_IN_DAY } from '../../../util/dateFormat';
|
||||||
|
import useInterval from '../../../hooks/useInterval';
|
||||||
|
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||||
|
import { selectChat } from '../../../modules/selectors';
|
||||||
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
import NothingFound from '../../common/NothingFound';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import DropdownMenu from '../../ui/DropdownMenu';
|
||||||
|
import MenuItem from '../../ui/MenuItem';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
onScreenSelect: (screen: ManagementScreens) => void;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
exportedInvites?: ApiExportedInvite[];
|
||||||
|
serverTimeOffset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BULLET = '\u2022';
|
||||||
|
|
||||||
|
function inviteComparator(i1: ApiExportedInvite, i2: ApiExportedInvite) {
|
||||||
|
const { isPermanent: i1IsPermanent, usage: i1Usage = 0, date: i1Date } = i1;
|
||||||
|
const { isPermanent: i2IsPermanent, usage: i2Usage = 0, date: i2Date } = i2;
|
||||||
|
if (i1IsPermanent || i2IsPermanent) return Number(i1IsPermanent) - Number(i2IsPermanent);
|
||||||
|
if (i1Usage || i2Usage) return i2Usage - i1Usage;
|
||||||
|
return i2Date - i1Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||||
|
chatId,
|
||||||
|
chat,
|
||||||
|
exportedInvites,
|
||||||
|
isActive,
|
||||||
|
serverTimeOffset,
|
||||||
|
onClose,
|
||||||
|
onScreenSelect,
|
||||||
|
}) => {
|
||||||
|
const { setEditingExportedInvite, showNotification, editExportedChatInvite } = getDispatch();
|
||||||
|
useHistoryBack(isActive, onClose);
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const hasDetailedCountdown = useMemo(() => {
|
||||||
|
if (!exportedInvites) return undefined;
|
||||||
|
return exportedInvites
|
||||||
|
.some(({ expireDate }) => (
|
||||||
|
expireDate && (expireDate - getServerTime(serverTimeOffset) < MILLISECONDS_IN_DAY / 1000)
|
||||||
|
));
|
||||||
|
}, [exportedInvites, serverTimeOffset]);
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
useInterval(() => {
|
||||||
|
forceUpdate();
|
||||||
|
}, hasDetailedCountdown ? 1000 : undefined);
|
||||||
|
|
||||||
|
const primaryInvite = exportedInvites?.find(({ isPermanent }) => isPermanent);
|
||||||
|
const primaryInviteLink = chat?.username ? `t.me/${chat.username}` : primaryInvite?.link;
|
||||||
|
const temporalInvites = useMemo(() => {
|
||||||
|
const invites = chat?.username ? exportedInvites : exportedInvites?.filter(({ isPermanent }) => !isPermanent);
|
||||||
|
return invites?.filter(({ isRevoked }) => !isRevoked)
|
||||||
|
.sort(inviteComparator);
|
||||||
|
}, [chat?.username, exportedInvites]);
|
||||||
|
|
||||||
|
const editInvite = (invite: ApiExportedInvite) => {
|
||||||
|
setEditingExportedInvite({ chatId, invite });
|
||||||
|
onScreenSelect(ManagementScreens.EditInvite);
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeInvite = useCallback((invite: ApiExportedInvite) => {
|
||||||
|
const {
|
||||||
|
link, title, isRequestNeeded, expireDate, usageLimit,
|
||||||
|
} = invite;
|
||||||
|
editExportedChatInvite({
|
||||||
|
chatId,
|
||||||
|
link,
|
||||||
|
title,
|
||||||
|
isRequestNeeded,
|
||||||
|
expireDate,
|
||||||
|
usageLimit,
|
||||||
|
isRevoked: true,
|
||||||
|
});
|
||||||
|
}, [chatId, editExportedChatInvite]);
|
||||||
|
|
||||||
|
const handleCreateNewClick = useCallback(() => {
|
||||||
|
onScreenSelect(ManagementScreens.EditInvite);
|
||||||
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
|
const handlePrimaryRevoke = useCallback(() => {
|
||||||
|
if (primaryInvite) {
|
||||||
|
revokeInvite(primaryInvite);
|
||||||
|
}
|
||||||
|
}, [primaryInvite, revokeInvite]);
|
||||||
|
|
||||||
|
const copyLink = useCallback((link: string) => {
|
||||||
|
copyTextToClipboard(link);
|
||||||
|
showNotification({
|
||||||
|
message: lang('LinkCopied'),
|
||||||
|
});
|
||||||
|
}, [lang, showNotification]);
|
||||||
|
|
||||||
|
const handleCopyPrimaryClicked = useCallback(() => {
|
||||||
|
copyLink(primaryInviteLink!);
|
||||||
|
}, [copyLink, primaryInviteLink]);
|
||||||
|
|
||||||
|
const prepareUsageText = (invite: ApiExportedInvite) => {
|
||||||
|
const {
|
||||||
|
usage = 0, usageLimit, expireDate, isPermanent, requested,
|
||||||
|
} = invite;
|
||||||
|
let text = '';
|
||||||
|
if (usageLimit && usage < usageLimit) {
|
||||||
|
text = lang('CanJoin', usageLimit - usage);
|
||||||
|
} else if (usage) {
|
||||||
|
text = lang('PeopleJoined', usage);
|
||||||
|
} else {
|
||||||
|
text = lang('NoOneJoined');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requested) {
|
||||||
|
text += ` ${BULLET} ${lang('JoinRequests', requested)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usageLimit !== undefined && usage === usageLimit) {
|
||||||
|
text += ` ${BULLET} ${lang('LinkLimitReached')}`;
|
||||||
|
} else if (expireDate) {
|
||||||
|
const diff = (expireDate - getServerTime(serverTimeOffset)) * 1000;
|
||||||
|
text += ` ${BULLET} `;
|
||||||
|
if (diff > 0) {
|
||||||
|
text += lang('InviteLink.ExpiresIn', formatCountdown(lang, diff));
|
||||||
|
} else {
|
||||||
|
text += lang('InviteLink.Expired');
|
||||||
|
}
|
||||||
|
} else if (isPermanent) {
|
||||||
|
text += ` ${BULLET} ${lang('Permanent')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareContextActions = (invite: ApiExportedInvite) => {
|
||||||
|
const actions = [];
|
||||||
|
actions.push({
|
||||||
|
title: lang('Copy'),
|
||||||
|
icon: 'copy',
|
||||||
|
handler: () => copyLink(invite.link),
|
||||||
|
});
|
||||||
|
if (!invite.isPermanent) {
|
||||||
|
actions.push({
|
||||||
|
title: lang('Edit'),
|
||||||
|
icon: lang('edit'),
|
||||||
|
handler: () => editInvite(invite),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
actions.push({
|
||||||
|
title: lang('RevokeButton'),
|
||||||
|
icon: lang('delete'),
|
||||||
|
handler: () => revokeInvite(invite),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PrimaryLinkMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||||
|
return ({ onTrigger, isOpen }) => (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
className={isOpen ? 'active' : ''}
|
||||||
|
onClick={onTrigger}
|
||||||
|
ariaLabel="Actions"
|
||||||
|
>
|
||||||
|
<i className="icon-more" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Management ManageInvites">
|
||||||
|
<div className="custom-scroll">
|
||||||
|
{primaryInviteLink && (
|
||||||
|
<div className="section">
|
||||||
|
<p className="text-muted">
|
||||||
|
{chat?.username ? lang('PublicLink') : lang('lng_create_permanent_link_title')}
|
||||||
|
</p>
|
||||||
|
<div className="primary-link">
|
||||||
|
<input
|
||||||
|
className="form-control primary-link-input"
|
||||||
|
value={primaryInviteLink}
|
||||||
|
readOnly
|
||||||
|
onClick={handleCopyPrimaryClicked}
|
||||||
|
/>
|
||||||
|
<DropdownMenu
|
||||||
|
className="primary-link-more-menu"
|
||||||
|
trigger={PrimaryLinkMenuButton}
|
||||||
|
positionX="right"
|
||||||
|
>
|
||||||
|
<MenuItem icon="copy" onClick={handleCopyPrimaryClicked}>{lang('Copy')}</MenuItem>
|
||||||
|
{!chat?.username && (
|
||||||
|
<MenuItem icon="delete" onClick={handlePrimaryRevoke} destructive>{lang('RevokeButton')}</MenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleCopyPrimaryClicked}>{lang('CopyLink')}</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="section" teactFastList>
|
||||||
|
<Button isText key="create" className="create-link" onClick={handleCreateNewClick}>
|
||||||
|
{lang('CreateNewLink')}
|
||||||
|
</Button>
|
||||||
|
{!temporalInvites && <NothingFound text="No links found" key="nothing" />}
|
||||||
|
{temporalInvites?.map((invite) => (
|
||||||
|
<ListItem
|
||||||
|
icon="link"
|
||||||
|
secondaryIcon="more"
|
||||||
|
multiline
|
||||||
|
onClick={() => copyLink(invite.link)}
|
||||||
|
contextActions={prepareContextActions(invite)}
|
||||||
|
key={invite.link}
|
||||||
|
>
|
||||||
|
<span className="title">{invite.title || invite.link}</span>
|
||||||
|
<span className="subtitle" dir="auto">
|
||||||
|
{prepareUsageText(invite)}
|
||||||
|
</span>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
<p className="text-muted hint" key="links-hint">{lang('ManageLinksInfoHelp')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const { invites } = global.management.byChatId[chatId];
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exportedInvites: invites,
|
||||||
|
chat,
|
||||||
|
serverTimeOffset: global.serverTimeOffset,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(ManageInvites));
|
||||||
@ -47,13 +47,13 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ListItem {
|
.ListItem {
|
||||||
margin: 0 -.75rem;
|
margin: 0 -0.75rem;
|
||||||
|
|
||||||
.Reaction {
|
.Reaction {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -71,6 +71,9 @@
|
|||||||
|
|
||||||
.multiline-item .subtitle {
|
.multiline-item .subtitle {
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.picker-list-item) .Checkbox {
|
&:not(.picker-list-item) .Checkbox {
|
||||||
@ -94,10 +97,10 @@
|
|||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: .9375rem;
|
font-size: 0.9375rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|
||||||
&[dir=auto] {
|
&[dir="auto"] {
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,17 +109,17 @@
|
|||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
line-height: 1.375rem;
|
line-height: 1.375rem;
|
||||||
|
|
||||||
&[dir=auto] {
|
&[dir="auto"] {
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-info {
|
.section-info {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: .875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[dir=rtl] {
|
&[dir="rtl"] {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,20 +140,20 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: .625rem;
|
margin-bottom: 0.625rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Radio-main {
|
.Radio-main {
|
||||||
&::before {
|
&::before {
|
||||||
left: 0.125rem;
|
left: 0.125rem;
|
||||||
top: .25rem;
|
top: 0.25rem;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
left: 0.4375rem;
|
left: 0.4375rem;
|
||||||
top: .5625rem;
|
top: 0.5625rem;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,3 +163,54 @@
|
|||||||
.ManageGroupMembers {
|
.ManageGroupMembers {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ManageInvites {
|
||||||
|
.primary-link {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-link-input {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-link-more-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-link {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ManageInvite {
|
||||||
|
.link-name {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expire-limit {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-limit {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ManageInvite, .ManageInvites {
|
||||||
|
.hint {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,8 @@ import ManageGroupRecentActions from './ManageGroupRecentActions';
|
|||||||
import ManageGroupAdminRights from './ManageGroupAdminRights';
|
import ManageGroupAdminRights from './ManageGroupAdminRights';
|
||||||
import ManageGroupMembers from './ManageGroupMembers';
|
import ManageGroupMembers from './ManageGroupMembers';
|
||||||
import ManageGroupUserPermissionsCreate from './ManageGroupUserPermissionsCreate';
|
import ManageGroupUserPermissionsCreate from './ManageGroupUserPermissionsCreate';
|
||||||
|
import ManageInvites from './ManageInvites';
|
||||||
|
import ManageInvite from './ManageInvite';
|
||||||
import ManageReactions from './ManageReactions';
|
import ManageReactions from './ManageReactions';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
@ -228,7 +230,24 @@ const Management: FC<OwnProps & StateProps> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case ManagementScreens.Invites:
|
||||||
|
return (
|
||||||
|
<ManageInvites
|
||||||
|
chatId={chatId}
|
||||||
|
isActive={isActive}
|
||||||
|
onClose={onClose}
|
||||||
|
onScreenSelect={onScreenSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case ManagementScreens.EditInvite:
|
||||||
|
return (
|
||||||
|
<ManageInvite
|
||||||
|
chatId={chatId}
|
||||||
|
isActive={isActive}
|
||||||
|
onClose={onClose}
|
||||||
|
onScreenSelect={onScreenSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case ManagementScreens.GroupAddAdmins:
|
case ManagementScreens.GroupAddAdmins:
|
||||||
return (
|
return (
|
||||||
<ManageGroupMembers
|
<ManageGroupMembers
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { ChangeEvent, FormEvent, RefObject } from 'react';
|
import {
|
||||||
|
ChangeEvent, FormEvent, RefObject,
|
||||||
|
} from 'react';
|
||||||
import React, { FC, memo } from '../../lib/teact/teact';
|
import React, { FC, memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|||||||
@ -349,6 +349,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.multiline-item {
|
.multiline-item {
|
||||||
|
flex-grow: 1;
|
||||||
white-space: initial;
|
white-space: initial;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -361,6 +362,7 @@
|
|||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import useLang from '../../hooks/useLang';
|
|||||||
import RippleEffect from './RippleEffect';
|
import RippleEffect from './RippleEffect';
|
||||||
import Menu from './Menu';
|
import Menu from './Menu';
|
||||||
import MenuItem from './MenuItem';
|
import MenuItem from './MenuItem';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
import './ListItem.scss';
|
import './ListItem.scss';
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ interface OwnProps {
|
|||||||
ref?: RefObject<HTMLDivElement>;
|
ref?: RefObject<HTMLDivElement>;
|
||||||
buttonRef?: RefObject<HTMLDivElement>;
|
buttonRef?: RefObject<HTMLDivElement>;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
secondaryIcon?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: string;
|
style?: string;
|
||||||
children: any;
|
children: any;
|
||||||
@ -40,6 +42,7 @@ interface OwnProps {
|
|||||||
contextActions?: MenuItemContextAction[];
|
contextActions?: MenuItemContextAction[];
|
||||||
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem: FC<OwnProps> = (props) => {
|
const ListItem: FC<OwnProps> = (props) => {
|
||||||
@ -47,6 +50,7 @@ const ListItem: FC<OwnProps> = (props) => {
|
|||||||
ref,
|
ref,
|
||||||
buttonRef,
|
buttonRef,
|
||||||
icon,
|
icon,
|
||||||
|
secondaryIcon,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
@ -61,6 +65,7 @@ const ListItem: FC<OwnProps> = (props) => {
|
|||||||
contextActions,
|
contextActions,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
onClick,
|
onClick,
|
||||||
|
onSecondaryIconClick,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -107,6 +112,17 @@ const ListItem: FC<OwnProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [disabled, markIsTouched, onClick, ripple, unmarkIsTouched]);
|
}, [disabled, markIsTouched, onClick, ripple, unmarkIsTouched]);
|
||||||
|
|
||||||
|
const handleSecondaryIconClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
if (disabled || e.button !== 0 || (!onSecondaryIconClick && !contextActions)) return;
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onSecondaryIconClick) {
|
||||||
|
onSecondaryIconClick(e);
|
||||||
|
} else {
|
||||||
|
handleContextMenu(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
if (inactive || IS_TOUCH_ENV) {
|
if (inactive || IS_TOUCH_ENV) {
|
||||||
return;
|
return;
|
||||||
@ -154,9 +170,9 @@ const ListItem: FC<OwnProps> = (props) => {
|
|||||||
role="button"
|
role="button"
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={!inactive && IS_TOUCH_ENV ? handleClick : undefined}
|
onClick={(!inactive && IS_TOUCH_ENV) ? handleClick : undefined}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onContextMenu={!inactive && contextActions ? handleContextMenu : undefined}
|
onContextMenu={(!inactive && contextActions) ? handleContextMenu : undefined}
|
||||||
>
|
>
|
||||||
{icon && (
|
{icon && (
|
||||||
<i className={`icon-${icon}`} />
|
<i className={`icon-${icon}`} />
|
||||||
@ -166,6 +182,17 @@ const ListItem: FC<OwnProps> = (props) => {
|
|||||||
{!disabled && !inactive && ripple && (
|
{!disabled && !inactive && ripple && (
|
||||||
<RippleEffect />
|
<RippleEffect />
|
||||||
)}
|
)}
|
||||||
|
{secondaryIcon && (
|
||||||
|
<Button
|
||||||
|
className="secondary-icon"
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
onMouseDown={handleSecondaryIconClick}
|
||||||
|
>
|
||||||
|
<i className={`icon-${secondaryIcon}`} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{contextActions && contextMenuPosition !== undefined && (
|
{contextActions && contextMenuPosition !== undefined && (
|
||||||
<Menu
|
<Menu
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import {
|
|||||||
InlineBotSettings,
|
InlineBotSettings,
|
||||||
NewChatMembersProgress,
|
NewChatMembersProgress,
|
||||||
AudioOrigin,
|
AudioOrigin,
|
||||||
|
ManagementState,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export type MessageListType =
|
export type MessageListType =
|
||||||
@ -360,11 +361,7 @@ export type GlobalState = {
|
|||||||
|
|
||||||
management: {
|
management: {
|
||||||
progress?: ManagementProgress;
|
progress?: ManagementProgress;
|
||||||
byChatId: Record<string, {
|
byChatId: Record<string, ManagementState>;
|
||||||
isActive: boolean;
|
|
||||||
isUsernameAvailable?: boolean;
|
|
||||||
error?: string;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mediaViewer: {
|
mediaViewer: {
|
||||||
@ -557,6 +554,7 @@ export type ActionTypes = (
|
|||||||
'searchTextMessagesLocal' | 'searchMediaMessagesLocal' | 'searchMessagesByDate' |
|
'searchTextMessagesLocal' | 'searchMediaMessagesLocal' | 'searchMessagesByDate' |
|
||||||
// management
|
// management
|
||||||
'toggleManagement' | 'closeManagement' | 'checkPublicLink' | 'updatePublicLink' | 'updatePrivateLink' |
|
'toggleManagement' | 'closeManagement' | 'checkPublicLink' | 'updatePublicLink' | 'updatePrivateLink' |
|
||||||
|
'setEditingExportedInvite' | 'loadExportedChatInvites' | 'editExportedChatInvite' | 'exportChatInvite' |
|
||||||
// groups
|
// groups
|
||||||
'togglePreHistoryHidden' | 'updateChatDefaultBannedRights' | 'updateChatMemberBannedRights' | 'updateChatAdmin' |
|
'togglePreHistoryHidden' | 'updateChatDefaultBannedRights' | 'updateChatMemberBannedRights' | 'updateChatAdmin' |
|
||||||
'acceptInviteConfirmation' |
|
'acceptInviteConfirmation' |
|
||||||
|
|||||||
@ -1082,6 +1082,9 @@ messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.Disc
|
|||||||
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
||||||
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
||||||
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||||
|
messages.getExportedChatInvites#a2b5a3f6 flags:# revoked:flags.3?true peer:InputPeer admin_id:InputUser offset_date:flags.2?int offset_link:flags.2?string limit:int = messages.ExportedChatInvites;
|
||||||
|
messages.editExportedChatInvite#bdca2f75 flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int request_needed:flags.3?Bool title:flags.4?string = messages.ExportedChatInvite;
|
||||||
|
messages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool;
|
||||||
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
|
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
|
||||||
messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates;
|
messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates;
|
||||||
messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;
|
messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;
|
||||||
|
|||||||
@ -56,9 +56,10 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stripTl(tl) {
|
function stripTl(tl) {
|
||||||
return tl.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
|
return tl.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '') // Remove comments
|
||||||
.replace(/\n\s*\n/g, '\n')
|
.replace(/\n\s*\n/g, '\n') // Trim & add newline
|
||||||
.replace(/`/g, '\\`');
|
.replace(/`/g, '\\`') // Escape backticks
|
||||||
|
.replace(/\r/g, ''); // Remove carriage return
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@ -75,6 +75,9 @@
|
|||||||
"messages.addChatUser",
|
"messages.addChatUser",
|
||||||
"messages.deleteChatUser",
|
"messages.deleteChatUser",
|
||||||
"messages.createChat",
|
"messages.createChat",
|
||||||
|
"messages.getExportedChatInvites",
|
||||||
|
"messages.editExportedChatInvite",
|
||||||
|
"messages.deleteExportedChatInvite",
|
||||||
"messages.getDhConfig",
|
"messages.getDhConfig",
|
||||||
"messages.readMessageContents",
|
"messages.readMessageContents",
|
||||||
"messages.getStickers",
|
"messages.getStickers",
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
updateChatListSecondaryInfo,
|
updateChatListSecondaryInfo,
|
||||||
updateManagementProgress,
|
updateManagementProgress,
|
||||||
leaveChat,
|
leaveChat,
|
||||||
|
updateManagement,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
@ -610,6 +611,82 @@ addReducer('acceptInviteConfirmation', (global, actions, payload) => {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('loadExportedChatInvites', (global, actions, payload) => {
|
||||||
|
const {
|
||||||
|
chatId, adminId, isRevoked, limit,
|
||||||
|
} = payload!;
|
||||||
|
const peer = selectChat(global, chatId);
|
||||||
|
const admin = selectUser(global, adminId || global.currentUserId);
|
||||||
|
if (!peer || !admin) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const result = await callApi('fetchExportedChatInvites', {
|
||||||
|
peer, admin, isRevoked, limit,
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobal(updateManagement(getGlobal(), chatId, { invites: result }));
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('editExportedChatInvite', (global, actions, payload) => {
|
||||||
|
const {
|
||||||
|
chatId, link, isRevoked, expireDate, usageLimit, isRequestNeeded, title,
|
||||||
|
} = payload!;
|
||||||
|
const peer = selectChat(global, chatId);
|
||||||
|
if (!peer) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const result = await callApi('editExportedChatInvite', {
|
||||||
|
peer,
|
||||||
|
link,
|
||||||
|
isRevoked,
|
||||||
|
expireDate,
|
||||||
|
usageLimit,
|
||||||
|
isRequestNeeded,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global = getGlobal();
|
||||||
|
let invites = global.management.byChatId[chatId].invites || [];
|
||||||
|
const { oldInvite, newInvite } = result;
|
||||||
|
invites = invites.filter((current) => current.link !== oldInvite.link);
|
||||||
|
setGlobal(updateManagement(global, chatId, {
|
||||||
|
invites: [...invites, newInvite],
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('exportChatInvite', (global, actions, payload) => {
|
||||||
|
const {
|
||||||
|
chatId, expireDate, usageLimit, isRequestNeeded, title,
|
||||||
|
} = payload!;
|
||||||
|
const peer = selectChat(global, chatId);
|
||||||
|
if (!peer) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const result = await callApi('exportChatInvite', {
|
||||||
|
peer,
|
||||||
|
expireDate,
|
||||||
|
usageLimit,
|
||||||
|
isRequestNeeded,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global = getGlobal();
|
||||||
|
const invites = global.management.byChatId[chatId].invites || [];
|
||||||
|
setGlobal(updateManagement(global, chatId, {
|
||||||
|
invites: [...invites, result],
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
addReducer('openChatByUsername', (global, actions, payload) => {
|
addReducer('openChatByUsername', (global, actions, payload) => {
|
||||||
const {
|
const {
|
||||||
username, messageId, commentId, startParam,
|
username, messageId, commentId, startParam,
|
||||||
|
|||||||
@ -76,3 +76,9 @@ addReducer('updatePrivateLink', (global) => {
|
|||||||
|
|
||||||
callApi('updatePrivateLink', { chat });
|
callApi('updatePrivateLink', { chat });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('setEditingExportedInvite', (global, actions, payload) => {
|
||||||
|
const { chatId, invite } = payload;
|
||||||
|
|
||||||
|
setGlobal(updateManagement(global, chatId, { editingInvite: invite }));
|
||||||
|
});
|
||||||
|
|||||||
@ -203,7 +203,7 @@ export function getMessageSendingRestrictionReason(
|
|||||||
'Channel.Persmission.Denied.SendMessages.Until',
|
'Channel.Persmission.Denied.SendMessages.Until',
|
||||||
lang(
|
lang(
|
||||||
'formatDateAtTime',
|
'formatDateAtTime',
|
||||||
[formatDateToString(new Date(untilDate * 1000), lang.code), formatTime(untilDate * 1000, lang)],
|
[formatDateToString(new Date(untilDate * 1000), lang.code), formatTime(lang, untilDate * 1000)],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: lang('Channel.Persmission.Denied.SendMessages.Forever');
|
: lang('Channel.Persmission.Denied.SendMessages.Forever');
|
||||||
|
|||||||
@ -131,7 +131,7 @@ export function getUserStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// other
|
// other
|
||||||
return lang('LastSeen.TodayAt', formatTime(wasOnlineDate, lang));
|
return lang('LastSeen.TodayAt', formatTime(lang, wasOnlineDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
// yesterday
|
// yesterday
|
||||||
@ -140,7 +140,7 @@ export function getUserStatus(
|
|||||||
yesterday.setHours(0, 0, 0, 0);
|
yesterday.setHours(0, 0, 0, 0);
|
||||||
const serverYesterday = new Date(yesterday.getTime() + serverTimeOffset * 1000);
|
const serverYesterday = new Date(yesterday.getTime() + serverTimeOffset * 1000);
|
||||||
if (wasOnlineDate > serverYesterday) {
|
if (wasOnlineDate > serverYesterday) {
|
||||||
return lang('LastSeen.YesterdayAt', formatTime(wasOnlineDate, lang));
|
return lang('LastSeen.YesterdayAt', formatTime(lang, wasOnlineDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
return lang('LastSeen.AtDate', formatFullDate(lang, wasOnlineDate));
|
return lang('LastSeen.AtDate', formatFullDate(lang, wasOnlineDate));
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
import { GlobalState } from '../../global/types';
|
import { GlobalState } from '../../global/types';
|
||||||
import { ManagementProgress } from '../../types';
|
import { ManagementProgress, ManagementState } from '../../types';
|
||||||
|
|
||||||
interface ManagementState {
|
|
||||||
isActive: boolean;
|
|
||||||
isUsernameAvailable?: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateManagementProgress(global: GlobalState, progress: ManagementProgress): GlobalState {
|
export function updateManagementProgress(global: GlobalState, progress: ManagementProgress): GlobalState {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
||||||
|
ApiExportedInvite,
|
||||||
ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet,
|
ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
|
|
||||||
@ -279,6 +280,14 @@ export enum ManagementProgress {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ManagementState {
|
||||||
|
isActive: boolean;
|
||||||
|
isUsernameAvailable?: boolean;
|
||||||
|
error?: string;
|
||||||
|
invites?: ApiExportedInvite[];
|
||||||
|
editingInvite?: ApiExportedInvite;
|
||||||
|
}
|
||||||
|
|
||||||
export enum NewChatMembersProgress {
|
export enum NewChatMembersProgress {
|
||||||
Closed,
|
Closed,
|
||||||
InProgress,
|
InProgress,
|
||||||
@ -321,6 +330,8 @@ export enum ManagementScreens {
|
|||||||
ChatNewAdminRights,
|
ChatNewAdminRights,
|
||||||
GroupMembers,
|
GroupMembers,
|
||||||
GroupAddAdmins,
|
GroupAddAdmins,
|
||||||
|
Invites,
|
||||||
|
EditInvite,
|
||||||
Reactions,
|
Reactions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ function toIsoString(date: Date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @optimization `toLocaleTimeString` is avoided because of bad performance
|
// @optimization `toLocaleTimeString` is avoided because of bad performance
|
||||||
export function formatTime(datetime: number | Date, lang: LangFn) {
|
export function formatTime(lang: LangFn, datetime: number | Date) {
|
||||||
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
||||||
const timeFormat = lang.timeFormat || '24h';
|
const timeFormat = lang.timeFormat || '24h';
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ export function formatPastTimeShort(lang: LangFn, datetime: number | Date) {
|
|||||||
|
|
||||||
const today = getDayStart(new Date());
|
const today = getDayStart(new Date());
|
||||||
if (date >= today) {
|
if (date >= today) {
|
||||||
return formatTime(date, lang);
|
return formatTime(lang, date);
|
||||||
}
|
}
|
||||||
|
|
||||||
const weekAgo = new Date(today);
|
const weekAgo = new Date(today);
|
||||||
@ -82,6 +82,26 @@ export function formatMonthAndYear(lang: LangFn, date: Date, isShort = false) {
|
|||||||
return formatDate(lang, date, format);
|
return formatDate(lang, date, format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatCountdown(
|
||||||
|
lang: LangFn,
|
||||||
|
msLeft: number,
|
||||||
|
) {
|
||||||
|
const days = Math.floor(msLeft / MILLISECONDS_IN_DAY);
|
||||||
|
if (msLeft < 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (days < 1) {
|
||||||
|
return formatMediaDuration(msLeft / 1000);
|
||||||
|
} else if (days < 7) {
|
||||||
|
return lang('Days', days);
|
||||||
|
} else if (days < 30) {
|
||||||
|
return lang('Weeks', Math.floor(days / 7));
|
||||||
|
} else if (days < 365) {
|
||||||
|
return lang('Months', Math.floor(days / 30));
|
||||||
|
} else {
|
||||||
|
return lang('Years', Math.floor(days / 365));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function formatHumanDate(
|
export function formatHumanDate(
|
||||||
lang: LangFn,
|
lang: LangFn,
|
||||||
datetime: number | Date,
|
datetime: number | Date,
|
||||||
@ -148,7 +168,7 @@ export function formatMediaDateTime(
|
|||||||
) {
|
) {
|
||||||
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
||||||
|
|
||||||
return `${formatHumanDate(lang, date, true, undefined, isUpperFirst)}, ${formatTime(date, lang)}`;
|
return `${formatHumanDate(lang, date, true, undefined, isUpperFirst)}, ${formatTime(lang, date)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMediaDuration(duration: number, maxValue?: number) {
|
export function formatMediaDuration(duration: number, maxValue?: number) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user