Page MenuHomePhabricator

D9442.diff
No OneTemporary

D9442.diff

diff --git a/keyserver/src/fetchers/update-fetchers.js b/keyserver/src/fetchers/update-fetchers.js
--- a/keyserver/src/fetchers/update-fetchers.js
+++ b/keyserver/src/fetchers/update-fetchers.js
@@ -1,7 +1,6 @@
// @flow
-import invariant from 'invariant';
-
+import { updateSpecs } from 'lib/shared/updates/update-specs.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import { updateTypes, assertUpdateType } from 'lib/types/update-types-enum.js';
import { type RawUpdateInfo } from 'lib/types/update-types.js';
@@ -52,83 +51,9 @@
return fetchUpdateInfosWithQuery({ viewer, calendarQuery }, query);
}
-// ESLint doesn't recognize that invariant always throws
-// eslint-disable-next-line consistent-return
function rawUpdateInfoFromRow(row: Object): RawUpdateInfo {
const type = assertUpdateType(row.type);
- if (type === updateTypes.DELETE_ACCOUNT) {
- const content = JSON.parse(row.content);
- return {
- type: updateTypes.DELETE_ACCOUNT,
- id: row.id.toString(),
- time: row.time,
- deletedUserID: content.deletedUserID,
- };
- } else if (type === updateTypes.UPDATE_THREAD) {
- const { threadID } = JSON.parse(row.content);
- return {
- type: updateTypes.UPDATE_THREAD,
- id: row.id.toString(),
- time: row.time,
- threadID,
- };
- } else if (type === updateTypes.UPDATE_THREAD_READ_STATUS) {
- const { threadID, unread } = JSON.parse(row.content);
- return {
- type: updateTypes.UPDATE_THREAD_READ_STATUS,
- id: row.id.toString(),
- time: row.time,
- threadID,
- unread,
- };
- } else if (type === updateTypes.DELETE_THREAD) {
- const { threadID } = JSON.parse(row.content);
- return {
- type: updateTypes.DELETE_THREAD,
- id: row.id.toString(),
- time: row.time,
- threadID,
- };
- } else if (type === updateTypes.JOIN_THREAD) {
- const { threadID } = JSON.parse(row.content);
- return {
- type: updateTypes.JOIN_THREAD,
- id: row.id.toString(),
- time: row.time,
- threadID,
- };
- } else if (type === updateTypes.BAD_DEVICE_TOKEN) {
- const { deviceToken } = JSON.parse(row.content);
- return {
- type: updateTypes.BAD_DEVICE_TOKEN,
- id: row.id.toString(),
- time: row.time,
- deviceToken,
- };
- } else if (type === updateTypes.UPDATE_ENTRY) {
- const { entryID } = JSON.parse(row.content);
- return {
- type: updateTypes.UPDATE_ENTRY,
- id: row.id.toString(),
- time: row.time,
- entryID,
- };
- } else if (type === updateTypes.UPDATE_CURRENT_USER) {
- return {
- type: updateTypes.UPDATE_CURRENT_USER,
- id: row.id.toString(),
- time: row.time,
- };
- } else if (type === updateTypes.UPDATE_USER) {
- const content = JSON.parse(row.content);
- return {
- type: updateTypes.UPDATE_USER,
- id: row.id.toString(),
- time: row.time,
- updatedUserID: content.updatedUserID,
- };
- }
- invariant(false, `unrecognized updateType ${type}`);
+ return updateSpecs[type].rawUpdateInfoFromRow(row);
}
const entryIDExtractString = '$.entryID';
diff --git a/lib/shared/updates/bad-device-token-spec.js b/lib/shared/updates/bad-device-token-spec.js
--- a/lib/shared/updates/bad-device-token-spec.js
+++ b/lib/shared/updates/bad-device-token-spec.js
@@ -1,7 +1,23 @@
// @flow
import type { UpdateSpec } from './update-spec.js';
-import type { BadDeviceTokenUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ BadDeviceTokenRawUpdateInfo,
+ BadDeviceTokenUpdateInfo,
+} from '../../types/update-types.js';
-export const badDeviceTokenSpec: UpdateSpec<BadDeviceTokenUpdateInfo> =
- Object.freeze({});
+export const badDeviceTokenSpec: UpdateSpec<
+ BadDeviceTokenUpdateInfo,
+ BadDeviceTokenRawUpdateInfo,
+> = Object.freeze({
+ rawUpdateInfoFromRow(row: Object) {
+ const { deviceToken } = JSON.parse(row.content);
+ return {
+ type: updateTypes.BAD_DEVICE_TOKEN,
+ id: row.id.toString(),
+ time: row.time,
+ deviceToken,
+ };
+ },
+});
diff --git a/lib/shared/updates/delete-account-spec.js b/lib/shared/updates/delete-account-spec.js
--- a/lib/shared/updates/delete-account-spec.js
+++ b/lib/shared/updates/delete-account-spec.js
@@ -2,43 +2,58 @@
import type { UpdateSpec } from './update-spec.js';
import type { RawThreadInfos } from '../../types/thread-types.js';
-import type { AccountDeletionUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ AccountDeletionRawUpdateInfo,
+ AccountDeletionUpdateInfo,
+} from '../../types/update-types.js';
import type { UserInfos } from '../../types/user-types.js';
-export const deleteAccountSpec: UpdateSpec<AccountDeletionUpdateInfo> =
- Object.freeze({
- generateOpsForThreadUpdates(
- storeThreadInfos: RawThreadInfos,
- update: AccountDeletionUpdateInfo,
- ) {
- const operations = [];
- for (const threadID in storeThreadInfos) {
- const threadInfo = storeThreadInfos[threadID];
- const newMembers = threadInfo.members.filter(
- member => member.id !== update.deletedUserID,
- );
- if (newMembers.length < threadInfo.members.length) {
- const updatedThread = {
- ...threadInfo,
- members: newMembers,
- };
- operations.push({
- type: 'replace',
- payload: {
- id: threadID,
- threadInfo: updatedThread,
- },
- });
- }
+export const deleteAccountSpec: UpdateSpec<
+ AccountDeletionUpdateInfo,
+ AccountDeletionRawUpdateInfo,
+> = Object.freeze({
+ generateOpsForThreadUpdates(
+ storeThreadInfos: RawThreadInfos,
+ update: AccountDeletionUpdateInfo,
+ ) {
+ const operations = [];
+ for (const threadID in storeThreadInfos) {
+ const threadInfo = storeThreadInfos[threadID];
+ const newMembers = threadInfo.members.filter(
+ member => member.id !== update.deletedUserID,
+ );
+ if (newMembers.length < threadInfo.members.length) {
+ const updatedThread = {
+ ...threadInfo,
+ members: newMembers,
+ };
+ operations.push({
+ type: 'replace',
+ payload: {
+ id: threadID,
+ threadInfo: updatedThread,
+ },
+ });
}
- return operations;
- },
- reduceUserInfos(state: UserInfos, update: AccountDeletionUpdateInfo) {
- const { deletedUserID } = update;
- if (!state[deletedUserID]) {
- return state;
- }
- const { [deletedUserID]: deleted, ...rest } = state;
- return rest;
- },
- });
+ }
+ return operations;
+ },
+ reduceUserInfos(state: UserInfos, update: AccountDeletionUpdateInfo) {
+ const { deletedUserID } = update;
+ if (!state[deletedUserID]) {
+ return state;
+ }
+ const { [deletedUserID]: deleted, ...rest } = state;
+ return rest;
+ },
+ rawUpdateInfoFromRow(row: Object) {
+ const content = JSON.parse(row.content);
+ return {
+ type: updateTypes.DELETE_ACCOUNT,
+ id: row.id.toString(),
+ time: row.time,
+ deletedUserID: content.deletedUserID,
+ };
+ },
+});
diff --git a/lib/shared/updates/delete-thread-spec.js b/lib/shared/updates/delete-thread-spec.js
--- a/lib/shared/updates/delete-thread-spec.js
+++ b/lib/shared/updates/delete-thread-spec.js
@@ -2,35 +2,48 @@
import type { UpdateSpec } from './update-spec.js';
import type { RawThreadInfos } from '../../types/thread-types.js';
-import type { ThreadDeletionUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ ThreadDeletionRawUpdateInfo,
+ ThreadDeletionUpdateInfo,
+} from '../../types/update-types.js';
-export const deleteThreadSpec: UpdateSpec<ThreadDeletionUpdateInfo> =
- Object.freeze({
- generateOpsForThreadUpdates(
- storeThreadInfos: RawThreadInfos,
- update: ThreadDeletionUpdateInfo,
- ) {
- if (storeThreadInfos[update.threadID]) {
- return [
- {
- type: 'remove',
- payload: {
- ids: [update.threadID],
- },
+export const deleteThreadSpec: UpdateSpec<
+ ThreadDeletionUpdateInfo,
+ ThreadDeletionRawUpdateInfo,
+> = Object.freeze({
+ generateOpsForThreadUpdates(
+ storeThreadInfos: RawThreadInfos,
+ update: ThreadDeletionUpdateInfo,
+ ) {
+ if (storeThreadInfos[update.threadID]) {
+ return [
+ {
+ type: 'remove',
+ payload: {
+ ids: [update.threadID],
},
- ];
- }
- return null;
- },
- reduceCalendarThreadFilters(
- filteredThreadIDs: $ReadOnlySet<string>,
- update: ThreadDeletionUpdateInfo,
- ) {
- if (!filteredThreadIDs.has(update.threadID)) {
- return filteredThreadIDs;
- }
- return new Set(
- [...filteredThreadIDs].filter(id => id !== update.threadID),
- );
- },
- });
+ },
+ ];
+ }
+ return null;
+ },
+ reduceCalendarThreadFilters(
+ filteredThreadIDs: $ReadOnlySet<string>,
+ update: ThreadDeletionUpdateInfo,
+ ) {
+ if (!filteredThreadIDs.has(update.threadID)) {
+ return filteredThreadIDs;
+ }
+ return new Set([...filteredThreadIDs].filter(id => id !== update.threadID));
+ },
+ rawUpdateInfoFromRow(row: Object) {
+ const { threadID } = JSON.parse(row.content);
+ return {
+ type: updateTypes.DELETE_THREAD,
+ id: row.id.toString(),
+ time: row.time,
+ threadID,
+ };
+ },
+});
diff --git a/lib/shared/updates/join-thread-spec.js b/lib/shared/updates/join-thread-spec.js
--- a/lib/shared/updates/join-thread-spec.js
+++ b/lib/shared/updates/join-thread-spec.js
@@ -9,11 +9,18 @@
MessageTruncationStatuses,
} from '../../types/message-types.js';
import type { RawThreadInfos } from '../../types/thread-types.js';
-import type { ThreadJoinUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ ThreadJoinUpdateInfo,
+ ThreadJoinRawUpdateInfo,
+} from '../../types/update-types.js';
import { combineTruncationStatuses } from '../message-utils.js';
import { threadInFilterList } from '../thread-utils.js';
-export const joinThreadSpec: UpdateSpec<ThreadJoinUpdateInfo> = Object.freeze({
+export const joinThreadSpec: UpdateSpec<
+ ThreadJoinUpdateInfo,
+ ThreadJoinRawUpdateInfo,
+> = Object.freeze({
generateOpsForThreadUpdates(
storeThreadInfos: RawThreadInfos,
update: ThreadJoinUpdateInfo,
@@ -80,4 +87,13 @@
truncationStatuses[update.threadInfo.id],
);
},
+ rawUpdateInfoFromRow(row: Object) {
+ const { threadID } = JSON.parse(row.content);
+ return {
+ type: updateTypes.JOIN_THREAD,
+ id: row.id.toString(),
+ time: row.time,
+ threadID,
+ };
+ },
});
diff --git a/lib/shared/updates/update-current-user-spec.js b/lib/shared/updates/update-current-user-spec.js
--- a/lib/shared/updates/update-current-user-spec.js
+++ b/lib/shared/updates/update-current-user-spec.js
@@ -3,15 +3,28 @@
import _isEqual from 'lodash/fp/isEqual.js';
import type { UpdateSpec } from './update-spec.js';
-import type { CurrentUserUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ CurrentUserUpdateInfo,
+ CurrentUserRawUpdateInfo,
+} from '../../types/update-types.js';
import type { CurrentUserInfo } from '../../types/user-types.js';
-export const updateCurrentUserSpec: UpdateSpec<CurrentUserUpdateInfo> =
- Object.freeze({
- reduceCurrentUser(state: ?CurrentUserInfo, update: CurrentUserUpdateInfo) {
- if (!_isEqual(update.currentUserInfo)(state)) {
- return update.currentUserInfo;
- }
- return state;
- },
- });
+export const updateCurrentUserSpec: UpdateSpec<
+ CurrentUserUpdateInfo,
+ CurrentUserRawUpdateInfo,
+> = Object.freeze({
+ reduceCurrentUser(state: ?CurrentUserInfo, update: CurrentUserUpdateInfo) {
+ if (!_isEqual(update.currentUserInfo)(state)) {
+ return update.currentUserInfo;
+ }
+ return state;
+ },
+ rawUpdateInfoFromRow(row: Object) {
+ return {
+ type: updateTypes.UPDATE_CURRENT_USER,
+ id: row.id.toString(),
+ time: row.time,
+ };
+ },
+});
diff --git a/lib/shared/updates/update-entry-spec.js b/lib/shared/updates/update-entry-spec.js
--- a/lib/shared/updates/update-entry-spec.js
+++ b/lib/shared/updates/update-entry-spec.js
@@ -2,20 +2,34 @@
import type { UpdateSpec } from './update-spec.js';
import type { RawEntryInfo } from '../../types/entry-types.js';
-import type { EntryUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ EntryUpdateInfo,
+ EntryRawUpdateInfo,
+} from '../../types/update-types.js';
-export const updateEntrySpec: UpdateSpec<EntryUpdateInfo> = Object.freeze({
- mergeEntryInfos(
- entryIDs: Set<string>,
- mergedEntryInfos: Array<RawEntryInfo>,
- update: EntryUpdateInfo,
- ) {
- const { entryInfo } = update;
- const entryID = entryInfo.id;
- if (!entryID || entryIDs.has(entryID)) {
- return;
- }
- mergedEntryInfos.push(entryInfo);
- entryIDs.add(entryID);
- },
-});
+export const updateEntrySpec: UpdateSpec<EntryUpdateInfo, EntryRawUpdateInfo> =
+ Object.freeze({
+ mergeEntryInfos(
+ entryIDs: Set<string>,
+ mergedEntryInfos: Array<RawEntryInfo>,
+ update: EntryUpdateInfo,
+ ) {
+ const { entryInfo } = update;
+ const entryID = entryInfo.id;
+ if (!entryID || entryIDs.has(entryID)) {
+ return;
+ }
+ mergedEntryInfos.push(entryInfo);
+ entryIDs.add(entryID);
+ },
+ rawUpdateInfoFromRow(row: Object) {
+ const { entryID } = JSON.parse(row.content);
+ return {
+ type: updateTypes.UPDATE_ENTRY,
+ id: row.id.toString(),
+ time: row.time,
+ entryID,
+ };
+ },
+ });
diff --git a/lib/shared/updates/update-spec.js b/lib/shared/updates/update-spec.js
--- a/lib/shared/updates/update-spec.js
+++ b/lib/shared/updates/update-spec.js
@@ -7,10 +7,13 @@
MessageTruncationStatuses,
} from '../../types/message-types.js';
import type { RawThreadInfos } from '../../types/thread-types.js';
-import type { ClientUpdateInfo } from '../../types/update-types.js';
+import type {
+ ClientUpdateInfo,
+ RawUpdateInfo,
+} from '../../types/update-types.js';
import type { CurrentUserInfo, UserInfos } from '../../types/user-types.js';
-export type UpdateSpec<UpdateInfo: ClientUpdateInfo> = {
+export type UpdateSpec<UpdateInfo: ClientUpdateInfo, RawInfo: RawUpdateInfo> = {
+generateOpsForThreadUpdates?: (
storeThreadInfos: RawThreadInfos,
update: UpdateInfo,
@@ -36,4 +39,5 @@
truncationStatuses: MessageTruncationStatuses,
update: UpdateInfo,
) => void,
+ +rawUpdateInfoFromRow: (row: Object) => RawInfo,
};
diff --git a/lib/shared/updates/update-specs.js b/lib/shared/updates/update-specs.js
--- a/lib/shared/updates/update-specs.js
+++ b/lib/shared/updates/update-specs.js
@@ -13,7 +13,7 @@
import { updateTypes, type UpdateType } from '../../types/update-types-enum.js';
export const updateSpecs: {
- +[UpdateType]: UpdateSpec<*>,
+ +[UpdateType]: UpdateSpec<*, *>,
} = Object.freeze({
[updateTypes.DELETE_ACCOUNT]: deleteAccountSpec,
[updateTypes.UPDATE_THREAD]: updateThreadSpec,
diff --git a/lib/shared/updates/update-thread-read-status-spec.js b/lib/shared/updates/update-thread-read-status-spec.js
--- a/lib/shared/updates/update-thread-read-status-spec.js
+++ b/lib/shared/updates/update-thread-read-status-spec.js
@@ -5,36 +5,52 @@
RawThreadInfo,
RawThreadInfos,
} from '../../types/thread-types.js';
-import type { ThreadReadStatusUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ ThreadReadStatusUpdateInfo,
+ ThreadReadStatusRawUpdateInfo,
+} from '../../types/update-types.js';
-export const updateThreadReadStatusSpec: UpdateSpec<ThreadReadStatusUpdateInfo> =
- Object.freeze({
- generateOpsForThreadUpdates(
- storeThreadInfos: RawThreadInfos,
- update: ThreadReadStatusUpdateInfo,
+export const updateThreadReadStatusSpec: UpdateSpec<
+ ThreadReadStatusUpdateInfo,
+ ThreadReadStatusRawUpdateInfo,
+> = Object.freeze({
+ generateOpsForThreadUpdates(
+ storeThreadInfos: RawThreadInfos,
+ update: ThreadReadStatusUpdateInfo,
+ ) {
+ const storeThreadInfo: ?RawThreadInfo = storeThreadInfos[update.threadID];
+ if (
+ !storeThreadInfo ||
+ storeThreadInfo.currentUser.unread === update.unread
) {
- const storeThreadInfo: ?RawThreadInfo = storeThreadInfos[update.threadID];
- if (
- !storeThreadInfo ||
- storeThreadInfo.currentUser.unread === update.unread
- ) {
- return null;
- }
- const updatedThread = {
- ...storeThreadInfo,
- currentUser: {
- ...storeThreadInfo.currentUser,
- unread: update.unread,
- },
- };
- return [
- {
- type: 'replace',
- payload: {
- id: update.threadID,
- threadInfo: updatedThread,
- },
+ return null;
+ }
+ const updatedThread = {
+ ...storeThreadInfo,
+ currentUser: {
+ ...storeThreadInfo.currentUser,
+ unread: update.unread,
+ },
+ };
+ return [
+ {
+ type: 'replace',
+ payload: {
+ id: update.threadID,
+ threadInfo: updatedThread,
},
- ];
- },
- });
+ },
+ ];
+ },
+ rawUpdateInfoFromRow(row: Object) {
+ const { threadID, unread } = JSON.parse(row.content);
+ return {
+ type: updateTypes.UPDATE_THREAD_READ_STATUS,
+ id: row.id.toString(),
+ time: row.time,
+ threadID,
+ unread,
+ };
+ },
+});
diff --git a/lib/shared/updates/update-thread-spec.js b/lib/shared/updates/update-thread-spec.js
--- a/lib/shared/updates/update-thread-spec.js
+++ b/lib/shared/updates/update-thread-spec.js
@@ -4,10 +4,17 @@
import type { UpdateSpec } from './update-spec.js';
import type { RawThreadInfos } from '../../types/thread-types.js';
-import type { ThreadUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ ThreadUpdateInfo,
+ ThreadRawUpdateInfo,
+} from '../../types/update-types.js';
import { threadInFilterList } from '../thread-utils.js';
-export const updateThreadSpec: UpdateSpec<ThreadUpdateInfo> = Object.freeze({
+export const updateThreadSpec: UpdateSpec<
+ ThreadUpdateInfo,
+ ThreadRawUpdateInfo,
+> = Object.freeze({
generateOpsForThreadUpdates(
storeThreadInfos: RawThreadInfos,
update: ThreadUpdateInfo,
@@ -39,4 +46,13 @@
[...filteredThreadIDs].filter(id => id !== update.threadInfo.id),
);
},
+ rawUpdateInfoFromRow(row: Object) {
+ const { threadID } = JSON.parse(row.content);
+ return {
+ type: updateTypes.UPDATE_THREAD,
+ id: row.id.toString(),
+ time: row.time,
+ threadID,
+ };
+ },
});
diff --git a/lib/shared/updates/update-user-spec.js b/lib/shared/updates/update-user-spec.js
--- a/lib/shared/updates/update-user-spec.js
+++ b/lib/shared/updates/update-user-spec.js
@@ -1,6 +1,21 @@
// @flow
import type { UpdateSpec } from './update-spec.js';
-import type { UserUpdateInfo } from '../../types/update-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type {
+ UserUpdateInfo,
+ UserRawUpdateInfo,
+} from '../../types/update-types.js';
-export const updateUserSpec: UpdateSpec<UserUpdateInfo> = Object.freeze({});
+export const updateUserSpec: UpdateSpec<UserUpdateInfo, UserRawUpdateInfo> =
+ Object.freeze({
+ rawUpdateInfoFromRow(row: Object) {
+ const content = JSON.parse(row.content);
+ return {
+ type: updateTypes.UPDATE_USER,
+ id: row.id.toString(),
+ time: row.time,
+ updatedUserID: content.updatedUserID,
+ };
+ },
+ });
diff --git a/lib/types/update-types.js b/lib/types/update-types.js
--- a/lib/types/update-types.js
+++ b/lib/types/update-types.js
@@ -118,47 +118,47 @@
+id: string,
+time: number,
};
-type AccountDeletionRawUpdateInfo = {
+export type AccountDeletionRawUpdateInfo = {
...SharedRawUpdateInfo,
...AccountDeletionData,
+type: 0,
};
-type ThreadRawUpdateInfo = {
+export type ThreadRawUpdateInfo = {
...SharedRawUpdateInfo,
...ThreadData,
+type: 1,
};
-type ThreadReadStatusRawUpdateInfo = {
+export type ThreadReadStatusRawUpdateInfo = {
...SharedRawUpdateInfo,
...ThreadReadStatusData,
+type: 2,
};
-type ThreadDeletionRawUpdateInfo = {
+export type ThreadDeletionRawUpdateInfo = {
...SharedRawUpdateInfo,
...ThreadDeletionData,
+type: 3,
};
-type ThreadJoinRawUpdateInfo = {
+export type ThreadJoinRawUpdateInfo = {
...SharedRawUpdateInfo,
...ThreadJoinData,
+type: 4,
};
-type BadDeviceTokenRawUpdateInfo = {
+export type BadDeviceTokenRawUpdateInfo = {
...SharedRawUpdateInfo,
...BadDeviceTokenData,
+type: 5,
};
-type EntryRawUpdateInfo = {
+export type EntryRawUpdateInfo = {
...SharedRawUpdateInfo,
...EntryData,
+type: 6,
};
-type CurrentUserRawUpdateInfo = {
+export type CurrentUserRawUpdateInfo = {
...SharedRawUpdateInfo,
...CurrentUserData,
+type: 7,
};
-type UserRawUpdateInfo = {
+export type UserRawUpdateInfo = {
...SharedRawUpdateInfo,
...UserData,
+type: 8,

File Metadata

Mime Type
text/plain
Expires
Tue, Nov 26, 11:09 PM (21 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2586802
Default Alt Text
D9442.diff (21 KB)

Event Timeline