Merge pull request #47 from Bilb/allow-mention-yourself-everywhere

feat: allow to mention ourselves everywhere
pull/3281/head
Audric Ackermann 3 months ago committed by GitHub
commit 4f2330ea95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -526,7 +526,7 @@ export class SessionConversation extends Component<Props, State> {
return { return {
id: pubKey, id: pubKey,
authorProfileName: profileName, display: profileName,
}; };
}); });

@ -1,10 +1,9 @@
import _, { debounce, isEmpty } from 'lodash'; import _, { debounce, isEmpty, uniq } from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { AbortController } from 'abort-controller'; import { AbortController } from 'abort-controller';
import { SuggestionDataItem } from 'react-mentions';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { Component, RefObject, createRef } from 'react'; import { Component, RefObject, createRef } from 'react';
@ -16,7 +15,7 @@ import { SessionRecording } from '../SessionRecording';
import { SettingsKey } from '../../../data/settings-key'; import { SettingsKey } from '../../../data/settings-key';
import { showLinkSharingConfirmationModalDialog } from '../../../interactions/conversationInteractions'; import { showLinkSharingConfirmationModalDialog } from '../../../interactions/conversationInteractions';
import { ConvoHub } from '../../../session/conversations'; import { ConvoHub } from '../../../session/conversations';
import { ToastUtils } from '../../../session/utils'; import { ToastUtils, UserUtils } from '../../../session/utils';
import { ReduxConversationType } from '../../../state/ducks/conversations'; import { ReduxConversationType } from '../../../state/ducks/conversations';
import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments'; import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
import { StateType } from '../../../state/reducer'; import { StateType } from '../../../state/reducer';
@ -32,7 +31,6 @@ import {
} from '../../../state/selectors/selectedConversation'; } from '../../../state/selectors/selectedConversation';
import { AttachmentType } from '../../../types/Attachment'; import { AttachmentType } from '../../../types/Attachment';
import { processNewAttachment } from '../../../types/MessageAttachment'; import { processNewAttachment } from '../../../types/MessageAttachment';
import { FixedBaseEmoji } from '../../../types/Reaction';
import { AttachmentUtil } from '../../../util'; import { AttachmentUtil } from '../../../util';
import { import {
StagedAttachmentImportedType, StagedAttachmentImportedType,
@ -60,6 +58,9 @@ import { CompositionTextArea } from './CompositionTextArea';
import { cleanMentions, mentionsRegex } from './UserMentions'; import { cleanMentions, mentionsRegex } from './UserMentions';
import { HTMLDirection } from '../../../util/i18n/rtlSupport'; import { HTMLDirection } from '../../../util/i18n/rtlSupport';
import { PubKey } from '../../../session/types'; import { PubKey } from '../../../session/types';
import { localize } from '../../../localization/localeTools';
import type { FixedBaseEmoji } from '../../../types/Reaction';
import type { SessionSuggestionDataItem } from './types';
export interface ReplyingToMessageProps { export interface ReplyingToMessageProps {
convoId: string; convoId: string;
@ -441,7 +442,7 @@ class CompositionBoxInner extends Component<Props, State> {
}} }}
container={this.container} container={this.container}
textAreaRef={this.textarea} textAreaRef={this.textarea}
fetchUsersForGroup={this.fetchUsersForGroup} fetchMentionData={this.fetchMentionData}
typingEnabled={this.props.typingEnabled} typingEnabled={this.props.typingEnabled}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
/> />
@ -464,85 +465,49 @@ class CompositionBoxInner extends Component<Props, State> {
); );
} }
/* eslint-enable @typescript-eslint/no-misused-promises */ private filterMentionDataByQuery(query: string, mentionData: Array<SessionSuggestionDataItem>) {
private fetchUsersForOpenGroup( return (
query: string, mentionData
callback: (data: Array<SuggestionDataItem>) => void
) {
const mentionsInput = getMentionsInput(window?.inboxStore?.getState() || []);
const filtered =
mentionsInput
.filter(d => !!d) .filter(d => !!d)
.filter(d => d.authorProfileName !== 'Anonymous') .filter(
.filter(d => d.authorProfileName?.toLowerCase()?.includes(query.toLowerCase())) d =>
// Transform the users to what react-mentions expects d.display?.toLowerCase()?.includes(query.toLowerCase()) ||
.map(user => { d.id?.toLowerCase()?.includes(query.toLowerCase())
) || []
);
}
private membersInThisChat(): Array<SessionSuggestionDataItem> {
const { selectedConversation } = this.props;
if (!selectedConversation) {
return [];
}
if (selectedConversation.isPublic) {
return getMentionsInput(window?.inboxStore?.getState() || []);
}
const members = selectedConversation.isPrivate
? uniq([UserUtils.getOurPubKeyStrFromCache(), selectedConversation.id])
: selectedConversation.members || [];
return members.map(m => {
return { return {
display: user.authorProfileName, id: m,
id: user.id, display: UserUtils.isUsFromCache(m)
? localize('you').toString()
: ConvoHub.use().get(m)?.getNicknameOrRealUsernameOrPlaceholder() || PubKey.shorten(m),
}; };
}) || []; });
callback(filtered);
} }
private fetchUsersForGroup(query: string, callback: (data: Array<SuggestionDataItem>) => void) { private fetchMentionData(query: string): Array<SessionSuggestionDataItem> {
let overriddenQuery = query; let overriddenQuery = query;
if (!query) { if (!query) {
overriddenQuery = ''; overriddenQuery = '';
} }
if (!this.props.selectedConversation) { if (!this.props.selectedConversation) {
return; return [];
}
if (this.props.selectedConversation.isPrivate) {
return;
}
if (this.props.selectedConversation.isPublic) {
this.fetchUsersForOpenGroup(overriddenQuery, callback);
return;
}
// can only be a closed group here
this.fetchUsersForClosedGroup(overriddenQuery, callback);
}
private fetchUsersForClosedGroup(
query: string,
callback: (data: Array<SuggestionDataItem>) => void
) {
const { selectedConversation } = this.props;
if (!selectedConversation) {
return;
}
const allPubKeys = selectedConversation.members;
if (!allPubKeys || allPubKeys.length === 0) {
return;
} }
const allMembers = allPubKeys.map(pubKey => { return this.filterMentionDataByQuery(overriddenQuery, this.membersInThisChat());
const convo = ConvoHub.use().get(pubKey);
const profileName = convo?.getNicknameOrRealUsernameOrPlaceholder() || PubKey.shorten(pubKey);
return {
id: pubKey,
authorProfileName: profileName,
};
});
// keep anonymous members so we can still quote them with their id
const members = allMembers
.filter(d => !!d)
.filter(
d =>
d.authorProfileName?.toLowerCase()?.includes(query.toLowerCase()) || !d.authorProfileName
);
// Transform the users to what react-mentions expects
const mentionsData = members.map(user => ({
display: user.authorProfileName || PubKey.shorten(user.id),
id: user.id,
}));
callback(mentionsData);
} }
private renderStagedLinkPreview(): JSX.Element | null { private renderStagedLinkPreview(): JSX.Element | null {

@ -13,6 +13,7 @@ import { renderUserMentionRow, styleForCompositionBoxSuggestions } from './UserM
import { HTMLDirection, useHTMLDirection } from '../../../util/i18n/rtlSupport'; import { HTMLDirection, useHTMLDirection } from '../../../util/i18n/rtlSupport';
import { ConvoHub } from '../../../session/conversations'; import { ConvoHub } from '../../../session/conversations';
import { Constants } from '../../../session'; import { Constants } from '../../../session';
import type { SessionSuggestionDataItem } from './types';
const sendMessageStyle = (dir?: HTMLDirection) => { const sendMessageStyle = (dir?: HTMLDirection) => {
return { return {
@ -43,13 +44,13 @@ type Props = {
setDraft: (draft: string) => void; setDraft: (draft: string) => void;
container: RefObject<HTMLDivElement>; container: RefObject<HTMLDivElement>;
textAreaRef: RefObject<HTMLTextAreaElement>; textAreaRef: RefObject<HTMLTextAreaElement>;
fetchUsersForGroup: (query: string, callback: (data: any) => void) => void; fetchMentionData: (query: string) => Array<SessionSuggestionDataItem>;
typingEnabled: boolean; typingEnabled: boolean;
onKeyDown: (event: any) => void; onKeyDown: (event: any) => void;
}; };
export const CompositionTextArea = (props: Props) => { export const CompositionTextArea = (props: Props) => {
const { draft, setDraft, container, textAreaRef, fetchUsersForGroup, typingEnabled, onKeyDown } = const { draft, setDraft, container, textAreaRef, fetchMentionData, typingEnabled, onKeyDown } =
props; props;
const [lastBumpTypingMessageLength, setLastBumpTypingMessageLength] = useState(0); const [lastBumpTypingMessageLength, setLastBumpTypingMessageLength] = useState(0);
@ -139,10 +140,10 @@ export const CompositionTextArea = (props: Props) => {
markup="@ᅭ__id__ᅲ__display__ᅭ" // ᅭ = \uFFD2 is one of the forbidden char for a display name (check displayNameRegex) markup="@ᅭ__id__ᅲ__display__ᅭ" // ᅭ = \uFFD2 is one of the forbidden char for a display name (check displayNameRegex)
trigger="@" trigger="@"
// this is only for the composition box visible content. The real stuff on the backend box is the @markup // this is only for the composition box visible content. The real stuff on the backend box is the @markup
displayTransform={(_id, display) => displayTransform={(_id, display) => {
htmlDirection === 'rtl' ? `${display}@` : `@${display}` return htmlDirection === 'rtl' ? `${display}@` : `@${display}`;
} }}
data={fetchUsersForGroup} data={fetchMentionData}
renderSuggestion={renderUserMentionRow} renderSuggestion={renderUserMentionRow}
/> />
<Mention <Mention

@ -1,7 +1,8 @@
import { SearchIndex } from 'emoji-mart'; import { SearchIndex } from 'emoji-mart';
import { SuggestionDataItem } from 'react-mentions';
import styled from 'styled-components'; import styled from 'styled-components';
import type { SuggestionDataItem } from 'react-mentions';
import { searchSync } from '../../../util/emoji'; import { searchSync } from '../../../util/emoji';
import type { SessionSuggestionDataItem } from './types';
const EmojiQuickResult = styled.span` const EmojiQuickResult = styled.span`
display: flex; display: flex;
@ -27,7 +28,7 @@ export const renderEmojiQuickResultRow = (suggestion: SuggestionDataItem) => {
); );
}; };
export const searchEmojiForQuery = (query: string): Array<SuggestionDataItem> => { export const searchEmojiForQuery = (query: string): Array<SessionSuggestionDataItem> => {
if (query.length === 0 || !SearchIndex) { if (query.length === 0 || !SearchIndex) {
return []; return [];
} }

@ -1,4 +1,4 @@
import { SuggestionDataItem } from 'react-mentions'; import type { SuggestionDataItem } from 'react-mentions';
import { MemberListItem } from '../../MemberListItem'; import { MemberListItem } from '../../MemberListItem';
import { HTMLDirection } from '../../../util/i18n/rtlSupport'; import { HTMLDirection } from '../../../util/i18n/rtlSupport';
@ -36,7 +36,7 @@ export const styleForCompositionBoxSuggestions = (dir: HTMLDirection = 'ltr') =>
return styles; return styles;
}; };
export const renderUserMentionRow = (suggestion: SuggestionDataItem) => { export const renderUserMentionRow = (suggestion: Pick<SuggestionDataItem, 'id'>) => {
return ( return (
<MemberListItem <MemberListItem
key={`suggestion-list-${suggestion.id}`} key={`suggestion-list-${suggestion.id}`}

@ -0,0 +1,4 @@
export interface SessionSuggestionDataItem {
id: string;
display: string;
}

@ -1,6 +1,6 @@
import type { SetupI18nReturnType } from '../types/localizer'; import type { SetupI18nReturnType } from '../types/localizer';
import { setupI18n } from '../util/i18n/i18n'; import { setupI18n } from '../util/i18n/i18n';
import {CrowdinLocale, isCrowdinLocale} from '../localization/constants'; import { CrowdinLocale, isCrowdinLocale } from '../localization/constants';
export function normalizeLocaleName(locale: string) { export function normalizeLocaleName(locale: string) {
const dashedLocale = locale.replaceAll('_', '-'); const dashedLocale = locale.replaceAll('_', '-');
@ -28,7 +28,7 @@ export function normalizeLocaleName(locale: string) {
} }
function resolveLocale(crowdinLocale: string): CrowdinLocale { function resolveLocale(crowdinLocale: string): CrowdinLocale {
const locale = normalizeLocaleName(crowdinLocale) const locale = normalizeLocaleName(crowdinLocale);
if (isCrowdinLocale(locale)) { if (isCrowdinLocale(locale)) {
return locale; return locale;
} }

@ -27,6 +27,7 @@ import { AttachmentType } from '../../types/Attachment';
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/types'; import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/types';
import { WithConvoId, WithMessageHash, WithMessageId } from '../../session/types/with'; import { WithConvoId, WithMessageHash, WithMessageId } from '../../session/types/with';
import { cancelUpdatesToDispatch } from '../../models/message'; import { cancelUpdatesToDispatch } from '../../models/message';
import type { SessionSuggestionDataItem } from '../../components/conversation/composition/types';
export type MessageModelPropsWithoutConvoProps = { export type MessageModelPropsWithoutConvoProps = {
propsForMessage: PropsForMessageWithoutConvoProps; propsForMessage: PropsForMessageWithoutConvoProps;
@ -295,14 +296,9 @@ export type ConversationsStateType = {
animateQuotedMessageId?: string; animateQuotedMessageId?: string;
shouldHighlightMessage: boolean; shouldHighlightMessage: boolean;
nextMessageToPlayId?: string; nextMessageToPlayId?: string;
mentionMembers: MentionsMembersType; mentionMembers: Array<SessionSuggestionDataItem>;
}; };
export type MentionsMembersType = Array<{
id: string;
authorProfileName: string;
}>;
function buildQuoteId(sender: string, timestamp: number) { function buildQuoteId(sender: string, timestamp: number) {
return `${timestamp}-${sender}`; return `${timestamp}-${sender}`;
} }
@ -915,7 +911,7 @@ const conversationsSlice = createSlice({
}, },
updateMentionsMembers( updateMentionsMembers(
state: ConversationsStateType, state: ConversationsStateType,
action: PayloadAction<MentionsMembersType> action: PayloadAction<Array<SessionSuggestionDataItem>>
) { ) {
window?.log?.info('updating mentions input members length', action.payload?.length); window?.log?.info('updating mentions input members length', action.payload?.length);
state.mentionMembers = action.payload; state.mentionMembers = action.payload;

@ -8,7 +8,6 @@ import {
ConversationLookupType, ConversationLookupType,
ConversationsStateType, ConversationsStateType,
lookupQuote, lookupQuote,
MentionsMembersType,
MessageModelPropsWithConvoProps, MessageModelPropsWithConvoProps,
MessageModelPropsWithoutConvoProps, MessageModelPropsWithoutConvoProps,
PropsForQuote, PropsForQuote,
@ -40,6 +39,7 @@ import { PubKey } from '../../session/types';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { getSelectedConversationKey } from './selectedConversation'; import { getSelectedConversationKey } from './selectedConversation';
import { getModeratorsOutsideRedux } from './sogsRoomInfo'; import { getModeratorsOutsideRedux } from './sogsRoomInfo';
import type { SessionSuggestionDataItem } from '../../components/conversation/composition/types';
export const getConversations = (state: StateType): ConversationsStateType => state.conversations; export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
@ -532,7 +532,7 @@ export const getShouldHighlightMessage = (state: StateType): boolean =>
export const getNextMessageToPlayId = (state: StateType): string | undefined => export const getNextMessageToPlayId = (state: StateType): string | undefined =>
state.conversations.nextMessageToPlayId || undefined; state.conversations.nextMessageToPlayId || undefined;
export const getMentionsInput = (state: StateType): MentionsMembersType => export const getMentionsInput = (state: StateType): Array<SessionSuggestionDataItem> =>
state.conversations.mentionMembers; state.conversations.mentionMembers;
/// Those calls are just related to ordering messages in the redux store. /// Those calls are just related to ordering messages in the redux store.

Loading…
Cancel
Save