diff --git a/keyserver/.flowconfig b/keyserver/.flowconfig index 61cf49e24..ce04bf1cf 100644 --- a/keyserver/.flowconfig +++ b/keyserver/.flowconfig @@ -1,43 +1,42 @@ [ignore] /dist /node_modules/google-gax/node_modules/grpc/node_modules/protobufjs/src/bower.json .*/node_modules/protobufjs/src/bower.json .*/web/flow-typed [include] ../landing ../lib ../web [libs] ../lib/flow-typed ../web/flow-typed [options] module.file_ext=.js module.file_ext=.cjs module.file_ext=.json exact_by_default=true format.bracket_spacing=false [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn unnecessary-invariant=warn -signature-verification-failure=warn [strict] deprecated-type nonstrict-import sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import diff --git a/keyserver/package.json b/keyserver/package.json index ed749a612..d51176865 100644 --- a/keyserver/package.json +++ b/keyserver/package.json @@ -1,102 +1,102 @@ { "name": "keyserver", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "main": "dist/keyserver", "scripts": { "clean": "rm -rf dist/ && rm -rf node_modules/ && mkdir dist", "babel-build": ". bash/source-nvm.sh && yarn --silent babel src/ --out-dir dist/ --config-file ./babel.config.cjs --verbose --ignore 'src/landing/flow-typed','src/landing/node_modules','src/landing/package.json','src/lib/flow-typed','src/lib/node_modules','src/lib/package.json','src/web/flow-typed','src/web/node_modules','src/web/package.json','src/web/dist','src/web/webpack.config.js','src/web/account-bar.react.js','src/web/app.react.js','src/web/calendar','src/web/chat','src/web/flow','src/web/loading-indicator.react.js','src/web/modals','src/web/root.js','src/web/router-history.js','src/web/script.js','src/web/selectors/chat-selectors.js','src/web/selectors/entry-selectors.js','src/web/splash','src/web/vector-utils.js','src/web/vectors.react.js'", "rsync": "rsync -rLpmuv --exclude '*/package.json' --exclude '*/node_modules/*' --include '*.json' --include '*.cjs' --include '*.node' --exclude '*.*' src/ dist/", "prod-build": "yarn babel-build && yarn rsync && yarn update-geoip", "update-geoip": "yarn script dist/scripts/update-geoip.js", "prod": "node --trace-warnings --experimental-json-modules --loader=./loader.mjs --experimental-specifier-resolution=node dist/keyserver", "dev-rsync": "yarn --silent chokidar --initial --silent -s 'src/**/*.json' 'src/**/*.cjs' -c 'yarn rsync > /dev/null 2>&1'", "dev": ". bash/source-nvm.sh && yarn concurrently --names=\"BABEL,RSYNC,NODEM\" -c \"bgBlue.bold,bgMagenta.bold,bgGreen.bold\" \"yarn babel-build --source-maps --watch\" \"yarn dev-rsync\" \". bash/source-nvm.sh && NODE_ENV=development nodemon -e js,json,cjs --watch dist --experimental-json-modules --loader=./loader.mjs --experimental-specifier-resolution=node dist/keyserver\"", "script": ". bash/source-nvm.sh && NODE_ENV=development node --experimental-json-modules --loader=./loader.mjs --experimental-specifier-resolution=node", "test": "jest" }, "devDependencies": { "@babel/cli": "^7.13.14", "@babel/core": "^7.13.14", "@babel/node": "^7.13.13", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "babel-jest": "^26.6.3", "chokidar-cli": "^2.1.0", "concurrently": "^5.3.0", - "flow-bin": "^0.158.0", + "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "jest": "^26.6.3", "nodemon": "^2.0.4" }, "dependencies": { "@babel/runtime": "^7.13.10", "@grpc/grpc-js": "^1.7.1", "@grpc/proto-loader": "^0.7.3", "@matrix-org/olm": "3.2.4", "@parse/node-apn": "^3.2.0", "@vingle/bmp-js": "^0.2.5", "JSONStream": "^1.3.5", "common-tags": "^1.7.2", "cookie-parser": "^1.4.3", "dateformat": "^3.0.3", "ethers": "^5.7.2", "express": "^4.17.3", "express-ws": "^4.0.0", "firebase-admin": "^10.1.0", "geoip-lite": "^1.4.5", "invariant": "^2.2.4", "landing": "0.0.1", "lib": "0.0.1", "lodash": "^4.17.21", "multer": "^1.4.1", "mysql2": "^2.3.3", "node-schedule": "^2.1.0", "nodemailer": "^6.6.1", "opaque-ke-napi": "0.0.1", "react": "18.1.0", "react-dom": "18.1.0", "react-html-email": "^3.0.0", "react-redux": "^7.1.1", "react-router": "^5.2.0", "redis": "^3.1.1", "redux": "^4.0.4", "replacestream": "^4.0.3", "rereadable-stream": "^1.4.5", "sharp": "^0.30.5", "siwe": "^1.1.6", "sql-template-strings": "^2.2.2", "stream-combiner": "^0.2.2", "tcomb": "^3.2.29", "twin-bcrypt": "^2.1.1", "uuid": "^3.3.3", "web": "0.0.1" }, "optionalDependencies": { "bufferutil": "^4.0.5", "utf-8-validate": "^5.0.7" }, "nodemonConfig": { "delay": "200" }, "jest": { "roots": [ "/src" ], "transform": { "\\.js$": "babel-jest" }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ] } } diff --git a/keyserver/src/database/database.js b/keyserver/src/database/database.js index 25be77cc4..8d6e9d6d8 100644 --- a/keyserver/src/database/database.js +++ b/keyserver/src/database/database.js @@ -1,206 +1,206 @@ // @flow import type { ConnectionOptions, QueryResults, PoolOptions } from 'mysql'; import mysql from 'mysql2'; import mysqlPromise from 'mysql2/promise'; import SQL from 'sql-template-strings'; import { getScriptContext } from '../scripts/script-context'; import { connectionLimit, queryWarnTime } from './consts'; import { getDBConfig } from './db-config'; import DatabaseMonitor from './monitor'; import type { Pool, SQLOrString, SQLStatementType } from './types'; const SQLStatement: SQLStatementType = SQL.SQLStatement; let migrationConnection; async function getMigrationConnection() { if (migrationConnection) { return migrationConnection; } const { dbType, ...dbConfig } = await getDBConfig(); const options: ConnectionOptions = dbConfig; migrationConnection = await mysqlPromise.createConnection(options); return migrationConnection; } let pool, databaseMonitor; async function loadPool(): Promise { if (pool) { return pool; } const scriptContext = getScriptContext(); const { dbType, ...dbConfig } = await getDBConfig(); const options: PoolOptions = { ...dbConfig, connectionLimit, multipleStatements: !!( scriptContext && scriptContext.allowMultiStatementSQLQueries ), }; // This function can be run asynchronously multiple times, // the previous check is not enough because the function will await // on `getDBConfig()` and as result we might get there // while the pool is already defined, which will result with // creating a new pool and losing the previous one which will stay open if (pool) { return pool; } pool = mysqlPromise.createPool(options); databaseMonitor = new DatabaseMonitor(pool); return pool; } function endPool() { pool?.end(); } function appendSQLArray( sql: SQLStatementType, sqlArray: $ReadOnlyArray, delimeter: SQLOrString, ): SQLStatementType { if (sqlArray.length === 0) { return sql; } const [first, ...rest] = sqlArray; sql.append(first); if (rest.length === 0) { return sql; } return rest.reduce( (prev: SQLStatementType, curr: SQLStatementType) => prev.append(delimeter).append(curr), sql, ); } function mergeConditions( conditions: $ReadOnlyArray, delimiter: SQLStatementType, ): SQLStatementType { const sql = SQL` (`; appendSQLArray(sql, conditions, delimiter); sql.append(SQL`) `); return sql; } function mergeAndConditions( andConditions: $ReadOnlyArray, ): SQLStatementType { return mergeConditions(andConditions, SQL` AND `); } function mergeOrConditions( andConditions: $ReadOnlyArray, ): SQLStatementType { return mergeConditions(andConditions, SQL` OR `); } // We use this fake result for dry runs -function FakeSQLResult() { - this.insertId = -1; -} -FakeSQLResult.prototype = Array.prototype; -const fakeResult: QueryResults = (new FakeSQLResult(): any); +const fakeResult: QueryResults = (() => { + const result: any = []; + result.insertId = -1; + return result; +})(); const MYSQL_DEADLOCK_ERROR_CODE = 1213; type ConnectionContext = { +migrationsActive?: boolean, }; let connectionContext = { migrationsActive: false, }; function setConnectionContext(newContext: ConnectionContext) { connectionContext = { ...connectionContext, ...newContext, }; if (!connectionContext.migrationsActive && migrationConnection) { migrationConnection.end(); migrationConnection = undefined; } } type QueryOptions = { +triesLeft?: number, +multipleStatements?: boolean, }; async function dbQuery( statement: SQLStatementType, options?: QueryOptions, ): Promise { const triesLeft = options?.triesLeft ?? 2; const multipleStatements = options?.multipleStatements ?? false; let connection; if (connectionContext.migrationsActive) { connection = await getMigrationConnection(); } if (multipleStatements) { connection = await getMultipleStatementsConnection(); } if (!connection) { connection = await loadPool(); } const timeoutID = setTimeout( () => databaseMonitor.reportLaggingQuery(statement.sql), queryWarnTime, ); const scriptContext = getScriptContext(); try { const sql = statement.sql.trim(); if ( scriptContext && scriptContext.dryRun && (sql.startsWith('INSERT') || sql.startsWith('DELETE') || sql.startsWith('UPDATE')) ) { console.log(rawSQL(statement)); return ([fakeResult]: any); } return await connection.query(statement); } catch (e) { if (e.errno === MYSQL_DEADLOCK_ERROR_CODE && triesLeft > 0) { console.log('deadlock occurred, trying again', e); return await dbQuery(statement, { ...options, triesLeft: triesLeft - 1 }); } e.query = statement.sql; throw e; } finally { clearTimeout(timeoutID); if (multipleStatements) { connection.end(); } } } function rawSQL(statement: SQLStatementType): string { return mysql.format(statement.sql, statement.values); } async function getMultipleStatementsConnection() { const { dbType, ...dbConfig } = await getDBConfig(); const options: ConnectionOptions = { ...dbConfig, multipleStatements: true, }; return await mysqlPromise.createConnection(options); } export { endPool, SQL, SQLStatement, appendSQLArray, mergeAndConditions, mergeOrConditions, setConnectionContext, dbQuery, rawSQL, }; diff --git a/keyserver/src/updaters/thread-permission-updaters.js b/keyserver/src/updaters/thread-permission-updaters.js index ec7638f42..2f40f4b14 100644 --- a/keyserver/src/updaters/thread-permission-updaters.js +++ b/keyserver/src/updaters/thread-permission-updaters.js @@ -1,1296 +1,1300 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual'; import bots from 'lib/facts/bots'; import genesis from 'lib/facts/genesis'; import { makePermissionsBlob, makePermissionsForChildrenBlob, getRoleForPermissions, } from 'lib/permissions/thread-permissions'; import type { CalendarQuery } from 'lib/types/entry-types'; import { type ThreadPermissionsBlob, type ThreadRolePermissionsBlob, type ThreadType, assertThreadType, } from 'lib/types/thread-types'; import { updateTypes, type ServerUpdateInfo, type CreateUpdatesResult, } from 'lib/types/update-types'; import { pushAll } from 'lib/utils/array'; import { ServerError } from 'lib/utils/errors'; import { createUpdates, type UpdatesForCurrentSession, } from '../creators/update-creator'; import { dbQuery, SQL } from '../database/database'; import { fetchServerThreadInfos, rawThreadInfosFromServerThreadInfos, type FetchThreadInfosResult, } from '../fetchers/thread-fetchers'; import { rescindPushNotifs } from '../push/rescind'; import { createScriptViewer } from '../session/scripts'; import type { Viewer } from '../session/viewer'; import { updateRoles } from '../updaters/role-updaters'; import DepthQueue from '../utils/depth-queue'; import RelationshipChangeset from '../utils/relationship-changeset'; import { updateChangedUndirectedRelationships } from './relationship-updaters'; export type MembershipRowToSave = { +operation: 'save', +intent: 'join' | 'leave' | 'none', +userID: string, +threadID: string, +userNeedsFullThreadDetails: boolean, +permissions: ?ThreadPermissionsBlob, +permissionsForChildren: ?ThreadPermissionsBlob, // null role represents by "0" +role: string, +oldRole: string, +unread?: boolean, }; type MembershipRowToDelete = { +operation: 'delete', +intent: 'join' | 'leave' | 'none', +userID: string, +threadID: string, +oldRole: string, }; type MembershipRow = MembershipRowToSave | MembershipRowToDelete; type Changeset = { +membershipRows: MembershipRow[], +relationshipChangeset: RelationshipChangeset, }; // 0 role means to remove the user from the thread // null role means to set the user to the default role // string role means to set the user to the role with that ID // -1 role means to set the user as a "ghost" (former member) type ChangeRoleOptions = { +setNewMembersToUnread?: boolean, }; type ChangeRoleMemberInfo = { permissionsFromParent?: ?ThreadPermissionsBlob, memberOfContainingThread?: boolean, }; async function changeRole( threadID: string, userIDs: $ReadOnlyArray, role: string | -1 | 0 | null, options?: ChangeRoleOptions, ): Promise { const intent = role === -1 || role === 0 ? 'leave' : 'join'; const setNewMembersToUnread = options?.setNewMembersToUnread && intent === 'join'; if (userIDs.length === 0) { return { membershipRows: [], relationshipChangeset: new RelationshipChangeset(), }; } const membershipQuery = SQL` SELECT user, role, permissions, permissions_for_children FROM memberships WHERE thread = ${threadID} `; const parentMembershipQuery = SQL` SELECT pm.user, pm.permissions_for_children AS permissions_from_parent FROM threads t INNER JOIN memberships pm ON pm.thread = t.parent_thread_id WHERE t.id = ${threadID} AND (pm.user IN (${userIDs}) OR t.parent_thread_id != ${genesis.id}) `; const containingMembershipQuery = SQL` SELECT cm.user, cm.role AS containing_role FROM threads t INNER JOIN memberships cm ON cm.thread = t.containing_thread_id WHERE t.id = ${threadID} AND cm.user IN (${userIDs}) `; const [ [membershipResults], [parentMembershipResults], containingMembershipResults, roleThreadResult, ] = await Promise.all([ dbQuery(membershipQuery), dbQuery(parentMembershipQuery), (async () => { if (intent === 'leave') { // Membership in the container only needs to be checked for members return []; } const [result] = await dbQuery(containingMembershipQuery); return result; })(), changeRoleThreadQuery(threadID, role), ]); const { roleColumnValue: intendedRole, threadType, parentThreadID, hasContainingThreadID, rolePermissions: intendedRolePermissions, depth, } = roleThreadResult; const existingMembershipInfo = new Map(); for (const row of membershipResults) { const userID = row.user.toString(); existingMembershipInfo.set(userID, { oldRole: row.role.toString(), oldPermissions: JSON.parse(row.permissions), oldPermissionsForChildren: JSON.parse(row.permissions_for_children), }); } const ancestorMembershipInfo: Map = new Map(); for (const row of parentMembershipResults) { const userID = row.user.toString(); if (!userIDs.includes(userID)) { continue; } ancestorMembershipInfo.set(userID, { permissionsFromParent: JSON.parse(row.permissions_from_parent), }); } for (const row of containingMembershipResults) { const userID = row.user.toString(); const ancestorMembership = ancestorMembershipInfo.get(userID); const memberOfContainingThread = row.containing_role > 0; if (ancestorMembership) { ancestorMembership.memberOfContainingThread = memberOfContainingThread; } else { ancestorMembershipInfo.set(userID, { memberOfContainingThread, }); } } const relationshipChangeset = new RelationshipChangeset(); const existingMemberIDs = [...existingMembershipInfo.keys()]; if (threadID !== genesis.id) { relationshipChangeset.setAllRelationshipsExist(existingMemberIDs); } const parentMemberIDs = parentMembershipResults.map(row => row.user.toString(), ); if (parentThreadID && parentThreadID !== genesis.id) { relationshipChangeset.setAllRelationshipsExist(parentMemberIDs); } const membershipRows = []; const toUpdateDescendants = new Map(); for (const userID of userIDs) { const existingMembership = existingMembershipInfo.get(userID); const oldRole = existingMembership?.oldRole ?? '-1'; const oldPermissions = existingMembership?.oldPermissions ?? null; const oldPermissionsForChildren = existingMembership?.oldPermissionsForChildren ?? null; if (existingMembership && oldRole === intendedRole) { // If the old role is the same as the new one, we have nothing to update continue; } else if (Number(oldRole) > 0 && role === null) { // In the case where we're just trying to add somebody to a thread, if // they already have a role with a nonzero role then we don't need to do // anything continue; } let permissionsFromParent = null; let memberOfContainingThread = false; const ancestorMembership = ancestorMembershipInfo.get(userID); if (ancestorMembership) { permissionsFromParent = ancestorMembership.permissionsFromParent; memberOfContainingThread = ancestorMembership.memberOfContainingThread; } if (!hasContainingThreadID) { memberOfContainingThread = true; } const rolePermissions = memberOfContainingThread ? intendedRolePermissions : null; const targetRole = memberOfContainingThread ? intendedRole : '-1'; const permissions = makePermissionsBlob( rolePermissions, permissionsFromParent, threadID, threadType, ); const permissionsForChildren = makePermissionsForChildrenBlob(permissions); const newRole = getRoleForPermissions(targetRole, permissions); const userBecameMember = Number(oldRole) <= 0 && Number(newRole) > 0; const userLostMembership = Number(oldRole) > 0 && Number(newRole) <= 0; if ( (intent === 'join' && Number(newRole) <= 0) || (intent === 'leave' && Number(newRole) > 0) ) { throw new ServerError('invalid_parameters'); } else if (intendedRole !== newRole) { console.warn( `changeRole called for role=${intendedRole}, but ended up setting ` + `role=${newRole} for userID ${userID} and threadID ${threadID}, ` + 'probably because KNOW_OF permission was unexpectedly present or ' + 'missing', ); } if ( existingMembership && _isEqual(permissions)(oldPermissions) && oldRole === newRole ) { // This thread and all of its descendants need no updates for this user, // since the corresponding memberships row is unchanged by this operation continue; } if (permissions) { membershipRows.push({ operation: 'save', intent, userID, threadID, userNeedsFullThreadDetails: userBecameMember, permissions, permissionsForChildren, role: newRole, oldRole, unread: userBecameMember && setNewMembersToUnread, }); } else { membershipRows.push({ operation: 'delete', intent, userID, threadID, oldRole, }); } if (permissions && !existingMembership && threadID !== genesis.id) { relationshipChangeset.setRelationshipsNeeded(userID, existingMemberIDs); } if ( userLostMembership || !_isEqual(permissionsForChildren)(oldPermissionsForChildren) ) { toUpdateDescendants.set(userID, { userIsMember: Number(newRole) > 0, permissionsForChildren, }); } } if (toUpdateDescendants.size > 0) { const { membershipRows: descendantMembershipRows, relationshipChangeset: descendantRelationshipChangeset, } = await updateDescendantPermissions({ threadID, depth, changesByUser: toUpdateDescendants, }); pushAll(membershipRows, descendantMembershipRows); relationshipChangeset.addAll(descendantRelationshipChangeset); } return { membershipRows, relationshipChangeset }; } type RoleThreadResult = { +roleColumnValue: string, +depth: number, +threadType: ThreadType, +parentThreadID: ?string, +hasContainingThreadID: boolean, +rolePermissions: ?ThreadRolePermissionsBlob, }; async function changeRoleThreadQuery( threadID: string, role: string | -1 | 0 | null, ): Promise { if (role === 0 || role === -1) { const query = SQL` SELECT type, depth, parent_thread_id, containing_thread_id FROM threads WHERE id = ${threadID} `; const [result] = await dbQuery(query); if (result.length === 0) { throw new ServerError('internal_error'); } const row = result[0]; return { roleColumnValue: role.toString(), depth: row.depth, threadType: assertThreadType(row.type), parentThreadID: row.parent_thread_id ? row.parent_thread_id.toString() : null, hasContainingThreadID: row.containing_thread_id !== null, rolePermissions: null, }; } else if (role !== null) { const query = SQL` SELECT t.type, t.depth, t.parent_thread_id, t.containing_thread_id, r.permissions FROM threads t INNER JOIN roles r ON r.thread = t.id AND r.id = ${role} WHERE t.id = ${threadID} `; const [result] = await dbQuery(query); if (result.length === 0) { throw new ServerError('internal_error'); } const row = result[0]; return { roleColumnValue: role, depth: row.depth, threadType: assertThreadType(row.type), parentThreadID: row.parent_thread_id ? row.parent_thread_id.toString() : null, hasContainingThreadID: row.containing_thread_id !== null, rolePermissions: JSON.parse(row.permissions), }; } else { const query = SQL` SELECT t.type, t.depth, t.parent_thread_id, t.containing_thread_id, t.default_role, r.permissions FROM threads t INNER JOIN roles r ON r.thread = t.id AND r.id = t.default_role WHERE t.id = ${threadID} `; const [result] = await dbQuery(query); if (result.length === 0) { throw new ServerError('internal_error'); } const row = result[0]; return { roleColumnValue: row.default_role.toString(), depth: row.depth, threadType: assertThreadType(row.type), parentThreadID: row.parent_thread_id ? row.parent_thread_id.toString() : null, hasContainingThreadID: row.containing_thread_id !== null, rolePermissions: JSON.parse(row.permissions), }; } } type ChangedAncestor = { +threadID: string, +depth: number, +changesByUser: Map, }; type AncestorChanges = { +userIsMember: boolean, +permissionsForChildren: ?ThreadPermissionsBlob, }; async function updateDescendantPermissions( initialChangedAncestor: ChangedAncestor, ): Promise { const membershipRows = []; const relationshipChangeset = new RelationshipChangeset(); const initialDescendants = await fetchDescendantsForUpdate([ initialChangedAncestor, ]); const depthQueue = new DepthQueue( getDescendantDepth, getDescendantKey, mergeDescendants, ); depthQueue.addInfos(initialDescendants); let descendants; while ((descendants = depthQueue.getNextDepth())) { const descendantsAsAncestors = []; for (const descendant of descendants) { const { threadID, threadType, depth, users } = descendant; const existingMembers = [...users.entries()]; const existingMemberIDs = existingMembers .filter(([, { curRole }]) => curRole) .map(([userID]) => userID); if (threadID !== genesis.id) { relationshipChangeset.setAllRelationshipsExist(existingMemberIDs); } const usersForNextLayer = new Map(); for (const [userID, user] of users) { const { curRolePermissions, curPermissionsFromParent, curMemberOfContainingThread, nextMemberOfContainingThread, nextPermissionsFromParent, potentiallyNeedsUpdate, } = user; const existingMembership = !!user.curRole; const curRole = user.curRole ?? '-1'; const curPermissions = user.curPermissions ?? null; const curPermissionsForChildren = user.curPermissionsForChildren ?? null; if (!potentiallyNeedsUpdate) { continue; } const permissionsFromParent = nextPermissionsFromParent === undefined ? curPermissionsFromParent : nextPermissionsFromParent; const memberOfContainingThread = nextMemberOfContainingThread === undefined ? curMemberOfContainingThread : nextMemberOfContainingThread; const targetRole = memberOfContainingThread ? curRole : '-1'; const rolePermissions = memberOfContainingThread ? curRolePermissions : null; const permissions = makePermissionsBlob( rolePermissions, permissionsFromParent, threadID, threadType, ); const permissionsForChildren = makePermissionsForChildrenBlob( permissions, ); const newRole = getRoleForPermissions(targetRole, permissions); const userLostMembership = Number(curRole) > 0 && Number(newRole) <= 0; if (_isEqual(permissions)(curPermissions) && curRole === newRole) { // This thread and all of its descendants need no updates for this // user, since the corresponding memberships row is unchanged by this // operation continue; } if (permissions) { membershipRows.push({ operation: 'save', intent: 'none', userID, threadID, userNeedsFullThreadDetails: false, permissions, permissionsForChildren, role: newRole, oldRole: curRole, }); } else { membershipRows.push({ operation: 'delete', intent: 'none', userID, threadID, oldRole: curRole, }); } if (permissions && !existingMembership && threadID !== genesis.id) { // If there was no membership row before, and we are creating one, // we'll need to make sure the new member has a relationship row with // each existing member. We expect that whoever called us already // generated memberships row for the new members, will will lead // saveMemberships to generate relationships rows between those new // users. relationshipChangeset.setRelationshipsNeeded( userID, existingMemberIDs, ); } if ( userLostMembership || !_isEqual(permissionsForChildren)(curPermissionsForChildren) ) { usersForNextLayer.set(userID, { userIsMember: Number(newRole) > 0, permissionsForChildren, }); } } if (usersForNextLayer.size > 0) { descendantsAsAncestors.push({ threadID, depth, changesByUser: usersForNextLayer, }); } } const nextDescendants = await fetchDescendantsForUpdate( descendantsAsAncestors, ); depthQueue.addInfos(nextDescendants); } return { membershipRows, relationshipChangeset }; } type DescendantUserInfo = $Shape<{ curRole?: string, curRolePermissions?: ?ThreadRolePermissionsBlob, curPermissions?: ?ThreadPermissionsBlob, curPermissionsForChildren?: ?ThreadPermissionsBlob, curPermissionsFromParent?: ?ThreadPermissionsBlob, curMemberOfContainingThread?: boolean, nextPermissionsFromParent?: ?ThreadPermissionsBlob, nextMemberOfContainingThread?: boolean, potentiallyNeedsUpdate?: boolean, }>; type DescendantInfo = { +threadID: string, +parentThreadID: string, +containingThreadID: string, +threadType: ThreadType, +depth: number, +users: Map, }; const fetchDescendantsBatchSize = 10; async function fetchDescendantsForUpdate( ancestors: $ReadOnlyArray, ): Promise { const threadIDs = ancestors.map(ancestor => ancestor.threadID); const rows = []; while (threadIDs.length > 0) { const batch = threadIDs.splice(0, fetchDescendantsBatchSize); const query = SQL` SELECT t.id, m.user, t.type, t.depth, t.parent_thread_id, t.containing_thread_id, r.permissions AS role_permissions, m.permissions, m.permissions_for_children, m.role, pm.permissions_for_children AS permissions_from_parent, cm.role AS containing_role FROM threads t INNER JOIN memberships m ON m.thread = t.id LEFT JOIN memberships pm ON pm.thread = t.parent_thread_id AND pm.user = m.user LEFT JOIN memberships cm ON cm.thread = t.containing_thread_id AND cm.user = m.user LEFT JOIN roles r ON r.id = m.role WHERE t.parent_thread_id IN (${batch}) OR t.containing_thread_id IN (${batch}) `; const [results] = await dbQuery(query); pushAll(rows, results); } const descendantThreadInfos: Map = new Map(); for (const row of rows) { const descendantThreadID = row.id.toString(); if (!descendantThreadInfos.has(descendantThreadID)) { descendantThreadInfos.set(descendantThreadID, { threadID: descendantThreadID, parentThreadID: row.parent_thread_id.toString(), containingThreadID: row.containing_thread_id.toString(), threadType: assertThreadType(row.type), depth: row.depth, users: new Map(), }); } const descendantThreadInfo = descendantThreadInfos.get(descendantThreadID); invariant( descendantThreadInfo, `value should exist for key ${descendantThreadID}`, ); const userID = row.user.toString(); descendantThreadInfo.users.set(userID, { curRole: row.role.toString(), curRolePermissions: JSON.parse(row.role_permissions), curPermissions: JSON.parse(row.permissions), curPermissionsForChildren: JSON.parse(row.permissions_for_children), curPermissionsFromParent: JSON.parse(row.permissions_from_parent), curMemberOfContainingThread: row.containing_role > 0, }); } for (const ancestor of ancestors) { const { threadID, changesByUser } = ancestor; for (const [userID, changes] of changesByUser) { for (const descendantThreadInfo of descendantThreadInfos.values()) { const { users, parentThreadID, containingThreadID, } = descendantThreadInfo; if (threadID !== parentThreadID && threadID !== containingThreadID) { continue; } let user = users.get(userID); if (!user) { user = {}; users.set(userID, user); } if (threadID === parentThreadID) { user.nextPermissionsFromParent = changes.permissionsForChildren; user.potentiallyNeedsUpdate = true; } if (threadID === containingThreadID) { user.nextMemberOfContainingThread = changes.userIsMember; if (!user.nextMemberOfContainingThread) { user.potentiallyNeedsUpdate = true; } } } } } return [...descendantThreadInfos.values()]; } function getDescendantDepth(descendant: DescendantInfo): number { return descendant.depth; } function getDescendantKey(descendant: DescendantInfo): string { return descendant.threadID; } function mergeDescendants( a: DescendantInfo, b: DescendantInfo, ): DescendantInfo { const { users: usersA, ...restA } = a; const { users: usersB, ...restB } = b; if (!_isEqual(restA)(restB)) { console.warn( `inconsistent descendantInfos ${JSON.stringify(restA)}, ` + JSON.stringify(restB), ); throw new ServerError('internal_error'); } const newUsers = new Map(usersA); for (const [userID, userFromB] of usersB) { const userFromA = newUsers.get(userID); if (!userFromA) { newUsers.set(userID, userFromB); } else { newUsers.set(userID, { ...userFromA, ...userFromB }); } } return { ...a, users: newUsers }; } type RecalculatePermissionsMemberInfo = { role?: ?string, permissions?: ?ThreadPermissionsBlob, permissionsForChildren?: ?ThreadPermissionsBlob, rolePermissions?: ?ThreadRolePermissionsBlob, memberOfContainingThread?: boolean, permissionsFromParent?: ?ThreadPermissionsBlob, }; async function recalculateThreadPermissions( threadID: string, ): Promise { const threadQuery = SQL` SELECT type, depth, parent_thread_id, containing_thread_id FROM threads WHERE id = ${threadID} `; const membershipQuery = SQL` SELECT m.user, m.role, m.permissions, m.permissions_for_children, r.permissions AS role_permissions, cm.role AS containing_role FROM threads t INNER JOIN memberships m ON m.thread = t.id LEFT JOIN roles r ON r.id = m.role LEFT JOIN memberships cm ON cm.user = m.user AND cm.thread = t.containing_thread_id WHERE t.id = ${threadID} `; const parentMembershipQuery = SQL` SELECT pm.user, pm.permissions_for_children AS permissions_from_parent FROM threads t INNER JOIN memberships pm ON pm.thread = t.parent_thread_id WHERE t.id = ${threadID} `; const [ [threadResults], [membershipResults], [parentMembershipResults], ] = await Promise.all([ dbQuery(threadQuery), dbQuery(membershipQuery), dbQuery(parentMembershipQuery), ]); if (threadResults.length !== 1) { throw new ServerError('internal_error'); } const [threadResult] = threadResults; const threadType = assertThreadType(threadResult.type); const depth = threadResult.depth; const hasContainingThreadID = threadResult.containing_thread_id !== null; const parentThreadID = threadResult.parent_thread_id?.toString(); const membershipInfo: Map< string, RecalculatePermissionsMemberInfo, > = new Map(); for (const row of membershipResults) { const userID = row.user.toString(); membershipInfo.set(userID, { role: row.role.toString(), permissions: JSON.parse(row.permissions), permissionsForChildren: JSON.parse(row.permissions_for_children), rolePermissions: JSON.parse(row.role_permissions), memberOfContainingThread: !!( row.containing_role && row.containing_role > 0 ), }); } for (const row of parentMembershipResults) { const userID = row.user.toString(); const permissionsFromParent = JSON.parse(row.permissions_from_parent); const membership = membershipInfo.get(userID); if (membership) { membership.permissionsFromParent = permissionsFromParent; } else { membershipInfo.set(userID, { permissionsFromParent: permissionsFromParent, }); } } const relationshipChangeset = new RelationshipChangeset(); const existingMemberIDs = membershipResults.map(row => row.user.toString()); if (threadID !== genesis.id) { relationshipChangeset.setAllRelationshipsExist(existingMemberIDs); } const parentMemberIDs = parentMembershipResults.map(row => row.user.toString(), ); if (parentThreadID && parentThreadID !== genesis.id) { relationshipChangeset.setAllRelationshipsExist(parentMemberIDs); } const membershipRows = []; const toUpdateDescendants = new Map(); for (const [userID, membership] of membershipInfo) { const { rolePermissions: intendedRolePermissions, permissionsFromParent, } = membership; const oldPermissions = membership?.permissions ?? null; const oldPermissionsForChildren = membership?.permissionsForChildren ?? null; const existingMembership = membership.role !== undefined; const oldRole = membership.role ?? '-1'; const memberOfContainingThread = hasContainingThreadID ? !!membership.memberOfContainingThread : true; const targetRole = memberOfContainingThread ? oldRole : '-1'; const rolePermissions = memberOfContainingThread ? intendedRolePermissions : null; const permissions = makePermissionsBlob( rolePermissions, permissionsFromParent, threadID, threadType, ); const permissionsForChildren = makePermissionsForChildrenBlob(permissions); const newRole = getRoleForPermissions(targetRole, permissions); const userLostMembership = Number(oldRole) > 0 && Number(newRole) <= 0; if (_isEqual(permissions)(oldPermissions) && oldRole === newRole) { // This thread and all of its descendants need no updates for this user, // since the corresponding memberships row is unchanged by this operation continue; } if (permissions) { membershipRows.push({ operation: 'save', intent: 'none', userID, threadID, userNeedsFullThreadDetails: false, permissions, permissionsForChildren, role: newRole, oldRole, }); } else { membershipRows.push({ operation: 'delete', intent: 'none', userID, threadID, oldRole, }); } if (permissions && !existingMembership && threadID !== genesis.id) { // If there was no membership row before, and we are creating one, // we'll need to make sure the new member has a relationship row with // each existing member. We handle guaranteeing that new members have // relationship rows with each other in saveMemberships. relationshipChangeset.setRelationshipsNeeded(userID, existingMemberIDs); } if ( userLostMembership || !_isEqual(permissionsForChildren)(oldPermissionsForChildren) ) { toUpdateDescendants.set(userID, { userIsMember: Number(newRole) > 0, permissionsForChildren, }); } } if (toUpdateDescendants.size > 0) { const { membershipRows: descendantMembershipRows, relationshipChangeset: descendantRelationshipChangeset, } = await updateDescendantPermissions({ threadID, depth, changesByUser: toUpdateDescendants, }); pushAll(membershipRows, descendantMembershipRows); relationshipChangeset.addAll(descendantRelationshipChangeset); } return { membershipRows, relationshipChangeset }; } const defaultSubscriptionString = JSON.stringify({ home: false, pushNotifs: false, }); const joinSubscriptionString = JSON.stringify({ home: true, pushNotifs: true }); const membershipInsertBatchSize = 50; async function saveMemberships(toSave: $ReadOnlyArray) { if (toSave.length === 0) { return; } const time = Date.now(); const insertRows = []; for (const rowToSave of toSave) { insertRows.push([ rowToSave.userID, rowToSave.threadID, rowToSave.role, time, rowToSave.intent === 'join' ? joinSubscriptionString : defaultSubscriptionString, rowToSave.permissions ? JSON.stringify(rowToSave.permissions) : null, rowToSave.permissionsForChildren ? JSON.stringify(rowToSave.permissionsForChildren) : null, rowToSave.unread ? 1 : 0, 0, ]); } // Logic below will only update an existing membership row's `subscription` // column if the user is either joining or leaving the thread. That means // there's no way to use this function to update a user's subscription without // also making them join or leave the thread. The reason we do this is because // we need to specify a value for `subscription` here, as it's a non-null // column and this is an INSERT, but we don't want to require people to have // to know the current `subscription` when they're just using this function to // update the permissions of an existing membership row. while (insertRows.length > 0) { const batch = insertRows.splice(0, membershipInsertBatchSize); const query = SQL` INSERT INTO memberships (user, thread, role, creation_time, subscription, permissions, permissions_for_children, last_message, last_read_message) VALUES ${batch} ON DUPLICATE KEY UPDATE subscription = IF( (role <= 0 AND VALUE(role) > 0) OR (role > 0 AND VALUE(role) <= 0), VALUE(subscription), subscription ), role = VALUE(role), permissions = VALUE(permissions), permissions_for_children = VALUE(permissions_for_children) `; await dbQuery(query); } } async function deleteMemberships( toDelete: $ReadOnlyArray, ) { if (toDelete.length === 0) { return; } const time = Date.now(); const insertRows = toDelete.map(rowToDelete => [ rowToDelete.userID, rowToDelete.threadID, -1, time, defaultSubscriptionString, null, null, 0, 0, ]); while (insertRows.length > 0) { const batch = insertRows.splice(0, membershipInsertBatchSize); const query = SQL` INSERT INTO memberships (user, thread, role, creation_time, subscription, permissions, permissions_for_children, last_message, last_read_message) VALUES ${batch} ON DUPLICATE KEY UPDATE role = -1, permissions = NULL, permissions_for_children = NULL, subscription = ${defaultSubscriptionString}, last_message = 0, last_read_message = 0 `; await dbQuery(query); } } +const emptyCommitMembershipChangesetConfig = Object.freeze({}); + // Specify non-empty changedThreadIDs to force updates to be generated for those // threads, presumably for reasons not covered in the changeset. calendarQuery // only needs to be specified if a JOIN_THREAD update will be generated for the // viewer, in which case it's necessary for knowing the set of entries to fetch. type ChangesetCommitResult = { ...FetchThreadInfosResult, ...CreateUpdatesResult, }; async function commitMembershipChangeset( viewer: Viewer, changeset: Changeset, { changedThreadIDs = new Set(), calendarQuery, updatesForCurrentSession = 'return', }: { +changedThreadIDs?: Set, +calendarQuery?: ?CalendarQuery, +updatesForCurrentSession?: UpdatesForCurrentSession, - } = {}, + } = emptyCommitMembershipChangesetConfig, ): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const { membershipRows, relationshipChangeset } = changeset; const membershipRowMap = new Map(); for (const row of membershipRows) { const { userID, threadID } = row; changedThreadIDs.add(threadID); const pairString = `${userID}|${threadID}`; const existing = membershipRowMap.get(pairString); invariant( !existing || existing.intent === 'none' || row.intent === 'none', `multiple intents provided for ${pairString}`, ); if (!existing || existing.intent === 'none') { membershipRowMap.set(pairString, row); } } const toSave = [], toDelete = [], toRescindPushNotifs = []; for (const row of membershipRowMap.values()) { if ( row.operation === 'delete' || (row.operation === 'save' && Number(row.role) <= 0) ) { const { userID, threadID } = row; toRescindPushNotifs.push({ userID, threadID }); } if (row.operation === 'delete') { toDelete.push(row); } else { toSave.push(row); } } const threadsToSavedUsers = new Map(); for (const row of membershipRowMap.values()) { const { userID, threadID } = row; let savedUsers = threadsToSavedUsers.get(threadID); if (!savedUsers) { savedUsers = []; threadsToSavedUsers.set(threadID, savedUsers); } savedUsers.push(userID); } for (const [threadID, savedUsers] of threadsToSavedUsers) { if (threadID !== genesis.id) { relationshipChangeset.setAllRelationshipsNeeded(savedUsers); } } const relationshipRows = relationshipChangeset.getRows(); const [updateDatas] = await Promise.all([ updateChangedUndirectedRelationships(relationshipRows), saveMemberships(toSave), deleteMemberships(toDelete), rescindPushNotifsForMemberDeletion(toRescindPushNotifs), ]); // We fetch all threads here because old clients still expect the full list of // threads on most thread operations. Once verifyClientSupported gates on // codeVersion 62, we can add a WHERE clause on changedThreadIDs here const serverThreadInfoFetchResult = await fetchServerThreadInfos(); const { threadInfos: serverThreadInfos } = serverThreadInfoFetchResult; const time = Date.now(); for (const changedThreadID of changedThreadIDs) { const serverThreadInfo = serverThreadInfos[changedThreadID]; for (const memberInfo of serverThreadInfo.members) { const pairString = `${memberInfo.id}|${serverThreadInfo.id}`; const membershipRow = membershipRowMap.get(pairString); if (membershipRow) { continue; } updateDatas.push({ type: updateTypes.UPDATE_THREAD, userID: memberInfo.id, time, threadID: changedThreadID, }); } } for (const row of membershipRowMap.values()) { const { userID, threadID } = row; if (row.operation === 'delete' || row.role === '-1') { if (row.oldRole !== '-1') { updateDatas.push({ type: updateTypes.DELETE_THREAD, userID, time, threadID, }); } } else if (row.userNeedsFullThreadDetails) { updateDatas.push({ type: updateTypes.JOIN_THREAD, userID, time, threadID, }); } else { updateDatas.push({ type: updateTypes.UPDATE_THREAD, userID, time, threadID, }); } } const threadInfoFetchResult = rawThreadInfosFromServerThreadInfos( viewer, serverThreadInfoFetchResult, ); const { viewerUpdates, userInfos } = await createUpdates(updateDatas, { viewer, calendarQuery, ...threadInfoFetchResult, updatesForCurrentSession, }); return { ...threadInfoFetchResult, userInfos, viewerUpdates, }; } +const emptyGetChangesetCommitResultConfig = Object.freeze({}); + // When the user tries to create a new thread, it's possible for the client to // fail the creation even if a row gets added to the threads table. This may // occur due to a timeout (on either the client or server side), or due to some // error in the server code following the INSERT operation. Handling the error // scenario is more challenging since it would require detecting which set of // operations failed so we could retry them. As a result, this code is geared at // only handling the timeout scenario. async function getChangesetCommitResultForExistingThread( viewer: Viewer, threadID: string, otherUpdates: $ReadOnlyArray, { calendarQuery, updatesForCurrentSession = 'return', }: { +calendarQuery?: ?CalendarQuery, +updatesForCurrentSession?: UpdatesForCurrentSession, - } = {}, + } = emptyGetChangesetCommitResultConfig, ): Promise { for (const update of otherUpdates) { if ( update.type === updateTypes.JOIN_THREAD && update.threadInfo.id === threadID ) { // If the JOIN_THREAD is already there we can expect // the appropriate UPDATE_USERs to be covered as well return { viewerUpdates: otherUpdates, userInfos: {} }; } } const time = Date.now(); const updateDatas = [ { type: updateTypes.JOIN_THREAD, userID: viewer.userID, time, threadID, targetSession: viewer.session, }, ]; // To figure out what UserInfos might be missing, we consider the worst case: // the same client previously attempted to create a thread with a non-friend // they found via search results, but the request timed out. In this scenario // the viewer might never have received the UPDATE_USER that would add that // UserInfo to their UserStore, but the server assumed the client had gotten // it because createUpdates was called with UpdatesForCurrentSession=return. // For completeness here we query for the full list of memberships rows in the // thread. We can't use fetchServerThreadInfos because it skips role=-1 rows const membershipsQuery = SQL` SELECT user FROM memberships WHERE thread = ${threadID} AND user != ${viewer.userID} `; const [results] = await dbQuery(membershipsQuery); for (const row of results) { updateDatas.push({ type: updateTypes.UPDATE_USER, userID: viewer.userID, time, updatedUserID: row.user.toString(), targetSession: viewer.session, }); } const { viewerUpdates, userInfos } = await createUpdates(updateDatas, { viewer, calendarQuery, updatesForCurrentSession, }); return { viewerUpdates: [...otherUpdates, ...viewerUpdates], userInfos }; } const rescindPushNotifsBatchSize = 3; async function rescindPushNotifsForMemberDeletion( toRescindPushNotifs: $ReadOnlyArray<{ +userID: string, +threadID: string }>, ): Promise { const queue = [...toRescindPushNotifs]; while (queue.length > 0) { const batch = queue.splice(0, rescindPushNotifsBatchSize); await Promise.all( batch.map(({ userID, threadID }) => rescindPushNotifs( SQL`n.thread = ${threadID} AND n.user = ${userID}`, SQL`IF(m.thread = ${threadID}, NULL, m.thread)`, ), ), ); } } async function recalculateAllThreadPermissions() { const getAllThreads = SQL`SELECT id FROM threads`; const [result] = await dbQuery(getAllThreads); // We handle each thread one-by-one to avoid a situation where a permission // calculation for a child thread, done during a call to // recalculateThreadPermissions for the parent thread, can be incorrectly // overriden by a call to recalculateThreadPermissions for the child thread. // If the changeset resulting from the parent call isn't committed before the // calculation is done for the child, the calculation done for the child can // be incorrect. const viewer = createScriptViewer(bots.commbot.userID); for (const row of result) { const threadID = row.id.toString(); const changeset = await recalculateThreadPermissions(threadID); await commitMembershipChangeset(viewer, changeset); } } async function updateRolesAndPermissionsForAllThreads() { const batchSize = 10; const fetchThreads = SQL`SELECT id, type, depth FROM threads`; const [result] = await dbQuery(fetchThreads); const allThreads = result.map(row => { return { id: row.id.toString(), type: assertThreadType(row.type), depth: row.depth, }; }); const viewer = createScriptViewer(bots.commbot.userID); const maxDepth = Math.max(...allThreads.map(row => row.depth)); for (let depth = 0; depth <= maxDepth; depth++) { const threads = allThreads.filter(row => row.depth === depth); console.log(`recalculating permissions for threads with depth ${depth}`); while (threads.length > 0) { const batch = threads.splice(0, batchSize); const membershipRows = []; const relationshipChangeset = new RelationshipChangeset(); await Promise.all( batch.map(async thread => { console.log(`updating roles for ${thread.id}`); await updateRoles(viewer, thread.id, thread.type); console.log(`recalculating permissions for ${thread.id}`); const { membershipRows: threadMembershipRows, relationshipChangeset: threadRelationshipChangeset, } = await recalculateThreadPermissions(thread.id); membershipRows.push(...threadMembershipRows); relationshipChangeset.addAll(threadRelationshipChangeset); }), ); console.log(`committing batch ${JSON.stringify(batch)}`); await commitMembershipChangeset(viewer, { membershipRows, relationshipChangeset, }); } } } export { changeRole, recalculateThreadPermissions, getChangesetCommitResultForExistingThread, saveMemberships, commitMembershipChangeset, recalculateAllThreadPermissions, updateRolesAndPermissionsForAllThreads, }; diff --git a/landing/.flowconfig b/landing/.flowconfig index ee46ee52e..e6271ab66 100644 --- a/landing/.flowconfig +++ b/landing/.flowconfig @@ -1,32 +1,31 @@ [include] ../lib [libs] ../lib/flow-typed [options] module.name_mapper.extension='css' -> '/flow/CSSModule.js.flow' exact_by_default=true format.bracket_spacing=false [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn unnecessary-invariant=warn -signature-verification-failure=warn [strict] deprecated-type nonstrict-import sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import diff --git a/landing/package.json b/landing/package.json index 67df6cf14..33e87db1a 100644 --- a/landing/package.json +++ b/landing/package.json @@ -1,67 +1,67 @@ { "name": "landing", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "rm -rf dist/ && rm -rf node_modules/", "dev": "yarn concurrently --names=\"NODESSR,BROWSER\" -c \"bgBlue.bold,bgMagenta.bold\" \"yarn webpack --config webpack.config.cjs --config-name=server --watch\" \"yarn webpack-dev-server --hot --config webpack.config.cjs --config-name=browser\"", "prod": "yarn webpack --config webpack.config.cjs --env prod --progress" }, "devDependencies": { "@babel/core": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-react-constant-elements": "^7.13.13", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "@hot-loader/react-dom": "16.13.0", "assets-webpack-plugin": "^3.9.7", "babel-loader": "^8.1.0", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^3.0.0", "concurrently": "^5.3.0", "css-loader": "^4.3.0", - "flow-bin": "^0.158.0", + "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "mini-css-extract-plugin": "^0.11.2", "optimize-css-assets-webpack-plugin": "^5.0.3", "style-loader": "^1.2.1", "terser-webpack-plugin": "^2.1.2", "url-loader": "^2.2.0", "webpack": "^4.41.0", "webpack-cli": "^3.3.9", "webpack-dev-server": "^3.11.0" }, "dependencies": { "@babel/runtime": "^7.13.10", "@fortawesome/fontawesome-svg-core": "1.2.25", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.11.2", "@fortawesome/free-solid-svg-icons": "5.11.2", "@fortawesome/react-fontawesome": "0.1.5", "@lottiefiles/lottie-interactivity": "^0.1.4", "@lottiefiles/lottie-player": "^1.0.3", "@rainbow-me/rainbowkit": "^0.5.0", "classnames": "^2.2.5", "core-js": "^3.6.5", "ethers": "^5.7.0", "invariant": "^2.2.4", "isomorphic-fetch": "^3.0.0", "lib": "0.0.1", "lodash": "^4.17.21", "react": "18.1.0", "react-dom": "18.1.0", "react-hot-loader": "^4.12.14", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-text-loop": "^2.3.0", "siwe": "^1.1.6", "wagmi": "^0.6.0" } } diff --git a/lib/.flowconfig b/lib/.flowconfig index 9ad552401..49b2e57eb 100644 --- a/lib/.flowconfig +++ b/lib/.flowconfig @@ -1,30 +1,29 @@ [ignore] [include] [libs] [options] exact_by_default=true format.bracket_spacing=false [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn unnecessary-invariant=warn -signature-verification-failure=warn [strict] deprecated-type nonstrict-import sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import diff --git a/lib/package.json b/lib/package.json index 12a0f1f83..ea2258181 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,54 +1,54 @@ { "name": "lib", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "rm -rf node_modules/", "test": "jest" }, "devDependencies": { "@babel/core": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "babel-jest": "^26.6.3", - "flow-bin": "^0.158.0", + "flow-bin": "^0.182.0", "flow-typed": "^3.2.1" }, "dependencies": { "dateformat": "^3.0.3", "emoji-regex": "^9.2.0", "fast-json-stable-stringify": "^2.0.0", "file-type": "^12.3.0", "invariant": "^2.2.4", "just-clone": "^3.2.1", "lodash": "^4.17.21", "react": "18.1.0", "react-redux": "^7.1.1", "reselect": "^4.0.0", "reselect-map": "^1.0.5", "simple-markdown": "^0.7.2", "string-hash": "^1.1.3", "tcomb": "^3.2.29", "tinycolor2": "^1.4.1", "tokenize-text": "^1.1.3", "url-parse-lax": "^3.0.0", "util-inspect": "^0.1.8", "utils-copy-error": "^1.0.1" }, "jest": { "transform": { "\\.js$": "babel-jest" }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ] } } diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js index 995bd1ddb..864baad9a 100644 --- a/lib/shared/notif-utils.js +++ b/lib/shared/notif-utils.js @@ -1,134 +1,134 @@ // @flow import invariant from 'invariant'; import { type MessageInfo, type RawMessageInfo, type RobotextMessageInfo, type MessageType, } from '../types/message-types'; import type { NotifTexts } from '../types/notif-types'; import type { ThreadInfo, ThreadType } from '../types/thread-types'; import type { RelativeUserInfo } from '../types/user-types'; import { trimText } from '../utils/text-utils'; import { robotextForMessageInfo, robotextToRawString } from './message-utils'; import { messageSpecs } from './messages/message-specs'; import { threadNoun } from './thread-utils'; import { stringForUser } from './user-utils'; function notifTextsForMessageInfo( messageInfos: MessageInfo[], threadInfo: ThreadInfo, ): NotifTexts { const fullNotifTexts = fullNotifTextsForMessageInfo(messageInfos, threadInfo); - return { - merged: trimText(fullNotifTexts.merged, 300), - body: trimText(fullNotifTexts.body, 300), - title: trimText(fullNotifTexts.title, 100), - ...(fullNotifTexts.prefix && { - prefix: trimText(fullNotifTexts.prefix, 50), - }), - }; + const merged = trimText(fullNotifTexts.merged, 300); + const body = trimText(fullNotifTexts.body, 300); + const title = trimText(fullNotifTexts.title, 100); + if (!fullNotifTexts.prefix) { + return { merged, body, title }; + } + const prefix = trimText(fullNotifTexts.prefix, 50); + return { merged, body, title, prefix }; } const notifTextForSubthreadCreation = ( creator: RelativeUserInfo, threadType: ThreadType, parentThreadInfo: ThreadInfo, childThreadName: ?string, childThreadUIName: string, ) => { const prefix = stringForUser(creator); let body = `created a new ${threadNoun(threadType)}`; if (parentThreadInfo.name) { body += ` in ${parentThreadInfo.name}`; } let merged = `${prefix} ${body}`; if (childThreadName) { merged += ` called "${childThreadName}"`; } return { merged, body, title: childThreadUIName, prefix, }; }; function notifThreadName(threadInfo: ThreadInfo): string { if (threadInfo.name) { return threadInfo.name; } else { return 'your chat'; } } function mostRecentMessageInfoType( messageInfos: $ReadOnlyArray, ): MessageType { if (messageInfos.length === 0) { throw new Error('expected MessageInfo, but none present!'); } return messageInfos[0].type; } function fullNotifTextsForMessageInfo( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): NotifTexts { const mostRecentType = mostRecentMessageInfoType(messageInfos); const messageSpec = messageSpecs[mostRecentType]; invariant( messageSpec.notificationTexts, `we're not aware of messageType ${mostRecentType}`, ); return messageSpec.notificationTexts(messageInfos, threadInfo, { notifThreadName, notifTextForSubthreadCreation, strippedRobotextForMessageInfo, notificationTexts: fullNotifTextsForMessageInfo, }); } function strippedRobotextForMessageInfo( messageInfo: RobotextMessageInfo, threadInfo: ThreadInfo, ): string { const robotext = robotextForMessageInfo(messageInfo, threadInfo); const threadEntityRegex = new RegExp(`<[^<>\\|]+\\|t${threadInfo.id}>`); const threadMadeExplicit = robotext.replace( threadEntityRegex, notifThreadName(threadInfo), ); return robotextToRawString(threadMadeExplicit); } function notifCollapseKeyForRawMessageInfo( rawMessageInfo: RawMessageInfo, ): ?string { const messageSpec = messageSpecs[rawMessageInfo.type]; return messageSpec.notificationCollapseKey?.(rawMessageInfo) ?? null; } type Unmerged = $ReadOnly<{ body: string, title: string, prefix?: string, ... }>; type Merged = { body: string, title: string, }; function mergePrefixIntoBody(unmerged: Unmerged): Merged { const { body, title, prefix } = unmerged; const merged = prefix ? `${prefix} ${body}` : body; return { body: merged, title }; } export { notifTextsForMessageInfo, notifCollapseKeyForRawMessageInfo, mergePrefixIntoBody, }; diff --git a/native/.flowconfig b/native/.flowconfig index 8e9fb02aa..f6b73a5bb 100644 --- a/native/.flowconfig +++ b/native/.flowconfig @@ -1,75 +1,77 @@ [ignore] ; We fork some components by platform .*/*[.]android.js .*/node_modules/react-native-fast-image/src/index.js.flow .*/node_modules/react-native-fs/FS.common.js .*/node_modules/react-native-gesture-handler/Swipeable.js +.*/fbjs/lib/keyMirrorRecursive.js.flow ; Flow doesn't support platforms .*/Libraries/Utilities/LoadingView.js +.*/node_modules/resolve/test/resolver/malformed_package_json/package\.json$ + .*/comm/web/.* .*/comm/keyserver/.* .*/android/app/build/.* [untyped] .*/node_modules/@react-native-community/cli/.*/.* [declarations] .*/node_modules/react-native-camera/* .*/node_modules/react-native-firebase/* [include] ../node_modules ../lib [libs] ../node_modules/react-native/interface.js ../node_modules/react-native/flow/ ../lib/flow-typed/ [options] emoji=true exact_by_default=true format.bracket_spacing=false module.file_ext=.js module.file_ext=.json module.file_ext=.ios.js munge_underscores=true module.name_mapper='^react-native/\(.*\)$' -> '/../node_modules/react-native/\1' module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn unnecessary-invariant=warn -signature-verification-failure=warn [strict] deprecated-type nonstrict-import sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import [version] -^0.158.0 +^0.182.0 diff --git a/native/calendar/calendar.react.js b/native/calendar/calendar.react.js index c19f848b2..d23d3415f 100644 --- a/native/calendar/calendar.react.js +++ b/native/calendar/calendar.react.js @@ -1,1096 +1,1103 @@ // @flow import invariant from 'invariant'; import _filter from 'lodash/fp/filter'; import _find from 'lodash/fp/find'; import _findIndex from 'lodash/fp/findIndex'; import _map from 'lodash/fp/map'; import _pickBy from 'lodash/fp/pickBy'; import _size from 'lodash/fp/size'; import _sum from 'lodash/fp/sum'; import _throttle from 'lodash/throttle'; import * as React from 'react'; import { View, Text, FlatList, AppState as NativeAppState, Platform, LayoutAnimation, TouchableWithoutFeedback, } from 'react-native'; import SafeAreaView from 'react-native-safe-area-view'; import { updateCalendarQueryActionTypes, updateCalendarQuery, } from 'lib/actions/entry-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { entryKey } from 'lib/shared/entry-utils'; import type { EntryInfo, CalendarQuery, CalendarQueryUpdateResult, } from 'lib/types/entry-types'; import type { CalendarFilter } from 'lib/types/filter-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { ConnectionStatus } from 'lib/types/socket-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils'; import { dateString, prettyDate, dateFromString } from 'lib/utils/date-utils'; import sleep from 'lib/utils/sleep'; import ContentLoading from '../components/content-loading.react'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react'; import ListLoadingIndicator from '../components/list-loading-indicator.react'; import NodeHeightMeasurer from '../components/node-height-measurer.react'; import { addKeyboardShowListener, addKeyboardDismissListener, removeKeyboardListener, } from '../keyboard/keyboard'; import DisconnectedBar from '../navigation/disconnected-bar.react'; import { createIsForegroundSelector, createActiveTabSelector, } from '../navigation/nav-selectors'; import { NavContext } from '../navigation/navigation-context'; import { CalendarRouteName, ThreadPickerModalRouteName, } from '../navigation/route-names'; import type { NavigationRoute } from '../navigation/route-names'; import type { TabNavigationProp } from '../navigation/tab-navigator.react'; import { useSelector } from '../redux/redux-utils'; import { calendarListData } from '../selectors/calendar-selectors'; import type { CalendarItem, SectionHeaderItem, SectionFooterItem, LoaderItem, } from '../selectors/calendar-selectors'; import { type DerivedDimensionsInfo, derivedDimensionsInfoSelector, } from '../selectors/dimensions-selectors'; import { useColors, useStyles, useIndicatorStyle, type Colors, type IndicatorStyle, } from '../themes/colors'; import type { EventSubscription, ScrollEvent, ViewableItemsChange, KeyboardEvent, } from '../types/react-native'; import CalendarInputBar from './calendar-input-bar.react'; import { Entry, InternalEntry, dummyNodeForEntryHeightMeasurement, } from './entry.react'; import SectionFooter from './section-footer.react'; export type EntryInfoWithHeight = { ...EntryInfo, +textHeight: number, }; type CalendarItemWithHeight = | LoaderItem | SectionHeaderItem | SectionFooterItem | { itemType: 'entryInfo', entryInfo: EntryInfoWithHeight, threadInfo: ThreadInfo, }; type ExtraData = { +activeEntries: { +[key: string]: boolean }, +visibleEntries: { +[key: string]: boolean }, }; const safeAreaViewForceInset = { top: 'always', bottom: 'never', }; type BaseProps = { +navigation: TabNavigationProp<'Calendar'>, +route: NavigationRoute<'Calendar'>, }; type Props = { ...BaseProps, // Nav state +calendarActive: boolean, // Redux state +listData: ?$ReadOnlyArray, +startDate: string, +endDate: string, +calendarFilters: $ReadOnlyArray, +dimensions: DerivedDimensionsInfo, +loadingStatus: LoadingStatus, +connectionStatus: ConnectionStatus, +colors: Colors, +styles: typeof unboundStyles, +indicatorStyle: IndicatorStyle, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +updateCalendarQuery: ( calendarQuery: CalendarQuery, reduxAlreadyUpdated?: boolean, ) => Promise, }; type State = { +listDataWithHeights: ?$ReadOnlyArray, +readyToShowList: boolean, +extraData: ExtraData, +currentlyEditing: $ReadOnlyArray, }; class Calendar extends React.PureComponent { flatList: ?FlatList = null; currentState: ?string = NativeAppState.currentState; + appStateListener: ?EventSubscription; lastForegrounded = 0; lastCalendarReset = 0; currentScrollPosition: ?number = null; // We don't always want an extraData update to trigger a state update, so we // cache the most recent value as a member here latestExtraData: ExtraData; // For some reason, we have to delay the scrollToToday call after the first // scroll upwards firstScrollComplete = false; // When an entry becomes active, we make a note of its key so that once the // keyboard event happens, we know where to move the scrollPos to lastEntryKeyActive: ?string = null; keyboardShowListener: ?EventSubscription; keyboardDismissListener: ?EventSubscription; keyboardShownHeight: ?number = null; // If the query fails, we try it again topLoadingFromScroll: ?CalendarQuery = null; bottomLoadingFromScroll: ?CalendarQuery = null; // We wait until the loaders leave view before letting them be triggered again topLoaderWaitingToLeaveView = true; bottomLoaderWaitingToLeaveView = true; // We keep refs to the entries so CalendarInputBar can save them entryRefs = new Map(); constructor(props: Props) { super(props); this.latestExtraData = { activeEntries: {}, visibleEntries: {}, }; this.state = { listDataWithHeights: null, readyToShowList: false, extraData: this.latestExtraData, currentlyEditing: [], }; } componentDidMount() { - NativeAppState.addEventListener('change', this.handleAppStateChange); + this.appStateListener = NativeAppState.addEventListener( + 'change', + this.handleAppStateChange, + ); this.keyboardShowListener = addKeyboardShowListener(this.keyboardShow); this.keyboardDismissListener = addKeyboardDismissListener( this.keyboardDismiss, ); this.props.navigation.addListener('tabPress', this.onTabPress); } componentWillUnmount() { - NativeAppState.removeEventListener('change', this.handleAppStateChange); + if (this.appStateListener) { + this.appStateListener.remove(); + this.appStateListener = null; + } if (this.keyboardShowListener) { removeKeyboardListener(this.keyboardShowListener); this.keyboardShowListener = null; } if (this.keyboardDismissListener) { removeKeyboardListener(this.keyboardDismissListener); this.keyboardDismissListener = null; } this.props.navigation.removeListener('tabPress', this.onTabPress); } handleAppStateChange = (nextAppState: ?string) => { const lastState = this.currentState; this.currentState = nextAppState; if ( !lastState || !lastState.match(/inactive|background/) || this.currentState !== 'active' ) { // We're only handling foregrounding here return; } if (Date.now() - this.lastCalendarReset < 500) { // If the calendar got reset right before this callback triggered, that // indicates we should reset the scroll position this.lastCalendarReset = 0; this.scrollToToday(false); } else { // Otherwise, it's possible that the calendar is about to get reset. We // record a timestamp here so we can scrollToToday there. this.lastForegrounded = Date.now(); } }; onTabPress = () => { if (this.props.navigation.isFocused()) { this.scrollToToday(); } }; componentDidUpdate(prevProps: Props, prevState: State) { if (!this.props.listData && this.props.listData !== prevProps.listData) { this.latestExtraData = { activeEntries: {}, visibleEntries: {}, }; this.setState({ listDataWithHeights: null, readyToShowList: false, extraData: this.latestExtraData, }); this.firstScrollComplete = false; this.topLoaderWaitingToLeaveView = true; this.bottomLoaderWaitingToLeaveView = true; } const { loadingStatus, connectionStatus } = this.props; const { loadingStatus: prevLoadingStatus, connectionStatus: prevConnectionStatus, } = prevProps; if ( (loadingStatus === 'error' && prevLoadingStatus === 'loading') || (connectionStatus === 'connected' && prevConnectionStatus !== 'connected') ) { this.loadMoreAbove(); this.loadMoreBelow(); } const lastLDWH = prevState.listDataWithHeights; const newLDWH = this.state.listDataWithHeights; if (!newLDWH) { return; } else if (!lastLDWH) { if (!this.props.calendarActive) { // FlatList has an initialScrollIndex prop, which is usually close to // centering but can be off when there is a particularly large Entry in // the list. scrollToToday lets us actually center, but gets overriden // by initialScrollIndex if we call it right after the FlatList mounts sleep(50).then(() => this.scrollToToday()); } return; } if (newLDWH.length < lastLDWH.length) { this.topLoaderWaitingToLeaveView = true; this.bottomLoaderWaitingToLeaveView = true; if (this.flatList) { if (!this.props.calendarActive) { // If the currentCalendarQuery gets reset we scroll to the center this.scrollToToday(); } else if (Date.now() - this.lastForegrounded < 500) { // If the app got foregrounded right before the calendar got reset, // that indicates we should reset the scroll position this.lastForegrounded = 0; this.scrollToToday(false); } else { // Otherwise, it's possible that we got triggered before the // foreground callback. Let's record a timestamp here so we can call // scrollToToday there this.lastCalendarReset = Date.now(); } } } const { lastStartDate, newStartDate, lastEndDate, newEndDate, } = Calendar.datesFromListData(lastLDWH, newLDWH); if (newStartDate > lastStartDate || newEndDate < lastEndDate) { // If there are fewer items in our new data, which happens when the // current calendar query gets reset due to inactivity, let's reset the // scroll position to the center (today) if (!this.props.calendarActive) { sleep(50).then(() => this.scrollToToday()); } this.firstScrollComplete = false; } else if (newStartDate < lastStartDate) { this.updateScrollPositionAfterPrepend(lastLDWH, newLDWH); } else if (newEndDate > lastEndDate) { this.firstScrollComplete = true; } else if (newLDWH.length > lastLDWH.length) { LayoutAnimation.easeInEaseOut(); } if (newStartDate < lastStartDate) { this.topLoadingFromScroll = null; } if (newEndDate > lastEndDate) { this.bottomLoadingFromScroll = null; } const { keyboardShownHeight, lastEntryKeyActive } = this; if (keyboardShownHeight && lastEntryKeyActive) { this.scrollToKey(lastEntryKeyActive, keyboardShownHeight); this.lastEntryKeyActive = null; } } static datesFromListData( lastLDWH: $ReadOnlyArray, newLDWH: $ReadOnlyArray, ) { const lastSecondItem = lastLDWH[1]; const newSecondItem = newLDWH[1]; invariant( newSecondItem.itemType === 'header' && lastSecondItem.itemType === 'header', 'second item in listData should be a header', ); const lastStartDate = dateFromString(lastSecondItem.dateString); const newStartDate = dateFromString(newSecondItem.dateString); const lastPenultimateItem = lastLDWH[lastLDWH.length - 2]; const newPenultimateItem = newLDWH[newLDWH.length - 2]; invariant( newPenultimateItem.itemType === 'footer' && lastPenultimateItem.itemType === 'footer', 'penultimate item in listData should be a footer', ); const lastEndDate = dateFromString(lastPenultimateItem.dateString); const newEndDate = dateFromString(newPenultimateItem.dateString); return { lastStartDate, newStartDate, lastEndDate, newEndDate }; } /** * When prepending list items, FlatList isn't smart about preserving scroll * position. If we're at the start of the list before prepending, FlatList * will just keep us at the front after prepending. But we want to preserve * the previous on-screen items, so we have to do a calculation to get the new * scroll position. (And deal with the inherent glitchiness of trying to time * that change with the items getting prepended... *sigh*.) */ updateScrollPositionAfterPrepend( lastLDWH: $ReadOnlyArray, newLDWH: $ReadOnlyArray, ) { const existingKeys = new Set(_map(Calendar.keyExtractor)(lastLDWH)); const newItems = _filter( (item: CalendarItemWithHeight) => !existingKeys.has(Calendar.keyExtractor(item)), )(newLDWH); const heightOfNewItems = Calendar.heightOfItems(newItems); const flatList = this.flatList; invariant(flatList, 'flatList should be set'); const scrollAction = () => { invariant( this.currentScrollPosition !== undefined && this.currentScrollPosition !== null, 'currentScrollPosition should be set', ); const currentScrollPosition = Math.max(this.currentScrollPosition, 0); const offset = currentScrollPosition + heightOfNewItems; flatList.scrollToOffset({ offset, animated: false, }); }; scrollAction(); if (!this.firstScrollComplete) { setTimeout(scrollAction, 0); this.firstScrollComplete = true; } } scrollToToday(animated: ?boolean = undefined) { if (animated === undefined) { animated = this.props.calendarActive; } const ldwh = this.state.listDataWithHeights; if (!ldwh) { return; } const todayIndex = _findIndex(['dateString', dateString(new Date())])(ldwh); invariant(this.flatList, "scrollToToday called, but flatList isn't set"); this.flatList.scrollToIndex({ index: todayIndex, animated, viewPosition: 0.5, }); } renderItem = (row: { item: CalendarItemWithHeight, ... }) => { const item = row.item; if (item.itemType === 'loader') { return ; } else if (item.itemType === 'header') { return this.renderSectionHeader(item); } else if (item.itemType === 'entryInfo') { const key = entryKey(item.entryInfo); return ( ); } else if (item.itemType === 'footer') { return this.renderSectionFooter(item); } invariant(false, 'renderItem conditions should be exhaustive'); }; renderSectionHeader = (item: SectionHeaderItem) => { let date = prettyDate(item.dateString); if (dateString(new Date()) === item.dateString) { date += ' (today)'; } const dateObj = dateFromString(item.dateString).getDay(); const weekendStyle = dateObj === 0 || dateObj === 6 ? this.props.styles.weekendSectionHeader : null; return ( {date} ); }; renderSectionFooter = (item: SectionFooterItem) => { return ( ); }; onAdd = (dayString: string) => { this.props.navigation.navigate(ThreadPickerModalRouteName, { presentedFrom: this.props.route.key, dateString: dayString, }); }; static keyExtractor = (item: CalendarItemWithHeight | CalendarItem) => { if (item.itemType === 'loader') { return item.key; } else if (item.itemType === 'header') { return item.dateString + '/header'; } else if (item.itemType === 'entryInfo') { return entryKey(item.entryInfo); } else if (item.itemType === 'footer') { return item.dateString + '/footer'; } invariant(false, 'keyExtractor conditions should be exhaustive'); }; static getItemLayout = ( data: ?$ReadOnlyArray, index: number, ) => { if (!data) { return { length: 0, offset: 0, index }; } const offset = Calendar.heightOfItems(data.filter((_, i) => i < index)); const item = data[index]; const length = item ? Calendar.itemHeight(item) : 0; return { length, offset, index }; }; static itemHeight = (item: CalendarItemWithHeight) => { if (item.itemType === 'loader') { return 56; } else if (item.itemType === 'header') { return 31; } else if (item.itemType === 'entryInfo') { const verticalPadding = 10; return verticalPadding + item.entryInfo.textHeight; } else if (item.itemType === 'footer') { return 40; } invariant(false, 'itemHeight conditions should be exhaustive'); }; static heightOfItems = (data: $ReadOnlyArray) => { return _sum(data.map(Calendar.itemHeight)); }; render() { const { listDataWithHeights } = this.state; let flatList = null; if (listDataWithHeights) { const flatListStyle = { opacity: this.state.readyToShowList ? 1 : 0 }; const initialScrollIndex = this.initialScrollIndex(listDataWithHeights); flatList = ( ); } let loadingIndicator = null; if (!listDataWithHeights || !this.state.readyToShowList) { loadingIndicator = ( ); } const disableInputBar = this.state.currentlyEditing.length === 0; return ( <> {loadingIndicator} {flatList} ); } flatListHeight() { const { safeAreaHeight, tabBarHeight } = this.props.dimensions; return safeAreaHeight - tabBarHeight; } initialScrollIndex(data: $ReadOnlyArray) { const todayIndex = _findIndex(['dateString', dateString(new Date())])(data); const heightOfTodayHeader = Calendar.itemHeight(data[todayIndex]); let returnIndex = todayIndex; let heightLeft = (this.flatListHeight() - heightOfTodayHeader) / 2; while (heightLeft > 0) { heightLeft -= Calendar.itemHeight(data[--returnIndex]); } return returnIndex; } flatListRef = (flatList: ?FlatList) => { this.flatList = flatList; }; entryRef = (inEntryKey: string, entry: ?InternalEntry) => { this.entryRefs.set(inEntryKey, entry); }; makeAllEntriesInactive = () => { if (_size(this.state.extraData.activeEntries) === 0) { if (_size(this.latestExtraData.activeEntries) !== 0) { this.latestExtraData = { visibleEntries: this.latestExtraData.visibleEntries, activeEntries: this.state.extraData.activeEntries, }; } return; } this.latestExtraData = { visibleEntries: this.latestExtraData.visibleEntries, activeEntries: {}, }; this.setState({ extraData: this.latestExtraData }); }; makeActive = (key: string, active: boolean) => { if (!active) { const activeKeys = Object.keys(this.latestExtraData.activeEntries); if (activeKeys.length === 0) { if (Object.keys(this.state.extraData.activeEntries).length !== 0) { this.setState({ extraData: this.latestExtraData }); } return; } const activeKey = activeKeys[0]; if (activeKey === key) { this.latestExtraData = { visibleEntries: this.latestExtraData.visibleEntries, activeEntries: {}, }; this.setState({ extraData: this.latestExtraData }); } return; } if ( _size(this.state.extraData.activeEntries) === 1 && this.state.extraData.activeEntries[key] ) { if ( _size(this.latestExtraData.activeEntries) !== 1 || !this.latestExtraData.activeEntries[key] ) { this.latestExtraData = { visibleEntries: this.latestExtraData.visibleEntries, activeEntries: this.state.extraData.activeEntries, }; } return; } this.latestExtraData = { visibleEntries: this.latestExtraData.visibleEntries, activeEntries: { [key]: true }, }; this.setState({ extraData: this.latestExtraData }); }; onEnterEntryEditMode = (entryInfo: EntryInfoWithHeight) => { const key = entryKey(entryInfo); const keyboardShownHeight = this.keyboardShownHeight; if (keyboardShownHeight && this.state.listDataWithHeights) { this.scrollToKey(key, keyboardShownHeight); } else { this.lastEntryKeyActive = key; } const newCurrentlyEditing = [ ...new Set([...this.state.currentlyEditing, key]), ]; if (newCurrentlyEditing.length > this.state.currentlyEditing.length) { this.setState({ currentlyEditing: newCurrentlyEditing }); } }; onConcludeEntryEditMode = (entryInfo: EntryInfoWithHeight) => { const key = entryKey(entryInfo); const newCurrentlyEditing = this.state.currentlyEditing.filter( k => k !== key, ); if (newCurrentlyEditing.length < this.state.currentlyEditing.length) { this.setState({ currentlyEditing: newCurrentlyEditing }); } }; keyboardShow = (event: KeyboardEvent) => { // flatListHeight() factors in the size of the tab bar, // but it is hidden by the keyboard since it is at the bottom const { bottomInset, tabBarHeight } = this.props.dimensions; const inputBarHeight = Platform.OS === 'android' ? 37.7 : 35.5; const keyboardHeight = Platform.select({ // Android doesn't include the bottomInset in this height measurement android: event.endCoordinates.height, default: Math.max(event.endCoordinates.height - bottomInset, 0), }); const keyboardShownHeight = inputBarHeight + Math.max(keyboardHeight - tabBarHeight, 0); this.keyboardShownHeight = keyboardShownHeight; const lastEntryKeyActive = this.lastEntryKeyActive; if (lastEntryKeyActive && this.state.listDataWithHeights) { this.scrollToKey(lastEntryKeyActive, keyboardShownHeight); this.lastEntryKeyActive = null; } }; keyboardDismiss = () => { this.keyboardShownHeight = null; }; scrollToKey(lastEntryKeyActive: string, keyboardHeight: number) { const data = this.state.listDataWithHeights; invariant(data, 'should be set'); const index = data.findIndex( (item: CalendarItemWithHeight) => Calendar.keyExtractor(item) === lastEntryKeyActive, ); if (index === -1) { return; } const itemStart = Calendar.heightOfItems(data.filter((_, i) => i < index)); const itemHeight = Calendar.itemHeight(data[index]); const entryAdditionalActiveHeight = Platform.OS === 'android' ? 21 : 20; const itemEnd = itemStart + itemHeight + entryAdditionalActiveHeight; const visibleHeight = this.flatListHeight() - keyboardHeight; if ( this.currentScrollPosition !== undefined && this.currentScrollPosition !== null && itemStart > this.currentScrollPosition && itemEnd < this.currentScrollPosition + visibleHeight ) { return; } const offset = itemStart - (visibleHeight - itemHeight) / 2; invariant(this.flatList, 'flatList should be set'); this.flatList.scrollToOffset({ offset, animated: true }); } heightMeasurerKey = (item: CalendarItem) => { if (item.itemType !== 'entryInfo') { return null; } return item.entryInfo.text; }; heightMeasurerDummy = (item: CalendarItem) => { invariant( item.itemType === 'entryInfo', 'NodeHeightMeasurer asked for dummy for non-entryInfo item', ); return dummyNodeForEntryHeightMeasurement(item.entryInfo.text); }; heightMeasurerMergeItem = (item: CalendarItem, height: ?number) => { if (item.itemType !== 'entryInfo') { return item; } invariant(height !== null && height !== undefined, 'height should be set'); const { entryInfo } = item; return { itemType: 'entryInfo', entryInfo: Calendar.entryInfoWithHeight(entryInfo, height), threadInfo: item.threadInfo, }; }; static entryInfoWithHeight( entryInfo: EntryInfo, textHeight: number, ): EntryInfoWithHeight { // Blame Flow for not accepting object spread on exact types if (entryInfo.id && entryInfo.localID) { return { id: entryInfo.id, localID: entryInfo.localID, threadID: entryInfo.threadID, text: entryInfo.text, year: entryInfo.year, month: entryInfo.month, day: entryInfo.day, creationTime: entryInfo.creationTime, creator: entryInfo.creator, deleted: entryInfo.deleted, textHeight: Math.ceil(textHeight), }; } else if (entryInfo.id) { return { id: entryInfo.id, threadID: entryInfo.threadID, text: entryInfo.text, year: entryInfo.year, month: entryInfo.month, day: entryInfo.day, creationTime: entryInfo.creationTime, creator: entryInfo.creator, deleted: entryInfo.deleted, textHeight: Math.ceil(textHeight), }; } else { return { localID: entryInfo.localID, threadID: entryInfo.threadID, text: entryInfo.text, year: entryInfo.year, month: entryInfo.month, day: entryInfo.day, creationTime: entryInfo.creationTime, creator: entryInfo.creator, deleted: entryInfo.deleted, textHeight: Math.ceil(textHeight), }; } } allHeightsMeasured = ( listDataWithHeights: $ReadOnlyArray, ) => { this.setState({ listDataWithHeights }); }; onViewableItemsChanged = (info: ViewableItemsChange) => { const ldwh = this.state.listDataWithHeights; if (!ldwh) { // This indicates the listData was cleared (set to null) right before this // callback was called. Since this leads to the FlatList getting cleared, // we'll just ignore this callback. return; } const visibleEntries = {}; for (const token of info.viewableItems) { if (token.item.itemType === 'entryInfo') { visibleEntries[entryKey(token.item.entryInfo)] = true; } } this.latestExtraData = { activeEntries: _pickBy((_, key: string) => { if (visibleEntries[key]) { return true; } // We don't automatically set scrolled-away entries to be inactive // because entries can be out-of-view at creation time if they need to // be scrolled into view (see onEnterEntryEditMode). If Entry could // distinguish the reasons its active prop gets set to false, it could // differentiate the out-of-view case from the something-pressed case, // and then we could set scrolled-away entries to be inactive without // worrying about this edge case. Until then... const foundItem = _find( item => item.entryInfo && entryKey(item.entryInfo) === key, )(ldwh); return !!foundItem; })(this.latestExtraData.activeEntries), visibleEntries, }; const topLoader = _find({ key: 'TopLoader' })(info.viewableItems); if (this.topLoaderWaitingToLeaveView && !topLoader) { this.topLoaderWaitingToLeaveView = false; this.topLoadingFromScroll = null; } const bottomLoader = _find({ key: 'BottomLoader' })(info.viewableItems); if (this.bottomLoaderWaitingToLeaveView && !bottomLoader) { this.bottomLoaderWaitingToLeaveView = false; this.bottomLoadingFromScroll = null; } if ( !this.state.readyToShowList && !this.topLoaderWaitingToLeaveView && !this.bottomLoaderWaitingToLeaveView && info.viewableItems.length > 0 ) { this.setState({ readyToShowList: true, extraData: this.latestExtraData, }); } if ( topLoader && !this.topLoaderWaitingToLeaveView && !this.topLoadingFromScroll ) { this.topLoaderWaitingToLeaveView = true; const start = dateFromString(this.props.startDate); start.setDate(start.getDate() - 31); const startDate = dateString(start); const endDate = this.props.endDate; this.topLoadingFromScroll = { startDate, endDate, filters: this.props.calendarFilters, }; this.loadMoreAbove(); } else if ( bottomLoader && !this.bottomLoaderWaitingToLeaveView && !this.bottomLoadingFromScroll ) { this.bottomLoaderWaitingToLeaveView = true; const end = dateFromString(this.props.endDate); end.setDate(end.getDate() + 31); const endDate = dateString(end); const startDate = this.props.startDate; this.bottomLoadingFromScroll = { startDate, endDate, filters: this.props.calendarFilters, }; this.loadMoreBelow(); } }; dispatchCalendarQueryUpdate(calendarQuery: CalendarQuery) { this.props.dispatchActionPromise( updateCalendarQueryActionTypes, this.props.updateCalendarQuery(calendarQuery), ); } loadMoreAbove = _throttle(() => { if ( this.topLoadingFromScroll && this.topLoaderWaitingToLeaveView && this.props.connectionStatus === 'connected' ) { this.dispatchCalendarQueryUpdate(this.topLoadingFromScroll); } }, 1000); loadMoreBelow = _throttle(() => { if ( this.bottomLoadingFromScroll && this.bottomLoaderWaitingToLeaveView && this.props.connectionStatus === 'connected' ) { this.dispatchCalendarQueryUpdate(this.bottomLoadingFromScroll); } }, 1000); onScroll = (event: ScrollEvent) => { this.currentScrollPosition = event.nativeEvent.contentOffset.y; }; // When the user "flicks" the scroll view, this callback gets triggered after // the scrolling ends onMomentumScrollEnd = () => { this.setState({ extraData: this.latestExtraData }); }; // This callback gets triggered when the user lets go of scrolling the scroll // view, regardless of whether it was a "flick" or a pan onScrollEndDrag = () => { // We need to figure out if this was a flick or not. If it's a flick, we'll // let onMomentumScrollEnd handle it once scroll position stabilizes const currentScrollPosition = this.currentScrollPosition; setTimeout(() => { if (this.currentScrollPosition === currentScrollPosition) { this.setState({ extraData: this.latestExtraData }); } }, 50); }; onSaveEntry = () => { const entryKeys = Object.keys(this.latestExtraData.activeEntries); if (entryKeys.length === 0) { return; } const entryRef = this.entryRefs.get(entryKeys[0]); if (entryRef) { entryRef.completeEdit(); } }; } const unboundStyles = { container: { backgroundColor: 'listBackground', flex: 1, }, flatList: { backgroundColor: 'listBackground', flex: 1, }, keyboardAvoidingViewContainer: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, }, keyboardAvoidingView: { position: 'absolute', left: 0, right: 0, bottom: 0, }, sectionHeader: { backgroundColor: 'panelSecondaryForeground', borderBottomWidth: 2, borderColor: 'listBackground', height: 31, }, sectionHeaderText: { color: 'listSeparatorLabel', fontWeight: 'bold', padding: 5, }, weekendSectionHeader: {}, }; const loadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); const activeTabSelector = createActiveTabSelector(CalendarRouteName); const activeThreadPickerSelector = createIsForegroundSelector( ThreadPickerModalRouteName, ); const ConnectedCalendar: React.ComponentType = React.memo( function ConnectedCalendar(props: BaseProps) { const navContext = React.useContext(NavContext); const calendarActive = activeTabSelector(navContext) || activeThreadPickerSelector(navContext); const listData = useSelector(calendarListData); const startDate = useSelector(state => state.navInfo.startDate); const endDate = useSelector(state => state.navInfo.endDate); const calendarFilters = useSelector(state => state.calendarFilters); const dimensions = useSelector(derivedDimensionsInfoSelector); const loadingStatus = useSelector(loadingStatusSelector); const connectionStatus = useSelector(state => state.connection.status); const colors = useColors(); const styles = useStyles(unboundStyles); const indicatorStyle = useIndicatorStyle(); const dispatchActionPromise = useDispatchActionPromise(); const callUpdateCalendarQuery = useServerCall(updateCalendarQuery); return ( ); }, ); export default ConnectedCalendar; diff --git a/native/calendar/entry.react.js b/native/calendar/entry.react.js index e8bde14ec..df28480bd 100644 --- a/native/calendar/entry.react.js +++ b/native/calendar/entry.react.js @@ -1,808 +1,808 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual'; import _omit from 'lodash/fp/omit'; import * as React from 'react'; import { View, Text, TextInput as BaseTextInput, Platform, TouchableWithoutFeedback, Alert, LayoutAnimation, Keyboard, } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import { useDispatch } from 'react-redux'; import shallowequal from 'shallowequal'; import tinycolor from 'tinycolor2'; import { createEntryActionTypes, createEntry, saveEntryActionTypes, saveEntry, deleteEntryActionTypes, deleteEntry, concurrentModificationResetActionType, } from 'lib/actions/entry-actions'; import { registerFetchKey } from 'lib/reducers/loading-reducer'; import { entryKey } from 'lib/shared/entry-utils'; import { colorIsDark, threadHasPermission } from 'lib/shared/thread-utils'; import type { Shape } from 'lib/types/core'; import type { CreateEntryInfo, SaveEntryInfo, SaveEntryResult, SaveEntryPayload, CreateEntryPayload, DeleteEntryInfo, DeleteEntryResult, CalendarQuery, } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { Dispatch } from 'lib/types/redux-types'; import { type ThreadInfo, threadPermissions } from 'lib/types/thread-types'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils'; import { dateString } from 'lib/utils/date-utils'; import { ServerError } from 'lib/utils/errors'; import sleep from 'lib/utils/sleep'; import { type MessageListParams, useNavigateToThread, } from '../chat/message-list-types'; import Button from '../components/button.react'; import { SingleLine } from '../components/single-line.react'; import TextInput from '../components/text-input.react'; import Markdown from '../markdown/markdown.react'; import { inlineMarkdownRules } from '../markdown/rules.react'; import { createIsForegroundSelector, nonThreadCalendarQuery, } from '../navigation/nav-selectors'; import { NavContext } from '../navigation/navigation-context'; import { ThreadPickerModalRouteName } from '../navigation/route-names'; import type { TabNavigationProp } from '../navigation/tab-navigator.react'; import { useSelector } from '../redux/redux-utils'; import { colors, useStyles } from '../themes/colors'; import type { LayoutEvent } from '../types/react-native'; import { waitForInteractions } from '../utils/timers'; import type { EntryInfoWithHeight } from './calendar.react'; import LoadingIndicator from './loading-indicator.react'; function hueDistance(firstColor: string, secondColor: string): number { const firstHue = tinycolor(firstColor).toHsv().h; const secondHue = tinycolor(secondColor).toHsv().h; const distance = Math.abs(firstHue - secondHue); return distance > 180 ? 360 - distance : distance; } const omitEntryInfo = _omit(['entryInfo']); function dummyNodeForEntryHeightMeasurement( entryText: string, ): React.Element { const text = entryText === '' ? ' ' : entryText; return ( {text} ); } type BaseProps = { +navigation: TabNavigationProp<'Calendar'>, +entryInfo: EntryInfoWithHeight, +threadInfo: ThreadInfo, +visible: boolean, +active: boolean, +makeActive: (entryKey: string, active: boolean) => void, +onEnterEditMode: (entryInfo: EntryInfoWithHeight) => void, +onConcludeEditMode: (entryInfo: EntryInfoWithHeight) => void, +onPressWhitespace: () => void, +entryRef: (entryKey: string, entry: ?InternalEntry) => void, }; type Props = { ...BaseProps, // Redux state +calendarQuery: () => CalendarQuery, +online: boolean, +styles: typeof unboundStyles, // Nav state +threadPickerActive: boolean, +navigateToThread: (params: MessageListParams) => void, // Redux dispatch functions +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +createEntry: (info: CreateEntryInfo) => Promise, +saveEntry: (info: SaveEntryInfo) => Promise, +deleteEntry: (info: DeleteEntryInfo) => Promise, }; type State = { +editing: boolean, +text: string, +loadingStatus: LoadingStatus, +height: number, }; class InternalEntry extends React.Component { textInput: ?React.ElementRef; creating: boolean = false; needsUpdateAfterCreation: boolean = false; needsDeleteAfterCreation: boolean = false; nextSaveAttemptIndex: number = 0; mounted: boolean = false; deleted: boolean = false; currentlySaving: ?string; constructor(props: Props) { super(props); this.state = { editing: false, text: props.entryInfo.text, loadingStatus: 'inactive', height: props.entryInfo.textHeight, }; this.state = { ...this.state, editing: InternalEntry.isActive(props, this.state), }; } guardedSetState(input: Shape) { if (this.mounted) { this.setState(input); } } shouldComponentUpdate(nextProps: Props, nextState: State): boolean { return ( !shallowequal(nextState, this.state) || !shallowequal(omitEntryInfo(nextProps), omitEntryInfo(this.props)) || !_isEqual(nextProps.entryInfo)(this.props.entryInfo) ); } componentDidUpdate(prevProps: Props, prevState: State) { const wasActive = InternalEntry.isActive(prevProps, prevState); const isActive = InternalEntry.isActive(this.props, this.state); if ( !isActive && (this.props.entryInfo.text !== prevProps.entryInfo.text || this.props.entryInfo.textHeight !== prevProps.entryInfo.textHeight) && (this.props.entryInfo.text !== this.state.text || this.props.entryInfo.textHeight !== this.state.height) ) { this.guardedSetState({ text: this.props.entryInfo.text, height: this.props.entryInfo.textHeight, }); this.currentlySaving = null; } if ( !this.props.active && this.state.text === prevState.text && this.state.height !== prevState.height && this.state.height !== this.props.entryInfo.textHeight ) { const approxMeasuredHeight = Math.round(this.state.height * 1000) / 1000; const approxExpectedHeight = Math.round(this.props.entryInfo.textHeight * 1000) / 1000; console.log( `Entry height for ${entryKey(this.props.entryInfo)} was expected to ` + `be ${approxExpectedHeight} but is actually ` + `${approxMeasuredHeight}. This means Calendar's FlatList isn't ` + 'getting the right item height for some of its nodes, which is ' + 'guaranteed to cause glitchy behavior. Please investigate!!', ); } // Our parent will set the active prop to false if something else gets // pressed or if the Entry is scrolled out of view. In either of those cases // we should complete the edit process. if (!this.props.active && prevProps.active) { this.completeEdit(); } if (this.state.height !== prevState.height || isActive !== wasActive) { LayoutAnimation.easeInEaseOut(); } if ( this.props.online && !prevProps.online && this.state.loadingStatus === 'error' ) { this.save(); } if ( this.state.editing && prevState.editing && (this.state.text.trim() === '') !== (prevState.text.trim() === '') ) { LayoutAnimation.easeInEaseOut(); } } componentDidMount() { this.mounted = true; this.props.entryRef(entryKey(this.props.entryInfo), this); } componentWillUnmount() { this.mounted = false; this.props.entryRef(entryKey(this.props.entryInfo), null); this.props.onConcludeEditMode(this.props.entryInfo); } static isActive(props: Props, state: State): boolean { return ( props.active || state.editing || !props.entryInfo.id || state.loadingStatus !== 'inactive' ); } render(): React.Node { const active = InternalEntry.isActive(this.props, this.state); const { editing } = this.state; const threadColor = `#${this.props.threadInfo.color}`; const darkColor = colorIsDark(this.props.threadInfo.color); let actionLinks = null; if (active) { const actionLinksColor = darkColor ? '#D3D3D3' : '#404040'; const actionLinksTextStyle = { color: actionLinksColor }; const { modalIosHighlightUnderlay: actionLinksUnderlayColor } = darkColor ? colors.dark : colors.light; const loadingIndicatorCanUseRed = hueDistance('red', threadColor) > 50; let editButtonContent = null; if (editing && this.state.text.trim() === '') { // nothing } else if (editing) { editButtonContent = ( SAVE ); } else { editButtonContent = ( EDIT ); } actionLinks = ( ); } const textColor = darkColor ? 'white' : 'black'; let textInput; if (editing) { const textInputStyle = { color: textColor, backgroundColor: threadColor, }; const selectionColor = darkColor ? '#129AFF' : '#036AFF'; textInput = ( ); } let rawText = this.state.text; if (rawText === '' || rawText.slice(-1) === '\n') { rawText += ' '; } const textStyle = { ...this.props.styles.text, color: textColor, opacity: textInput ? 0 : 1, }; // We use an empty View to set the height of the entry, and then position // the Text and TextInput absolutely. This allows to measure height changes // to the Text while controlling the actual height of the entry. const heightStyle = { height: this.state.height }; const entryStyle = { backgroundColor: threadColor }; const opacity = editing ? 1.0 : 0.6; const canEditEntry = threadHasPermission( this.props.threadInfo, threadPermissions.EDIT_ENTRIES, ); return ( ); } textInputRef: ( textInput: ?React.ElementRef, ) => void = textInput => { this.textInput = textInput; if (textInput && this.state.editing) { this.enterEditMode(); } }; enterEditMode: () => Promise = async () => { this.setActive(); this.props.onEnterEditMode(this.props.entryInfo); if (Platform.OS === 'android') { // If we don't do this, the TextInput focuses // but the soft keyboard doesn't come up await waitForInteractions(); await sleep(15); } this.focus(); }; focus: () => void = () => { const { textInput } = this; if (!textInput) { return; } textInput.focus(); }; onFocus: () => void = () => { if (this.props.threadPickerActive) { this.props.navigation.goBack(); } }; setActive: () => void = () => this.makeActive(true); completeEdit: () => void = () => { // This gets called from CalendarInputBar (save button above keyboard), // onPressEdit (save button in Entry action links), and in // componentDidUpdate above when Calendar sets this Entry to inactive. // Calendar does this if something else gets pressed or the Entry is // scrolled out of view. Note that an Entry won't consider itself inactive // until it's done updating the server with its state, and if the network // requests fail it may stay "active". if (this.textInput) { this.textInput.blur(); } this.onBlur(); }; onBlur: () => void = () => { if (this.state.text.trim() === '') { this.delete(); } else if (this.props.entryInfo.text !== this.state.text) { this.save(); } this.guardedSetState({ editing: false }); this.makeActive(false); this.props.onConcludeEditMode(this.props.entryInfo); }; save: () => void = () => { this.dispatchSave(this.props.entryInfo.id, this.state.text); }; onTextContainerLayout: (event: LayoutEvent) => void = event => { this.guardedSetState({ height: Math.ceil(event.nativeEvent.layout.height), }); }; onChangeText: (newText: string) => void = newText => { this.guardedSetState({ text: newText }); }; makeActive(active: boolean) { const { threadInfo } = this.props; if (!threadHasPermission(threadInfo, threadPermissions.EDIT_ENTRIES)) { return; } this.props.makeActive(entryKey(this.props.entryInfo), active); } dispatchSave(serverID: ?string, newText: string) { if (this.currentlySaving === newText) { return; } this.currentlySaving = newText; if (newText.trim() === '') { // We don't save the empty string, since as soon as the element becomes // inactive it'll get deleted return; } if (!serverID) { if (this.creating) { // We need the first save call to return so we know the ID of the entry // we're updating, so we'll need to handle this save later this.needsUpdateAfterCreation = true; return; } else { this.creating = true; } } this.guardedSetState({ loadingStatus: 'loading' }); if (!serverID) { this.props.dispatchActionPromise( createEntryActionTypes, this.createAction(newText), ); } else { this.props.dispatchActionPromise( saveEntryActionTypes, this.saveAction(serverID, newText), ); } } async createAction(text: string): Promise { const localID = this.props.entryInfo.localID; invariant(localID, "if there's no serverID, there should be a localID"); const curSaveAttempt = this.nextSaveAttemptIndex++; try { const response = await this.props.createEntry({ text, timestamp: this.props.entryInfo.creationTime, date: dateString( this.props.entryInfo.year, this.props.entryInfo.month, this.props.entryInfo.day, ), threadID: this.props.entryInfo.threadID, localID, calendarQuery: this.props.calendarQuery(), }); if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) { this.guardedSetState({ loadingStatus: 'inactive' }); } this.creating = false; if (this.needsUpdateAfterCreation) { this.needsUpdateAfterCreation = false; this.dispatchSave(response.entryID, this.state.text); } if (this.needsDeleteAfterCreation) { this.needsDeleteAfterCreation = false; this.dispatchDelete(response.entryID); } return response; } catch (e) { if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) { this.guardedSetState({ loadingStatus: 'error' }); } this.currentlySaving = null; this.creating = false; throw e; } } async saveAction( entryID: string, newText: string, ): Promise { const curSaveAttempt = this.nextSaveAttemptIndex++; try { const response = await this.props.saveEntry({ entryID, text: newText, prevText: this.props.entryInfo.text, timestamp: Date.now(), calendarQuery: this.props.calendarQuery(), }); if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) { this.guardedSetState({ loadingStatus: 'inactive' }); } return { ...response, threadID: this.props.entryInfo.threadID }; } catch (e) { if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) { this.guardedSetState({ loadingStatus: 'error' }); } this.currentlySaving = null; if (e instanceof ServerError && e.message === 'concurrent_modification') { - const revertedText = e.payload.db; + const revertedText = e.payload?.db; const onRefresh = () => { this.guardedSetState({ loadingStatus: 'inactive', text: revertedText, }); this.props.dispatch({ type: concurrentModificationResetActionType, payload: { id: entryID, dbText: revertedText }, }); }; Alert.alert( 'Concurrent modification', 'It looks like somebody is attempting to modify that field at the ' + 'same time as you! Please try again.', [{ text: 'OK', onPress: onRefresh }], { cancelable: false }, ); } throw e; } } delete: () => void = () => { this.dispatchDelete(this.props.entryInfo.id); }; onPressEdit: () => void = () => { if (this.state.editing) { this.completeEdit(); } else { this.guardedSetState({ editing: true }); } }; dispatchDelete(serverID: ?string) { if (this.deleted) { return; } this.deleted = true; LayoutAnimation.easeInEaseOut(); const { localID } = this.props.entryInfo; this.props.dispatchActionPromise( deleteEntryActionTypes, this.deleteAction(serverID), undefined, { localID, serverID }, ); } async deleteAction(serverID: ?string): Promise { if (serverID) { return await this.props.deleteEntry({ entryID: serverID, prevText: this.props.entryInfo.text, calendarQuery: this.props.calendarQuery(), }); } else if (this.creating) { this.needsDeleteAfterCreation = true; } return null; } onPressThreadName: () => void = () => { Keyboard.dismiss(); this.props.navigateToThread({ threadInfo: this.props.threadInfo }); }; } const unboundStyles = { actionLinks: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', marginTop: -5, }, button: { padding: 5, }, buttonContents: { flex: 1, flexDirection: 'row', }, container: { backgroundColor: 'listBackground', }, entry: { borderRadius: 8, margin: 5, overflow: 'hidden', }, leftLinks: { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', paddingHorizontal: 5, }, leftLinksText: { fontSize: 12, fontWeight: 'bold', paddingLeft: 5, }, pencilIcon: { lineHeight: 13, paddingTop: 1, }, rightLinks: { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', paddingHorizontal: 5, }, rightLinksText: { fontSize: 12, fontWeight: 'bold', }, text: { fontFamily: 'System', fontSize: 16, }, textContainer: { position: 'absolute', top: 0, paddingBottom: 6, paddingLeft: 10, paddingRight: 10, paddingTop: 5, transform: (Platform.select({ ios: [{ translateY: -1 / 3 }], default: [], }): $ReadOnlyArray<{ +translateY: number }>), }, textInput: { fontFamily: 'System', fontSize: 16, left: ((Platform.OS === 'android' ? 9.8 : 10): number), margin: 0, padding: 0, position: 'absolute', right: 10, top: ((Platform.OS === 'android' ? 4.8 : 0.5): number), }, }; registerFetchKey(saveEntryActionTypes); registerFetchKey(deleteEntryActionTypes); const activeThreadPickerSelector = createIsForegroundSelector( ThreadPickerModalRouteName, ); const Entry: React.ComponentType = React.memo( function ConnectedEntry(props: BaseProps) { const navContext = React.useContext(NavContext); const threadPickerActive = activeThreadPickerSelector(navContext); const calendarQuery = useSelector(state => nonThreadCalendarQuery({ redux: state, navContext, }), ); const online = useSelector( state => state.connection.status === 'connected', ); const styles = useStyles(unboundStyles); const navigateToThread = useNavigateToThread(); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callCreateEntry = useServerCall(createEntry); const callSaveEntry = useServerCall(saveEntry); const callDeleteEntry = useServerCall(deleteEntry); return ( ); }, ); export { InternalEntry, Entry, dummyNodeForEntryHeightMeasurement }; diff --git a/native/flow-typed/npm/@react-navigation/native_v6.x.x.js b/native/flow-typed/npm/@react-navigation/native_v6.x.x.js index 65a2b1393..9e1001e52 100644 --- a/native/flow-typed/npm/@react-navigation/native_v6.x.x.js +++ b/native/flow-typed/npm/@react-navigation/native_v6.x.x.js @@ -1,2393 +1,2393 @@ // flow-typed signature: 219fd8b2f5868928e02073db11adb8a5 // flow-typed version: dc2d6a22c7/@react-navigation/native_v5.x.x/flow_>=v0.104.x declare module '@react-navigation/native' { //--------------------------------------------------------------------------- // SECTION 1: IDENTICAL TYPE DEFINITIONS // This section is identical across all React Navigation libdefs and contains // shared definitions. We wish we could make it DRY and import from a shared // definition, but that isn't yet possible. //--------------------------------------------------------------------------- /** * We start with some definitions that we have copy-pasted from React Native * source files. */ // This is a bastardization of the true StyleObj type located in // react-native/Libraries/StyleSheet/StyleSheetTypes. We unfortunately can't // import that here, and it's too lengthy (and consequently too brittle) to // copy-paste here either. declare type StyleObj = | null | void | number | false | '' | $ReadOnlyArray | { [name: string]: any, ... }; declare type ViewStyleProp = StyleObj; declare type TextStyleProp = StyleObj; declare type AnimatedViewStyleProp = StyleObj; declare type AnimatedTextStyleProp = StyleObj; // Vaguely copied from // react-native/Libraries/Animated/src/animations/Animation.js declare type EndResult = { finished: boolean, ... }; declare type EndCallback = (result: EndResult) => void; declare interface Animation { start( fromValue: number, onUpdate: (value: number) => void, onEnd: ?EndCallback, previousAnimation: ?Animation, animatedValue: AnimatedValue, ): void; stop(): void; } declare type AnimationConfig = { isInteraction?: boolean, useNativeDriver: boolean, onComplete?: ?EndCallback, iterations?: number, ... }; // Vaguely copied from // react-native/Libraries/Animated/src/nodes/AnimatedTracking.js declare interface AnimatedTracking { constructor( value: AnimatedValue, parent: any, animationClass: any, animationConfig: Object, callback?: ?EndCallback, ): void; update(): void; } // Vaguely copied from // react-native/Libraries/Animated/src/nodes/AnimatedValue.js declare type ValueListenerCallback = (state: { value: number, ... }) => void; declare interface AnimatedValue { constructor(value: number): void; setValue(value: number): void; setOffset(offset: number): void; flattenOffset(): void; extractOffset(): void; addListener(callback: ValueListenerCallback): string; removeListener(id: string): void; removeAllListeners(): void; stopAnimation(callback?: ?(value: number) => void): void; resetAnimation(callback?: ?(value: number) => void): void; interpolate(config: InterpolationConfigType): AnimatedInterpolation; animate(animation: Animation, callback: ?EndCallback): void; stopTracking(): void; track(tracking: AnimatedTracking): void; } // Copied from // react-native/Libraries/Animated/src/animations/TimingAnimation.js declare type TimingAnimationConfigSingle = AnimationConfig & { toValue: number | AnimatedValue, easing?: (value: number) => number, duration?: number, delay?: number, ... }; // Copied from // react-native/Libraries/Animated/src/animations/SpringAnimation.js declare type SpringAnimationConfigSingle = AnimationConfig & { toValue: number | AnimatedValue, overshootClamping?: boolean, restDisplacementThreshold?: number, restSpeedThreshold?: number, velocity?: number, bounciness?: number, speed?: number, tension?: number, friction?: number, stiffness?: number, damping?: number, mass?: number, delay?: number, ... }; // Copied from react-native/Libraries/Types/CoreEventTypes.js declare type SyntheticEvent = $ReadOnly<{| bubbles: ?boolean, cancelable: ?boolean, currentTarget: number, defaultPrevented: ?boolean, dispatchConfig: $ReadOnly<{| registrationName: string, |}>, eventPhase: ?number, preventDefault: () => void, isDefaultPrevented: () => boolean, stopPropagation: () => void, isPropagationStopped: () => boolean, isTrusted: ?boolean, nativeEvent: T, persist: () => void, target: ?number, timeStamp: number, type: ?string, |}>; declare type Layout = $ReadOnly<{| x: number, y: number, width: number, height: number, |}>; declare type LayoutEvent = SyntheticEvent< $ReadOnly<{| layout: Layout, |}>, >; declare type BlurEvent = SyntheticEvent< $ReadOnly<{| target: number, |}>, >; declare type FocusEvent = SyntheticEvent< $ReadOnly<{| target: number, |}>, >; declare type ResponderSyntheticEvent = $ReadOnly<{| ...SyntheticEvent, touchHistory: $ReadOnly<{| indexOfSingleActiveTouch: number, mostRecentTimeStamp: number, numberActiveTouches: number, touchBank: $ReadOnlyArray< $ReadOnly<{| touchActive: boolean, startPageX: number, startPageY: number, startTimeStamp: number, currentPageX: number, currentPageY: number, currentTimeStamp: number, previousPageX: number, previousPageY: number, previousTimeStamp: number, |}>, >, |}>, |}>; declare type PressEvent = ResponderSyntheticEvent< $ReadOnly<{| changedTouches: $ReadOnlyArray<$PropertyType>, force: number, identifier: number, locationX: number, locationY: number, pageX: number, pageY: number, target: ?number, timestamp: number, touches: $ReadOnlyArray<$PropertyType>, |}>, >; // Vaguely copied from // react-native/Libraries/Animated/src/nodes/AnimatedInterpolation.js declare type ExtrapolateType = 'extend' | 'identity' | 'clamp'; declare type InterpolationConfigType = { inputRange: Array, outputRange: Array | Array, easing?: (input: number) => number, extrapolate?: ExtrapolateType, extrapolateLeft?: ExtrapolateType, extrapolateRight?: ExtrapolateType, ... }; declare interface AnimatedInterpolation { interpolate(config: InterpolationConfigType): AnimatedInterpolation; } // Copied from react-native/Libraries/Components/View/ViewAccessibility.js declare type AccessibilityRole = | 'none' | 'button' | 'link' | 'search' | 'image' | 'keyboardkey' | 'text' | 'adjustable' | 'imagebutton' | 'header' | 'summary' | 'alert' | 'checkbox' | 'combobox' | 'menu' | 'menubar' | 'menuitem' | 'progressbar' | 'radio' | 'radiogroup' | 'scrollbar' | 'spinbutton' | 'switch' | 'tab' | 'tablist' | 'timer' | 'toolbar'; declare type AccessibilityActionInfo = $ReadOnly<{ name: string, label?: string, ... }>; declare type AccessibilityActionEvent = SyntheticEvent< $ReadOnly<{actionName: string, ...}>, >; declare type AccessibilityState = { disabled?: boolean, selected?: boolean, checked?: ?boolean | 'mixed', busy?: boolean, expanded?: boolean, ... }; declare type AccessibilityValue = $ReadOnly<{| min?: number, max?: number, now?: number, text?: string, |}>; // Copied from // react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js declare type Stringish = string; declare type EdgeInsetsProp = $ReadOnly<$Partial>; declare type TouchableWithoutFeedbackProps = $ReadOnly<{| accessibilityActions?: ?$ReadOnlyArray, accessibilityElementsHidden?: ?boolean, accessibilityHint?: ?Stringish, accessibilityIgnoresInvertColors?: ?boolean, accessibilityLabel?: ?Stringish, accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'), accessibilityRole?: ?AccessibilityRole, accessibilityState?: ?AccessibilityState, accessibilityValue?: ?AccessibilityValue, accessibilityViewIsModal?: ?boolean, accessible?: ?boolean, children?: ?React$Node, delayLongPress?: ?number, delayPressIn?: ?number, delayPressOut?: ?number, disabled?: ?boolean, focusable?: ?boolean, hitSlop?: ?EdgeInsetsProp, importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'), nativeID?: ?string, onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, onBlur?: ?(event: BlurEvent) => mixed, onFocus?: ?(event: FocusEvent) => mixed, onLayout?: ?(event: LayoutEvent) => mixed, onLongPress?: ?(event: PressEvent) => mixed, onPress?: ?(event: PressEvent) => mixed, onPressIn?: ?(event: PressEvent) => mixed, onPressOut?: ?(event: PressEvent) => mixed, pressRetentionOffset?: ?EdgeInsetsProp, rejectResponderTermination?: ?boolean, testID?: ?string, touchSoundDisabled?: ?boolean, |}>; // Copied from react-native/Libraries/Image/ImageSource.js declare type ImageURISource = $ReadOnly<{ uri?: ?string, bundle?: ?string, method?: ?string, headers?: ?Object, body?: ?string, cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached'), width?: ?number, height?: ?number, scale?: ?number, ... }>; /** * The following is copied from react-native-gesture-handler's libdef */ declare type StateUndetermined = 0; declare type StateFailed = 1; declare type StateBegan = 2; declare type StateCancelled = 3; declare type StateActive = 4; declare type StateEnd = 5; declare type GestureHandlerState = | StateUndetermined | StateFailed | StateBegan | StateCancelled | StateActive | StateEnd; declare type $SyntheticEvent = { +nativeEvent: $ReadOnly<$Exact>, ... }; declare type $Event = $SyntheticEvent<{ handlerTag: number, numberOfPointers: number, state: GestureHandlerState, oldState: GestureHandlerState, ...$Exact, ... }>; declare type $EventHandlers = {| onGestureEvent?: ($Event) => mixed, onHandlerStateChange?: ($Event) => mixed, onBegan?: ($Event) => mixed, onFailed?: ($Event) => mixed, onCancelled?: ($Event) => mixed, onActivated?: ($Event) => mixed, onEnded?: ($Event) => mixed, |}; declare type HitSlop = | number | {| left?: number, top?: number, right?: number, bottom?: number, vertical?: number, horizontal?: number, width?: number, height?: number, |} | {| width: number, left: number, |} | {| width: number, right: number, |} | {| height: number, top: number, |} | {| height: number, bottom: number, |}; declare type $GestureHandlerProps< AdditionalProps: {...}, ExtraEventsProps: {...} > = $ReadOnly<{| ...$Exact, ...$EventHandlers, id?: string, enabled?: boolean, waitFor?: React$Ref | Array>, simultaneousHandlers?: React$Ref | Array>, shouldCancelWhenOutside?: boolean, minPointers?: number, hitSlop?: HitSlop, children?: React$Node, |}>; declare type PanGestureHandlerProps = $GestureHandlerProps< { activeOffsetY?: number | [number, number], activeOffsetX?: number | [number, number], failOffsetY?: number | [number, number], failOffsetX?: number | [number, number], minDist?: number, minVelocity?: number, minVelocityX?: number, minVelocityY?: number, minPointers?: number, maxPointers?: number, avgTouches?: boolean, ... }, { x: number, y: number, absoluteX: number, absoluteY: number, translationX: number, translationY: number, velocityX: number, velocityY: number, ... } >; /** * MAGIC */ declare type $If = $Call< ((true, Then, Else) => Then) & ((false, Then, Else) => Else), Test, Then, Else, >; declare type $IsA = $Call< (Y => true) & (mixed => false), X, >; declare type $IsUndefined = $IsA; declare type $Partial = $ReadOnly<$Rest>; // If { ...T, ... } counts as a T, then we're inexact declare type $IsExact = $Call< (T => false) & (mixed => true), { ...T, ... }, >; /** * Actions, state, etc. */ declare export type ScreenParams = { +[key: string]: mixed, ... }; declare export type BackAction = {| +type: 'GO_BACK', +source?: string, +target?: string, |}; declare export type NavigateAction = {| +type: 'NAVIGATE', +payload: | {| +key: string, +params?: ScreenParams |} | {| +name: string, +key?: string, +params?: ScreenParams |}, +source?: string, +target?: string, |}; declare export type ResetAction = {| +type: 'RESET', +payload: StaleNavigationState, +source?: string, +target?: string, |}; declare export type SetParamsAction = {| +type: 'SET_PARAMS', +payload: {| +params?: ScreenParams |}, +source?: string, +target?: string, |}; declare export type CommonAction = | BackAction | NavigateAction | ResetAction | SetParamsAction; declare type NavigateActionCreator = {| (routeName: string, params?: ScreenParams): NavigateAction, ( | {| +key: string, +params?: ScreenParams |} | {| +name: string, +key?: string, +params?: ScreenParams |}, ): NavigateAction, |}; declare export type CommonActionsType = {| +navigate: NavigateActionCreator, +goBack: () => BackAction, +reset: (state: PossiblyStaleNavigationState) => ResetAction, +setParams: (params: ScreenParams) => SetParamsAction, |}; declare export type GenericNavigationAction = {| +type: string, +payload?: { +[key: string]: mixed, ... }, +source?: string, +target?: string, |}; declare export type LeafRoute = {| +key: string, +name: RouteName, +params?: ScreenParams, |}; declare export type StateRoute = {| ...LeafRoute, +state: NavigationState | StaleNavigationState, |}; declare export type Route = | LeafRoute | StateRoute; declare export type NavigationState = {| +key: string, +index: number, +routeNames: $ReadOnlyArray, +history?: $ReadOnlyArray, +routes: $ReadOnlyArray>, +type: string, +stale: false, |}; declare export type StaleLeafRoute = {| +key?: string, +name: RouteName, +params?: ScreenParams, |}; declare export type StaleStateRoute = {| ...StaleLeafRoute, +state: StaleNavigationState, |}; declare export type StaleRoute = | StaleLeafRoute | StaleStateRoute; declare export type StaleNavigationState = {| // It's possible to pass React Nav a StaleNavigationState with an undefined // index, but React Nav will always return one with the index set. This is // the same as for the type property below, but in the case of index we tend // to rely on it being set more... +index: number, +history?: $ReadOnlyArray, +routes: $ReadOnlyArray>, +type?: string, +stale?: true, |}; declare export type PossiblyStaleNavigationState = | NavigationState | StaleNavigationState; declare export type PossiblyStaleRoute = | Route | StaleRoute; /** * Routers */ declare type ActionCreators< State: NavigationState, Action: GenericNavigationAction, > = { +[key: string]: (...args: any) => (Action | State => Action), ... }; declare export type DefaultRouterOptions = { +initialRouteName?: string, ... }; declare export type RouterFactory< State: NavigationState, Action: GenericNavigationAction, RouterOptions: DefaultRouterOptions, > = (options: RouterOptions) => Router; declare export type ParamListBase = { +[key: string]: ?ScreenParams, ... }; declare export type RouterConfigOptions = {| +routeNames: $ReadOnlyArray, +routeParamList: ParamListBase, |}; declare export type Router< State: NavigationState, Action: GenericNavigationAction, > = {| +type: $PropertyType, +getInitialState: (options: RouterConfigOptions) => State, +getRehydratedState: ( partialState: PossiblyStaleNavigationState, options: RouterConfigOptions, ) => State, +getStateForRouteNamesChange: ( state: State, options: RouterConfigOptions, ) => State, +getStateForRouteFocus: (state: State, key: string) => State, +getStateForAction: ( state: State, action: Action, options: RouterConfigOptions, ) => ?PossiblyStaleNavigationState; +shouldActionChangeFocus: (action: GenericNavigationAction) => boolean, +actionCreators?: ActionCreators, |}; /** * Stack actions and router */ declare export type StackNavigationState = {| ...NavigationState, +type: 'stack', |}; declare export type ReplaceAction = {| +type: 'REPLACE', +payload: {| +name: string, +key?: ?string, +params?: ScreenParams |}, +source?: string, +target?: string, |}; declare export type PushAction = {| +type: 'PUSH', +payload: {| +name: string, +key?: ?string, +params?: ScreenParams |}, +source?: string, +target?: string, |}; declare export type PopAction = {| +type: 'POP', +payload: {| +count: number |}, +source?: string, +target?: string, |}; declare export type PopToTopAction = {| +type: 'POP_TO_TOP', +source?: string, +target?: string, |}; declare export type StackAction = | CommonAction | ReplaceAction | PushAction | PopAction | PopToTopAction; declare export type StackActionsType = {| +replace: (routeName: string, params?: ScreenParams) => ReplaceAction, +push: (routeName: string, params?: ScreenParams) => PushAction, +pop: (count?: number) => PopAction, +popToTop: () => PopToTopAction, |}; declare export type StackRouterOptions = $Exact; /** * Tab actions and router */ declare export type TabNavigationState = {| ...NavigationState, +type: 'tab', +history: $ReadOnlyArray<{| type: 'route', key: string |}>, |}; declare export type JumpToAction = {| +type: 'JUMP_TO', +payload: {| +name: string, +params?: ScreenParams |}, +source?: string, +target?: string, |}; declare export type TabAction = | CommonAction | JumpToAction; declare export type TabActionsType = {| +jumpTo: string => JumpToAction, |}; declare export type TabRouterOptions = {| ...$Exact, +backBehavior?: 'initialRoute' | 'order' | 'history' | 'none', |}; /** * Drawer actions and router */ declare type DrawerHistoryEntry = | {| +type: 'route', +key: string |} | {| +type: 'drawer' |}; declare export type DrawerNavigationState = {| ...NavigationState, +type: 'drawer', +history: $ReadOnlyArray, |}; declare export type OpenDrawerAction = {| +type: 'OPEN_DRAWER', +source?: string, +target?: string, |}; declare export type CloseDrawerAction = {| +type: 'CLOSE_DRAWER', +source?: string, +target?: string, |}; declare export type ToggleDrawerAction = {| +type: 'TOGGLE_DRAWER', +source?: string, +target?: string, |}; declare export type DrawerAction = | TabAction | OpenDrawerAction | CloseDrawerAction | ToggleDrawerAction; declare export type DrawerActionsType = {| ...TabActionsType, +openDrawer: () => OpenDrawerAction, +closeDrawer: () => CloseDrawerAction, +toggleDrawer: () => ToggleDrawerAction, |}; declare export type DrawerRouterOptions = {| ...TabRouterOptions, +defaultStatus?: 'open' | 'closed', |}; /** * Events */ declare export type EventMapBase = { +[name: string]: {| +data?: mixed, +canPreventDefault?: boolean, |}, ... }; declare type EventPreventDefaultProperties = $If< Test, {| +defaultPrevented: boolean, +preventDefault: () => void |}, {| |}, >; declare type EventDataProperties = $If< $IsUndefined, {| |}, {| +data: Data |}, >; declare type EventArg< EventName: string, CanPreventDefault: ?boolean = false, Data = void, > = {| ...EventPreventDefaultProperties, ...EventDataProperties, +type: EventName, +target?: string, |}; declare type GlobalEventMap = {| +state: {| +data: {| +state: State |}, +canPreventDefault: false |}, |}; declare type EventMapCore = {| ...GlobalEventMap, +focus: {| +data: void, +canPreventDefault: false |}, +blur: {| +data: void, +canPreventDefault: false |}, +beforeRemove: {| +data: {| +action: GenericNavigationAction |}, +canPreventDefault: true, |}, |}; declare type EventListenerCallback< EventName: string, State: PossiblyStaleNavigationState = NavigationState, EventMap: EventMapBase = EventMapCore, > = (e: EventArg< EventName, $PropertyType< $ElementType< {| ...EventMap, ...EventMapCore |}, EventName, >, 'canPreventDefault', >, $PropertyType< $ElementType< {| ...EventMap, ...EventMapCore |}, EventName, >, 'data', >, >) => mixed; /** * Navigation prop */ declare type PartialWithMergeProperty = $If< $IsExact, { ...$Partial, +merge: true }, { ...$Partial, +merge: true, ... }, >; declare type EitherExactOrPartialWithMergeProperty = | ParamsType | PartialWithMergeProperty; declare export type SimpleNavigate = >( routeName: DestinationRouteName, params: EitherExactOrPartialWithMergeProperty< $ElementType, >, ) => void; declare export type Navigate = & SimpleNavigate & >( route: $If< $IsUndefined<$ElementType>, | {| +key: string |} | {| +name: DestinationRouteName, +key?: string |}, | {| +key: string, +params?: EitherExactOrPartialWithMergeProperty< $ElementType, >, |} | {| +name: DestinationRouteName, +key?: string, +params?: EitherExactOrPartialWithMergeProperty< $ElementType, >, |}, >, ) => void; declare type CoreNavigationHelpers< ParamList: ParamListBase, State: PossiblyStaleNavigationState = PossiblyStaleNavigationState, EventMap: EventMapBase = EventMapCore, > = { +navigate: Navigate, +dispatch: ( action: | GenericNavigationAction | (State => GenericNavigationAction), ) => void, +reset: PossiblyStaleNavigationState => void, +goBack: () => void, +isFocused: () => boolean, +canGoBack: () => boolean, +getId: () => string | void, +getParent: >(id?: string) => ?Parent, +getState: () => NavigationState, +addListener: |}, >>( name: EventName, callback: EventListenerCallback, ) => () => void, +removeListener: |}, >>( name: EventName, callback: EventListenerCallback, ) => void, ... }; declare export type NavigationHelpers< ParamList: ParamListBase, State: PossiblyStaleNavigationState = PossiblyStaleNavigationState, EventMap: EventMapBase = EventMapCore, > = { ...$Exact>, +setParams: (params: ScreenParams) => void, ... }; declare type SetParamsInput< ParamList: ParamListBase, RouteName: $Keys = $Keys, > = $If< $IsUndefined<$ElementType>, empty, $Partial<$NonMaybeType<$ElementType>>, >; declare export type NavigationProp< ParamList: ParamListBase, RouteName: $Keys = $Keys, State: PossiblyStaleNavigationState = PossiblyStaleNavigationState, ScreenOptions: {...} = {...}, EventMap: EventMapBase = EventMapCore, > = { ...$Exact>, +setOptions: (options: $Partial) => void, +setParams: (params: SetParamsInput) => void, ... }; /** * CreateNavigator */ declare export type RouteProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, > = {| ...LeafRoute, +params: $ElementType, +path?: string, |}; declare type ScreenOptionsProp< ScreenOptions: {...}, RouteParam, NavHelpers, > = | ScreenOptions | ({| +route: RouteParam, +navigation: NavHelpers |}) => ScreenOptions; declare export type ScreenListeners< State: NavigationState = NavigationState, EventMap: EventMapBase = EventMapCore, > = $ObjMapi< {| [name: $Keys]: empty |}, >(K, empty) => EventListenerCallback, >; declare type ScreenListenersProp< ScreenListenersParam: {...}, RouteParam, NavHelpers, > = | ScreenListenersParam | ({| +route: RouteParam, +navigation: NavHelpers |}) => ScreenListenersParam; declare type BaseScreenProps< ParamList: ParamListBase, NavProp, RouteName: $Keys = $Keys, State: NavigationState = NavigationState, ScreenOptions: {...} = {...}, EventMap: EventMapBase = EventMapCore, > = {| +name: RouteName, +options?: ScreenOptionsProp< ScreenOptions, RouteProp, NavProp, >, +listeners?: ScreenListenersProp< ScreenListeners, RouteProp, NavProp, >, +initialParams?: $Partial<$ElementType>, +getId?: ({ +params: $ElementType, }) => string | void, +navigationKey?: string, |}; declare export type ScreenProps< ParamList: ParamListBase, NavProp, RouteName: $Keys = $Keys, State: NavigationState = NavigationState, ScreenOptions: {...} = {...}, EventMap: EventMapBase = EventMapCore, > = | {| ...BaseScreenProps< ParamList, NavProp, RouteName, State, ScreenOptions, EventMap, >, +component: React$ComponentType<{| +route: RouteProp, +navigation: NavProp, |}>, |} | {| ...BaseScreenProps< ParamList, NavProp, RouteName, State, ScreenOptions, EventMap, >, +getComponent: () => React$ComponentType<{| +route: RouteProp, +navigation: NavProp, |}>, |} | {| ...BaseScreenProps< ParamList, NavProp, RouteName, State, ScreenOptions, EventMap, >, +children: ({| +route: RouteProp, +navigation: NavProp, |}) => React$Node, |}; declare export type ScreenComponent< GlobalParamList: ParamListBase, ParamList: ParamListBase, State: NavigationState = NavigationState, ScreenOptions: {...} = {...}, EventMap: EventMapBase = EventMapCore, > = < RouteName: $Keys, NavProp: NavigationProp< GlobalParamList, RouteName, State, ScreenOptions, EventMap, >, >(props: ScreenProps< ParamList, NavProp, RouteName, State, ScreenOptions, EventMap, >) => React$Node; declare type ScreenOptionsProps< ScreenOptions: {...}, RouteParam, NavHelpers, > = {| +screenOptions?: ScreenOptionsProp, |}; declare type ScreenListenersProps< ScreenListenersParam: {...}, RouteParam, NavHelpers, > = {| +screenListeners?: ScreenListenersProp< ScreenListenersParam, RouteParam, NavHelpers, >, |}; declare export type ExtraNavigatorPropsBase = { ...$Exact, +id?: string, +children?: React$Node, ... }; declare export type NavigatorProps< ScreenOptions: {...}, ScreenListenersParam, RouteParam, NavHelpers, ExtraNavigatorProps: ExtraNavigatorPropsBase, > = { ...$Exact, ...ScreenOptionsProps, ...ScreenListenersProps, +defaultScreenOptions?: | ScreenOptions | ({| +route: RouteParam, +navigation: NavHelpers, +options: ScreenOptions, |}) => ScreenOptions, ... }; declare export type NavigatorPropsBase< ScreenOptions: {...}, ScreenListenersParam: {...}, NavHelpers, > = NavigatorProps< ScreenOptions, ScreenListenersParam, RouteProp<>, NavHelpers, ExtraNavigatorPropsBase, >; declare export type CreateNavigator< State: NavigationState, ScreenOptions: {...}, EventMap: EventMapBase, ExtraNavigatorProps: ExtraNavigatorPropsBase, > = < GlobalParamList: ParamListBase, ParamList: ParamListBase, NavHelpers: NavigationHelpers< GlobalParamList, State, EventMap, >, >() => {| +Screen: ScreenComponent< GlobalParamList, ParamList, State, ScreenOptions, EventMap, >, +Navigator: React$ComponentType<$Exact, RouteProp, NavHelpers, ExtraNavigatorProps, >>>, +Group: React$ComponentType<{| ...ScreenOptionsProps, NavHelpers>, +children: React$Node, +navigationKey?: string, |}>, |}; declare export type CreateNavigatorFactory = < State: NavigationState, ScreenOptions: {...}, EventMap: EventMapBase, NavHelpers: NavigationHelpers< ParamListBase, State, EventMap, >, ExtraNavigatorProps: ExtraNavigatorPropsBase, >( navigator: React$ComponentType<$Exact, RouteProp<>, NavHelpers, ExtraNavigatorProps, >>>, ) => CreateNavigator; /** * useNavigationBuilder */ declare export type Descriptor< NavHelpers, ScreenOptions: {...} = {...}, > = {| +render: () => React$Node, +options: $ReadOnly, +navigation: NavHelpers, |}; declare export type UseNavigationBuilder = < State: NavigationState, Action: GenericNavigationAction, ScreenOptions: {...}, RouterOptions: DefaultRouterOptions, NavHelpers, EventMap: EventMapBase, ExtraNavigatorProps: ExtraNavigatorPropsBase, >( routerFactory: RouterFactory, options: $Exact, RouteProp<>, NavHelpers, ExtraNavigatorProps, >>, ) => {| +id?: string, +state: State, +descriptors: {| +[key: string]: Descriptor |}, +navigation: NavHelpers, |}; /** * EdgeInsets */ declare type EdgeInsets = {| +top: number, +right: number, +bottom: number, +left: number, |}; /** * TransitionPreset */ declare export type TransitionSpec = | {| animation: 'spring', config: $Diff< SpringAnimationConfigSingle, { toValue: number | AnimatedValue, ... }, >, |} | {| animation: 'timing', config: $Diff< TimingAnimationConfigSingle, { toValue: number | AnimatedValue, ... }, >, |}; declare export type StackCardInterpolationProps = {| +current: {| +progress: AnimatedInterpolation, |}, +next?: {| +progress: AnimatedInterpolation, |}, +index: number, +closing: AnimatedInterpolation, +swiping: AnimatedInterpolation, +inverted: AnimatedInterpolation, +layouts: {| +screen: {| +width: number, +height: number |}, |}, +insets: EdgeInsets, |}; declare export type StackCardInterpolatedStyle = {| containerStyle?: AnimatedViewStyleProp, cardStyle?: AnimatedViewStyleProp, overlayStyle?: AnimatedViewStyleProp, shadowStyle?: AnimatedViewStyleProp, |}; declare export type StackCardStyleInterpolator = ( props: StackCardInterpolationProps, ) => StackCardInterpolatedStyle; declare export type StackHeaderInterpolationProps = {| +current: {| +progress: AnimatedInterpolation, |}, +next?: {| +progress: AnimatedInterpolation, |}, +layouts: {| +header: {| +width: number, +height: number |}, +screen: {| +width: number, +height: number |}, +title?: {| +width: number, +height: number |}, +leftLabel?: {| +width: number, +height: number |}, |}, |}; declare export type StackHeaderInterpolatedStyle = {| leftLabelStyle?: AnimatedViewStyleProp, leftButtonStyle?: AnimatedViewStyleProp, rightButtonStyle?: AnimatedViewStyleProp, titleStyle?: AnimatedViewStyleProp, backgroundStyle?: AnimatedViewStyleProp, |}; declare export type StackHeaderStyleInterpolator = ( props: StackHeaderInterpolationProps, ) => StackHeaderInterpolatedStyle; declare type GestureDirection = | 'horizontal' | 'horizontal-inverted' | 'vertical' | 'vertical-inverted'; declare export type TransitionPreset = {| +gestureDirection: GestureDirection, +transitionSpec: {| +open: TransitionSpec, +close: TransitionSpec, |}, +cardStyleInterpolator: StackCardStyleInterpolator, +headerStyleInterpolator: StackHeaderStyleInterpolator, |}; /** * Stack options */ declare export type StackDescriptor = Descriptor< StackNavigationHelpers<>, StackOptions, >; declare type Scene = {| +route: T, +descriptor: StackDescriptor, +progress: {| +current: AnimatedInterpolation, +next?: AnimatedInterpolation, +previous?: AnimatedInterpolation, |}, |}; declare export type StackHeaderProps = {| +navigation: StackNavigationHelpers<>, +route: RouteProp<>, +options: StackOptions, +layout: {| +width: number, +height: number |}, +progress: AnimatedInterpolation, +back?: {| +title: string |}, +styleInterpolator: StackHeaderStyleInterpolator, |}; declare export type StackHeaderLeftButtonProps = $Partial<{| +onPress: (() => void), +pressColorAndroid: string; +backImage: (props: {| tintColor: string |}) => React$Node, +tintColor: string, +label: string, +truncatedLabel: string, +labelVisible: boolean, +labelStyle: AnimatedTextStyleProp, +allowFontScaling: boolean, +onLabelLayout: LayoutEvent => void, +screenLayout: {| +width: number, +height: number |}, +titleLayout: {| +width: number, +height: number |}, +canGoBack: boolean, |}>; declare type StackHeaderTitleInputBase = { +onLayout: LayoutEvent => void, +children: string, +allowFontScaling: ?boolean, +tintColor: ?string, +style: ?AnimatedTextStyleProp, ... }; declare export type StackHeaderTitleInputProps = $Exact; declare export type StackOptions = $Partial<{| +title: string, +cardShadowEnabled: boolean, +cardOverlayEnabled: boolean, +cardOverlay: {| style: ViewStyleProp |} => React$Node, +cardStyle: ViewStyleProp, +animationEnabled: boolean, +animationTypeForReplace: 'push' | 'pop', +gestureEnabled: boolean, +gestureResponseDistance: number, +gestureVelocityImpact: number, +safeAreaInsets: $Partial, +keyboardHandlingEnabled: boolean, +presentation: 'card' | 'modal' | 'transparentModal', // Transition ...TransitionPreset, // Header +header: StackHeaderProps => React$Node, +headerShown: boolean, +headerMode: 'float' | 'screen', +headerTitle: string | (StackHeaderTitleInputProps => React$Node), +headerTitleAlign: 'left' | 'center', +headerTitleStyle: AnimatedTextStyleProp, +headerTitleContainerStyle: ViewStyleProp, +headerTintColor: string, +headerTitleAllowFontScaling: boolean, +headerBackAllowFontScaling: boolean, +headerBackTitle: string | null, +headerBackTitleStyle: TextStyleProp, +headerBackTitleVisible: boolean, +headerTruncatedBackTitle: string, +headerLeft: StackHeaderLeftButtonProps => React$Node, +headerLeftContainerStyle: ViewStyleProp, +headerRight: {| tintColor?: string |} => React$Node, +headerRightContainerStyle: ViewStyleProp, +headerBackImage: $PropertyType, +headerPressColorAndroid: string, +headerBackground: ({| style: ViewStyleProp |}) => React$Node, +headerStyle: ViewStyleProp, +headerTransparent: boolean, +headerStatusBarHeight: number, |}>; /** * Stack navigation prop */ declare export type StackNavigationEventMap = {| ...EventMapCore, +transitionStart: {| +data: {| +closing: boolean |}, +canPreventDefault: false, |}, +transitionEnd: {| +data: {| +closing: boolean |}, +canPreventDefault: false, |}, +gestureStart: {| +data: void, +canPreventDefault: false |}, +gestureEnd: {| +data: void, +canPreventDefault: false |}, +gestureCancel: {| +data: void, +canPreventDefault: false |}, |}; declare type StackExtraNavigationHelpers< ParamList: ParamListBase = ParamListBase, > = {| +replace: SimpleNavigate, +push: SimpleNavigate, +pop: (count?: number) => void, +popToTop: () => void, |}; declare export type StackNavigationHelpers< ParamList: ParamListBase = ParamListBase, EventMap: EventMapBase = StackNavigationEventMap, > = { ...$Exact>, ...StackExtraNavigationHelpers, ... }; declare export type StackNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, Options: {...} = StackOptions, EventMap: EventMapBase = StackNavigationEventMap, > = {| ...$Exact>, ...StackExtraNavigationHelpers, |}; /** * Miscellaneous stack exports */ declare type StackNavigationConfig = {| +detachInactiveScreens?: boolean, |}; declare export type ExtraStackNavigatorProps = {| ...$Exact, ...StackRouterOptions, ...StackNavigationConfig, |}; declare export type StackNavigatorProps< NavHelpers: StackNavigationHelpers<> = StackNavigationHelpers<>, > = $Exact, RouteProp<>, NavHelpers, ExtraStackNavigatorProps, >>; /** * Bottom tab options */ declare export type BottomTabBarButtonProps = {| ...$Diff< TouchableWithoutFeedbackProps, {| onPress?: ?(event: PressEvent) => mixed |}, >, +to?: string, +children: React$Node, +onPress?: (MouseEvent | PressEvent) => void, |}; declare export type TabBarVisibilityAnimationConfig = | {| +animation: 'spring', +config?: $Diff< SpringAnimationConfigSingle, { toValue: number | AnimatedValue, useNativeDriver: boolean, ... }, >, |} | {| +animation: 'timing', +config?: $Diff< TimingAnimationConfigSingle, { toValue: number | AnimatedValue, useNativeDriver: boolean, ... }, >, |}; declare export type BottomTabOptions = $Partial<{| +title: string, +tabBarLabel: | string | ({| focused: boolean, color: string |}) => React$Node, +tabBarIcon: ({| focused: boolean, color: string, size: number, |}) => React$Node, +tabBarBadge: number | string, +tabBarBadgeStyle: TextStyleProp, +tabBarAccessibilityLabel: string, +tabBarTestID: string, +tabBarVisibilityAnimationConfig: $Partial<{| +show: TabBarVisibilityAnimationConfig, +hide: TabBarVisibilityAnimationConfig, |}>, +tabBarButton: BottomTabBarButtonProps => React$Node, +tabBarHideOnKeyboard: boolean, +tabBarActiveTintColor: string, +tabBarInactiveTintColor: string, +tabBarActiveBackgroundColor: string, +tabBarInactiveBackgroundColor: string, +tabBarAllowFontScaling: boolean, +tabBarShowLabel: boolean, +tabBarLabelStyle: TextStyleProp, +tabBarIconStyle: TextStyleProp, +tabBarItemStyle: ViewStyleProp, +tabBarLabelPosition: 'beside-icon' | 'below-icon', +tabBarStyle: ViewStyleProp, +unmountOnBlur: boolean, +headerShown: boolean, +lazy: boolean, |}>; /** * Bottom tab navigation prop */ declare export type BottomTabNavigationEventMap = {| ...EventMapCore, +tabPress: {| +data: void, +canPreventDefault: true |}, +tabLongPress: {| +data: void, +canPreventDefault: false |}, |}; declare type TabExtraNavigationHelpers< ParamList: ParamListBase = ParamListBase, > = {| +jumpTo: SimpleNavigate, |}; declare export type BottomTabNavigationHelpers< ParamList: ParamListBase = ParamListBase, EventMap: EventMapBase = BottomTabNavigationEventMap, > = { ...$Exact>, ...TabExtraNavigationHelpers, ... }; declare export type BottomTabNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, Options: {...} = BottomTabOptions, EventMap: EventMapBase = BottomTabNavigationEventMap, > = {| ...$Exact>, ...TabExtraNavigationHelpers, |}; /** * Miscellaneous bottom tab exports */ declare export type BottomTabDescriptor = Descriptor< BottomTabNavigationHelpers<>, BottomTabOptions, >; declare type BottomTabNavigationBuilderResult = {| +state: TabNavigationState, +navigation: BottomTabNavigationHelpers<>, +descriptors: {| +[key: string]: BottomTabDescriptor |}, |}; declare export type BottomTabBarProps = BottomTabNavigationBuilderResult; declare type BottomTabNavigationConfig = {| +tabBar?: BottomTabBarProps => React$Node, +safeAreaInsets?: $Partial, +detachInactiveScreens?: boolean, |}; declare export type ExtraBottomTabNavigatorProps = {| ...$Exact, ...TabRouterOptions, ...BottomTabNavigationConfig, |}; declare export type BottomTabNavigatorProps< NavHelpers: BottomTabNavigationHelpers<> = BottomTabNavigationHelpers<>, > = $Exact, RouteProp<>, NavHelpers, ExtraBottomTabNavigatorProps, >>; /** * Material bottom tab options */ declare export type MaterialBottomTabOptions = $Partial<{| +title: string, +tabBarColor: string, +tabBarLabel: string, +tabBarIcon: | string | ({| +focused: boolean, +color: string |}) => React$Node, +tabBarBadge: boolean | number | string, +tabBarAccessibilityLabel: string, +tabBarTestID: string, |}>; /** * Material bottom tab navigation prop */ declare export type MaterialBottomTabNavigationEventMap = {| ...EventMapCore, +tabPress: {| +data: void, +canPreventDefault: true |}, |}; declare export type MaterialBottomTabNavigationHelpers< ParamList: ParamListBase = ParamListBase, EventMap: EventMapBase = MaterialBottomTabNavigationEventMap, > = { ...$Exact>, ...TabExtraNavigationHelpers, ... }; declare export type MaterialBottomTabNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, Options: {...} = MaterialBottomTabOptions, EventMap: EventMapBase = MaterialBottomTabNavigationEventMap, > = {| ...$Exact>, ...TabExtraNavigationHelpers, |}; /** * Miscellaneous material bottom tab exports */ declare export type PaperFont = {| +fontFamily: string, +fontWeight?: | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900', |}; declare export type PaperFonts = {| +regular: PaperFont, +medium: PaperFont, +light: PaperFont, +thin: PaperFont, |}; declare export type PaperTheme = {| +dark: boolean, +mode?: 'adaptive' | 'exact', +roundness: number, +colors: {| +primary: string, +background: string, +surface: string, +accent: string, +error: string, +text: string, +onSurface: string, +onBackground: string, +disabled: string, +placeholder: string, +backdrop: string, +notification: string, |}, +fonts: PaperFonts, +animation: {| +scale: number, |}, |}; declare export type PaperRoute = {| +key: string, +title?: string, +icon?: any, +badge?: string | number | boolean, +color?: string, +accessibilityLabel?: string, +testID?: string, |}; declare export type PaperTouchableProps = {| ...TouchableWithoutFeedbackProps, +key: string, +route: PaperRoute, +children: React$Node, +borderless?: boolean, +centered?: boolean, +rippleColor?: string, |}; declare export type MaterialBottomTabNavigationConfig = {| +shifting?: boolean, +labeled?: boolean, +renderTouchable?: PaperTouchableProps => React$Node, +activeColor?: string, +inactiveColor?: string, +sceneAnimationEnabled?: boolean, +keyboardHidesNavigationBar?: boolean, +barStyle?: ViewStyleProp, +style?: ViewStyleProp, +theme?: PaperTheme, |}; declare export type ExtraMaterialBottomTabNavigatorProps = {| ...$Exact, ...TabRouterOptions, ...MaterialBottomTabNavigationConfig, |}; declare export type MaterialBottomTabNavigatorProps< NavHelpers: MaterialBottomTabNavigationHelpers<> = MaterialBottomTabNavigationHelpers<>, > = $Exact, RouteProp<>, NavHelpers, ExtraMaterialBottomTabNavigatorProps, >>; /** * Material top tab options */ declare export type MaterialTopTabOptions = $Partial<{| +title: string, +tabBarLabel: | string | ({| +focused: boolean, +color: string |}) => React$Node, +tabBarIcon: ({| +focused: boolean, +color: string |}) => React$Node, +tabBarAccessibilityLabel: string, +tabBarTestID: string, +tabBarActiveTintColor: string, +tabBarInactiveTintColor: string, +tabBarPressColor: string, +tabBarPressOpacity: number, +tabBarShowLabel: boolean, +tabBarShowIcon: boolean, +tabBarAllowFontScaling: boolean, +tabBarBounces: boolean, +tabBarScrollEnabled: boolean, +tabBarIconStyle: ViewStyleProp, +tabBarLabelStyle: TextStyleProp, +tabBarItemStyle: ViewStyleProp, +tabBarIndicatorStyle: ViewStyleProp, +tabBarIndicatorContainerStyle: ViewStyleProp, +tabBarContentContainerStyle: ViewStyleProp, +tabBarStyle: ViewStyleProp, +tabBarBadge: () => React$Node, +tabBarIndicator: MaterialTopTabBarIndicatorProps => React$Node, +lazy: boolean, +lazyPlaceholder: ({| +route: Route<> |}) => React$Node, |}>; /** * Material top tab navigation prop */ declare export type MaterialTopTabNavigationEventMap = {| ...EventMapCore, +tabPress: {| +data: void, +canPreventDefault: true |}, +tabLongPress: {| +data: void, +canPreventDefault: false |}, +swipeStart: {| +data: void, +canPreventDefault: false |}, +swipeEnd: {| +data: void, +canPreventDefault: false |}, |}; declare export type MaterialTopTabNavigationHelpers< ParamList: ParamListBase = ParamListBase, EventMap: EventMapBase = MaterialTopTabNavigationEventMap, > = { ...$Exact>, ...TabExtraNavigationHelpers, ... }; declare export type MaterialTopTabNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, Options: {...} = MaterialTopTabOptions, EventMap: EventMapBase = MaterialTopTabNavigationEventMap, > = {| ...$Exact>, ...TabExtraNavigationHelpers, |}; /** * Miscellaneous material top tab exports */ declare type MaterialTopTabPagerCommonProps = {| +keyboardDismissMode: 'none' | 'on-drag' | 'auto', +swipeEnabled: boolean, +swipeVelocityImpact?: number, +springVelocityScale?: number, +springConfig: $Partial<{| +damping: number, +mass: number, +stiffness: number, +restSpeedThreshold: number, +restDisplacementThreshold: number, |}>, +timingConfig: $Partial<{| +duration: number, |}>, |}; declare export type MaterialTopTabPagerProps = {| ...MaterialTopTabPagerCommonProps, +onSwipeStart?: () => void, +onSwipeEnd?: () => void, +onIndexChange: (index: number) => void, +navigationState: TabNavigationState, +layout: {| +width: number, +height: number |}, +removeClippedSubviews: boolean, +children: ({| +addListener: (type: 'enter', listener: number => void) => void, +removeListener: (type: 'enter', listener: number => void) => void, +position: any, // Reanimated.Node +render: React$Node => React$Node, +jumpTo: string => void, |}) => React$Node, +gestureHandlerProps: PanGestureHandlerProps, |}; declare export type MaterialTopTabBarIndicatorProps = {| +state: TabNavigationState, +width: string, +style?: ViewStyleProp, +getTabWidth: number => number, |}; declare export type MaterialTopTabDescriptor = Descriptor< MaterialBottomTabNavigationHelpers<>, MaterialBottomTabOptions, >; declare type MaterialTopTabNavigationBuilderResult = {| +state: TabNavigationState, +navigation: MaterialTopTabNavigationHelpers<>, +descriptors: {| +[key: string]: MaterialTopTabDescriptor |}, |}; declare export type MaterialTopTabBarProps = {| ...MaterialTopTabNavigationBuilderResult, +layout: {| +width: number, +height: number |}, +position: any, // Reanimated.Node +jumpTo: string => void, |}; declare export type MaterialTopTabNavigationConfig = {| ...$Partial, +position?: any, // Reanimated.Value +tabBarPosition?: 'top' | 'bottom', +initialLayout?: $Partial<{| +width: number, +height: number |}>, +lazyPreloadDistance?: number, +removeClippedSubviews?: boolean, +sceneContainerStyle?: ViewStyleProp, +style?: ViewStyleProp, +gestureHandlerProps?: PanGestureHandlerProps, +pager?: MaterialTopTabPagerProps => React$Node, +tabBar?: MaterialTopTabBarProps => React$Node, |}; declare export type ExtraMaterialTopTabNavigatorProps = {| ...$Exact, ...TabRouterOptions, ...MaterialTopTabNavigationConfig, |}; declare export type MaterialTopTabNavigatorProps< NavHelpers: MaterialTopTabNavigationHelpers<> = MaterialTopTabNavigationHelpers<>, > = $Exact, RouteProp<>, NavHelpers, ExtraMaterialTopTabNavigatorProps, >>; /** * Drawer options */ declare export type DrawerOptions = $Partial<{| +title: string, +lazy: boolean, +drawerLabel: | string | ({| +color: string, +focused: boolean |}) => React$Node, +drawerIcon: ({| +color: string, +size: number, +focused: boolean, |}) => React$Node, +drawerActiveTintColor: string, +drawerActiveBackgroundColor: string, +drawerInactiveTintColor: string, +drawerInactiveBackgroundColor: string, +drawerItemStyle: ViewStyleProp, +drawerLabelStyle: TextStyleProp, +drawerContentContainerStyle: ViewStyleProp, +drawerContentStyle: ViewStyleProp, +drawerStyle: ViewStyleProp, +drawerPosition: 'left' | 'right', +drawerType: 'front' | 'back' | 'slide' | 'permanent', +drawerHideStatusBarOnOpen: boolean, +drawerStatusBarAnimation: 'slide' | 'none' | 'fade', +overlayColor: string, +sceneContainerStyle: ViewStyleProp, +gestureHandlerProps: PanGestureHandlerProps, +swipeEnabled: boolean, +swipeEdgeWidth: number, +swipeMinDistance: number, +keyboardDismissMode: 'on-drag' | 'none', +unmountOnBlur: boolean, +headerShown: boolean, |}>; /** * Drawer navigation prop */ declare export type DrawerNavigationEventMap = EventMapCore; declare type DrawerExtraNavigationHelpers< ParamList: ParamListBase = ParamListBase, > = {| +jumpTo: SimpleNavigate, +openDrawer: () => void, +closeDrawer: () => void, +toggleDrawer: () => void, |}; declare export type DrawerNavigationHelpers< ParamList: ParamListBase = ParamListBase, EventMap: EventMapBase = DrawerNavigationEventMap, > = { ...$Exact>, ...DrawerExtraNavigationHelpers, ... }; declare export type DrawerNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, Options: {...} = DrawerOptions, EventMap: EventMapBase = DrawerNavigationEventMap, > = {| ...$Exact>, ...DrawerExtraNavigationHelpers, |}; /** * Miscellaneous drawer exports */ declare export type DrawerDescriptor = Descriptor< DrawerNavigationHelpers<>, DrawerOptions, >; declare type DrawerNavigationBuilderResult = {| +state: DrawerNavigationState, +navigation: DrawerNavigationHelpers<>, +descriptors: {| +[key: string]: DrawerDescriptor |}, |}; declare export type DrawerNavigationConfig = {| +drawerContent?: DrawerNavigationBuilderResult => React$Node, +detachInactiveScreens?: boolean, +useLegacyImplementation?: boolean, |}; declare export type ExtraDrawerNavigatorProps = {| ...$Exact, ...DrawerRouterOptions, ...DrawerNavigationConfig, |}; declare export type DrawerNavigatorProps< NavHelpers: DrawerNavigationHelpers<> = DrawerNavigationHelpers<>, > = $Exact, RouteProp<>, NavHelpers, ExtraDrawerNavigatorProps, >>; /** * BaseNavigationContainer */ declare export type BaseNavigationContainerProps = {| +children: React$Node, +initialState?: PossiblyStaleNavigationState, +onStateChange?: (state: ?PossiblyStaleNavigationState) => void, +independent?: boolean, |}; declare export type ContainerEventMap = {| ...GlobalEventMap, +options: {| +data: {| +options: { +[key: string]: mixed, ... } |}, +canPreventDefault: false, |}, +__unsafe_action__: {| +data: {| +action: GenericNavigationAction, +noop: boolean, |}, +canPreventDefault: false, |}, |}; declare export type BaseNavigationContainerInterface = {| ...$Exact>, +resetRoot: (state?: PossiblyStaleNavigationState) => void, +getRootState: () => NavigationState, +getCurrentRoute: () => RouteProp<> | void, +getCurrentOptions: () => Object | void, +isReady: () => boolean, |}; declare type BaseNavigationContainerInterfaceRef = {| ...BaseNavigationContainerInterface, +current: BaseNavigationContainerInterface | null, |}; /** * State utils */ declare export type GetStateFromPath = ( path: string, options?: LinkingConfig, ) => PossiblyStaleNavigationState; declare export type GetPathFromState = ( state?: ?PossiblyStaleNavigationState, options?: LinkingConfig, ) => string; declare export type GetFocusedRouteNameFromRoute = PossiblyStaleRoute => ?string; /** * Linking */ declare export type ScreenLinkingConfig = {| +path?: string, +exact?: boolean, +parse?: {| +[param: string]: string => mixed |}, +stringify?: {| +[param: string]: mixed => string |}, +screens?: ScreenLinkingConfigMap, +initialRouteName?: string, |}; declare export type ScreenLinkingConfigMap = {| +[routeName: string]: string | ScreenLinkingConfig, |}; declare export type LinkingConfig = {| +initialRouteName?: string, +screens: ScreenLinkingConfigMap, |}; declare export type LinkingOptions = {| +enabled?: boolean, +prefixes: $ReadOnlyArray, +config?: LinkingConfig, +getStateFromPath?: GetStateFromPath, +getPathFromState?: GetPathFromState, |}; /** * NavigationContainer */ declare export type Theme = {| +dark: boolean, +colors: {| +primary: string, +background: string, +card: string, +text: string, +border: string, |}, |}; declare export type NavigationContainerType = React$AbstractComponent< {| ...BaseNavigationContainerProps, +theme?: Theme, +linking?: LinkingOptions, +fallback?: React$Node, +onReady?: () => mixed, |}, BaseNavigationContainerInterface, >; //--------------------------------------------------------------------------- // SECTION 2: EXPORTED MODULE // This section defines the module exports and contains exported types that // are not present in any other React Navigation libdef. //--------------------------------------------------------------------------- /** * Actions and routers */ declare export var CommonActions: CommonActionsType; declare export var StackActions: StackActionsType; declare export var TabActions: TabActionsType; declare export var DrawerActions: DrawerActionsType; declare export var BaseRouter: RouterFactory< NavigationState, CommonAction, DefaultRouterOptions, >; declare export var StackRouter: RouterFactory< StackNavigationState, StackAction, StackRouterOptions, >; declare export var TabRouter: RouterFactory< TabNavigationState, TabAction, TabRouterOptions, >; declare export var DrawerRouter: RouterFactory< DrawerNavigationState, DrawerAction, DrawerRouterOptions, >; /** * Navigator utils */ declare export var BaseNavigationContainer: React$AbstractComponent< BaseNavigationContainerProps, BaseNavigationContainerInterface, >; declare export var createNavigatorFactory: CreateNavigatorFactory; declare export var useNavigationBuilder: UseNavigationBuilder; declare export var NavigationHelpersContext: React$Context< ?NavigationHelpers, >; /** * Navigation prop / route accessors */ declare export var NavigationContext: React$Context< ?NavigationProp, >; declare export function useNavigation(): NavigationProp; declare export var NavigationRouteContext: React$Context>; declare export function useRoute(): LeafRoute<>; declare export function useNavigationState( selector: NavigationState => T, ): T; /** * Focus utils */ declare export function useFocusEffect( effect: () => ?(() => mixed), ): void; declare export function useIsFocused(): boolean; /** * State utils */ declare export var getStateFromPath: GetStateFromPath; declare export var getPathFromState: GetPathFromState; declare export function getActionFromState( state: PossiblyStaleNavigationState, ): ?NavigateAction; declare export var getFocusedRouteNameFromRoute: GetFocusedRouteNameFromRoute; /** * useScrollToTop */ declare type ScrollToOptions = { y?: number, animated?: boolean, ... }; declare type ScrollToOffsetOptions = { offset: number, animated?: boolean, ... }; declare type ScrollableView = | { scrollToTop(): void, ... } | { scrollTo(options: ScrollToOptions): void, ... } | { scrollToOffset(options: ScrollToOffsetOptions): void, ... } | { scrollResponderScrollTo(options: ScrollToOptions): void, ... }; declare type ScrollableWrapper = | { getScrollResponder(): React$Node, ... } | { getNode(): ScrollableView, ... } | ScrollableView; declare export function useScrollToTop( ref: { +current: ?ScrollableWrapper, ... }, ): void; /** * Themes */ - declare export var DefaultTheme: Theme & { +dark: false, ... }; - declare export var DarkTheme: Theme & { +dark: true, ... }; + declare export var DefaultTheme: {| ...Theme, +dark: false |}; + declare export var DarkTheme: {| ...Theme, +dark: true |}; declare export function useTheme(): Theme; declare export var ThemeProvider: React$ComponentType<{| +value: Theme, +children: React$Node, |}>; /** * Linking */ declare export type LinkTo< ParamList: ParamListBase, RouteName: $Keys, > = | string | {| +screen: RouteName, +params?: $ElementType |}; declare export var Link: React$ComponentType<{ +to: LinkTo<>, +action?: GenericNavigationAction, +target?: string, +children: React$Node, ... }>; declare export function useLinkTo( ): (path: LinkTo) => void; declare export function useLinkProps< ParamList: ParamListBase, RouteName: $Keys, >(props: {| +to: LinkTo, +action?: GenericNavigationAction, |}): {| +href: string, +accessibilityRole: 'link', +onPress: (MouseEvent | PressEvent) => void, |}; declare export function useLinkBuilder(): ( name: string, params?: ScreenParams, ) => ?string; /** * NavigationContainer */ declare export var NavigationContainer: NavigationContainerType; declare export function createNavigationContainerRef( ): BaseNavigationContainerInterfaceRef; declare export function useNavigationContainerRef( ): BaseNavigationContainerInterfaceRef; /** * useBackButton */ declare export function useBackButton( container: { +current: ?React$ElementRef, ... }, ): void; } diff --git a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js index e5299352e..05ba4dbe5 100644 --- a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js +++ b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js @@ -1,596 +1,596 @@ // flow-typed signature: 3742390ed7eeeb6c96844c62149ea639 // flow-typed version: <>/react-native-reanimated_v2.2.0/flow_v0.137.0 /** * This is an autogenerated libdef stub for: * * 'react-native-reanimated' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'react-native-reanimated' { // This was taken from the flow typed library definitions of bottom-tabs_v6 declare type StyleObj = | null | void | number | false | '' | $ReadOnlyArray | { [name: string]: any, ... }; declare type ViewStyleProp = StyleObj; declare type TextStyleProp = StyleObj; declare type StyleProps = {| ...ViewStyleProp, ...TextStyleProp, +originX?: number, +originY?: number, +[key: string]: any, |}; - declare class Node { } + declare class NodeImpl { } - declare class Value extends Node { + declare class ValueImpl extends NodeImpl { constructor(val: number): this; setValue(num: number): void; } - declare class Clock extends Node { } + declare class ClockImpl extends NodeImpl { } - declare class View extends React$Component<{ ... }> { } - declare class Text extends React$Component<{ ... }> { } - declare class Image extends React$Component<{ ... }> { } - declare class Code extends React$Component<{ - +exec: Node, + declare class ViewImpl extends React$Component<{ ... }> { } + declare class TextImpl extends React$Component<{ ... }> { } + declare class ImageImpl extends React$Component<{ ... }> { } + declare class CodeImpl extends React$Component<{ + +exec: NodeImpl, ... }> { } - declare type NodeOrNum = Node | number; + declare type NodeOrNum = NodeImpl | number; declare export type NodeParam = NodeOrNum | $ReadOnlyArray; - declare type NodeOrArrayOfNodes = Node | $ReadOnlyArray; + declare type NodeOrArrayOfNodes = NodeImpl | $ReadOnlyArray; declare export type Block = ( nodes: $ReadOnlyArray, - ) => Node; + ) => NodeImpl; - declare export type Set = (node: Value, val: NodeParam) => Node; + declare export type Set = (node: ValueImpl, val: NodeParam) => NodeImpl; declare type ToNumber = (val: mixed) => number; - declare export type Call = >( + declare export type Call = >( nodes: N, callback: (vals: $TupleMap) => mixed, - ) => Node; + ) => NodeImpl; declare export type Cond = ( cond: NodeParam, branch1: ?NodeParam, branch2?: ?NodeParam, - ) => Node; - - declare export type Not = Node => Node; - declare export type And = (...$ReadOnlyArray) => Node; - declare export type Or = (...$ReadOnlyArray) => Node; - declare export type Eq = (NodeParam, NodeParam) => Node; - declare export type Neq = (NodeParam, NodeParam) => Node; - declare export type LessThan = (NodeParam, NodeParam) => Node; - declare export type GreaterThan = (NodeParam, NodeParam) => Node; - declare export type LessOrEq = (NodeParam, NodeParam) => Node; - declare export type GreaterOrEq = (NodeParam, NodeParam) => Node; - declare export type Add = (...$ReadOnlyArray) => Node; - declare export type Sub = (...$ReadOnlyArray) => Node; - declare export type Multiply = (...$ReadOnlyArray) => Node; - declare export type Divide = (...$ReadOnlyArray) => Node; - declare export type Pow = (...$ReadOnlyArray) => Node; - declare export type Max = (NodeParam, NodeParam) => Node; - declare export type Min = (NodeParam, NodeParam) => Node; - declare export type Abs = (NodeParam) => Node; - declare export type Ceil = (NodeParam) => Node; - declare export type Floor = (NodeParam) => Node; - declare export type Round = (NodeParam) => Node; - - declare export type StartClock = Clock => Node; - declare export type StopClock = Clock => Node; - declare export type ClockRunning = Clock => Node; - - declare export type Debug = (string, NodeParam) => Node; + ) => NodeImpl; + + declare export type Not = NodeImpl => NodeImpl; + declare export type And = (...$ReadOnlyArray) => NodeImpl; + declare export type Or = (...$ReadOnlyArray) => NodeImpl; + declare export type Eq = (NodeParam, NodeParam) => NodeImpl; + declare export type Neq = (NodeParam, NodeParam) => NodeImpl; + declare export type LessThan = (NodeParam, NodeParam) => NodeImpl; + declare export type GreaterThan = (NodeParam, NodeParam) => NodeImpl; + declare export type LessOrEq = (NodeParam, NodeParam) => NodeImpl; + declare export type GreaterOrEq = (NodeParam, NodeParam) => NodeImpl; + declare export type Add = (...$ReadOnlyArray) => NodeImpl; + declare export type Sub = (...$ReadOnlyArray) => NodeImpl; + declare export type Multiply = (...$ReadOnlyArray) => NodeImpl; + declare export type Divide = (...$ReadOnlyArray) => NodeImpl; + declare export type Pow = (...$ReadOnlyArray) => NodeImpl; + declare export type Max = (NodeParam, NodeParam) => NodeImpl; + declare export type Min = (NodeParam, NodeParam) => NodeImpl; + declare export type Abs = (NodeParam) => NodeImpl; + declare export type Ceil = (NodeParam) => NodeImpl; + declare export type Floor = (NodeParam) => NodeImpl; + declare export type Round = (NodeParam) => NodeImpl; + + declare export type StartClock = ClockImpl => NodeImpl; + declare export type StopClock = ClockImpl => NodeImpl; + declare export type ClockRunning = ClockImpl => NodeImpl; + + declare export type Debug = (string, NodeParam) => NodeImpl; declare type ExtrapolateType = { ... }; declare type ExtrapolateModule = { +CLAMP: ExtrapolateType, ... }; declare export type InterpolationConfig = { +inputRange: $ReadOnlyArray, +outputRange: $ReadOnlyArray, +extrapolate?: ?ExtrapolateType, ... }; declare export type InterpolateNode = ( node: NodeParam, interpolationConfig: InterpolationConfig, - ) => Node; + ) => NodeImpl; declare export type InterpolateColorsConfig = { +inputRange: $ReadOnlyArray, +outputColorRange: $ReadOnlyArray, }; declare export type InterpolateColors = ( animationValue: NodeParam, interpolationConfig: InterpolateColorsConfig - ) => Node; + ) => NodeImpl; declare export type Interpolate = ( input: number, inputRange: $ReadOnlyArray, outputRange: $ReadOnlyArray, extrapolate?: ?ExtrapolateType, ) => number; declare type EasingType = { ... }; declare type EasingModule = { +ease: EasingType, +quad: EasingType, +in: EasingType => EasingType, +out: EasingType => EasingType, +inOut: EasingType => EasingType, ... }; declare export var EasingNode: EasingModule; declare type EasingFn = (t: number) => number; declare export type TimingState = { - +finished: Value, - +position: Value, - +frameTime: Value, - +time: Value, + +finished: ValueImpl, + +position: ValueImpl, + +frameTime: ValueImpl, + +time: ValueImpl, ... }; declare export type TimingConfig = { +duration: number, +toValue: NodeOrNum, +easing?: ?EasingType, ... }; declare type Animator = { +start: () => void, ... }; declare type Timing = {| ( - value: Value, + value: ValueImpl, config: TimingConfig, ): Animator, ( - clock: Clock, + clock: ClockImpl, state: TimingState, config: TimingConfig, - ): Node, + ): NodeImpl, |}; declare export type SpringConfig = { +overshootClamping: boolean, +damping: number, +mass: number, +toValue: NodeOrNum, ... }; declare type SpringUtilsModule = { +makeDefaultConfig: () => SpringConfig, +makeConfigFromBouncinessAndSpeed: ({ ...SpringConfig, +bounciness: ?number, +speed: ?number, ... }) => SpringConfig, ... }; declare export type SpringState = { - +finished: Value, - +position: Value, - +velocity: Value, - +time: Value, + +finished: ValueImpl, + +position: ValueImpl, + +velocity: ValueImpl, + +time: ValueImpl, ... }; declare type Spring = {| ( - value: Value, + value: ValueImpl, config: SpringConfig, ): Animator, ( - clock: Clock, + clock: ClockImpl, state: SpringState, config: SpringConfig, - ): Node, + ): NodeImpl, |}; declare export type DecayConfig = { +deceleration: number, ... }; declare export type DecayState = { - +finished: Value, - +position: Value, - +velocity: Value, - +time: Value, + +finished: ValueImpl, + +position: ValueImpl, + +velocity: ValueImpl, + +time: ValueImpl, ... }; declare type Decay = {| ( - value: Value, + value: ValueImpl, config: DecayConfig, ): Animator, ( - clock: Clock, + clock: ClockImpl, state: DecayState, config: DecayConfig, - ): Node, + ): NodeImpl, |}; declare type LayoutAnimation = {| +initialValues: StyleProps, +animations: StyleProps, +callback?: (finished: boolean) => void, |}; declare type AnimationFunction = (a?: any, b?: any, c?: any) => any; declare type EntryAnimationsValues = {| +targetOriginX: number, +targetOriginY: number, +targetWidth: number, +targetHeight: number, +targetGlobalOriginX: number, +targetGlobalOriginY: number, |}; declare type ExitAnimationsValues = {| +currentOriginX: number, +currentOriginY: number, +currentWidth: number, +currentHeight: number, +currentGlobalOriginX: number, +currentGlobalOriginY: number, |}; declare export type EntryExitAnimationFunction = ( targetValues: EntryAnimationsValues | ExitAnimationsValues, ) => LayoutAnimation; declare type AnimationConfigFunction = ( targetValues: T, ) => LayoutAnimation; declare type LayoutAnimationsValues = {| +currentOriginX: number, +currentOriginY: number, +currentWidth: number, +currentHeight: number, +currentGlobalOriginX: number, +currentGlobalOriginY: number, +targetOriginX: number, +targetOriginY: number, +targetWidth: number, +targetHeight: number, +targetGlobalOriginX: number, +argetGlobalOriginY: number, +windowWidth: number, +windowHeight: number, |}; declare type LayoutAnimationFunction = ( targetValues: LayoutAnimationsValues, ) => LayoutAnimation; declare type BaseLayoutAnimationConfig = {| +duration?: number, +easing?: EasingFn, +type?: AnimationFunction, +damping?: number, +mass?: number, +stiffness?: number, +overshootClamping?: number, +restDisplacementThreshold?: number, +restSpeedThreshold?: number, |}; declare type BaseBuilderAnimationConfig = {| ...BaseLayoutAnimationConfig, rotate?: number | string, |}; declare type LayoutAnimationAndConfig = [ AnimationFunction, BaseBuilderAnimationConfig, ]; declare export class BaseAnimationBuilder { static duration(durationMs: number): BaseAnimationBuilder; duration(durationMs: number): BaseAnimationBuilder; static delay(delayMs: number): BaseAnimationBuilder; delay(delayMs: number): BaseAnimationBuilder; static withCallback( callback: (finished: boolean) => void, ): BaseAnimationBuilder; withCallback(callback: (finished: boolean) => void): BaseAnimationBuilder; static getDuration(): number; getDuration(): number; static randomDelay(): BaseAnimationBuilder; randomDelay(): BaseAnimationBuilder; getDelay(): number; getDelayFunction(): AnimationFunction; static build(): EntryExitAnimationFunction | LayoutAnimationFunction; } declare export type ReanimatedAnimationBuilder = | Class | BaseAnimationBuilder; declare export class ComplexAnimationBuilder extends BaseAnimationBuilder { static easing(easingFunction: EasingFn): ComplexAnimationBuilder; easing(easingFunction: EasingFn): ComplexAnimationBuilder; static rotate(degree: string): ComplexAnimationBuilder; rotate(degree: string): ComplexAnimationBuilder; static springify(): ComplexAnimationBuilder; springify(): ComplexAnimationBuilder; static damping(damping: number): ComplexAnimationBuilder; damping(damping: number): ComplexAnimationBuilder; static mass(mass: number): ComplexAnimationBuilder; mass(mass: number): ComplexAnimationBuilder; static stiffness(stiffness: number): ComplexAnimationBuilder; stiffness(stiffness: number): ComplexAnimationBuilder; static overshootClamping( overshootClamping: number, ): ComplexAnimationBuilder; overshootClamping(overshootClamping: number): ComplexAnimationBuilder; static restDisplacementThreshold( restDisplacementThreshold: number, ): ComplexAnimationBuilder; restDisplacementThreshold( restDisplacementThreshold: number, ): ComplexAnimationBuilder; static restSpeedThreshold( restSpeedThreshold: number, ): ComplexAnimationBuilder; restSpeedThreshold(restSpeedThreshold: number): ComplexAnimationBuilder; static withInitialValues(values: StyleProps): BaseAnimationBuilder; withInitialValues(values: StyleProps): BaseAnimationBuilder; getAnimationAndConfig(): LayoutAnimationAndConfig; } declare export class SlideInDown extends ComplexAnimationBuilder { static createInstance(): SlideInDown; build(): AnimationConfigFunction; } declare export class SlideOutDown extends ComplexAnimationBuilder { static createInstance(): SlideOutDown; build(): AnimationConfigFunction; } declare type $SyntheticEvent = { +nativeEvent: $ReadOnly<$Exact>, ... }; declare type GestureStateUndetermined = 0; declare type GestureStateFailed = 1; declare type GestureStateBegan = 2; declare type GestureStateCancelled = 3; declare type GestureStateActive = 4; declare type GestureStateEnd = 5; declare type GestureState = | GestureStateUndetermined | GestureStateFailed | GestureStateBegan | GestureStateCancelled | GestureStateActive | GestureStateEnd; - declare type $Event = $SyntheticEvent<{ + declare type $Event = { handlerTag: number, numberOfPointers: number, state: GestureState, oldState: GestureState, ...$Exact, ... - }>; + }; - declare type ToValue = (val: mixed) => Value; + declare type ToValue = (val: mixed) => ValueImpl; declare type Event = >(defs: $ReadOnlyArray<{ +nativeEvent: $Shape<$ObjMap>, ... - }>) => E; + }>) => $SyntheticEvent => void; - declare type UseValue = (initialVal: number) => Value; + declare type UseValue = (initialVal: number) => ValueImpl; declare type AnimatedGestureHandlerEventCallback> = ( event: $Shape, context: {| [name: string]: mixed |}, ) => mixed; declare type UseAnimatedGestureHandler = >( callbacks: $Shape<{| +onStart: AnimatedGestureHandlerEventCallback, +onActive: AnimatedGestureHandlerEventCallback, +onEnd: AnimatedGestureHandlerEventCallback, +onFail: AnimatedGestureHandlerEventCallback, +onCancel: AnimatedGestureHandlerEventCallback, +onFinish: AnimatedGestureHandlerEventCallback, |}>, dependencies?: $ReadOnlyArray, ) => E; declare export type SharedValue = { value: T, ... }; declare type UseSharedValue = (val: T) => SharedValue; declare type UseDerivedValue = ( updater: () => T, dependencies?: $ReadOnlyArray, ) => SharedValue; declare type UseAnimatedStyle = ( styleSelector: () => T, dependencies?: $ReadOnlyArray, ) => T; declare type WithSpringConfig = $Shape<{| +stiffness: number, +damping: number, +mass: number, +overshootClamping: boolean, +restDisplacementThreshold: number, +restSpeedThreshold: number, +velocity: number, |}>; // Doesn't actually return a number, but sharedValue.value has a differently // typed getter vs. setter, and Flow doesn't support that declare type WithSpring = ( toValue: number | string, springConfig?: WithSpringConfig, ) => number; declare type RunOnJS = (func: F) => F; declare type CancelAnimation = (animation: number) => void; - declare export var Node: typeof Node; - declare export var Value: typeof Value; - declare export var Clock: typeof Clock; - declare export var View: typeof View; - declare export var Text: typeof Text; - declare export var Image: typeof Image; - declare export var Code: typeof Code; + declare export var Node: typeof NodeImpl; + declare export var Value: typeof ValueImpl; + declare export var Clock: typeof ClockImpl; + declare export var View: typeof ViewImpl; + declare export var Text: typeof TextImpl; + declare export var Image: typeof ImageImpl; + declare export var Code: typeof CodeImpl; declare export var block: Block; declare export var set: Set; declare export var call: Call; declare export var cond: Cond; declare export var not: Not; declare export var and: And; declare export var or: Or; declare export var eq: Eq; declare export var neq: Neq; declare export var lessThan: LessThan; declare export var greaterThan: GreaterThan; declare export var lessOrEq: LessOrEq; declare export var greaterOrEq: GreaterOrEq; declare export var add: Add; declare export var sub: Sub; declare export var multiply: Multiply; declare export var divide: Divide; declare export var pow: Pow; declare export var max: Max; declare export var min: Min; declare export var abs: Abs; declare export var ceil: Ceil; declare export var floor: Floor; declare export var round: Round; declare export var startClock: StartClock; declare export var stopClock: StopClock; declare export var clockRunning: ClockRunning; declare export var debug: Debug; declare export var interpolateNode: InterpolateNode; declare export var interpolateColors: InterpolateColors; declare export var interpolate: Interpolate; declare export var Extrapolate: ExtrapolateModule; declare export var timing: Timing; declare export var SpringUtils: SpringUtilsModule; declare export var spring: Spring; declare export var decay: Decay; declare export var event: Event; declare export var useValue: UseValue; declare export var useAnimatedGestureHandler: UseAnimatedGestureHandler; declare export var useSharedValue: UseSharedValue; declare export var useDerivedValue: UseDerivedValue; declare export var useAnimatedStyle: UseAnimatedStyle; declare export var withSpring: WithSpring; declare export var runOnJS: RunOnJS; declare export var cancelAnimation: CancelAnimation; declare export default { - +Node: typeof Node, - +Value: typeof Value, - +Clock: typeof Clock, - +View: typeof View, - +Text: typeof Text, - +Image: typeof Image, - +Code: typeof Code, + +Node: typeof NodeImpl, + +Value: typeof ValueImpl, + +Clock: typeof ClockImpl, + +View: typeof ViewImpl, + +Text: typeof TextImpl, + +Image: typeof ImageImpl, + +Code: typeof CodeImpl, +block: Block, +set: Set, +call: Call, +cond: Cond, +not: Not, +and: And, +or: Or, +eq: Eq, +neq: Neq, +lessThan: LessThan, +greaterThan: GreaterThan, +lessOrEq: LessOrEq, +greaterOrEq: GreaterOrEq, +add: Add, +sub: Sub, +multiply: Multiply, +divide: Divide, +pow: Pow, +max: Max, +min: Min, +abs: Abs, +ceil: Ceil, +floor: Floor, +round: Round, +startClock: StartClock, +stopClock: StopClock, +clockRunning: ClockRunning, +debug: Debug, +interpolateNode: InterpolateNode, +interpolateColors: InterpolateColors, +interpolate: Interpolate, +Extrapolate: ExtrapolateModule, +timing: Timing, +spring: Spring, +decay: Decay, +SpringUtils: SpringUtilsModule, +event: Event, +useValue: UseValue, +useAnimatedGestureHandler: UseAnimatedGestureHandler, +useSharedValue: UseSharedValue, +useDerivedValue: UseDerivedValue, +useAnimatedStyle: UseAnimatedStyle, +withSpring: WithSpring, +runOnJS: RunOnJS, +cancelAnimation: CancelAnimation, ... }; } diff --git a/native/media/file-utils.js b/native/media/file-utils.js index 82d75a462..e02004b54 100644 --- a/native/media/file-utils.js +++ b/native/media/file-utils.js @@ -1,442 +1,442 @@ // @flow import base64 from 'base-64'; import * as MediaLibrary from 'expo-media-library'; import invariant from 'invariant'; import { Platform } from 'react-native'; import filesystem from 'react-native-fs'; import { mediaConfig, pathFromURI, fileInfoFromData, bytesNeededForFileTypeCheck, } from 'lib/media/file-utils'; import type { Shape } from 'lib/types/core'; import type { MediaMissionStep, MediaMissionFailure, MediaType, ReadFileHeaderMediaMissionStep, DisposeTemporaryFileMediaMissionStep, MakeDirectoryMediaMissionStep, AndroidScanFileMediaMissionStep, FetchFileHashMediaMissionStep, CopyFileMediaMissionStep, } from 'lib/types/media-types'; import { getMessageForException } from 'lib/utils/errors'; import { stringToIntArray } from './blob-utils'; import { ffmpeg } from './ffmpeg'; const defaultInputs = Object.freeze({}); const defaultFields = Object.freeze({}); type FetchFileInfoResult = { +success: true, +uri: string, +orientation: ?number, +fileSize: number, +mime: ?string, +mediaType: ?MediaType, }; type OptionalInputs = Shape<{ +mediaNativeID: ?string }>; type OptionalFields = Shape<{ +orientation: boolean, +mediaType: boolean, +mime: boolean, }>; async function fetchFileInfo( inputURI: string, optionalInputs?: OptionalInputs = defaultInputs, optionalFields?: OptionalFields = defaultFields, ): Promise<{ steps: $ReadOnlyArray, result: MediaMissionFailure | FetchFileInfoResult, }> { const { mediaNativeID } = optionalInputs; const steps = []; let assetInfoPromise, newLocalURI; const inputPath = pathFromURI(inputURI); if (mediaNativeID && (!inputPath || optionalFields.orientation)) { assetInfoPromise = (async () => { const { steps: assetInfoSteps, result: assetInfoResult, } = await fetchAssetInfo(mediaNativeID); steps.push(...assetInfoSteps); newLocalURI = assetInfoResult.localURI; return assetInfoResult; })(); } const getLocalURIPromise = (async () => { if (inputPath) { return { localURI: inputURI, path: inputPath }; } if (!assetInfoPromise) { return null; } const { localURI } = await assetInfoPromise; if (!localURI) { return null; } const path = pathFromURI(localURI); if (!path) { return null; } return { localURI, path }; })(); const getOrientationPromise = (async () => { if (!optionalFields.orientation || !assetInfoPromise) { return null; } const { orientation } = await assetInfoPromise; return orientation; })(); const getFileSizePromise = (async () => { const localURIResult = await getLocalURIPromise; if (!localURIResult) { return null; } const { localURI } = localURIResult; const { steps: fileSizeSteps, result: fileSize } = await fetchFileSize( localURI, ); steps.push(...fileSizeSteps); return fileSize; })(); const getTypesPromise = (async () => { if (!optionalFields.mime && !optionalFields.mediaType) { return { mime: null, mediaType: null }; } const [localURIResult, fileSize] = await Promise.all([ getLocalURIPromise, getFileSizePromise, ]); if (!localURIResult || !fileSize) { return { mime: null, mediaType: null }; } const { localURI, path } = localURIResult; const readFileStep = await readFileHeader(localURI, fileSize); steps.push(readFileStep); const { mime, mediaType: baseMediaType } = readFileStep; if (!optionalFields.mediaType || !mime || !baseMediaType) { return { mime, mediaType: null }; } const { steps: getMediaTypeSteps, result: mediaType, } = await getMediaTypeInfo(path, mime, baseMediaType); steps.push(...getMediaTypeSteps); return { mime, mediaType }; })(); const [localURIResult, orientation, fileSize, types] = await Promise.all([ getLocalURIPromise, getOrientationPromise, getFileSizePromise, getTypesPromise, ]); if (!localURIResult) { return { steps, result: { success: false, reason: 'no_file_path' } }; } const uri = localURIResult.localURI; if (!fileSize) { return { steps, result: { success: false, reason: 'file_stat_failed', uri }, }; } let finalURI = uri; if (newLocalURI && newLocalURI !== uri) { + finalURI = newLocalURI; console.log( 'fetchAssetInfo returned localURI ' + `${newLocalURI} when we already had ${uri}`, ); - finalURI = newLocalURI; } return { steps, result: { success: true, uri: finalURI, orientation, fileSize, mime: types.mime, mediaType: types.mediaType, }, }; } async function fetchAssetInfo( mediaNativeID: string, ): Promise<{ steps: $ReadOnlyArray, result: { localURI: ?string, orientation: ?number }, }> { let localURI, orientation, success = false, exceptionMessage; const start = Date.now(); try { const assetInfo = await MediaLibrary.getAssetInfoAsync(mediaNativeID); success = true; localURI = assetInfo.localUri; if (Platform.OS === 'ios') { orientation = assetInfo.orientation; } else { orientation = assetInfo.exif && assetInfo.exif.Orientation; } } catch (e) { exceptionMessage = getMessageForException(e); } return { steps: [ { step: 'asset_info_fetch', success, exceptionMessage, time: Date.now() - start, localURI, orientation, }, ], result: { localURI, orientation, }, }; } async function fetchFileSize( uri: string, ): Promise<{ steps: $ReadOnlyArray, result: ?number, }> { let fileSize, success = false, exceptionMessage; const statStart = Date.now(); try { const result = await filesystem.stat(uri); success = true; fileSize = result.size; } catch (e) { exceptionMessage = getMessageForException(e); } return { steps: [ { step: 'stat_file', success, exceptionMessage, time: Date.now() - statStart, uri, fileSize, }, ], result: fileSize, }; } async function readFileHeader( localURI: string, fileSize: number, ): Promise { const fetchBytes = Math.min(fileSize, bytesNeededForFileTypeCheck); const start = Date.now(); let fileData, success = false, exceptionMessage; try { fileData = await filesystem.read(localURI, fetchBytes, 0, 'base64'); success = true; } catch (e) { exceptionMessage = getMessageForException(e); } let mime, mediaType; if (fileData) { const utf8 = base64.decode(fileData); const intArray = stringToIntArray(utf8); ({ mime, mediaType } = fileInfoFromData(intArray)); } return { step: 'read_file_header', success, exceptionMessage, time: Date.now() - start, uri: localURI, mime, mediaType, }; } async function getMediaTypeInfo( path: string, mime: string, baseMediaType: MediaType, ): Promise<{ steps: $ReadOnlyArray, result: ?MediaType, }> { if (!mediaConfig[mime] || mediaConfig[mime].mediaType !== 'photo_or_video') { return { steps: [], result: baseMediaType }; } let hasMultipleFrames, success = false, exceptionMessage; const start = Date.now(); try { hasMultipleFrames = await ffmpeg.hasMultipleFrames(path); success = true; } catch (e) { exceptionMessage = getMessageForException(e); } const steps = [ { step: 'frame_count', success, exceptionMessage, time: Date.now() - start, path, mime, hasMultipleFrames, }, ]; const result = hasMultipleFrames ? 'video' : 'photo'; return { steps, result }; } async function disposeTempFile( path: string, ): Promise { let success = false, exceptionMessage; const start = Date.now(); try { await filesystem.unlink(path); success = true; } catch (e) { exceptionMessage = getMessageForException(e); } return { step: 'dispose_temporary_file', success, exceptionMessage, time: Date.now() - start, path, }; } async function mkdir(path: string): Promise { let success = false, exceptionMessage; const start = Date.now(); try { await filesystem.mkdir(path); success = true; } catch (e) { exceptionMessage = getMessageForException(e); } return { step: 'make_directory', success, exceptionMessage, time: Date.now() - start, path, }; } async function androidScanFile( path: string, ): Promise { invariant(Platform.OS === 'android', 'androidScanFile only works on Android'); let success = false, exceptionMessage; const start = Date.now(); try { await filesystem.scanFile(path); success = true; } catch (e) { exceptionMessage = getMessageForException(e); } return { step: 'android_scan_file', success, exceptionMessage, time: Date.now() - start, path, }; } async function fetchFileHash( path: string, ): Promise { let hash, exceptionMessage; const start = Date.now(); try { hash = await filesystem.hash(path, 'md5'); } catch (e) { exceptionMessage = getMessageForException(e); } return { step: 'fetch_file_hash', success: !!hash, exceptionMessage, time: Date.now() - start, path, hash, }; } async function copyFile( source: string, destination: string, ): Promise { let success = false, exceptionMessage; const start = Date.now(); try { await filesystem.copyFile(source, destination); success = true; } catch (e) { exceptionMessage = getMessageForException(e); } return { step: 'copy_file', success, exceptionMessage, time: Date.now() - start, source, destination, }; } const temporaryDirectoryPath: string = Platform.select({ ios: filesystem.TemporaryDirectoryPath, default: `${filesystem.TemporaryDirectoryPath}/`, }); export { fetchAssetInfo, fetchFileInfo, temporaryDirectoryPath, disposeTempFile, mkdir, androidScanFile, fetchFileHash, copyFile, }; diff --git a/native/navigation/disconnected-bar.react.js b/native/navigation/disconnected-bar.react.js index 2e0214211..d27efcc83 100644 --- a/native/navigation/disconnected-bar.react.js +++ b/native/navigation/disconnected-bar.react.js @@ -1,104 +1,104 @@ // @flow import * as React from 'react'; import { Text, Platform, Animated, Easing } from 'react-native'; import { useDisconnectedBar, useShouldShowDisconnectedBar, } from 'lib/hooks/disconnected-bar'; import { useStyles } from '../themes/colors'; const expandedHeight = Platform.select({ android: 29.5, default: 27, }); const timingConfig = { useNativeDriver: false, duration: 200, // $FlowFixMe[method-unbinding] easing: Easing.inOut(Easing.ease), }; type Props = { +visible: boolean, }; function DisconnectedBar(props: Props): React.Node { const { shouldShowDisconnectedBar } = useShouldShowDisconnectedBar(); - const showingRef = new React.useRef(); + const showingRef = React.useRef(); if (!showingRef.current) { showingRef.current = new Animated.Value(shouldShowDisconnectedBar ? 1 : 0); } const showing = showingRef.current; const { visible } = props; const changeShowing = React.useCallback( (toState: boolean) => { const toValue = toState ? 1 : 0; if (!visible) { showing.setValue(toValue); return; } Animated.timing(showing, { ...timingConfig, toValue, }).start(); }, [visible, showing], ); const barCause = useDisconnectedBar(changeShowing); const heightStyle = React.useMemo( () => ({ height: showing.interpolate({ inputRange: [0, 1], outputRange: ([0, expandedHeight]: number[]), // Flow... }), }), [showing], ); const styles = useStyles(unboundStyles); const text = barCause === 'disconnected' ? 'DISCONNECTED' : 'CONNECTING…'; const viewStyle = barCause === 'disconnected' ? styles.disconnected : styles.connecting; const textStyle = barCause === 'disconnected' ? styles.disconnectedText : styles.connectingText; return ( {text} ); } const unboundStyles = { disconnected: { backgroundColor: '#CC0000', overflow: 'hidden', }, connecting: { backgroundColor: 'disconnectedBarBackground', overflow: 'hidden', }, disconnectedText: { color: 'white', fontSize: 14, padding: 5, textAlign: 'center', }, connectingText: { color: 'panelForegroundLabel', fontSize: 14, padding: 5, textAlign: 'center', }, }; export default DisconnectedBar; diff --git a/native/navigation/tab-bar.react.js b/native/navigation/tab-bar.react.js index 4072daff5..605aa1896 100644 --- a/native/navigation/tab-bar.react.js +++ b/native/navigation/tab-bar.react.js @@ -1,146 +1,146 @@ // @flow import { BottomTabBar } from '@react-navigation/bottom-tabs'; import * as React from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import Animated, { EasingNode } from 'react-native-reanimated'; import { useSafeArea } from 'react-native-safe-area-context'; import { useDispatch } from 'react-redux'; import { KeyboardContext } from '../keyboard/keyboard-state'; import { updateDimensionsActiveType } from '../redux/action-types'; import { useSelector } from '../redux/redux-utils'; import type { LayoutEvent } from '../types/react-native'; /* eslint-disable import/no-named-as-default-member */ const { Value, timing, interpolateNode } = Animated; /* eslint-enable import/no-named-as-default-member */ const tabBarAnimationDuration = 200; type Props = React.ElementConfig; function TabBar(props: Props) { - const tabBarVisibleRef = new React.useRef(); + const tabBarVisibleRef = React.useRef(); if (!tabBarVisibleRef.current) { tabBarVisibleRef.current = new Value(1); } const tabBarVisible = tabBarVisibleRef.current; const keyboardState = React.useContext(KeyboardContext); const shouldHideTabBar = keyboardState?.mediaGalleryOpen; const prevKeyboardStateRef = React.useRef(); React.useEffect(() => { prevKeyboardStateRef.current = keyboardState; }, [keyboardState]); const prevKeyboardState = prevKeyboardStateRef.current; const setTabBar = React.useCallback( toValue => { const keyboardIsShowing = keyboardState && keyboardState.keyboardShowing; const keyboardWasShowing = prevKeyboardState && prevKeyboardState.keyboardShowing; if (keyboardIsShowing === keyboardWasShowing) { tabBarVisible.setValue(toValue); return; } timing(tabBarVisible, { toValue, duration: tabBarAnimationDuration, easing: EasingNode.inOut(EasingNode.ease), }).start(); }, [keyboardState, prevKeyboardState, tabBarVisible], ); const prevShouldHideTabBarRef = React.useRef(false); React.useEffect(() => { const prevShouldHideTabBar = prevShouldHideTabBarRef.current; if (shouldHideTabBar && !prevShouldHideTabBar) { setTabBar(0); } else if (!shouldHideTabBar && prevShouldHideTabBar) { setTabBar(1); } prevShouldHideTabBarRef.current = shouldHideTabBar; }, [shouldHideTabBar, setTabBar]); const reduxTabBarHeight = useSelector(state => state.dimensions.tabBarHeight); const dispatch = useDispatch(); const setReduxTabBarHeight = React.useCallback( height => { if (height === reduxTabBarHeight) { return; } dispatch({ type: updateDimensionsActiveType, payload: { tabBarHeight: height }, }); }, [reduxTabBarHeight, dispatch], ); const [tabBarHeight, setTabBarHeight] = React.useState(0); const insets = useSafeArea(); const handleLayout = React.useCallback( (e: LayoutEvent) => { const rawHeight = Math.round(e.nativeEvent.layout.height); if (rawHeight > 100 || rawHeight <= 0) { return; } if (Platform.OS === 'android') { setTabBarHeight(rawHeight); } const height = rawHeight - insets.bottom; if (height > 0) { setReduxTabBarHeight(height); } }, [setTabBarHeight, setReduxTabBarHeight, insets], ); const containerHeight = React.useMemo( () => interpolateNode(tabBarVisible, { inputRange: [0, 1], outputRange: [0, tabBarHeight], }), [tabBarVisible, tabBarHeight], ); const containerStyle = React.useMemo( () => ({ height: tabBarHeight ? containerHeight : undefined, ...styles.container, }), [tabBarHeight, containerHeight], ); if (Platform.OS !== 'android') { return ( ); } return ( ); } const styles = StyleSheet.create({ container: { bottom: 0, left: 0, right: 0, }, }); // This is a render prop, not a component // eslint-disable-next-line react/display-name const tabBar = (props: Props): React.Node => ; export { tabBarAnimationDuration, tabBar }; diff --git a/native/package.json b/native/package.json index d877b2c28..f3d330623 100644 --- a/native/package.json +++ b/native/package.json @@ -1,122 +1,122 @@ { "name": "native", "version": "0.0.1", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "yarn clean-commoncpp && yarn clean-android && yarn clean-ios && rm -rf node_modules/ && (yarn clean-rust || true)", "clean-commoncpp": "rm -rf cpp/CommonCpp/build && rm -rf cpp/CommonCpp/CryptoTools/build && rm -rf cpp/CommonCpp/DatabaseManagers/build && rm -rf cpp/CommonCpp/NativeModules/build && rm -rf cpp/CommonCpp/Tools/build", "clean-rust": "cargo clean --manifest-path native_rust_library/Cargo.toml", "clean-android": "rm -rf android/build android/app/build android/app/.cxx", "clean-ios": "rm -rf ios/Pods/", "clean-all": "yarn clean && rm -rf ~/Library/Developer/Xcode/DerivedData/Comm-*; cd android && (./gradlew clean || true)", "postinstall": "cd ../ && echo '{\"name\": \"olm\", \"version\": \"3.2.4\"}' > ./node_modules/olm/package.json && yarn patch-package && yarn flow-mono create-symlinks native && cd native && yarn jetify && ((cd ios && PATH=/usr/bin:\"$PATH\" pod install) || true)", "start": "yarn react-native start", "dev": "yarn concurrently --names=\"REDUX,METRO\" -c \"bgGreen.bold,bgBlue.bold\" \"yarn redux-devtools\" \"yarn start\"", "test": "yarn jest", "logfirebase": "adb shell logcat | grep -E -i 'FIRMessagingModule|firebase'", "redux-devtools": "redux-devtools --port=8043", "codegen-jsi": "flow && babel codegen/src/ -d codegen/dist/ && node codegen/dist", "react-native": "PATH=/usr/bin:\"$PATH\" react-native" }, "devDependencies": { "@babel/cli": "^7.8.4", "@babel/core": "^7.13.14", "@babel/node": "^7.8.7", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/preset-flow": "^7.9.0", "@redux-devtools/cli": "1.0.0-10", "babel-jest": "^26.6.3", "babel-plugin-transform-remove-console": "^6.9.4", "babel-plugin-transform-remove-strict-mode": "0.0.2", "comm-react-native-codegen": "npm:react-native-codegen@0.0.7", - "flow-bin": "^0.158.0", + "flow-bin": "^0.182.0", "flow-mono-cli": "^1.5.0", "flow-typed": "^3.2.1", "fs-extra": "^8.1.0", "googleapis": "^89.0.0", "jest": "^26.6.3", "jetifier": "^1.6.4", "jsonwebtoken": "^8.5.1", "metro-react-native-babel-preset": "^0.72.3", "patch-package": "^6.4.7", "postinstall-postinstall": "^2.0.0", "react-test-renderer": "18.1.0", "remote-redux-devtools": "git+https://git@github.com/zalmoxisus/remote-redux-devtools.git", "remotedev": "git+https://git@github.com/zalmoxisus/remotedev.git" }, "dependencies": { "@commapp/sqlcipher-amalgamation": "^4.4.3-a", "@expo/react-native-action-sheet": "^3.14.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-community/art": "^1.2.0", "@react-native-community/netinfo": "^6.0.0", "@react-native-masked-view/masked-view": "^0.2.8", "@react-navigation/bottom-tabs": "^6.4.0", "@react-navigation/devtools": "^6.0.10", "@react-navigation/drawer": "^6.5.0", "@react-navigation/elements": "^1.3.6", "@react-navigation/material-top-tabs": "^6.3.0", "@react-navigation/native": "^6.0.13", "@react-navigation/stack": "^6.3.2", "base-64": "^0.1.0", "expo": "43.0.0", "expo-haptics": "~11.0.3", "expo-image-manipulator": "~10.1.2", "expo-media-library": "~13.0.3", "expo-secure-store": "~11.0.3", "expo-splash-screen": "~0.13.5", "find-root": "^1.1.0", "invariant": "^2.2.4", "lib": "0.0.1", "lodash": "^4.17.21", "lottie-ios": "3.1.8", "lottie-react-native": "^4.0.2", "md5": "^2.2.1", "olm": "git+https://gitlab.matrix.org/matrix-org/olm.git#v3.2.4", "react": "18.1.0", "react-native": "^0.70.6", "react-native-background-upload": "^6.5.1", "react-native-camera": "^3.31.0", "react-native-device-info": "^8.0.7", "react-native-exit-app": "^1.1.0", "react-native-fast-image": "^8.3.0", "react-native-ffmpeg": "^0.4.4", "react-native-figma-squircle": "^0.1.2", "react-native-firebase": "^5.6.0", "react-native-floating-action": "^1.21.0", "react-native-fs": "2.15.2", "react-native-gesture-handler": "^1.10.3", "react-native-in-app-message": "^1.0.2", "react-native-keyboard-input": "6.0.1", "react-native-keychain": "^8.0.0", "react-native-notifications": "git+https://git@github.com/ashoat/react-native-notifications.git", "react-native-orientation-locker": "^1.1.6", "react-native-pager-view": "^6.0.1", "react-native-progress": "^4.1.2", "react-native-reanimated": "^2.10.0", "react-native-safe-area-context": "^3.1.9", "react-native-safe-area-view": "^2.0.0", "react-native-screens": "~3.8.0", "react-native-svg": "^12.3.0", "react-native-tab-view": "^3.3.0", "react-native-vector-icons": "^6.6.0", "react-native-video": "~5.1.1", "react-native-webview": "^11.23.0", "react-redux": "^7.1.1", "reactotron-react-native": "^5.0.3", "reactotron-redux": "^3.1.3", "redux": "^4.0.4", "redux-persist": "^6.0.0", "redux-thunk": "^2.2.0", "reselect": "^4.0.0", "shallowequal": "^1.0.2", "simple-markdown": "^0.7.2", "tinycolor2": "^1.4.1" }, "jest": { "preset": "react-native" } } diff --git a/patches/react-native-firebase+5.6.0.patch b/patches/react-native-firebase+5.6.0.patch index 72feee934..f00fea573 100644 --- a/patches/react-native-firebase+5.6.0.patch +++ b/patches/react-native-firebase+5.6.0.patch @@ -1,759 +1,757 @@ diff --git a/node_modules/react-native-firebase/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java b/node_modules/react-native-firebase/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java index c9d63ff..ef725ad 100644 --- a/node_modules/react-native-firebase/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java +++ b/node_modules/react-native-firebase/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java @@ -475,7 +475,7 @@ public class DisplayNotificationTask extends AsyncTask { context, notificationId.hashCode(), intent, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE ); } diff --git a/node_modules/react-native-firebase/dist/common/commonTypes.flow.js.flow b/node_modules/react-native-firebase/dist/common/commonTypes.flow.js.flow index 228a72d..982a997 100644 --- a/node_modules/react-native-firebase/dist/common/commonTypes.flow.js.flow +++ b/node_modules/react-native-firebase/dist/common/commonTypes.flow.js.flow @@ -1,3 +1,5 @@ +// @flow + export type NativeErrorObject = { code: string, message: string, @@ -11,9 +13,9 @@ export type NativeErrorResponse = { [key: string]: ?any, }; -export interface NativeErrorInterface extends Error { +export interface NativeErrorInterface { +code: string; - +message: string; + message: string; +nativeErrorCode: string | number; +nativeErrorMessage: string; } diff --git a/node_modules/react-native-firebase/dist/index.js.flow b/node_modules/react-native-firebase/dist/index.js.flow index 04ee2d9..ba9fdcf 100644 --- a/node_modules/react-native-firebase/dist/index.js.flow +++ b/node_modules/react-native-firebase/dist/index.js.flow @@ -9,7 +9,8 @@ export * from './modules/core/firebase'; /* * Export App types */ -export type { default as App } from './modules/core/app'; +import type AppModule from './modules/core/app'; +export type App = AppModule; /* * Export Auth types diff --git a/node_modules/react-native-firebase/dist/modules/admob/AdRequest.js.flow b/node_modules/react-native-firebase/dist/modules/admob/AdRequest.js.flow index 41d2d5d..c2f5e0a 100644 --- a/node_modules/react-native-firebase/dist/modules/admob/AdRequest.js.flow +++ b/node_modules/react-native-firebase/dist/modules/admob/AdRequest.js.flow @@ -1,4 +1,18 @@ +// @flow + +type AdRequestProps = {| + keywords: string[], + testDevices: string[], + contentUrl?: string, + gender?: Gender, + requestAgent?: string, + isDesignedForFamilies?: boolean, + tagForChildDirectedTreatment?: boolean, +|}; +type Gender = 'male | female | unknown'; export default class AdRequest { + _props: AdRequestProps; + constructor() { this._props = { keywords: [], @@ -6,30 +20,30 @@ export default class AdRequest { }; } - build() { + build(): AdRequestProps { return this._props; } - addTestDevice(deviceId?: string) { + addTestDevice(deviceId?: string): AdRequest { this._props.testDevices.push(deviceId || 'DEVICE_ID_EMULATOR'); return this; } - addKeyword(keyword: string) { + addKeyword(keyword: string): AdRequest { this._props.keywords.push(keyword); return this; } - setBirthday() { + setBirthday(): void { // TODO } - setContentUrl(url: string) { + setContentUrl(url: string): AdRequest { this._props.contentUrl = url; return this; } - setGender(gender: 'male | female | unknown') { + setGender(gender: Gender): AdRequest { const genders = ['male', 'female', 'unknown']; if (genders.includes(gender)) { this._props.gender = gender; @@ -37,21 +51,21 @@ export default class AdRequest { return this; } - setLocation() { + setLocation(): void { // TODO } - setRequestAgent(requestAgent: string) { + setRequestAgent(requestAgent: string): AdRequest { this._props.requestAgent = requestAgent; return this; } - setIsDesignedForFamilies(isDesignedForFamilies: boolean) { + setIsDesignedForFamilies(isDesignedForFamilies: boolean): AdRequest { this._props.isDesignedForFamilies = isDesignedForFamilies; return this; } - tagForChildDirectedTreatment(tagForChildDirectedTreatment: boolean) { + tagForChildDirectedTreatment(tagForChildDirectedTreatment: boolean): AdRequest { this._props.tagForChildDirectedTreatment = tagForChildDirectedTreatment; return this; } diff --git a/node_modules/react-native-firebase/dist/modules/admob/Interstitial.js.flow b/node_modules/react-native-firebase/dist/modules/admob/Interstitial.js.flow index a2f4c82..db795d7 100644 --- a/node_modules/react-native-firebase/dist/modules/admob/Interstitial.js.flow +++ b/node_modules/react-native-firebase/dist/modules/admob/Interstitial.js.flow @@ -1,3 +1,5 @@ +// @flow + import { Platform } from 'react-native'; import { statics } from './'; import AdRequest from './AdRequest'; @@ -8,8 +10,10 @@ import type AdMob from './'; let subscriptions = []; -export default class Interstitial { +class Interstitial { _admob: AdMob; + adUnit: string; + loaded: boolean; constructor(admob: AdMob, adUnit: string) { // Interstitials on iOS require a new instance each time @@ -79,7 +83,7 @@ export default class Interstitial { * Return a local instance of isLoaded * @returns {boolean} */ - isLoaded() { + isLoaded(): boolean { return this.loaded; } @@ -87,7 +91,7 @@ export default class Interstitial { * Show the advert - will only show if loaded * @returns {*} */ - show() { + show(): void { if (this.loaded) { getNativeModule(this._admob).interstitialShowAd(this.adUnit); } @@ -99,7 +103,7 @@ export default class Interstitial { * @param listenerCb * @returns {null} */ - on(eventType, listenerCb) { + on(eventType: string, listenerCb: Function) { if (!statics.EventTypes[eventType]) { console.warn( `Invalid event type provided, must be one of: ${Object.keys( @@ -117,3 +121,5 @@ export default class Interstitial { return sub; } } + +export default Interstitial; diff --git a/node_modules/react-native-firebase/dist/modules/admob/RewardedVideo.js.flow b/node_modules/react-native-firebase/dist/modules/admob/RewardedVideo.js.flow index d89df42..ab3bc43 100644 --- a/node_modules/react-native-firebase/dist/modules/admob/RewardedVideo.js.flow +++ b/node_modules/react-native-firebase/dist/modules/admob/RewardedVideo.js.flow @@ -1,3 +1,5 @@ +// @flow + import { statics } from './'; import AdRequest from './AdRequest'; import { SharedEventEmitter } from '../../utils/events'; @@ -9,6 +11,8 @@ let subscriptions = []; export default class RewardedVideo { _admob: AdMob; + adUnit: string; + loaded: boolean; constructor(admob: AdMob, adUnit: string) { for (let i = 0, len = subscriptions.length; i < len; i++) { @@ -106,7 +110,7 @@ export default class RewardedVideo { * @param listenerCb * @returns {null} */ - on(eventType, listenerCb) { + on(eventType: string, listenerCb: Function) { const types = { ...statics.EventTypes, ...statics.RewardedVideoEventTypes, diff --git a/node_modules/react-native-firebase/dist/modules/auth/AuthSettings.js.flow b/node_modules/react-native-firebase/dist/modules/auth/AuthSettings.js.flow index 997efc7..7b84def 100644 --- a/node_modules/react-native-firebase/dist/modules/auth/AuthSettings.js.flow +++ b/node_modules/react-native-firebase/dist/modules/auth/AuthSettings.js.flow @@ -1,3 +1,5 @@ +// @flow + import { getNativeModule } from '../../utils/native'; import { isAndroid, isIOS } from '../../utils'; diff --git a/node_modules/react-native-firebase/dist/modules/auth/User.js.flow b/node_modules/react-native-firebase/dist/modules/auth/User.js.flow index 96026b6..86c9b24 100644 --- a/node_modules/react-native-firebase/dist/modules/auth/User.js.flow +++ b/node_modules/react-native-firebase/dist/modules/auth/User.js.flow @@ -16,10 +16,10 @@ import type { IdTokenResult, } from './types'; -type UpdateProfile = { - displayName?: string, - photoURL?: string, -}; +type UpdateProfile = $Shape<{| + +displayName?: string, + +photoURL?: string, +|}>; export default class User { _auth: Auth; diff --git a/node_modules/react-native-firebase/dist/modules/core/app.js.flow b/node_modules/react-native-firebase/dist/modules/core/app.js.flow -index 6e27c61..27d226d 100644 +index 6e27c61..0b0b492 100644 --- a/node_modules/react-native-firebase/dist/modules/core/app.js.flow +++ b/node_modules/react-native-firebase/dist/modules/core/app.js.flow @@ -21,7 +21,9 @@ import Links, { NAMESPACE as LinksNamespace } from '../links'; import Messaging, { NAMESPACE as MessagingNamespace } from '../messaging'; import Notifications, { NAMESPACE as NotificationsNamespace, + statics as NotificationsStatics, } from '../notifications'; +import type { NativeNotification } from '../notifications/types'; import Performance, { NAMESPACE as PerfNamespace } from '../perf'; import Storage, { NAMESPACE as StorageNamespace } from '../storage'; import Utils, { NAMESPACE as UtilsNamespace } from '../utils'; -@@ -63,7 +65,14 @@ export default class App { +@@ -63,7 +65,12 @@ export default class App { messaging: () => Messaging; - notifications: () => Notifications; -+ notifications: { -+ (): Notifications, -+ +Android: NotificationsStatics.Android, -+ +Notification: ( -+ nativeNotification?: NativeNotification, -+ notifications?: Notifications, -+ ) => NotificationsStatics.Notification, -+ }; ++ notifications: ++ & ((): Notifications) ++ & { ++ +Android: NotificationsStatics.Android, ++ +Notification: NotificationsStatics.Notification, ++ }; perf: () => Performance; -@@ -77,7 +86,7 @@ export default class App { +@@ -77,7 +84,7 @@ export default class App { fromNative: boolean = false ) { this._name = name; - this._options = Object.assign({}, options); + this._options = { ...options }; if (fromNative) { this._initialized = true; -@@ -129,7 +138,7 @@ export default class App { +@@ -129,7 +136,7 @@ export default class App { * @return {*} */ get options(): FirebaseOptions { - return Object.assign({}, this._options); + return { ...this._options }; } /** diff --git a/node_modules/react-native-firebase/dist/modules/firestore/DocumentReference.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/DocumentReference.js.flow index b8ef7c2..37fe40e 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/DocumentReference.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/DocumentReference.js.flow @@ -8,7 +8,7 @@ import CollectionReference from './CollectionReference'; import { parseUpdateArgs } from './utils'; import { buildNativeMap } from './utils/serialize'; import { getNativeModule } from '../../utils/native'; -import { firestoreAutoId, isFunction, isObject } from '../../utils'; +import { firestoreAutoId, isObject } from '../../utils'; import { getAppEventName, SharedEventEmitter } from '../../utils/events'; import type Firestore from './'; @@ -137,8 +137,8 @@ export default class DocumentReference { let observer: Observer; let docListenOptions = {}; // Called with: onNext, ?onError - if (isFunction(optionsOrObserverOrOnNext)) { - if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) { + if (typeof optionsOrObserverOrOnNext === 'function') { + if (observerOrOnNextOrOnError && typeof observerOrOnNextOrOnError !== 'function') { throw new Error( 'DocumentReference.onSnapshot failed: Second argument must be a valid function.' ); @@ -150,14 +150,14 @@ export default class DocumentReference { }; } else if ( optionsOrObserverOrOnNext && - isObject(optionsOrObserverOrOnNext) + typeof optionsOrObserverOrOnNext === 'object' ) { // Called with: Observer if (optionsOrObserverOrOnNext.next) { - if (isFunction(optionsOrObserverOrOnNext.next)) { + if (typeof optionsOrObserverOrOnNext.next === 'function') { if ( optionsOrObserverOrOnNext.error && - !isFunction(optionsOrObserverOrOnNext.error) + typeof optionsOrObserverOrOnNext.error !== 'function' ) { throw new Error( 'DocumentReference.onSnapshot failed: Observer.error must be a valid function.' @@ -181,27 +181,27 @@ export default class DocumentReference { ) { docListenOptions = optionsOrObserverOrOnNext; // Called with: Options, onNext, ?onError - if (isFunction(observerOrOnNextOrOnError)) { - if (onError && !isFunction(onError)) { + if (typeof observerOrOnNextOrOnError === 'function') { + if (onError && typeof onError !== 'function') { throw new Error( 'DocumentReference.onSnapshot failed: Third argument must be a valid function.' ); } - // $FlowExpectedError: Not coping with the overloaded method signature observer = { + // $FlowExpectedError: Not coping with the overloaded method signature next: observerOrOnNextOrOnError, error: onError, }; // Called with Options, Observer } else if ( observerOrOnNextOrOnError && - isObject(observerOrOnNextOrOnError) && + typeof observerOrOnNextOrOnError === 'object' && observerOrOnNextOrOnError.next ) { - if (isFunction(observerOrOnNextOrOnError.next)) { + if (typeof observerOrOnNextOrOnError.next === 'function') { if ( observerOrOnNextOrOnError.error && - !isFunction(observerOrOnNextOrOnError.error) + typeof observerOrOnNextOrOnError.error !== 'function' ) { throw new Error( 'DocumentReference.onSnapshot failed: Observer.error must be a valid function.' diff --git a/node_modules/react-native-firebase/dist/modules/firestore/FieldValue.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/FieldValue.js.flow index edbd9dc..c4e2598 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/FieldValue.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/FieldValue.js.flow @@ -2,7 +2,7 @@ * @flow * FieldValue representation wrapper */ -import AnyJs from './utils/any'; +import type { AnyJs } from './utils/any'; import { buildNativeArray } from './utils/serialize'; // TODO: Salakar: Refactor in v6 diff --git a/node_modules/react-native-firebase/dist/modules/firestore/Query.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/Query.js.flow index 1716dbe..80df382 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/Query.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/Query.js.flow @@ -8,7 +8,7 @@ import SnapshotError from './SnapshotError'; import DocumentSnapshot from './DocumentSnapshot'; import { getNativeModule } from '../../utils/native'; import { buildNativeArray, buildTypeMap } from './utils/serialize'; -import { firestoreAutoId, isFunction, isObject } from '../../utils'; +import { firestoreAutoId, isObject } from '../../utils'; import { getAppEventName, SharedEventEmitter } from '../../utils/events'; import type Firestore from './'; @@ -55,7 +55,7 @@ type FieldOrder = {| fieldPath: NativeFieldPath, |}; -type QueryOptions = { +type QueryOptions = $Shape<{| endAt?: any[], endBefore?: any[], limit?: number, @@ -63,7 +63,7 @@ type QueryOptions = { selectFields?: string[], startAfter?: any[], startAt?: any[], -}; +|}>; export type ObserverOnError = SnapshotError => void; export type ObserverOnNext = QuerySnapshot => void; @@ -274,8 +274,8 @@ export default class Query { let observer: Observer; let metadataChanges = {}; // Called with: onNext, ?onError - if (isFunction(optionsOrObserverOrOnNext)) { - if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) { + if (typeof optionsOrObserverOrOnNext === 'function') { + if (observerOrOnNextOrOnError && typeof observerOrOnNextOrOnError !== 'function') { throw new Error( 'Query.onSnapshot failed: Second argument must be a valid function.' ); @@ -287,14 +287,14 @@ export default class Query { }; } else if ( optionsOrObserverOrOnNext && - isObject(optionsOrObserverOrOnNext) + typeof optionsOrObserverOrOnNext === 'object' ) { // Called with: Observer if (optionsOrObserverOrOnNext.next) { - if (isFunction(optionsOrObserverOrOnNext.next)) { + if (typeof optionsOrObserverOrOnNext.next === 'function') { if ( optionsOrObserverOrOnNext.error && - !isFunction(optionsOrObserverOrOnNext.error) + typeof optionsOrObserverOrOnNext.error !== 'function' ) { throw new Error( 'Query.onSnapshot failed: Observer.error must be a valid function.' @@ -318,27 +318,27 @@ export default class Query { ) { metadataChanges = optionsOrObserverOrOnNext; // Called with: Options, onNext, ?onError - if (isFunction(observerOrOnNextOrOnError)) { - if (onError && !isFunction(onError)) { + if (typeof observerOrOnNextOrOnError === 'function') { + if (onError && typeof onError !== 'function') { throw new Error( 'Query.onSnapshot failed: Third argument must be a valid function.' ); } - // $FlowExpectedError: Not coping with the overloaded method signature observer = { + // $FlowExpectedError: Not coping with the overloaded method signature next: observerOrOnNextOrOnError, error: onError, }; // Called with Options, Observer } else if ( observerOrOnNextOrOnError && - isObject(observerOrOnNextOrOnError) && + typeof observerOrOnNextOrOnError === 'object' && observerOrOnNextOrOnError.next ) { - if (isFunction(observerOrOnNextOrOnError.next)) { + if (typeof observerOrOnNextOrOnError.next === 'function') { if ( observerOrOnNextOrOnError.error && - !isFunction(observerOrOnNextOrOnError.error) + typeof observerOrOnNextOrOnError.error !== 'function' ) { throw new Error( 'Query.onSnapshot failed: Observer.error must be a valid function.' diff --git a/node_modules/react-native-firebase/dist/modules/firestore/SnapshotError.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/SnapshotError.js.flow index 105574d..c34819e 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/SnapshotError.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/SnapshotError.js.flow @@ -1,3 +1,5 @@ +// @flow + import NativeError from '../../common/NativeError'; import type { SnapshotErrorInterface } from './firestoreTypes.flow'; import type { NativeErrorResponse } from '../../common/commonTypes.flow'; diff --git a/node_modules/react-native-firebase/dist/modules/firestore/Transaction.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/Transaction.js.flow index 0f96640..b564830 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/Transaction.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/Transaction.js.flow @@ -18,9 +18,9 @@ type Command = { options?: SetOptions | {}, }; -type SetOptions = { +type SetOptions = $Shape<{| merge: boolean, -}; +|}>; // TODO docs state all get requests must be made FIRST before any modifications // TODO so need to validate that diff --git a/node_modules/react-native-firebase/dist/modules/firestore/TransactionHandler.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/TransactionHandler.js.flow index 624acf0..f4b3039 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/TransactionHandler.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/TransactionHandler.js.flow @@ -18,7 +18,7 @@ const generateTransactionId = (): number => transactionId++; export type TransactionMeta = { id: number, - stack: string[], + stack: string, reject?: Function, resolve?: Function, transaction: Transaction, diff --git a/node_modules/react-native-firebase/dist/modules/firestore/utils/any.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/utils/any.js.flow index af9d484..caf587e 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/utils/any.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/utils/any.js.flow @@ -1,4 +1,7 @@ +// @flow + /** * @url https://github.com/firebase/firebase-js-sdk/blob/master/packages/firestore/src/util/misc.ts#L26 */ -export type AnyJs = null | undefined | boolean | number | string | object; +//export type AnyJs = null | void | boolean | number | string | Object; +export type AnyJs = any; diff --git a/node_modules/react-native-firebase/dist/modules/firestore/utils/serialize.js.flow b/node_modules/react-native-firebase/dist/modules/firestore/utils/serialize.js.flow index 0c87d1d..fcf97f0 100644 --- a/node_modules/react-native-firebase/dist/modules/firestore/utils/serialize.js.flow +++ b/node_modules/react-native-firebase/dist/modules/firestore/utils/serialize.js.flow @@ -33,7 +33,7 @@ export const buildNativeMap = (data: Object): { [string]: NativeTypeMap } => { return nativeData; }; -export const buildNativeArray = (array: Object[]): NativeTypeMap[] => { +export const buildNativeArray = (array: any[]): NativeTypeMap[] => { const nativeArray = []; if (array) { array.forEach(value => { diff --git a/node_modules/react-native-firebase/dist/modules/functions/HttpsError.js.flow b/node_modules/react-native-firebase/dist/modules/functions/HttpsError.js.flow index e2c7ed3..2c00aa6 100644 --- a/node_modules/react-native-firebase/dist/modules/functions/HttpsError.js.flow +++ b/node_modules/react-native-firebase/dist/modules/functions/HttpsError.js.flow @@ -1,9 +1,11 @@ +// @flow + import type { FunctionsErrorCode } from './types.flow'; export default class HttpsError extends Error { +details: ?any; - +message: string; + message: string; +code: FunctionsErrorCode; @@ -11,6 +13,8 @@ export default class HttpsError extends Error { super(message); this.code = code; this.details = details; - this.message = message; + if (message) { + this.message = message; + } } } diff --git a/node_modules/react-native-firebase/dist/modules/functions/types.flow.js.flow b/node_modules/react-native-firebase/dist/modules/functions/types.flow.js.flow index 8660827..0ef93c8 100644 --- a/node_modules/react-native-firebase/dist/modules/functions/types.flow.js.flow +++ b/node_modules/react-native-firebase/dist/modules/functions/types.flow.js.flow @@ -1,3 +1,7 @@ +// @flow + +import HttpsError from './HttpsError'; + export type HttpsCallableResult = { data: Object, }; diff --git a/node_modules/react-native-firebase/dist/modules/messaging/IOSMessaging.js.flow b/node_modules/react-native-firebase/dist/modules/messaging/IOSMessaging.js.flow index a97bf75..305b5bc 100644 --- a/node_modules/react-native-firebase/dist/modules/messaging/IOSMessaging.js.flow +++ b/node_modules/react-native-firebase/dist/modules/messaging/IOSMessaging.js.flow @@ -1,3 +1,5 @@ +// @flow + import { getNativeModule } from '../../utils/native'; import { isIOS } from '../../utils'; @@ -5,18 +7,20 @@ import { isIOS } from '../../utils'; import type Messaging from './'; export default class IOSMessaging { + _messaging: Messaging; + constructor(messaging: Messaging) { this._messaging = messaging; } - getAPNSToken(): Promise { + getAPNSToken(): null | Promise { if (!isIOS) { return null; } return getNativeModule(this._messaging).getAPNSToken(); } - registerForRemoteNotifications(): Promise { + registerForRemoteNotifications(): void | Promise { if (!isIOS) { return undefined; } diff --git a/node_modules/react-native-firebase/dist/modules/messaging/RemoteMessage.js.flow b/node_modules/react-native-firebase/dist/modules/messaging/RemoteMessage.js.flow index d076e23..3388775 100644 --- a/node_modules/react-native-firebase/dist/modules/messaging/RemoteMessage.js.flow +++ b/node_modules/react-native-firebase/dist/modules/messaging/RemoteMessage.js.flow @@ -89,7 +89,7 @@ export default class RemoteMessage { * @param data * @returns {RemoteMessage} */ - setData(data: { [string]: string } = {}) { + setData(data: { [string]: string } = {}): RemoteMessage { if (!isObject(data)) { throw new Error( `RemoteMessage:setData expects an object but got type '${typeof data}'.` diff --git a/node_modules/react-native-firebase/dist/modules/messaging/index.js.flow b/node_modules/react-native-firebase/dist/modules/messaging/index.js.flow index 988be94..a030db8 100644 --- a/node_modules/react-native-firebase/dist/modules/messaging/index.js.flow +++ b/node_modules/react-native-firebase/dist/modules/messaging/index.js.flow @@ -8,7 +8,6 @@ import INTERNALS from '../../utils/internals'; import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; -import { isFunction, isObject } from '../../utils'; import IOSMessaging from './IOSMessaging'; import RemoteMessage from './RemoteMessage'; @@ -89,10 +88,10 @@ export default class Messaging extends ModuleBase { onMessage(nextOrObserver: OnMessage | OnMessageObserver): () => any { let listener: RemoteMessage => any; - if (isFunction(nextOrObserver)) { + if (typeof nextOrObserver === "function") { // $FlowExpectedError: Not coping with the overloaded method signature listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + } else if (typeof nextOrObserver === "object" && typeof nextOrObserver.next === "function") { listener = nextOrObserver.next; } else { throw new Error( @@ -114,10 +113,10 @@ export default class Messaging extends ModuleBase { nextOrObserver: OnTokenRefresh | OnTokenRefreshObserver ): () => any { let listener: string => any; - if (isFunction(nextOrObserver)) { + if (typeof nextOrObserver === "function") { // $FlowExpectedError: Not coping with the overloaded method signature listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + } else if (typeof nextOrObserver === "object" && typeof nextOrObserver.next === "function") { listener = nextOrObserver.next; } else { throw new Error( diff --git a/node_modules/react-native-firebase/dist/modules/notifications/IOSNotifications.js.flow b/node_modules/react-native-firebase/dist/modules/notifications/IOSNotifications.js.flow index f2ea8ca..5058f52 100644 --- a/node_modules/react-native-firebase/dist/modules/notifications/IOSNotifications.js.flow +++ b/node_modules/react-native-firebase/dist/modules/notifications/IOSNotifications.js.flow @@ -1,3 +1,5 @@ +// @flow + import { getNativeModule } from '../../utils/native'; import type Notifications from './'; diff --git a/node_modules/react-native-firebase/dist/modules/notifications/index.js.flow b/node_modules/react-native-firebase/dist/modules/notifications/index.js.flow index 34e4ee3..22d5dbe 100644 --- a/node_modules/react-native-firebase/dist/modules/notifications/index.js.flow +++ b/node_modules/react-native-firebase/dist/modules/notifications/index.js.flow @@ -7,7 +7,6 @@ import { SharedEventEmitter } from '../../utils/events'; import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; -import { isFunction, isObject } from '../../utils'; import AndroidAction from './AndroidAction'; import AndroidChannel from './AndroidChannel'; import AndroidChannelGroup from './AndroidChannelGroup'; @@ -212,9 +211,9 @@ export default class Notifications extends ModuleBase { nextOrObserver: OnNotification | OnNotificationObserver ): () => any { let listener; - if (isFunction(nextOrObserver)) { + if (typeof nextOrObserver === "function") { listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + } else if (typeof nextOrObserver === "object" && typeof nextOrObserver.next === "function") { listener = nextOrObserver.next; } else { throw new Error( @@ -235,9 +234,9 @@ export default class Notifications extends ModuleBase { nextOrObserver: OnNotification | OnNotificationObserver ): () => any { let listener; - if (isFunction(nextOrObserver)) { + if (typeof nextOrObserver === "function") { listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + } else if (typeof nextOrObserver === "object" && typeof nextOrObserver.next === "function") { listener = nextOrObserver.next; } else { throw new Error( @@ -258,9 +257,9 @@ export default class Notifications extends ModuleBase { nextOrObserver: OnNotificationOpened | OnNotificationOpenedObserver ): () => any { let listener; - if (isFunction(nextOrObserver)) { + if (typeof nextOrObserver === "function") { listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + } else if (typeof nextOrObserver === "object" && typeof nextOrObserver.next === "function") { listener = nextOrObserver.next; } else { throw new Error( diff --git a/node_modules/react-native-firebase/dist/modules/utils/database.js.flow b/node_modules/react-native-firebase/dist/modules/utils/database.js.flow index 12fda54..d25b305 100644 --- a/node_modules/react-native-firebase/dist/modules/utils/database.js.flow +++ b/node_modules/react-native-firebase/dist/modules/utils/database.js.flow @@ -1,3 +1,5 @@ +// @flow + import SyncTree from '../../utils/SyncTree'; export default { diff --git a/node_modules/react-native-firebase/dist/modules/utils/index.js.flow b/node_modules/react-native-firebase/dist/modules/utils/index.js.flow index 463de92..84f24cf 100644 --- a/node_modules/react-native-firebase/dist/modules/utils/index.js.flow +++ b/node_modules/react-native-firebase/dist/modules/utils/index.js.flow @@ -29,7 +29,7 @@ export default class RNFirebaseUtils extends ModuleBase { }); } - get database(): DatabaseUtils { + get database(): typeof DatabaseUtils { return DatabaseUtils; } diff --git a/node_modules/react-native-firebase/dist/utils/SyncTree.js.flow b/node_modules/react-native-firebase/dist/utils/SyncTree.js.flow index 58d0300..ca6edba 100644 --- a/node_modules/react-native-firebase/dist/utils/SyncTree.js.flow +++ b/node_modules/react-native-firebase/dist/utils/SyncTree.js.flow @@ -19,6 +19,7 @@ type Registration = { listener: Listener, eventRegistrationKey: string, ref: DatabaseReference, + ... }; /** diff --git a/web/.flowconfig b/web/.flowconfig index ee46ee52e..e6271ab66 100644 --- a/web/.flowconfig +++ b/web/.flowconfig @@ -1,32 +1,31 @@ [include] ../lib [libs] ../lib/flow-typed [options] module.name_mapper.extension='css' -> '/flow/CSSModule.js.flow' exact_by_default=true format.bracket_spacing=false [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn unnecessary-invariant=warn -signature-verification-failure=warn [strict] deprecated-type nonstrict-import sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import diff --git a/web/calendar/entry.react.js b/web/calendar/entry.react.js index 67586fed2..120c6ce04 100644 --- a/web/calendar/entry.react.js +++ b/web/calendar/entry.react.js @@ -1,503 +1,503 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { createEntryActionTypes, createEntry, saveEntryActionTypes, saveEntry, deleteEntryActionTypes, deleteEntry, concurrentModificationResetActionType, } from 'lib/actions/entry-actions'; import { useModalContext, type PushModal, } from 'lib/components/modal-provider.react'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { entryKey } from 'lib/shared/entry-utils'; import { colorIsDark, threadHasPermission } from 'lib/shared/thread-utils'; import type { Shape } from 'lib/types/core'; import { type EntryInfo, type CreateEntryInfo, type SaveEntryInfo, type SaveEntryResult, type SaveEntryPayload, type CreateEntryPayload, type DeleteEntryInfo, type DeleteEntryResult, type CalendarQuery, } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { Dispatch } from 'lib/types/redux-types'; import { threadPermissions } from 'lib/types/thread-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import { dateString } from 'lib/utils/date-utils'; import { ServerError } from 'lib/utils/errors'; import LoadingIndicator from '../loading-indicator.react'; import LogInFirstModal from '../modals/account/log-in-first-modal.react'; import ConcurrentModificationModal from '../modals/concurrent-modification-modal.react'; import HistoryModal from '../modals/history/history-modal.react'; import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import { HistoryVector, DeleteVector } from '../vectors.react'; import css from './calendar.css'; type BaseProps = { +innerRef: (key: string, me: Entry) => void, +entryInfo: EntryInfo, +focusOnFirstEntryNewerThan: (time: number) => void, +tabIndex: number, }; type Props = { ...BaseProps, +threadInfo: ThreadInfo, +loggedIn: boolean, +calendarQuery: () => CalendarQuery, +online: boolean, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +createEntry: (info: CreateEntryInfo) => Promise, +saveEntry: (info: SaveEntryInfo) => Promise, +deleteEntry: (info: DeleteEntryInfo) => Promise, +pushModal: PushModal, +popModal: () => void, }; type State = { +focused: boolean, +loadingStatus: LoadingStatus, +text: string, }; class Entry extends React.PureComponent { textarea: ?HTMLTextAreaElement; creating: boolean; needsUpdateAfterCreation: boolean; needsDeleteAfterCreation: boolean; nextSaveAttemptIndex: number; mounted: boolean; currentlySaving: ?string; constructor(props: Props) { super(props); this.state = { focused: false, loadingStatus: 'inactive', text: props.entryInfo.text, }; this.creating = false; this.needsUpdateAfterCreation = false; this.needsDeleteAfterCreation = false; this.nextSaveAttemptIndex = 0; } guardedSetState(input: Shape) { if (this.mounted) { this.setState(input); } } componentDidMount() { this.mounted = true; this.props.innerRef(entryKey(this.props.entryInfo), this); this.updateHeight(); // Whenever a new Entry is created, focus on it if (!this.props.entryInfo.id) { this.focus(); } } componentDidUpdate(prevProps: Props) { if ( !this.state.focused && this.props.entryInfo.text !== this.state.text && this.props.entryInfo.text !== prevProps.entryInfo.text ) { this.setState({ text: this.props.entryInfo.text }); this.currentlySaving = null; } if ( this.props.online && !prevProps.online && this.state.loadingStatus === 'error' ) { this.save(); } } focus() { invariant( this.textarea instanceof HTMLTextAreaElement, 'textarea ref not set', ); this.textarea.focus(); } onMouseDown: (event: SyntheticEvent) => void = event => { if (this.state.focused && event.target !== this.textarea) { // Don't lose focus when some non-textarea part is clicked event.preventDefault(); } }; componentWillUnmount() { this.mounted = false; } updateHeight: () => void = () => { invariant( this.textarea instanceof HTMLTextAreaElement, 'textarea ref not set', ); this.textarea.style.height = 'auto'; this.textarea.style.height = this.textarea.scrollHeight + 'px'; }; render(): React.Node { let actionLinks = null; if (this.state.focused) { let historyButton = null; if (this.props.entryInfo.id) { historyButton = ( History ); } const rightActionLinksClassName = `${css.rightActionLinks} ${css.actionLinksText}`; actionLinks = (
Delete {historyButton} {this.props.threadInfo.uiName}
); } const darkColor = colorIsDark(this.props.threadInfo.color); const entryClasses = classNames({ [css.entry]: true, [css.darkEntry]: darkColor, [css.focusedEntry]: this.state.focused, }); const style = { backgroundColor: `#${this.props.threadInfo.color}` }; const loadingIndicatorColor = darkColor ? 'white' : 'black'; const canEditEntry = threadHasPermission( this.props.threadInfo, threadPermissions.EDIT_ENTRIES, ); return (