diff --git a/lib/shared/entry-utils.js b/lib/shared/entry-utils.js index d78aa40c9..6485eb5ea 100644 --- a/lib/shared/entry-utils.js +++ b/lib/shared/entry-utils.js @@ -1,264 +1,264 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual'; import { filteredThreadIDs, nonThreadCalendarFilters, filterExists, } from '../selectors/calendar-filter-selectors'; import type { RawEntryInfo, EntryInfo, CalendarQuery, } from '../types/entry-types'; import { calendarThreadFilterTypes } from '../types/filter-types'; import type { UserInfos } from '../types/user-types'; import { dateString, getDate, dateFromString } from '../utils/date-utils'; type HasEntryIDs = { localID?: string, id?: string, ... }; function entryKey(entryInfo: HasEntryIDs): string { if (entryInfo.localID) { return entryInfo.localID; } invariant(entryInfo.id, 'localID should exist if ID does not'); return entryInfo.id; } function entryID(entryInfo: HasEntryIDs): string { if (entryInfo.id) { return entryInfo.id; } invariant(entryInfo.localID, 'localID should exist if ID does not'); return entryInfo.localID; } function createEntryInfo( rawEntryInfo: RawEntryInfo, viewerID: ?string, userInfos: UserInfos, ): EntryInfo { const creatorInfo = userInfos[rawEntryInfo.creatorID]; return { id: rawEntryInfo.id, localID: rawEntryInfo.localID, threadID: rawEntryInfo.threadID, text: rawEntryInfo.text, year: rawEntryInfo.year, month: rawEntryInfo.month, day: rawEntryInfo.day, creationTime: rawEntryInfo.creationTime, - creator: creatorInfo && creatorInfo.username, + creator: creatorInfo, deleted: rawEntryInfo.deleted, }; } // Make sure EntryInfo is between startDate and endDate, and that if the // NOT_DELETED filter is active, the EntryInfo isn't deleted function rawEntryInfoWithinActiveRange( rawEntryInfo: RawEntryInfo, calendarQuery: CalendarQuery, ): boolean { const entryInfoDate = getDate( rawEntryInfo.year, rawEntryInfo.month, rawEntryInfo.day, ); const startDate = dateFromString(calendarQuery.startDate); const endDate = dateFromString(calendarQuery.endDate); if (entryInfoDate < startDate || entryInfoDate > endDate) { return false; } if ( rawEntryInfo.deleted && filterExists(calendarQuery.filters, calendarThreadFilterTypes.NOT_DELETED) ) { return false; } return true; } function rawEntryInfoWithinCalendarQuery( rawEntryInfo: RawEntryInfo, calendarQuery: CalendarQuery, ): boolean { if (!rawEntryInfoWithinActiveRange(rawEntryInfo, calendarQuery)) { return false; } const filterToThreadIDs = filteredThreadIDs(calendarQuery.filters); if (filterToThreadIDs && !filterToThreadIDs.has(rawEntryInfo.threadID)) { return false; } return true; } function filterRawEntryInfosByCalendarQuery( rawEntryInfos: { +[id: string]: RawEntryInfo }, calendarQuery: CalendarQuery, ): { +[id: string]: RawEntryInfo } { let filtered = false; const filteredRawEntryInfos = {}; for (const id in rawEntryInfos) { const rawEntryInfo = rawEntryInfos[id]; if (!rawEntryInfoWithinCalendarQuery(rawEntryInfo, calendarQuery)) { filtered = true; continue; } filteredRawEntryInfos[id] = rawEntryInfo; } return filtered ? filteredRawEntryInfos : rawEntryInfos; } function usersInRawEntryInfos( entryInfos: $ReadOnlyArray, ): string[] { const userIDs = new Set(); for (const entryInfo of entryInfos) { userIDs.add(entryInfo.creatorID); } return [...userIDs]; } // Note: fetchEntriesForSession expects that all of the CalendarQueries in the // resultant array either filter deleted entries or don't function calendarQueryDifference( oldCalendarQuery: CalendarQuery, newCalendarQuery: CalendarQuery, ): CalendarQuery[] { if (_isEqual(oldCalendarQuery)(newCalendarQuery)) { return []; } const deletedEntriesWereIncluded = filterExists( oldCalendarQuery.filters, calendarThreadFilterTypes.NOT_DELETED, ); const deletedEntriesAreIncluded = filterExists( newCalendarQuery.filters, calendarThreadFilterTypes.NOT_DELETED, ); if (!deletedEntriesWereIncluded && deletedEntriesAreIncluded) { // The new query includes all deleted entries, but the old one didn't. Since // we have no way to include ONLY deleted entries in a CalendarQuery, we // can't separate newCalendarQuery into a query for just deleted entries on // the old range, and a query for all entries on the full range. We'll have // to just query for the whole newCalendarQuery range directly. return [newCalendarQuery]; } const oldFilteredThreadIDs = filteredThreadIDs(oldCalendarQuery.filters); const newFilteredThreadIDs = filteredThreadIDs(newCalendarQuery.filters); if (oldFilteredThreadIDs && !newFilteredThreadIDs) { // The new query is for all thread IDs, but the old one had a THREAD_LIST. // Since we have no way to exclude particular thread IDs from a // CalendarQuery, we can't separate newCalendarQuery into a query for just // the new thread IDs on the old range, and a query for all the thread IDs // on the full range. We'll have to just query for the whole // newCalendarQuery range directly. return [newCalendarQuery]; } const difference = []; const oldStartDate = dateFromString(oldCalendarQuery.startDate); const oldEndDate = dateFromString(oldCalendarQuery.endDate); const newStartDate = dateFromString(newCalendarQuery.startDate); const newEndDate = dateFromString(newCalendarQuery.endDate); if ( oldFilteredThreadIDs && newFilteredThreadIDs && // This checks that there exists an intersection at all oldStartDate <= newEndDate && oldEndDate >= newStartDate ) { const newNotInOld = [...newFilteredThreadIDs].filter( x => !oldFilteredThreadIDs.has(x), ); if (newNotInOld.length > 0) { // In this case, we have added new threadIDs to the THREAD_LIST. // We should query the calendar range for these threads. const intersectionStartDate = oldStartDate < newStartDate ? newCalendarQuery.startDate : oldCalendarQuery.startDate; const intersectionEndDate = oldEndDate > newEndDate ? newCalendarQuery.endDate : oldCalendarQuery.endDate; difference.push({ startDate: intersectionStartDate, endDate: intersectionEndDate, filters: [ ...nonThreadCalendarFilters(newCalendarQuery.filters), { type: calendarThreadFilterTypes.THREAD_LIST, threadIDs: newNotInOld, }, ], }); } } if (newStartDate < oldStartDate) { const partialEndDate = new Date(oldStartDate.getTime()); partialEndDate.setDate(partialEndDate.getDate() - 1); difference.push({ filters: newCalendarQuery.filters, startDate: newCalendarQuery.startDate, endDate: dateString(partialEndDate), }); } if (newEndDate > oldEndDate) { const partialStartDate = new Date(oldEndDate.getTime()); partialStartDate.setDate(partialStartDate.getDate() + 1); difference.push({ filters: newCalendarQuery.filters, startDate: dateString(partialStartDate), endDate: newCalendarQuery.endDate, }); } return difference; } function serverEntryInfo(rawEntryInfo: RawEntryInfo): ?RawEntryInfo { const { id } = rawEntryInfo; if (!id) { return null; } const { localID, ...rest } = rawEntryInfo; return { ...rest }; // we only do this for Flow } function serverEntryInfosObject( array: $ReadOnlyArray, ): { +[id: string]: RawEntryInfo } { const obj = {}; for (const rawEntryInfo of array) { const entryInfo = serverEntryInfo(rawEntryInfo); if (!entryInfo) { continue; } const { id } = entryInfo; invariant(id, 'should be set'); obj[id] = entryInfo; } return obj; } export { entryKey, entryID, createEntryInfo, rawEntryInfoWithinActiveRange, rawEntryInfoWithinCalendarQuery, filterRawEntryInfosByCalendarQuery, usersInRawEntryInfos, calendarQueryDifference, serverEntryInfo, serverEntryInfosObject, }; diff --git a/lib/types/entry-types.js b/lib/types/entry-types.js index acc402cbc..03926eee0 100644 --- a/lib/types/entry-types.js +++ b/lib/types/entry-types.js @@ -1,210 +1,210 @@ // @flow import { fifteenDaysEarlier, fifteenDaysLater, thisMonthDates, } from '../utils/date-utils'; import type { Platform } from './device-types'; import { type CalendarFilter, defaultCalendarFilters } from './filter-types'; import type { RawMessageInfo } from './message-types'; import type { ServerCreateUpdatesResponse, ClientCreateUpdatesResponse, } from './update-types'; -import type { AccountUserInfo } from './user-types'; +import type { UserInfo, AccountUserInfo } from './user-types'; export type RawEntryInfo = { id?: string, // null if local copy without ID yet localID?: string, // for optimistic creations threadID: string, text: string, year: number, month: number, // 1-indexed day: number, // 1-indexed creationTime: number, // millisecond timestamp creatorID: string, deleted: boolean, }; export type EntryInfo = { id?: string, // null if local copy without ID yet localID?: string, // for optimistic creations threadID: string, text: string, year: number, month: number, // 1-indexed day: number, // 1-indexed creationTime: number, // millisecond timestamp - creator: ?string, + creator: ?UserInfo, deleted: boolean, }; export type EntryStore = { +entryInfos: { +[id: string]: RawEntryInfo }, +daysToEntries: { +[day: string]: string[] }, +lastUserInteractionCalendar: number, }; export type CalendarQuery = { +startDate: string, +endDate: string, +filters: $ReadOnlyArray, }; export const defaultCalendarQuery = ( platform: ?Platform, timeZone?: ?string, ): CalendarQuery => { if (platform === 'web') { return { ...thisMonthDates(timeZone), filters: defaultCalendarFilters, }; } else { return { startDate: fifteenDaysEarlier(timeZone).valueOf(), endDate: fifteenDaysLater(timeZone).valueOf(), filters: defaultCalendarFilters, }; } }; export type SaveEntryInfo = { +entryID: string, +text: string, +prevText: string, +timestamp: number, +calendarQuery: CalendarQuery, }; export type SaveEntryRequest = { +entryID: string, +text: string, +prevText: string, +timestamp: number, +calendarQuery?: CalendarQuery, }; export type SaveEntryResponse = { +entryID: string, +newMessageInfos: $ReadOnlyArray, +updatesResult: ServerCreateUpdatesResponse, }; export type SaveEntryResult = { +entryID: string, +newMessageInfos: $ReadOnlyArray, +updatesResult: ClientCreateUpdatesResponse, }; export type SaveEntryPayload = { ...SaveEntryResult, +threadID: string, }; export type CreateEntryInfo = { +text: string, +timestamp: number, +date: string, +threadID: string, +localID: string, +calendarQuery: CalendarQuery, }; export type CreateEntryRequest = { +text: string, +timestamp: number, +date: string, +threadID: string, +localID?: string, +calendarQuery?: CalendarQuery, }; export type CreateEntryPayload = { ...SaveEntryPayload, +localID: string, }; export type DeleteEntryInfo = { +entryID: string, +prevText: string, +calendarQuery: CalendarQuery, }; export type DeleteEntryRequest = { +entryID: string, +prevText: string, +timestamp: number, +calendarQuery?: CalendarQuery, }; export type RestoreEntryInfo = { +entryID: string, +calendarQuery: CalendarQuery, }; export type RestoreEntryRequest = { +entryID: string, +timestamp: number, +calendarQuery?: CalendarQuery, }; export type DeleteEntryResponse = { +newMessageInfos: $ReadOnlyArray, +threadID: string, +updatesResult: ServerCreateUpdatesResponse, }; export type DeleteEntryResult = { +newMessageInfos: $ReadOnlyArray, +threadID: string, +updatesResult: ClientCreateUpdatesResponse, }; export type RestoreEntryResponse = { +newMessageInfos: $ReadOnlyArray, +updatesResult: ServerCreateUpdatesResponse, }; export type RestoreEntryResult = { +newMessageInfos: $ReadOnlyArray, +updatesResult: ClientCreateUpdatesResponse, }; export type RestoreEntryPayload = { ...RestoreEntryResult, +threadID: string, }; export type FetchEntryInfosBase = { +rawEntryInfos: $ReadOnlyArray, }; export type FetchEntryInfosResponse = { ...FetchEntryInfosBase, +userInfos: { [id: string]: AccountUserInfo }, }; export type FetchEntryInfosResult = FetchEntryInfosBase; export type DeltaEntryInfosResponse = { +rawEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, }; export type DeltaEntryInfosResult = { +rawEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export type CalendarResult = { +rawEntryInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type CalendarQueryUpdateStartingPayload = { +calendarQuery?: CalendarQuery, }; export type CalendarQueryUpdateResult = { +rawEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, +calendarQuery: CalendarQuery, +calendarQueryAlreadyUpdated: boolean, }; diff --git a/web/modals/history/history-entry.react.js b/web/modals/history/history-entry.react.js index 96c3109a5..2f017e2f8 100644 --- a/web/modals/history/history-entry.react.js +++ b/web/modals/history/history-entry.react.js @@ -1,183 +1,187 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { restoreEntryActionTypes, restoreEntry, } from 'lib/actions/entry-actions'; +import { useENSNames } from 'lib/hooks/ens-cache'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { colorIsDark } from 'lib/shared/thread-utils'; import { type EntryInfo, type RestoreEntryInfo, type RestoreEntryResult, type CalendarQuery, } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { ThreadInfo } from 'lib/types/thread-types'; +import type { UserInfo } from 'lib/types/user-types'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import LoadingIndicator from '../../loading-indicator.react'; import { useSelector } from '../../redux/redux-utils'; import { nonThreadCalendarQuery } from '../../selectors/nav-selectors'; import css from './history.css'; type BaseProps = { +entryInfo: EntryInfo, +onClick: (entryID: string) => void, +animateAndLoadEntry: (entryID: string) => void, }; type Props = { ...BaseProps, +threadInfo: ThreadInfo, +loggedIn: boolean, +restoreLoadingStatus: LoadingStatus, +calendarQuery: () => CalendarQuery, +dispatchActionPromise: DispatchActionPromise, +restoreEntry: (info: RestoreEntryInfo) => Promise, + +creator: ?UserInfo, }; class HistoryEntry extends React.PureComponent { render() { let deleted = null; if (this.props.entryInfo.deleted) { let restore = null; if (this.props.loggedIn) { restore = ( ( restore ) ); } deleted = ( deleted {restore} ); } const textClasses = classNames({ [css.entry]: true, [css.darkEntry]: colorIsDark(this.props.threadInfo.color), }); const textStyle = { backgroundColor: '#' + this.props.threadInfo.color }; - const creator = - this.props.entryInfo.creator === null ? ( - 'Anonymous' - ) : ( - - {this.props.entryInfo.creator} - - ); + const creator = this.props.creator?.username ? ( + {this.props.creator.username} + ) : ( + 'anonymous' + ); return (
  • {this.props.entryInfo.text}
    {'created by '} {creator} {this.props.threadInfo.uiName}
  • ); } onRestore = (event: SyntheticEvent) => { event.preventDefault(); const entryID = this.props.entryInfo.id; invariant(entryID, 'entryInfo.id (serverID) should be set'); this.props.dispatchActionPromise( restoreEntryActionTypes, this.restoreEntryAction(), { customKeyName: `${restoreEntryActionTypes.started}:${entryID}` }, ); }; onClick = (event: SyntheticEvent) => { event.preventDefault(); const entryID = this.props.entryInfo.id; invariant(entryID, 'entryInfo.id (serverID) should be set'); this.props.onClick(entryID); }; async restoreEntryAction() { const entryID = this.props.entryInfo.id; invariant(entryID, 'entry should have ID'); const result = await this.props.restoreEntry({ entryID, calendarQuery: this.props.calendarQuery(), }); this.props.animateAndLoadEntry(entryID); return { ...result, threadID: this.props.threadInfo.id }; } } const ConnectedHistoryEntry: React.ComponentType = React.memo( function ConnectedHistoryEntry(props) { const entryID = props.entryInfo.id; invariant(entryID, 'entryInfo.id (serverID) should be set'); const threadInfo = useSelector( state => threadInfoSelector(state)[props.entryInfo.threadID], ); const loggedIn = useSelector( state => !!(state.currentUserInfo && !state.currentUserInfo.anonymous && true), ); const restoreLoadingStatus = useSelector( createLoadingStatusSelector( restoreEntryActionTypes, `${restoreEntryActionTypes.started}:${entryID}`, ), ); - const calanderQuery = useSelector(nonThreadCalendarQuery); + const calenderQuery = useSelector(nonThreadCalendarQuery); const callRestoreEntry = useServerCall(restoreEntry); const dispatchActionPromise = useDispatchActionPromise(); + const { creator } = props.entryInfo; + const [creatorWithENSName] = useENSNames([creator]); + return ( ); }, ); export default ConnectedHistoryEntry;