Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509139
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js
index 7301c8b46..6415b777b 100644
--- a/keyserver/src/responders/message-responders.js
+++ b/keyserver/src/responders/message-responders.js
@@ -1,290 +1,294 @@
// @flow
import invariant from 'invariant';
import t from 'tcomb';
import { onlyOneEmojiRegex } from 'lib/shared/emojis.js';
import {
createMediaMessageData,
trimMessage,
} from 'lib/shared/message-utils.js';
import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils.js';
import type { Media } from 'lib/types/media-types.js';
import {
messageTypes,
type SendTextMessageRequest,
type SendMultimediaMessageRequest,
type SendReactionMessageRequest,
type FetchMessageInfosResponse,
type FetchMessageInfosRequest,
defaultNumberPerThread,
type SendMessageResponse,
} from 'lib/types/message-types.js';
import type { ReactionMessageData } from 'lib/types/messages/reaction.js';
import type { TextMessageData } from 'lib/types/messages/text.js';
import { threadPermissions } from 'lib/types/thread-types.js';
import { ServerError } from 'lib/utils/errors.js';
import {
tRegex,
tShape,
tMediaMessageMedia,
} from 'lib/utils/validation-utils.js';
import createMessages from '../creators/message-creator.js';
import { SQL } from '../database/database.js';
import {
fetchMessageInfos,
fetchMessageInfoForLocalID,
fetchMessageInfoByID,
fetchThreadMessagesCount,
} from '../fetchers/message-fetchers.js';
import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js';
import {
fetchMedia,
fetchMediaFromMediaMessageContent,
} from '../fetchers/upload-fetchers.js';
import { fetchKnownUserInfos } from '../fetchers/user-fetchers.js';
import type { Viewer } from '../session/viewer.js';
import {
assignMedia,
assignMessageContainerToMedia,
} from '../updaters/upload-updaters.js';
import { validateInput } from '../utils/validation-utils.js';
const sendTextMessageRequestInputValidator = tShape({
threadID: t.String,
localID: t.maybe(t.String),
text: t.String,
sidebarCreation: t.maybe(t.Boolean),
});
async function textMessageCreationResponder(
viewer: Viewer,
input: any,
): Promise<SendMessageResponse> {
const request: SendTextMessageRequest = input;
await validateInput(viewer, sendTextMessageRequestInputValidator, request);
const { threadID, localID, text: rawText, sidebarCreation } = request;
const text = trimMessage(rawText);
if (!text) {
throw new ServerError('invalid_parameters');
}
const hasPermission = await checkThreadPermission(
viewer,
threadID,
threadPermissions.VOICED,
);
if (!hasPermission) {
throw new ServerError('invalid_parameters');
}
let messageData: TextMessageData = {
type: messageTypes.TEXT,
threadID,
creatorID: viewer.id,
time: Date.now(),
text,
};
if (localID) {
messageData = { ...messageData, localID };
}
if (sidebarCreation) {
const numMessages = await fetchThreadMessagesCount(threadID);
if (numMessages === 2) {
// sidebarCreation is set below to prevent double notifs from a sidebar
// creation. We expect precisely two messages to appear before a
// sidebarCreation: a SIDEBAR_SOURCE and a CREATE_SIDEBAR. If two users
// attempt to create a sidebar at the same time, then both clients will
// attempt to set sidebarCreation here, but we only want to suppress
// notifs for the client that won the race.
messageData = { ...messageData, sidebarCreation };
}
}
const rawMessageInfos = await createMessages(viewer, [messageData]);
return { newMessageInfo: rawMessageInfos[0] };
}
const fetchMessageInfosRequestInputValidator = tShape({
cursors: t.dict(t.String, t.maybe(t.String)),
numberPerThread: t.maybe(t.Number),
});
async function messageFetchResponder(
viewer: Viewer,
input: any,
): Promise<FetchMessageInfosResponse> {
const request: FetchMessageInfosRequest = input;
await validateInput(viewer, fetchMessageInfosRequestInputValidator, request);
const response = await fetchMessageInfos(
viewer,
{ threadCursors: request.cursors },
request.numberPerThread ? request.numberPerThread : defaultNumberPerThread,
);
return { ...response, userInfos: {} };
}
const sendMultimediaMessageRequestInputValidator = t.union([
tShape({
threadID: t.String,
localID: t.String,
sidebarCreation: t.maybe(t.Boolean),
mediaIDs: t.list(t.String),
}),
tShape({
threadID: t.String,
localID: t.String,
sidebarCreation: t.maybe(t.Boolean),
mediaMessageContents: t.list(tMediaMessageMedia),
}),
]);
async function multimediaMessageCreationResponder(
viewer: Viewer,
input: any,
): Promise<SendMessageResponse> {
const request: SendMultimediaMessageRequest = input;
await validateInput(
viewer,
sendMultimediaMessageRequestInputValidator,
request,
);
if (
(request.mediaIDs && request.mediaIDs.length === 0) ||
(request.mediaMessageContents && request.mediaMessageContents.length === 0)
) {
throw new ServerError('invalid_parameters');
}
const { threadID, localID, sidebarCreation } = request;
const hasPermission = await checkThreadPermission(
viewer,
threadID,
threadPermissions.VOICED,
);
if (!hasPermission) {
throw new ServerError('invalid_parameters');
}
const existingMessageInfoPromise = fetchMessageInfoForLocalID(
viewer,
localID,
);
const mediaPromise: Promise<$ReadOnlyArray<Media>> = request.mediaIDs
? fetchMedia(viewer, request.mediaIDs)
: fetchMediaFromMediaMessageContent(viewer, request.mediaMessageContents);
const [existingMessageInfo, media] = await Promise.all([
existingMessageInfoPromise,
mediaPromise,
]);
if (media.length === 0 && !existingMessageInfo) {
throw new ServerError('invalid_parameters');
}
const messageData = createMediaMessageData({
localID,
threadID,
creatorID: viewer.id,
media,
sidebarCreation,
});
const [newMessageInfo] = await createMessages(viewer, [messageData]);
const { id } = newMessageInfo;
invariant(
id !== null && id !== undefined,
'serverID should be set in createMessages result',
);
if (request.mediaIDs) {
await assignMedia(viewer, request.mediaIDs, id, threadID);
} else {
await assignMessageContainerToMedia(
viewer,
request.mediaMessageContents,
id,
threadID,
);
}
return { newMessageInfo };
}
const sendReactionMessageRequestInputValidator = tShape({
threadID: t.String,
localID: t.maybe(t.String),
targetMessageID: t.String,
reaction: tRegex(onlyOneEmojiRegex),
action: t.enums.of(['add_reaction', 'remove_reaction']),
});
async function reactionMessageCreationResponder(
viewer: Viewer,
input: any,
): Promise<SendMessageResponse> {
const request: SendReactionMessageRequest = input;
await validateInput(viewer, sendReactionMessageRequestInputValidator, input);
const { threadID, localID, targetMessageID, reaction, action } = request;
if (!targetMessageID || !reaction) {
throw new ServerError('invalid_parameters');
}
const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID);
if (!targetMessageInfo || !targetMessageInfo.id) {
throw new ServerError('invalid_parameters');
}
const [serverThreadInfos, hasPermission, targetMessageUserInfos] =
await Promise.all([
fetchServerThreadInfos(SQL`t.id = ${threadID}`),
- checkThreadPermission(viewer, threadID, threadPermissions.VOICED),
+ checkThreadPermission(
+ viewer,
+ threadID,
+ threadPermissions.REACT_TO_MESSAGE,
+ ),
fetchKnownUserInfos(viewer, [targetMessageInfo.creatorID]),
]);
const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID];
if (targetMessageThreadInfo.sourceMessageID === targetMessageID) {
throw new ServerError('invalid_parameters');
}
const targetMessageCreator =
targetMessageUserInfos[targetMessageInfo.creatorID];
const targetMessageCreatorRelationship =
targetMessageCreator?.relationshipStatus;
const creatorRelationshipHasBlock =
targetMessageCreatorRelationship &&
relationshipBlockedInEitherDirection(targetMessageCreatorRelationship);
if (!hasPermission || creatorRelationshipHasBlock) {
throw new ServerError('invalid_parameters');
}
let messageData: ReactionMessageData = {
type: messageTypes.REACTION,
threadID,
creatorID: viewer.id,
time: Date.now(),
targetMessageID,
reaction,
action,
};
if (localID) {
messageData = { ...messageData, localID };
}
const rawMessageInfos = await createMessages(viewer, [messageData]);
return { newMessageInfo: rawMessageInfos[0] };
}
export {
textMessageCreationResponder,
messageFetchResponder,
multimediaMessageCreationResponder,
reactionMessageCreationResponder,
};
diff --git a/lib/shared/reaction-utils.js b/lib/shared/reaction-utils.js
index fd4becb73..450f92cfd 100644
--- a/lib/shared/reaction-utils.js
+++ b/lib/shared/reaction-utils.js
@@ -1,107 +1,107 @@
// @flow
import _sortBy from 'lodash/fp/sortBy.js';
import * as React from 'react';
import { relationshipBlockedInEitherDirection } from './relationship-utils.js';
import { threadHasPermission } from './thread-utils.js';
import { stringForUserExplicit } from './user-utils.js';
import { useENSNames } from '../hooks/ens-cache.js';
import type { ReactionInfo } from '../selectors/chat-selectors.js';
import type {
RobotextMessageInfo,
ComposableMessageInfo,
} from '../types/message-types.js';
import { threadPermissions, type ThreadInfo } from '../types/thread-types.js';
import { useSelector } from '../utils/redux-utils.js';
function stringForReactionList(reactions: ReactionInfo): string {
const reactionText = [];
for (const reaction in reactions) {
const reactionInfo = reactions[reaction];
reactionText.push(reaction);
const { length: numberOfReacts } = reactionInfo.users;
if (numberOfReacts <= 1) {
continue;
}
reactionText.push(numberOfReacts > 9 ? '9+' : numberOfReacts.toString());
}
return reactionText.join(' ');
}
type MessageReactionListInfo = {
+id: string,
+isViewer: boolean,
+reaction: string,
+username: string,
};
function useMessageReactionsList(
reactions: ReactionInfo,
): $ReadOnlyArray<MessageReactionListInfo> {
const withoutENSNames = React.useMemo(() => {
const result = [];
for (const reaction in reactions) {
const reactionInfo = reactions[reaction];
reactionInfo.users.forEach(user => {
result.push({
...user,
username: stringForUserExplicit(user),
reaction,
});
});
}
const sortByNumReactions = (reactionInfo: MessageReactionListInfo) => {
const numOfReactions = reactions[reactionInfo.reaction].users.length;
return numOfReactions ? -numOfReactions : 0;
};
return _sortBy(
([sortByNumReactions, 'username']: $ReadOnlyArray<
((reactionInfo: MessageReactionListInfo) => mixed) | string,
>),
)(result);
}, [reactions]);
return useENSNames<MessageReactionListInfo>(withoutENSNames);
}
function useCanCreateReactionFromMessage(
threadInfo: ThreadInfo,
targetMessageInfo: ComposableMessageInfo | RobotextMessageInfo,
): boolean {
const targetMessageCreatorRelationship = useSelector(
state =>
state.userStore.userInfos[targetMessageInfo.creator.id]
?.relationshipStatus,
);
if (
!targetMessageInfo.id ||
threadInfo.sourceMessageID === targetMessageInfo.id
) {
return false;
}
const creatorRelationshipHasBlock =
targetMessageCreatorRelationship &&
relationshipBlockedInEitherDirection(targetMessageCreatorRelationship);
const hasPermission = threadHasPermission(
threadInfo,
- threadPermissions.VOICED,
+ threadPermissions.REACT_TO_MESSAGE,
);
return hasPermission && !creatorRelationshipHasBlock;
}
export {
stringForReactionList,
useMessageReactionsList,
useCanCreateReactionFromMessage,
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:35 AM (4 h, 28 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690163
Default Alt Text
(12 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment