You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/hooks/useParamSelector.ts

525 lines
16 KiB
TypeScript

import { createSelector } from '@reduxjs/toolkit';
import { PubkeyType } from 'libsession_util_nodejs';
import { compact, isEmpty, isFinite, isNumber, pick } from 'lodash';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import {
hasValidIncomingRequestValues,
hasValidOutgoingRequestValues,
} from '../models/conversation';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { isUsAnySogsFromCache } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { CONVERSATION } from '../session/constants';
import { TimerOptions, TimerOptionsArray } from '../session/disappearing_messages/timerOptions';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';
import { PropsForExpiringMessage } from '../state/ducks/conversations';
import { StateType } from '../state/reducer';
import {
getMessagePropsByMessageId,
getMessageReactsProps,
} from '../state/selectors/conversations';
import { useLibGroupAdmins, useLibGroupMembers, useLibGroupName } from '../state/selectors/groups';
import { isPrivateAndFriend } from '../state/selectors/selectedConversation';
import { useOurPkStr } from '../state/selectors/user';
import { useLibGroupInvitePending, useLibGroupKicked } from '../state/selectors/userGroups';
export function useAvatarPath(convoId: string | undefined) {
const convoProps = useConversationPropsById(convoId);
return convoProps?.avatarPath || null;
}
export function useOurAvatarPath() {
return useAvatarPath(UserUtils.getOurPubKeyStrFromCache());
}
/**
*
* @returns convo.nickname || convo.displayNameInProfile || convo.id or undefined if the convo is not found
*/
export function useConversationUsername(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
const groupName = useLibGroupName(convoId);
if (convoId && PubKey.is03Pubkey(convoId) && groupName) {
// when getting a new 03 group from the usergroup wrapper,
// we set the displayNameInProfile with the name from the wrapper.
// So let's keep falling back to convoProps?.displayNameInProfile if groupName is not set yet (it comes later through the groupInfos namespace)
return groupName;
}
return convoProps?.nickname || convoProps?.displayNameInProfile || convoId;
}
/**
* Returns either the nickname, displayNameInProfile, or the shorten pubkey
*/
export function useNicknameOrProfileNameOrShortenedPubkey(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return (
convoProps?.nickname ||
convoProps?.displayNameInProfile ||
(convoId && PubKey.shorten(convoId)) ||
window.i18n('unknown')
);
}
/**
* Returns the name of that conversation.
* This is the group name, or the realName of a user for a private conversation with a recent nickname set
*/
export function useConversationRealName(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return convoProps?.isPrivate ? convoProps?.displayNameInProfile : undefined;
}
function usernameForQuoteOrFullPk(pubkey: string, state: StateType) {
if (pubkey === UserUtils.getOurPubKeyStrFromCache() || pubkey.toLowerCase() === 'you') {
return window.i18n('you');
}
// use the name from the cached libsession wrappers if available
if (PubKey.is03Pubkey(pubkey)) {
const info = state.groups.infos[pubkey];
if (info && info.name) {
return info.name;
}
}
const convo = state.conversations.conversationLookup[pubkey];
const nameGot = convo?.nickname || convo?.displayNameInProfile;
return nameGot?.length ? nameGot : null;
}
/**
* Returns either the nickname, the profileName, in '"' or the full pubkeys given
*/
export function useConversationsUsernameWithQuoteOrFullPubkey(pubkeys: Array<string>) {
return useSelector((state: StateType) => {
return pubkeys.map(pubkey => {
const nameGot = usernameForQuoteOrFullPk(pubkey, state);
return nameGot?.length ? nameGot : pubkey;
});
});
}
/**
* Returns either the nickname, the profileName, a shortened pubkey, or "you" for our own pubkey
*/
export function useConversationsUsernameWithQuoteOrShortPk(pubkeys: Array<string>) {
return useSelector((state: StateType) => {
return pubkeys.map(pubkey => {
const nameGot = usernameForQuoteOrFullPk(pubkey, state);
return nameGot?.length ? nameGot : PubKey.shorten(pubkey);
});
});
}
export function useOurConversationUsername() {
return useConversationUsername(UserUtils.getOurPubKeyStrFromCache());
}
export function useIsMe(pubkey?: string) {
return Boolean(pubkey && pubkey === UserUtils.getOurPubKeyStrFromCache());
}
export function useIsClosedGroup(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return (convoProps && !convoProps.isPrivate && !convoProps.isPublic) || false;
}
export function useIsPrivate(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return Boolean(convoProps && convoProps.isPrivate);
}
export function useIsPrivateAndFriend(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
if (!convoProps) {
return false;
}
return isPrivateAndFriend({
approvedMe: convoProps.didApproveMe || false,
isApproved: convoProps.isApproved || false,
isPrivate: convoProps.isPrivate || false,
});
}
export function useIsBlinded(convoId?: string) {
if (!convoId) {
return false;
}
return Boolean(PubKey.isBlinded(convoId));
}
export function useHasNickname(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return Boolean(convoProps && !isEmpty(convoProps.nickname));
}
export function useNotificationSetting(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return convoProps?.currentNotificationSetting || 'all';
}
export function useIsGroupV2(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return convoId && convoProps?.type === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(convoId);
}
export function useIsPublic(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return Boolean(convoProps && convoProps.isPublic);
}
export function useIsBlocked(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return Boolean(convoProps && convoProps.isBlocked);
}
export function useActiveAt(convoId?: string): number | undefined {
const convoProps = useConversationPropsById(convoId);
return convoProps?.activeAt;
}
export function useIsActive(convoId?: string) {
return !!useActiveAt(convoId);
}
export function useIsKickedFromGroup(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
const libIsKicked = useLibGroupKicked(convoId);
if (convoId && PubKey.is03Pubkey(convoId)) {
return libIsKicked;
}
return Boolean(convoProps && (convoProps.isKickedFromGroup || libIsKicked)); // not ideal, but until we trust what we get from libsession for all cases, we have to either trust what we have in the DB
}
export function useWeAreAdmin(convoId?: string) {
const groupAdmins = useGroupAdmins(convoId);
const us = useOurPkStr();
return Boolean(groupAdmins.includes(us));
}
export function useGroupAdmins(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
const libMembers = useLibGroupAdmins(convoId);
if (convoId && PubKey.is03Pubkey(convoId)) {
return compact(libMembers?.slice()?.sort()) || [];
}
return convoProps?.groupAdmins || [];
}
export function useExpireTimer(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return convoProps && convoProps.expireTimer;
}
export function useIsPinned(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return Boolean(
convoProps &&
isNumber(convoProps.priority) &&
isFinite(convoProps.priority) &&
convoProps.priority > 0
);
}
export function useIsApproved(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return Boolean(convoProps && convoProps.isApproved);
}
export function useIsIncomingRequest(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
const invitePending = useLibGroupInvitePending(convoId) || false;
if (!convoProps) {
return false;
}
return Boolean(
convoProps &&
hasValidIncomingRequestValues({
id: convoProps.id,
isMe: convoProps.isMe || false,
isApproved: convoProps.isApproved || false,
isPrivate: convoProps.isPrivate || false,
isBlocked: convoProps.isBlocked || false,
didApproveMe: convoProps.didApproveMe || false,
activeAt: convoProps.activeAt || 0,
invitePending,
})
);
}
export function useIsOutgoingRequest(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
if (!convoProps) {
return false;
}
return Boolean(
convoProps &&
hasValidOutgoingRequestValues({
isMe: convoProps.isMe || false,
isApproved: convoProps.isApproved || false,
didApproveMe: convoProps.didApproveMe || false,
isPrivate: convoProps.isPrivate || false,
isBlocked: convoProps.isBlocked || false,
activeAt: convoProps.activeAt || 0,
})
);
}
/**
* Note: NOT to be exported:
* This selector is too generic and needs to be broken node in individual fields selectors.
* Make sure when writing a selector that you fetch the data from libsession if needed.
* (check useSortedGroupMembers() as an example)
*/
function useConversationPropsById(convoId?: string) {
return useSelector((state: StateType) => {
if (!convoId) {
return null;
}
const convo = state.conversations.conversationLookup[convoId];
if (!convo) {
return null;
}
return convo;
});
}
export function useZombies(convoId?: string) {
return useSelector((state: StateType) => {
if (!convoId) {
return null;
}
const convo = state.conversations.conversationLookup[convoId];
if (!convo) {
return null;
}
return convo.zombies;
});
}
export function useMessageReactsPropsById(messageId?: string) {
return useSelector((state: StateType) => {
if (!messageId) {
return null;
}
const messageReactsProps = getMessageReactsProps(state, messageId);
if (!messageReactsProps) {
return null;
}
return messageReactsProps;
});
}
/**
* Returns the unread count of that conversation, or 0 if none are found.
* Note: returned value is capped at a max of CONVERSATION.MAX_UNREAD_COUNT
*/
export function useUnreadCount(conversationId?: string): number {
const convoProps = useConversationPropsById(conversationId);
const convoUnreadCount = convoProps?.unreadCount || 0;
return Math.min(CONVERSATION.MAX_UNREAD_COUNT, convoUnreadCount);
}
export function useHasUnread(conversationId?: string): boolean {
return useUnreadCount(conversationId) > 0;
}
export function useIsForcedUnreadWithoutUnreadMsg(conversationId?: string): boolean {
const convoProps = useConversationPropsById(conversationId);
return convoProps?.isMarkedUnread || false;
}
function useMentionedUsUnread(conversationId?: string) {
const convoProps = useConversationPropsById(conversationId);
return convoProps?.mentionedUs || false;
}
export function useMentionedUs(conversationId?: string): boolean {
const hasMentionedUs = useMentionedUsUnread(conversationId);
const hasUnread = useHasUnread(conversationId);
return hasMentionedUs && hasUnread;
}
export function useIsTyping(conversationId?: string): boolean {
return useConversationPropsById(conversationId)?.isTyping || false;
}
const getMessageExpirationProps = createSelector(getMessagePropsByMessageId, (props):
| PropsForExpiringMessage
| undefined => {
if (!props || isEmpty(props)) {
return undefined;
}
const msgProps: PropsForExpiringMessage = {
...pick(props.propsForMessage, [
'convoId',
'direction',
'receivedAt',
'isUnread',
'expirationTimestamp',
'expirationDurationMs',
'isExpired',
]),
messageId: props.propsForMessage.id,
};
return msgProps;
});
export function useMessageExpirationPropsById(messageId?: string) {
return useSelector((state: StateType) => {
if (!messageId) {
return null;
}
const messageExpirationProps = getMessageExpirationProps(state, messageId);
if (!messageExpirationProps) {
return null;
}
return messageExpirationProps;
});
}
export function useTimerOptionsByMode(disappearingMessageMode?: string, hasOnlyOneMode?: boolean) {
return useMemo(() => {
const options: TimerOptionsArray = [];
if (hasOnlyOneMode) {
options.push({
name: TimerOptions.getName(TimerOptions.VALUES[0]),
value: TimerOptions.VALUES[0],
});
}
switch (disappearingMessageMode) {
// TODO legacy messages support will be removed in a future release
case 'legacy':
options.push(
...TimerOptions.DELETE_LEGACY.map(option => ({
name: TimerOptions.getName(option),
value: option,
}))
);
break;
case 'deleteAfterRead':
options.push(
...TimerOptions.DELETE_AFTER_READ.map(option => ({
name: TimerOptions.getName(option),
value: option,
}))
);
break;
case 'deleteAfterSend':
options.push(
...TimerOptions.DELETE_AFTER_SEND.map(option => ({
name: TimerOptions.getName(option),
value: option,
}))
);
break;
default:
return [];
}
return options;
}, [disappearingMessageMode, hasOnlyOneMode]);
}
export function useQuoteAuthorName(
authorId?: string
): {
authorName: string | undefined;
isMe: boolean;
} {
const convoProps = useConversationPropsById(authorId);
const isMe = Boolean(authorId && isUsAnySogsFromCache(authorId));
const authorName = isMe
? window.i18n('you')
: convoProps?.nickname || convoProps?.isPrivate
? convoProps?.displayNameInProfile
: undefined;
return { authorName, isMe };
}
function useMembers(convoId: string | undefined) {
const props = useConversationPropsById(convoId);
return props?.members || undefined;
}
/**
* Get the list of members of a closed group or []
* @param convoId the closed group id to extract members from
*/
export function useSortedGroupMembers(convoId: string | undefined): Array<PubkeyType> {
const members = useMembers(convoId);
const isPublic = useIsPublic(convoId);
const isPrivate = useIsPrivate(convoId);
const libMembers = useLibGroupMembers(convoId);
if (isPrivate || isPublic) {
return [];
}
if (convoId && PubKey.is03Pubkey(convoId)) {
return compact(libMembers?.slice()?.sort()) || [];
}
// we need to clone the array before being able to call sort() it
return (compact(members?.slice()?.sort()) || []) as Array<PubkeyType>;
}
export function useDisappearingMessageSettingText({
convoId,
abbreviate,
separator = ' - ',
}: {
convoId?: string;
abbreviate?: boolean;
separator?: string;
}): string {
const convoProps = useConversationPropsById(convoId);
if (!convoProps) {
return '';
}
const { expirationMode, expireTimer, isMe, isPublic } = convoProps;
const isGroup = !convoProps.isPrivate && !convoProps.isPublic;
// TODO legacy messages support will be removed in a future release
const expirationModeText =
expirationMode === 'deleteAfterRead'
? window.i18n('disappearingMessagesModeAfterRead')
: expirationMode === 'deleteAfterSend'
? window.i18n('disappearingMessagesModeAfterSend')
: expirationMode === 'legacy'
? isMe || (isGroup && !isPublic)
? window.i18n('disappearingMessagesModeAfterSend')
: window.i18n('disappearingMessagesModeAfterRead')
: null;
const expireTimerText = isNumber(expireTimer)
? abbreviate
? TimerOptions.getAbbreviated(expireTimer)
: TimerOptions.getName(expireTimer)
: null;
return expireTimer && expirationModeText
? `${expirationModeText}${expireTimerText ? `${separator}${expireTimerText}` : ''}`
: '';
}
export function useLastMessage(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
if (!convoId || !convoProps) {
return null;
}
return convoProps.lastMessage;
}