Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/api/__tests__/rawModelTypes-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/* @flow strict-local */
import {
transformFetchedMessage,
type FetchedMessage,
type FetchedReaction,
} from '../rawModelTypes';
import { identityOfAuth } from '../../account/accountMisc';
import * as eg from '../../__tests__/lib/exampleData';
import type { Message } from '../modelTypes';
import { GravatarURL } from '../../utils/avatar';

describe('transformFetchedMessage', () => {
const reactingUser = eg.makeUser();

const serverReaction: FetchedReaction = {
emoji_name: '+1',
reaction_type: 'unicode_emoji',
emoji_code: '1f44d',
user: {
email: reactingUser.email,
full_name: reactingUser.full_name,
id: reactingUser.user_id,
},
};

const fetchedMessage: FetchedMessage = {
...eg.streamMessage(),
reactions: [serverReaction],
avatar_url: null,
edit_history: [
{
prev_content: 'foo',
prev_rendered_content: '<p>foo</p>',
prev_stream: eg.stream.stream_id,
prev_topic: 'bar',
stream: eg.otherStream.stream_id,
timestamp: 0,
topic: 'bar!',
user_id: eg.selfUser.user_id,
},
],
};

describe('recent server', () => {
const input: FetchedMessage = fetchedMessage;

const expectedOutput: Message = {
...fetchedMessage,
reactions: [
{
user_id: reactingUser.user_id,
emoji_name: serverReaction.emoji_name,
reaction_type: serverReaction.reaction_type,
emoji_code: serverReaction.emoji_code,
},
],
avatar_url: GravatarURL.validateAndConstructInstance({ email: fetchedMessage.sender_email }),
edit_history:
// $FlowIgnore[incompatible-cast] - See MessageEdit type
(fetchedMessage.edit_history: Message['edit_history']),
};

const actualOutput: Message = transformFetchedMessage<Message>(
input,
identityOfAuth(eg.selfAuth),
eg.recentZulipFeatureLevel,
true,
);

test('In reactions, replace user object with `user_id`', () => {
expect(actualOutput.reactions).toEqual(expectedOutput.reactions);
});

test('Converts avatar_url correctly', () => {
expect(actualOutput.avatar_url).toEqual(expectedOutput.avatar_url);
});

test('Keeps edit_history, if allowEditHistory is true', () => {
expect(actualOutput.edit_history).toEqual(expectedOutput.edit_history);
});

test('Drops edit_history, if allowEditHistory is false', () => {
expect(
transformFetchedMessage<Message>(
input,
identityOfAuth(eg.selfAuth),
eg.recentZulipFeatureLevel,
false,
).edit_history,
).toEqual(null);
});
});

describe('drops edit_history from pre-118 server', () => {
expect(
transformFetchedMessage<Message>(
{
...fetchedMessage,
edit_history: [
{
prev_content: 'foo',
prev_rendered_content: '<p>foo</p>',
prev_stream: eg.stream.stream_id,
prev_subject: 'bar',
timestamp: 0,
user_id: eg.selfUser.user_id,
},
],
},
identityOfAuth(eg.selfAuth),
117,
true,
).edit_history,
).toEqual(null);
});
});
4 changes: 4 additions & 0 deletions src/api/eventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export type CustomProfileFieldsEvent = {|
export type MessageEvent = $ReadOnly<{|
...EventCommon,
type: typeof EventTypes.message,

// TODO: This doesn't describe what we get from the server (see, e.g.,
// avatar_url). Write a type that does; perhaps it can go in
// rawModelTypes.js.
message: Message,

/** See the same-named property on `Message`. */
Expand Down
2 changes: 2 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import deleteMessage from './messages/deleteMessage';
import deleteTopic from './messages/deleteTopic';
import getRawMessageContent from './messages/getRawMessageContent';
import getMessages from './messages/getMessages';
import getSingleMessage from './messages/getSingleMessage';
import getMessageHistory from './messages/getMessageHistory';
import messagesFlags from './messages/messagesFlags';
import sendMessage from './messages/sendMessage';
Expand Down Expand Up @@ -78,6 +79,7 @@ export {
deleteTopic,
getRawMessageContent,
getMessages,
getSingleMessage,
getMessageHistory,
messagesFlags,
sendMessage,
Expand Down
112 changes: 0 additions & 112 deletions src/api/messages/__tests__/migrateMessages-test.js

This file was deleted.

93 changes: 5 additions & 88 deletions src/api/messages/getMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
import type { Auth, ApiResponseSuccess } from '../transportTypes';
import type { Identity } from '../../types';
import type { Message, ApiNarrow } from '../apiTypes';
import type { PmMessage, StreamMessage, Reaction, UserId, MessageEdit } from '../modelTypes';
import { transformFetchedMessage, type FetchedMessage } from '../rawModelTypes';
import { apiGet } from '../apiFetch';
import { identityOfAuth } from '../../account/accountMisc';
import { AvatarURL } from '../../utils/avatar';

type ApiResponseMessages = {|
...$Exact<ApiResponseSuccess>,
Expand All @@ -16,102 +15,20 @@ type ApiResponseMessages = {|
messages: $ReadOnlyArray<Message>,
|};

/**
* The variant of `Reaction` found in the actual server response.
*
* Note that reaction events have a *different* variation; see their
* handling in `eventToAction`.
*/
// We shouldn't have to rely on this format on servers at feature
// level 2+; those newer servers include a top-level `user_id` field
// in addition to the `user` object. See #4072.
// TODO(server-3.0): Simplify this away.
export type ServerReaction = $ReadOnly<{|
...$Diff<Reaction, {| user_id: mixed |}>,
user: $ReadOnly<{|
email: string,
full_name: string,
id: UserId,
|}>,
|}>;

/**
* The elements of Message.edit_history found in the actual server response.
*
* Accurate for supported servers before and after FL 118. For convenience,
* we drop objects of this type if the FL is <118, so that the modern shape
* at Message.edit_history is the only shape we store in Redux; see there.
*/
// TODO(server-5.0): Simplify this away.
export type ServerMessageEdit = $ReadOnly<{|
prev_content?: string,
prev_rendered_content?: string,
prev_rendered_content_version?: number,
prev_stream?: number,
prev_topic?: string, // New in FL 118, replacing `prev_subject`.
prev_subject?: string, // Replaced in FL 118 by `prev_topic`.
stream?: number, // New in FL 118.
timestamp: number,
topic?: string, // New in FL 118.
user_id: UserId | null,
|}>;

// How `ServerMessage` relates to `Message`, in a way that applies
// uniformly to `Message`'s subtypes.
type ServerMessageOf<M: Message> = $ReadOnly<{|
...$Exact<M>,
avatar_url: string | null,
reactions: $ReadOnlyArray<ServerReaction>,
edit_history?: $ReadOnlyArray<ServerMessageEdit>,
|}>;

export type ServerMessage = ServerMessageOf<PmMessage> | ServerMessageOf<StreamMessage>;

// The actual response from the server. We convert the data from this to
// `ApiResponseMessages` before returning it to application code.
type ServerApiResponseMessages = {|
...ApiResponseMessages,
messages: $ReadOnlyArray<ServerMessage>,
messages: $ReadOnlyArray<FetchedMessage>,
|};

/** Exported for tests only. */
export const migrateMessages = (
messages: $ReadOnlyArray<ServerMessage>,
identity: Identity,
zulipFeatureLevel: number,
allowEditHistory: boolean,
): Message[] =>
messages.map(<M: Message>(message: ServerMessageOf<M>): M => ({
...message,
avatar_url: AvatarURL.fromUserOrBotData({
rawAvatarUrl: message.avatar_url,
email: message.sender_email,
userId: message.sender_id,
realm: identity.realm,
}),
reactions: message.reactions.map(reaction => {
const { user, ...restReaction } = reaction;
return {
...restReaction,
user_id: user.id,
};
}),

// Why condition on allowEditHistory? See MessageBase['edit_history'].
// Why FL 118 condition? See MessageEdit type.
edit_history:
/* eslint-disable operator-linebreak */
allowEditHistory && zulipFeatureLevel >= 118
? // $FlowIgnore[incompatible-cast] - See MessageEdit type
(message.edit_history: $ReadOnlyArray<MessageEdit> | void)
: null,
}));

const migrateResponse = (response, identity: Identity, zulipFeatureLevel, allowEditHistory) => {
const { messages, ...restResponse } = response;
return {
...restResponse,
messages: migrateMessages(messages, identity, zulipFeatureLevel, allowEditHistory),
messages: messages.map(message =>
transformFetchedMessage<Message>(message, identity, zulipFeatureLevel, allowEditHistory),
),
};
};

Expand Down
Loading