diff --git a/lib/utils/url-utils.js b/lib/utils/url-utils.js index d1f6954ab..6325aa08c 100644 --- a/lib/utils/url-utils.js +++ b/lib/utils/url-utils.js @@ -1,87 +1,98 @@ // @flow import urlParseLax from 'url-parse-lax'; import { locallyUniqueThreadIDRegex } from '../shared/thread-utils'; export type URLInfo = { +year?: number, +month?: number, // 1-indexed +verify?: string, +calendar?: boolean, +chat?: boolean, +apps?: boolean, +thread?: string, +settings?: 'account' | 'danger-zone', + +threadCreation?: boolean, + +selectedUserList?: $ReadOnlyArray, ... }; // We use groups to capture parts of the URL and any changes // to regexes must be reflected in infoFromURL. const yearRegex = new RegExp('(/|^)year/([0-9]+)(/|$)', 'i'); const monthRegex = new RegExp('(/|^)month/([0-9]+)(/|$)', 'i'); const threadRegex = new RegExp('(/|^)thread/([0-9]+)(/|$)', 'i'); const verifyRegex = new RegExp('(/|^)verify/([a-f0-9]+)(/|$)', 'i'); const calendarRegex = new RegExp('(/|^)calendar(/|$)', 'i'); const chatRegex = new RegExp('(/|^)chat(/|$)', 'i'); const appsRegex = new RegExp('(/|^)apps(/|$)', 'i'); const accountSettingsRegex = new RegExp('(/|^)settings/account(/|$)', 'i'); const dangerZoneRegex = new RegExp('(/|^)settings/danger-zone(/|$)', 'i'); const threadPendingRegex = new RegExp( `(/|^)thread/(${locallyUniqueThreadIDRegex})(/|$)`, 'i', ); +const threadCreationRegex = new RegExp( + '(/|^)thread/new(/([0-9]+([+][0-9]+)*))?(/|$)', + 'i', +); function infoFromURL(url: string): URLInfo { const yearMatches = yearRegex.exec(url); const monthMatches = monthRegex.exec(url); const threadMatches = threadRegex.exec(url); const verifyMatches = verifyRegex.exec(url); const calendarTest = calendarRegex.test(url); const chatTest = chatRegex.test(url); const appsTest = appsRegex.test(url); const accountSettingsTest = accountSettingsRegex.test(url); const dangerZoneTest = dangerZoneRegex.test(url); const threadPendingMatches = threadPendingRegex.exec(url); + const threadCreateMatches = threadCreationRegex.exec(url); const returnObj = {}; if (yearMatches) { returnObj.year = parseInt(yearMatches[2], 10); } if (monthMatches) { const month = parseInt(monthMatches[2], 10); if (month < 1 || month > 12) { throw new Error('invalid_month'); } returnObj.month = month; } if (threadMatches) { returnObj.thread = threadMatches[2]; } if (threadPendingMatches) { returnObj.thread = threadPendingMatches[2]; } + if (threadCreateMatches) { + returnObj.threadCreation = true; + returnObj.selectedUserList = threadCreateMatches[3]?.split('+') ?? []; + } if (verifyMatches) { returnObj.verify = verifyMatches[2]; } if (calendarTest) { returnObj.calendar = true; } else if (chatTest) { returnObj.chat = true; } else if (appsTest) { returnObj.apps = true; } else if (accountSettingsTest) { returnObj.settings = 'account'; } else if (dangerZoneTest) { returnObj.settings = 'danger-zone'; } return returnObj; } function normalizeURL(url: string): string { return urlParseLax(url).href; } const setURLPrefix = 'SET_URL_PREFIX'; export { infoFromURL, normalizeURL, setURLPrefix }; diff --git a/web/types/nav-types.js b/web/types/nav-types.js index b7645b59c..78cb59311 100644 --- a/web/types/nav-types.js +++ b/web/types/nav-types.js @@ -1,18 +1,22 @@ // @flow import type { BaseNavInfo } from 'lib/types/nav-types'; import type { ThreadInfo } from 'lib/types/thread-types'; export type NavigationTab = 'calendar' | 'chat' | 'apps' | 'settings'; export type NavigationSettingsSection = 'account' | 'danger-zone'; +export type NavigationChatMode = 'view' | 'create'; + export type NavInfo = { ...$Exact, +tab: NavigationTab, +activeChatThreadID: ?string, +pendingThread?: ThreadInfo, +settingsSection?: NavigationSettingsSection, + +selectedUserList?: $ReadOnlyArray, + +chatMode?: NavigationChatMode, }; export const updateNavInfoActionType = 'UPDATE_NAV_INFO'; diff --git a/web/url-utils.js b/web/url-utils.js index 1adb2a6f6..8812b541c 100644 --- a/web/url-utils.js +++ b/web/url-utils.js @@ -1,128 +1,138 @@ // @flow import invariant from 'invariant'; import { startDateForYearAndMonth, endDateForYearAndMonth, } from 'lib/utils/date-utils'; import { infoFromURL } from 'lib/utils/url-utils'; import { yearExtractor, monthExtractor } from './selectors/nav-selectors'; import type { NavInfo } from './types/nav-types'; function canonicalURLFromReduxState( navInfo: NavInfo, currentURL: string, loggedIn: boolean, ): string { const urlInfo = infoFromURL(currentURL); const today = new Date(); let newURL = `/`; if (loggedIn) { newURL += `${navInfo.tab}/`; if (navInfo.tab === 'calendar') { const { startDate, endDate } = navInfo; const year = yearExtractor(startDate, endDate); if (urlInfo.year !== undefined) { invariant( year !== null && year !== undefined, `${startDate} and ${endDate} aren't in the same year`, ); newURL += `year/${year}/`; } else if ( year !== null && year !== undefined && year !== today.getFullYear() ) { newURL += `year/${year}/`; } const month = monthExtractor(startDate, endDate); if (urlInfo.month !== undefined) { invariant( month !== null && month !== undefined, `${startDate} and ${endDate} aren't in the same month`, ); newURL += `month/${month}/`; } else if ( month !== null && month !== undefined && month !== today.getMonth() + 1 ) { newURL += `month/${month}/`; } } else if (navInfo.tab === 'chat') { - const activeChatThreadID = navInfo.activeChatThreadID; - if (activeChatThreadID) { - newURL += `thread/${activeChatThreadID}/`; + if (navInfo.chatMode === 'create') { + const users = navInfo.selectedUserList?.join('+') ?? ''; + const potentiallyTrailingSlash = users.length > 0 ? '/' : ''; + newURL += `thread/new/${users}${potentiallyTrailingSlash}`; + } else { + const activeChatThreadID = navInfo.activeChatThreadID; + if (activeChatThreadID) { + newURL += `thread/${activeChatThreadID}/`; + } } } else if (navInfo.tab === 'settings' && navInfo.settingsSection) { newURL += `${navInfo.settingsSection}/`; } } return newURL; } // Given a URL, this function parses out a navInfo object, leaving values as // default if they are unspecified. function navInfoFromURL( url: string, backupInfo: { now?: Date, navInfo?: NavInfo }, ): NavInfo { const urlInfo = infoFromURL(url); const { navInfo } = backupInfo; const now = backupInfo.now ? backupInfo.now : new Date(); let year = urlInfo.year; if (!year && navInfo) { year = yearExtractor(navInfo.startDate, navInfo.endDate); } if (!year) { year = now.getFullYear(); } let month = urlInfo.month; if (!month && navInfo) { month = monthExtractor(navInfo.startDate, navInfo.endDate); } if (!month) { month = now.getMonth() + 1; } let activeChatThreadID = null; if (urlInfo.thread) { activeChatThreadID = urlInfo.thread.toString(); } else if (navInfo) { activeChatThreadID = navInfo.activeChatThreadID; } let tab = 'chat'; if (urlInfo.calendar) { tab = 'calendar'; } else if (urlInfo.apps) { tab = 'apps'; } else if (urlInfo.settings) { tab = 'settings'; } - const newNavInfo = { + const chatMode = urlInfo.threadCreation ? 'create' : 'view'; + + const newNavInfo: NavInfo = { tab, startDate: startDateForYearAndMonth(year, month), endDate: endDateForYearAndMonth(year, month), activeChatThreadID, + chatMode, }; - if (!urlInfo.settings) { - return newNavInfo; + if (urlInfo.selectedUserList) { + newNavInfo.selectedUserList = urlInfo.selectedUserList; } - return { - ...newNavInfo, - settingsSection: urlInfo.settings, - }; + if (urlInfo.settings) { + newNavInfo.settingsSection = urlInfo.settings; + } + + return newNavInfo; } export { canonicalURLFromReduxState, navInfoFromURL };