diff --git a/modules/bottom-sheet/src/BottomSheet.types.js b/modules/bottom-sheet/src/BottomSheet.types.js new file mode 100644 index 0000000000..8695576020 --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheet.types.js @@ -0,0 +1,6 @@ +export var BottomSheetSnapPoint; +(function (BottomSheetSnapPoint) { + BottomSheetSnapPoint[BottomSheetSnapPoint["Hidden"] = 0] = "Hidden"; + BottomSheetSnapPoint[BottomSheetSnapPoint["Partial"] = 1] = "Partial"; + BottomSheetSnapPoint[BottomSheetSnapPoint["Full"] = 2] = "Full"; +})(BottomSheetSnapPoint || (BottomSheetSnapPoint = {})); diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.js b/modules/bottom-sheet/src/BottomSheetNativeComponent.js new file mode 100644 index 0000000000..2f849b3185 --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.js @@ -0,0 +1,196 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import * as React from 'react'; +import { Dimensions, Platform, useWindowDimensions, View, } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { requireNativeModule, requireNativeViewManager } from 'expo-modules-core'; +import { IS_IOS } from '#/env'; +import { BottomSheetPortalProvider, Context as PortalContext, } from './BottomSheetPortal'; +var NativeView = requireNativeViewManager('BottomSheet'); +var NativeModule = requireNativeModule('BottomSheet'); +var IS_IOS15 = Platform.OS === 'ios' && + // semvar - can be 3 segments, so can't use Number(Platform.Version) + Number(Platform.Version.split('.').at(0)) < 16; +var BottomSheetNativeComponent = /** @class */ (function (_super) { + __extends(BottomSheetNativeComponent, _super); + function BottomSheetNativeComponent(props) { + var _this = _super.call(this, props) || this; + _this.ref = React.createRef(); + _this.onStateChange = function (event) { + var _b, _c; + var state = event.nativeEvent.state; + var isOpen = state !== 'closed'; + _this.setState({ open: isOpen }); + (_c = (_b = _this.props).onStateChange) === null || _c === void 0 ? void 0 : _c.call(_b, event); + }; + _this.updateLayout = function () { + var _b; + (_b = _this.ref.current) === null || _b === void 0 ? void 0 : _b.updateLayout(); + }; + _this.state = { + open: false, + }; + return _this; + } + BottomSheetNativeComponent.prototype.present = function () { + this.setState({ open: true }); + }; + BottomSheetNativeComponent.prototype.dismiss = function () { + var _b; + (_b = this.ref.current) === null || _b === void 0 ? void 0 : _b.dismiss(); + }; + BottomSheetNativeComponent.prototype.render = function () { + var _this = this; + var _b; + var Portal = this.context; + if (!Portal) { + throw new Error('BottomSheet: You need to wrap your component tree with a to use the bottom sheet.'); + } + if (!this.state.open) { + return null; + } + var extraStyles; + if (IS_IOS15 && this.state.viewHeight) { + var screenHeight = Dimensions.get('screen').height; + var viewHeight = this.state.viewHeight; + var cornerRadius = (_b = this.props.cornerRadius) !== null && _b !== void 0 ? _b : 0; + if (viewHeight < screenHeight / 2) { + extraStyles = { + height: viewHeight, + marginTop: screenHeight / 2 - viewHeight, + borderTopLeftRadius: cornerRadius, + borderTopRightRadius: cornerRadius, + }; + } + } + return (_jsx(Portal, { children: _jsx(BottomSheetNativeComponentInner, __assign({}, this.props, { nativeViewRef: this.ref, onStateChange: this.onStateChange, extraStyles: extraStyles, onLayout: function (e) { + if (IS_IOS15) { + var height = e.nativeEvent.layout.height; + _this.setState({ viewHeight: height }); + } + if (Platform.OS === 'android') { + // TEMP HACKFIX: I had to timebox this, but this is Bad. + // On Android, if you run updateLayout() immediately, + // it will take ages to actually run on the native side. + // However, adding literally any delay will fix this, including + // a console.log() - just sending the log to the CLI is enough. + // TODO: Get to the bottom of this and fix it properly! -sfn + setTimeout(function () { return _this.updateLayout(); }); + } + else { + _this.updateLayout(); + } + } })) })); + }; + var _a; + _a = BottomSheetNativeComponent; + BottomSheetNativeComponent.contextType = PortalContext; + BottomSheetNativeComponent.dismissAll = function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(_a, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, NativeModule.dismissAll()]; + case 1: + _b.sent(); + return [2 /*return*/]; + } + }); + }); }; + return BottomSheetNativeComponent; +}(React.Component)); +export { BottomSheetNativeComponent }; +function BottomSheetNativeComponentInner(_b) { + var _c; + var children = _b.children, backgroundColor = _b.backgroundColor, onLayout = _b.onLayout, onStateChange = _b.onStateChange, nativeViewRef = _b.nativeViewRef, extraStyles = _b.extraStyles, rest = __rest(_b, ["children", "backgroundColor", "onLayout", "onStateChange", "nativeViewRef", "extraStyles"]); + var insets = useSafeAreaInsets(); + var cornerRadius = (_c = rest.cornerRadius) !== null && _c !== void 0 ? _c : 0; + var screenHeight = useWindowDimensions().height; + var sheetHeight = IS_IOS ? screenHeight - insets.top : screenHeight; + return (_jsx(NativeView, __assign({}, rest, { onStateChange: onStateChange, ref: nativeViewRef, style: { + position: 'absolute', + height: sheetHeight, + width: '100%', + }, containerBackgroundColor: backgroundColor, children: _jsx(View, { style: [ + { + flex: 1, + backgroundColor: backgroundColor, + }, + Platform.OS === 'android' && { + borderTopLeftRadius: cornerRadius, + borderTopRightRadius: cornerRadius, + overflow: 'hidden', + }, + extraStyles, + ], children: _jsx(View, { onLayout: onLayout, children: _jsx(BottomSheetPortalProvider, { children: children }) }) }) }))); +} diff --git a/modules/bottom-sheet/src/BottomSheetPortal.js b/modules/bottom-sheet/src/BottomSheetPortal.js new file mode 100644 index 0000000000..3f7a47ea0a --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheetPortal.js @@ -0,0 +1,19 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { createPortalGroup_INTERNAL } from './lib/Portal'; +export var Context = React.createContext({}); +Context.displayName = 'BottomSheetPortalContext'; +export var useBottomSheetPortal_INTERNAL = function () { return React.useContext(Context); }; +export function BottomSheetPortalProvider(_a) { + var children = _a.children; + var portal = React.useMemo(function () { + return createPortalGroup_INTERNAL(); + }, []); + return (_jsx(Context.Provider, { value: portal.Portal, children: _jsxs(portal.Provider, { children: [children, _jsx(portal.Outlet, {})] }) })); +} +var defaultPortal = createPortalGroup_INTERNAL(); +export var BottomSheetOutlet = defaultPortal.Outlet; +export function BottomSheetProvider(_a) { + var children = _a.children; + return (_jsx(Context.Provider, { value: defaultPortal.Portal, children: _jsx(defaultPortal.Provider, { children: children }) })); +} diff --git a/modules/bottom-sheet/src/lib/Portal.js b/modules/bottom-sheet/src/lib/Portal.js new file mode 100644 index 0000000000..4a58dfb5a4 --- /dev/null +++ b/modules/bottom-sheet/src/lib/Portal.js @@ -0,0 +1,45 @@ +import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +export function createPortalGroup_INTERNAL() { + var Context = React.createContext({ + outlet: null, + append: function () { }, + remove: function () { }, + }); + Context.displayName = 'BottomSheetPortalContext'; + function Provider(props) { + var map = React.useRef({}); + var _a = React.useState(null), outlet = _a[0], setOutlet = _a[1]; + var append = React.useCallback(function (id, component) { + if (map.current[id]) + return; + map.current[id] = _jsx(React.Fragment, { children: component }, id); + setOutlet(_jsx(_Fragment, { children: Object.values(map.current) })); + }, []); + var remove = React.useCallback(function (id) { + delete map.current[id]; + setOutlet(_jsx(_Fragment, { children: Object.values(map.current) })); + }, []); + var contextValue = React.useMemo(function () { return ({ + outlet: outlet, + append: append, + remove: remove, + }); }, [outlet, append, remove]); + return (_jsx(Context.Provider, { value: contextValue, children: props.children })); + } + function Outlet() { + var ctx = React.useContext(Context); + return ctx.outlet; + } + function Portal(_a) { + var children = _a.children; + var _b = React.useContext(Context), append = _b.append, remove = _b.remove; + var id = React.useId(); + React.useEffect(function () { + append(id, children); + return function () { return remove(id); }; + }, [id, children, append, remove]); + return null; + } + return { Provider: Provider, Outlet: Outlet, Portal: Portal }; +} diff --git a/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.js b/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.js new file mode 100644 index 0000000000..32a4e03763 --- /dev/null +++ b/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.js @@ -0,0 +1,113 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { BackgroundNotificationHandler } from './ExpoBackgroundNotificationHandlerModule'; +var Context = React.createContext({}); +export var useBackgroundNotificationPreferences = function () { + return React.useContext(Context); +}; +export function BackgroundNotificationPreferencesProvider(_a) { + var _this = this; + var children = _a.children; + var _b = React.useState({ + playSoundChat: true, + }), preferences = _b[0], setPreferences = _b[1]; + React.useEffect(function () { + ; + (function () { return __awaiter(_this, void 0, void 0, function () { + var prefs; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, BackgroundNotificationHandler.getAllPrefsAsync()]; + case 1: + prefs = _a.sent(); + setPreferences(prefs); + return [2 /*return*/]; + } + }); + }); })(); + }, []); + var value = React.useMemo(function () { return ({ + preferences: preferences, + setPref: function (k, v) { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = typeof v; + switch (_a) { + case 'boolean': return [3 /*break*/, 1]; + case 'string': return [3 /*break*/, 3]; + } + return [3 /*break*/, 5]; + case 1: return [4 /*yield*/, BackgroundNotificationHandler.setBoolAsync(k, v)]; + case 2: + _b.sent(); + return [3 /*break*/, 6]; + case 3: return [4 /*yield*/, BackgroundNotificationHandler.setStringAsync(k, v)]; + case 4: + _b.sent(); + return [3 /*break*/, 6]; + case 5: + { + throw new Error("Invalid type for value: ".concat(typeof v)); + } + _b.label = 6; + case 6: + setPreferences(function (prev) { + var _a; + return (__assign(__assign({}, prev), (_a = {}, _a[k] = v, _a))); + }); + return [2 /*return*/]; + } + }); + }); }, + }); }, [preferences]); + return _jsx(Context.Provider, { value: value, children: children }); +} diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.js b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.js @@ -0,0 +1 @@ +export {}; diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.js b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.js new file mode 100644 index 0000000000..c0c166c14f --- /dev/null +++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.js @@ -0,0 +1,2 @@ +import { requireNativeModule } from 'expo-modules-core'; +export var BackgroundNotificationHandler = requireNativeModule('ExpoBackgroundNotificationHandler'); diff --git a/modules/expo-bluesky-gif-view/src/GifView.types.js b/modules/expo-bluesky-gif-view/src/GifView.types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/modules/expo-bluesky-gif-view/src/GifView.types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/App.native.js b/src/App.native.js new file mode 100644 index 0000000000..6828b6710d --- /dev/null +++ b/src/App.native.js @@ -0,0 +1,191 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import '#/logger/sentry/setup'; +import '#/view/icons'; +import React, { useEffect, useState } from 'react'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { KeyboardProvider as KeyboardControllerProvider } from 'react-native-keyboard-controller'; +import { initialWindowMetrics, SafeAreaProvider, } from 'react-native-safe-area-context'; +import * as ScreenOrientation from 'expo-screen-orientation'; +import * as SplashScreen from 'expo-splash-screen'; +import * as SystemUI from 'expo-system-ui'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import * as Sentry from '@sentry/react-native'; +import { Provider as HideBottomBarBorderProvider } from '#/lib/hooks/useHideBottomBarBorder'; +import { QueryProvider } from '#/lib/react-query'; +import { s } from '#/lib/styles'; +import { ThemeProvider } from '#/lib/ThemeContext'; +import I18nProvider from '#/locale/i18nProvider'; +import { logger } from '#/logger'; +import { Provider as A11yProvider } from '#/state/a11y'; +import { prefetchAppConfig, Provider as AppConfigProvider, } from '#/state/appConfig'; +import { Provider as MutedThreadsProvider } from '#/state/cache/thread-mutes'; +import { Provider as DialogStateProvider } from '#/state/dialogs'; +import { Provider as EmailVerificationProvider } from '#/state/email-verification'; +import { listenSessionDropped } from '#/state/events'; +import { GlobalGestureEventsProvider } from '#/state/global-gesture-events'; +import { Provider as HomeBadgeProvider } from '#/state/home-badge'; +import { Provider as LightboxStateProvider } from '#/state/lightbox'; +import { MessagesProvider } from '#/state/messages'; +import { Provider as ModalStateProvider } from '#/state/modals'; +import { init as initPersistedState } from '#/state/persisted'; +import { Provider as PrefsStateProvider } from '#/state/preferences'; +import { Provider as LabelDefsProvider } from '#/state/preferences/label-defs'; +import { Provider as ModerationOptsProvider } from '#/state/preferences/moderation-opts'; +import { Provider as UnreadNotifsProvider } from '#/state/queries/notifications/unread'; +import { Provider as ServiceAccountManager } from '#/state/service-config'; +import { Provider as SessionProvider, useSession, useSessionApi, } from '#/state/session'; +import { readLastActiveAccount } from '#/state/session/util'; +import { Provider as ShellStateProvider } from '#/state/shell'; +import { Provider as ComposerProvider } from '#/state/shell/composer'; +import { Provider as LoggedOutViewProvider } from '#/state/shell/logged-out'; +import { Provider as OnboardingProvider } from '#/state/shell/onboarding'; +import { Provider as ProgressGuideProvider } from '#/state/shell/progress-guide'; +import { Provider as SelectedFeedProvider } from '#/state/shell/selected-feed'; +import { Provider as StarterPackProvider } from '#/state/shell/starter-pack'; +import { Provider as HiddenRepliesProvider } from '#/state/threadgate-hidden-replies'; +import { TestCtrls } from '#/view/com/testing/TestCtrls'; +import * as Toast from '#/view/com/util/Toast'; +import { Shell } from '#/view/shell'; +import { ThemeProvider as Alf } from '#/alf'; +import { useColorModeTheme } from '#/alf/util/useColorModeTheme'; +import { Provider as ContextMenuProvider } from '#/components/ContextMenu'; +import { useStarterPackEntry } from '#/components/hooks/useStarterPackEntry'; +import { Provider as IntentDialogProvider } from '#/components/intents/IntentDialogs'; +import { Provider as PolicyUpdateOverlayProvider } from '#/components/PolicyUpdateOverlay'; +import { Provider as PortalProvider } from '#/components/Portal'; +import { Provider as VideoVolumeProvider } from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'; +import { ToastOutlet } from '#/components/Toast'; +import { prefetchAgeAssuranceConfig, Provider as AgeAssuranceV2Provider, } from '#/ageAssurance'; +import { AnalyticsContext, AnalyticsFeaturesContext, features, setupDeviceId, } from '#/analytics'; +import { IS_ANDROID, IS_IOS } from '#/env'; +import { prefetchLiveEvents, Provider as LiveEventsProvider, } from '#/features/liveEvents/context'; +import * as Geo from '#/geolocation'; +import { Splash } from '#/Splash'; +import { BottomSheetProvider } from '../modules/bottom-sheet'; +import { BackgroundNotificationPreferencesProvider } from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'; +SplashScreen.preventAutoHideAsync(); +if (IS_IOS) { + SystemUI.setBackgroundColorAsync('black'); +} +if (IS_ANDROID) { + // iOS is handled by the config plugin -sfn + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(function (error) { + return logger.debug('Could not lock orientation', { safeMessage: error }); + }); +} +/** + * Begin geolocation ASAP + */ +Geo.resolve(); +prefetchAgeAssuranceConfig(); +prefetchLiveEvents(); +prefetchAppConfig(); +function InnerApp() { + var _a = React.useState(false), isReady = _a[0], setIsReady = _a[1]; + var currentAccount = useSession().currentAccount; + var resumeSession = useSessionApi().resumeSession; + var theme = useColorModeTheme(); + var _ = useLingui()._; + var hasCheckedReferrer = useStarterPackEntry(); + // init + useEffect(function () { + function onLaunch(account) { + return __awaiter(this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 5, 6, 7]); + if (!account) return [3 /*break*/, 2]; + return [4 /*yield*/, resumeSession(account)]; + case 1: + _a.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, features.init]; + case 3: + _a.sent(); + _a.label = 4; + case 4: return [3 /*break*/, 7]; + case 5: + e_1 = _a.sent(); + logger.error("session: resume failed", { message: e_1 }); + return [3 /*break*/, 7]; + case 6: + setIsReady(true); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); + } + var account = readLastActiveAccount(); + onLaunch(account); + }, [resumeSession]); + useEffect(function () { + return listenSessionDropped(function () { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sorry! Your session expired. Please sign in again."], ["Sorry! Your session expired. Please sign in again."])))), 'info'); + }); + }, [_]); + return (_jsx(Alf, { theme: theme, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(ContextMenuProvider, { children: _jsx(Splash, { isReady: isReady && hasCheckedReferrer, children: _jsx(VideoVolumeProvider, { children: _jsx(React.Fragment + // Resets the entire tree below when it changes: + , { children: _jsx(AnalyticsFeaturesContext, { children: _jsx(QueryProvider, { currentDid: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, children: _jsx(PolicyUpdateOverlayProvider, { children: _jsx(LiveEventsProvider, { children: _jsx(AgeAssuranceV2Provider, { children: _jsx(ComposerProvider, { children: _jsx(MessagesProvider, { children: _jsx(LabelDefsProvider, { children: _jsx(ModerationOptsProvider, { children: _jsx(LoggedOutViewProvider, { children: _jsx(SelectedFeedProvider, { children: _jsx(HiddenRepliesProvider, { children: _jsx(HomeBadgeProvider, { children: _jsx(UnreadNotifsProvider, { children: _jsx(BackgroundNotificationPreferencesProvider, { children: _jsx(MutedThreadsProvider, { children: _jsx(ProgressGuideProvider, { children: _jsx(ServiceAccountManager, { children: _jsx(EmailVerificationProvider, { children: _jsx(HideBottomBarBorderProvider, { children: _jsx(GestureHandlerRootView, { style: s.h100pct, children: _jsx(GlobalGestureEventsProvider, { children: _jsxs(IntentDialogProvider, { children: [_jsx(TestCtrls, {}), _jsx(Shell, {}), _jsx(ToastOutlet, {})] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) }) }) }) }) })); +} +function App() { + var _a = useState(false), isReady = _a[0], setReady = _a[1]; + React.useEffect(function () { + Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(function () { + return setReady(true); + }); + }, []); + if (!isReady) { + return null; + } + /* + * NOTE: only nothing here can depend on other data or session state, since + * that is set up in the InnerApp component above. + */ + return (_jsx(Geo.Provider, { children: _jsx(AppConfigProvider, { children: _jsx(A11yProvider, { children: _jsx(KeyboardControllerProvider, { children: _jsx(OnboardingProvider, { children: _jsx(AnalyticsContext, { children: _jsx(SessionProvider, { children: _jsx(PrefsStateProvider, { children: _jsx(I18nProvider, { children: _jsx(ShellStateProvider, { children: _jsx(ModalStateProvider, { children: _jsx(DialogStateProvider, { children: _jsx(LightboxStateProvider, { children: _jsx(PortalProvider, { children: _jsx(BottomSheetProvider, { children: _jsx(StarterPackProvider, { children: _jsx(SafeAreaProvider, { initialMetrics: initialWindowMetrics, children: _jsx(InnerApp, {}) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })); +} +export default Sentry.wrap(App); +var templateObject_1; diff --git a/src/App.web.js b/src/App.web.js new file mode 100644 index 0000000000..1170b36194 --- /dev/null +++ b/src/App.web.js @@ -0,0 +1,173 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import '#/logger/sentry/setup'; // must be near top +import '#/view/icons'; +import './style.css'; +import { Fragment, useEffect, useState } from 'react'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import * as Sentry from '@sentry/react-native'; +import { QueryProvider } from '#/lib/react-query'; +import { ThemeProvider } from '#/lib/ThemeContext'; +import I18nProvider from '#/locale/i18nProvider'; +import { logger } from '#/logger'; +import { Provider as A11yProvider } from '#/state/a11y'; +import { prefetchAppConfig, Provider as AppConfigProvider, } from '#/state/appConfig'; +import { Provider as MutedThreadsProvider } from '#/state/cache/thread-mutes'; +import { Provider as DialogStateProvider } from '#/state/dialogs'; +import { Provider as EmailVerificationProvider } from '#/state/email-verification'; +import { listenSessionDropped } from '#/state/events'; +import { Provider as HomeBadgeProvider } from '#/state/home-badge'; +import { Provider as LightboxStateProvider } from '#/state/lightbox'; +import { MessagesProvider } from '#/state/messages'; +import { Provider as ModalStateProvider } from '#/state/modals'; +import { init as initPersistedState } from '#/state/persisted'; +import { Provider as PrefsStateProvider } from '#/state/preferences'; +import { Provider as LabelDefsProvider } from '#/state/preferences/label-defs'; +import { Provider as ModerationOptsProvider } from '#/state/preferences/moderation-opts'; +import { Provider as UnreadNotifsProvider } from '#/state/queries/notifications/unread'; +import { Provider as ServiceConfigProvider } from '#/state/service-config'; +import { Provider as SessionProvider, useSession, useSessionApi, } from '#/state/session'; +import { readLastActiveAccount } from '#/state/session/util'; +import { Provider as ShellStateProvider } from '#/state/shell'; +import { Provider as ComposerProvider } from '#/state/shell/composer'; +import { Provider as LoggedOutViewProvider } from '#/state/shell/logged-out'; +import { Provider as OnboardingProvider } from '#/state/shell/onboarding'; +import { Provider as ProgressGuideProvider } from '#/state/shell/progress-guide'; +import { Provider as SelectedFeedProvider } from '#/state/shell/selected-feed'; +import { Provider as StarterPackProvider } from '#/state/shell/starter-pack'; +import { Provider as HiddenRepliesProvider } from '#/state/threadgate-hidden-replies'; +import * as Toast from '#/view/com/util/Toast'; +import { Shell } from '#/view/shell/index'; +import { ThemeProvider as Alf } from '#/alf'; +import { useColorModeTheme } from '#/alf/util/useColorModeTheme'; +import { Provider as ContextMenuProvider } from '#/components/ContextMenu'; +import { useStarterPackEntry } from '#/components/hooks/useStarterPackEntry'; +import { Provider as IntentDialogProvider } from '#/components/intents/IntentDialogs'; +import { Provider as PolicyUpdateOverlayProvider } from '#/components/PolicyUpdateOverlay'; +import { Provider as PortalProvider } from '#/components/Portal'; +import { Provider as ActiveVideoProvider } from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'; +import { Provider as VideoVolumeProvider } from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'; +import { ToastOutlet } from '#/components/Toast'; +import { prefetchAgeAssuranceConfig, Provider as AgeAssuranceV2Provider, } from '#/ageAssurance'; +import { AnalyticsContext, AnalyticsFeaturesContext, features, setupDeviceId, } from '#/analytics'; +import { prefetchLiveEvents, Provider as LiveEventsProvider, } from '#/features/liveEvents/context'; +import * as Geo from '#/geolocation'; +import { Splash } from '#/Splash'; +import { BackgroundNotificationPreferencesProvider } from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'; +import { Provider as HideBottomBarBorderProvider } from './lib/hooks/useHideBottomBarBorder'; +/** + * Begin geolocation ASAP + */ +Geo.resolve(); +prefetchAgeAssuranceConfig(); +prefetchLiveEvents(); +prefetchAppConfig(); +function InnerApp() { + var _a = useState(false), isReady = _a[0], setIsReady = _a[1]; + var currentAccount = useSession().currentAccount; + var resumeSession = useSessionApi().resumeSession; + var theme = useColorModeTheme(); + var _ = useLingui()._; + var hasCheckedReferrer = useStarterPackEntry(); + // init + useEffect(function () { + function onLaunch(account) { + return __awaiter(this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 5, 6, 7]); + if (!account) return [3 /*break*/, 2]; + return [4 /*yield*/, resumeSession(account)]; + case 1: + _a.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, features.init]; + case 3: + _a.sent(); + _a.label = 4; + case 4: return [3 /*break*/, 7]; + case 5: + e_1 = _a.sent(); + logger.error("session: resumeSession failed", { message: e_1 }); + return [3 /*break*/, 7]; + case 6: + setIsReady(true); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); + } + var account = readLastActiveAccount(); + onLaunch(account); + }, [resumeSession]); + useEffect(function () { + return listenSessionDropped(function () { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sorry! Your session expired. Please sign in again."], ["Sorry! Your session expired. Please sign in again."])))), 'info'); + }); + }, [_]); + return (_jsx(Alf, { theme: theme, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(ContextMenuProvider, { children: _jsx(Splash, { isReady: isReady && hasCheckedReferrer, children: _jsx(VideoVolumeProvider, { children: _jsx(ActiveVideoProvider, { children: _jsx(Fragment + // Resets the entire tree below when it changes: + , { children: _jsx(AnalyticsFeaturesContext, { children: _jsx(QueryProvider, { currentDid: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, children: _jsx(PolicyUpdateOverlayProvider, { children: _jsx(LiveEventsProvider, { children: _jsx(AgeAssuranceV2Provider, { children: _jsx(ComposerProvider, { children: _jsx(MessagesProvider, { children: _jsx(LabelDefsProvider, { children: _jsx(ModerationOptsProvider, { children: _jsx(LoggedOutViewProvider, { children: _jsx(SelectedFeedProvider, { children: _jsx(HiddenRepliesProvider, { children: _jsx(HomeBadgeProvider, { children: _jsx(UnreadNotifsProvider, { children: _jsx(BackgroundNotificationPreferencesProvider, { children: _jsx(MutedThreadsProvider, { children: _jsx(SafeAreaProvider, { children: _jsx(ProgressGuideProvider, { children: _jsx(ServiceConfigProvider, { children: _jsx(EmailVerificationProvider, { children: _jsx(HideBottomBarBorderProvider, { children: _jsxs(IntentDialogProvider, { children: [_jsx(Shell, {}), _jsx(ToastOutlet, {})] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) }) }) }) }) }) })); +} +function App() { + var _a = useState(false), isReady = _a[0], setReady = _a[1]; + useEffect(function () { + Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(function () { + return setReady(true); + }); + }, []); + if (!isReady) { + return null; + } + /* + * NOTE: only nothing here can depend on other data or session state, since + * that is set up in the InnerApp component above. + */ + return (_jsx(Geo.Provider, { children: _jsx(AppConfigProvider, { children: _jsx(A11yProvider, { children: _jsx(OnboardingProvider, { children: _jsx(AnalyticsContext, { children: _jsx(SessionProvider, { children: _jsx(PrefsStateProvider, { children: _jsx(I18nProvider, { children: _jsx(ShellStateProvider, { children: _jsx(ModalStateProvider, { children: _jsx(DialogStateProvider, { children: _jsx(LightboxStateProvider, { children: _jsx(PortalProvider, { children: _jsx(StarterPackProvider, { children: _jsx(InnerApp, {}) }) }) }) }) }) }) }) }) }) }) }) }) }) })); +} +export default Sentry.wrap(App); +var templateObject_1; diff --git a/src/Navigation.js b/src/Navigation.js new file mode 100644 index 0000000000..a7776e4378 --- /dev/null +++ b/src/Navigation.js @@ -0,0 +1,599 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useRef } from 'react'; +import * as Linking from 'expo-linking'; +import * as Notifications from 'expo-notifications'; +import { i18n } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { createBottomTabNavigator, } from '@react-navigation/bottom-tabs'; +import { CommonActions, createNavigationContainerRef, DarkTheme, DefaultTheme, NavigationContainer, StackActions, } from '@react-navigation/native'; +import { timeout } from '#/lib/async/timeout'; +import { useAccountSwitcher } from '#/lib/hooks/useAccountSwitcher'; +import { useColorSchemeStyle } from '#/lib/hooks/useColorSchemeStyle'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { getNotificationPayload, notificationToURL, storePayloadForAccountSwitch, } from '#/lib/hooks/useNotificationHandler'; +import { useWebScrollRestoration } from '#/lib/hooks/useWebScrollRestoration'; +import { useCallOnce } from '#/lib/once'; +import { buildStateObject } from '#/lib/routes/helpers'; +import { bskyTitle } from '#/lib/strings/headings'; +import { useUnreadNotifications } from '#/state/queries/notifications/unread'; +import { useSession } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { shouldRequestEmailConfirmation, snoozeEmailConfirmationPrompt, } from '#/state/shell/reminders'; +import { useCloseAllActiveElements } from '#/state/util'; +import { CommunityGuidelinesScreen } from '#/view/screens/CommunityGuidelines'; +import { CopyrightPolicyScreen } from '#/view/screens/CopyrightPolicy'; +import { DebugModScreen } from '#/view/screens/DebugMod'; +import { FeedsScreen } from '#/view/screens/Feeds'; +import { HomeScreen } from '#/view/screens/Home'; +import { ListsScreen } from '#/view/screens/Lists'; +import { ModerationBlockedAccounts } from '#/view/screens/ModerationBlockedAccounts'; +import { ModerationModlistsScreen } from '#/view/screens/ModerationModlists'; +import { ModerationMutedAccounts } from '#/view/screens/ModerationMutedAccounts'; +import { NotFoundScreen } from '#/view/screens/NotFound'; +import { NotificationsScreen } from '#/view/screens/Notifications'; +import { PostThreadScreen } from '#/view/screens/PostThread'; +import { PrivacyPolicyScreen } from '#/view/screens/PrivacyPolicy'; +import { ProfileScreen } from '#/view/screens/Profile'; +import { ProfileFeedLikedByScreen } from '#/view/screens/ProfileFeedLikedBy'; +import { StorybookScreen } from '#/view/screens/Storybook'; +import { SupportScreen } from '#/view/screens/Support'; +import { TermsOfServiceScreen } from '#/view/screens/TermsOfService'; +import { BottomBar } from '#/view/shell/bottom-bar/BottomBar'; +import { createNativeStackNavigatorWithAuth } from '#/view/shell/createNativeStackNavigatorWithAuth'; +import { BookmarksScreen } from '#/screens/Bookmarks'; +import { SharedPreferencesTesterScreen } from '#/screens/E2E/SharedPreferencesTesterScreen'; +import { FindContactsFlowScreen } from '#/screens/FindContactsFlowScreen'; +import HashtagScreen from '#/screens/Hashtag'; +import { LogScreen } from '#/screens/Log'; +import { MessagesScreen } from '#/screens/Messages/ChatList'; +import { MessagesConversationScreen } from '#/screens/Messages/Conversation'; +import { MessagesInboxScreen } from '#/screens/Messages/Inbox'; +import { MessagesSettingsScreen } from '#/screens/Messages/Settings'; +import { ModerationScreen } from '#/screens/Moderation'; +import { Screen as ModerationVerificationSettings } from '#/screens/Moderation/VerificationSettings'; +import { Screen as ModerationInteractionSettings } from '#/screens/ModerationInteractionSettings'; +import { NotificationsActivityListScreen } from '#/screens/Notifications/ActivityList'; +import { PostLikedByScreen } from '#/screens/Post/PostLikedBy'; +import { PostQuotesScreen } from '#/screens/Post/PostQuotes'; +import { PostRepostedByScreen } from '#/screens/Post/PostRepostedBy'; +import { ProfileKnownFollowersScreen } from '#/screens/Profile/KnownFollowers'; +import { ProfileFeedScreen } from '#/screens/Profile/ProfileFeed'; +import { ProfileFollowersScreen } from '#/screens/Profile/ProfileFollowers'; +import { ProfileFollowsScreen } from '#/screens/Profile/ProfileFollows'; +import { ProfileLabelerLikedByScreen } from '#/screens/Profile/ProfileLabelerLikedBy'; +import { ProfileSearchScreen } from '#/screens/Profile/ProfileSearch'; +import { ProfileListScreen } from '#/screens/ProfileList'; +import { SavedFeeds } from '#/screens/SavedFeeds'; +import { SearchScreen } from '#/screens/Search'; +import { AboutSettingsScreen } from '#/screens/Settings/AboutSettings'; +import { AccessibilitySettingsScreen } from '#/screens/Settings/AccessibilitySettings'; +import { AccountSettingsScreen } from '#/screens/Settings/AccountSettings'; +import { ActivityPrivacySettingsScreen } from '#/screens/Settings/ActivityPrivacySettings'; +import { AppearanceSettingsScreen } from '#/screens/Settings/AppearanceSettings'; +import { AppIconSettingsScreen } from '#/screens/Settings/AppIconSettings'; +import { AppPasswordsScreen } from '#/screens/Settings/AppPasswords'; +import { ContentAndMediaSettingsScreen } from '#/screens/Settings/ContentAndMediaSettings'; +import { ExternalMediaPreferencesScreen } from '#/screens/Settings/ExternalMediaPreferences'; +import { FindContactsSettingsScreen } from '#/screens/Settings/FindContactsSettings'; +import { FollowingFeedPreferencesScreen } from '#/screens/Settings/FollowingFeedPreferences'; +import { InterestsSettingsScreen } from '#/screens/Settings/InterestsSettings'; +import { LanguageSettingsScreen } from '#/screens/Settings/LanguageSettings'; +import { LegacyNotificationSettingsScreen } from '#/screens/Settings/LegacyNotificationSettings'; +import { NotificationSettingsScreen } from '#/screens/Settings/NotificationSettings'; +import { ActivityNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings'; +import { LikeNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/LikeNotificationSettings'; +import { LikesOnRepostsNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings'; +import { MentionNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/MentionNotificationSettings'; +import { MiscellaneousNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings'; +import { NewFollowerNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings'; +import { QuoteNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings'; +import { ReplyNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings'; +import { RepostNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/RepostNotificationSettings'; +import { RepostsOnRepostsNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings'; +import { PrivacyAndSecuritySettingsScreen } from '#/screens/Settings/PrivacyAndSecuritySettings'; +import { SettingsScreen } from '#/screens/Settings/Settings'; +import { ThreadPreferencesScreen } from '#/screens/Settings/ThreadPreferences'; +import { StarterPackScreen, StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen'; +import { Wizard } from '#/screens/StarterPack/Wizard'; +import TopicScreen from '#/screens/Topic'; +import { VideoFeed } from '#/screens/VideoFeed'; +import { useTheme } from '#/alf'; +import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog'; +import { useAnalytics } from '#/analytics'; +import { setNavigationMetadata } from '#/analytics/metadata'; +import { IS_LIQUID_GLASS, IS_NATIVE, IS_WEB } from '#/env'; +import { router } from '#/routes'; +import { Referrer } from '../modules/expo-bluesky-swiss-army'; +var navigationRef = createNavigationContainerRef(); +var HomeTab = createNativeStackNavigatorWithAuth(); +var SearchTab = createNativeStackNavigatorWithAuth(); +var NotificationsTab = createNativeStackNavigatorWithAuth(); +var MyProfileTab = createNativeStackNavigatorWithAuth(); +var MessagesTab = createNativeStackNavigatorWithAuth(); +var Flat = createNativeStackNavigatorWithAuth(); +var Tab = createBottomTabNavigator(); +/** + * These "common screens" are reused across stacks. + */ +function commonScreens(Stack, unreadCountLabel) { + var title = function (page) { + return bskyTitle(i18n._(page), unreadCountLabel); + }; + return (_jsxs(_Fragment, { children: [_jsx(Stack.Screen, { name: "NotFound", getComponent: function () { return NotFoundScreen; }, options: { title: title(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Not Found"], ["Not Found"])))) } }), _jsx(Stack.Screen, { name: "Lists", component: ListsScreen, options: { title: title(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Lists"], ["Lists"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Moderation", getComponent: function () { return ModerationScreen; }, options: { title: title(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Moderation"], ["Moderation"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationModlists", getComponent: function () { return ModerationModlistsScreen; }, options: { title: title(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Moderation Lists"], ["Moderation Lists"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationMutedAccounts", getComponent: function () { return ModerationMutedAccounts; }, options: { title: title(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Muted Accounts"], ["Muted Accounts"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationBlockedAccounts", getComponent: function () { return ModerationBlockedAccounts; }, options: { title: title(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Blocked Accounts"], ["Blocked Accounts"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationInteractionSettings", getComponent: function () { return ModerationInteractionSettings; }, options: { + title: title(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Post Interaction Settings"], ["Post Interaction Settings"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "ModerationVerificationSettings", getComponent: function () { return ModerationVerificationSettings; }, options: { + title: title(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Verification Settings"], ["Verification Settings"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "Settings", getComponent: function () { return SettingsScreen; }, options: { title: title(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Settings"], ["Settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "LanguageSettings", getComponent: function () { return LanguageSettingsScreen; }, options: { title: title(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Language Settings"], ["Language Settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Profile", getComponent: function () { return ProfileScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: bskyTitle("@".concat(route.params.name), unreadCountLabel), + }); + } }), _jsx(Stack.Screen, { name: "ProfileFollowers", getComponent: function () { return ProfileFollowersScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["People following @", ""], ["People following @", ""])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "ProfileFollows", getComponent: function () { return ProfileFollowsScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["People followed by @", ""], ["People followed by @", ""])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "ProfileKnownFollowers", getComponent: function () { return ProfileKnownFollowersScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Followers of @", " that you know"], ["Followers of @", " that you know"])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "ProfileList", getComponent: function () { return ProfileListScreen; }, options: { title: title(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["List"], ["List"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ProfileSearch", getComponent: function () { return ProfileSearchScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Search @", "'s posts"], ["Search @", "'s posts"])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "PostThread", getComponent: function () { return PostThreadScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "PostLikedBy", getComponent: function () { return PostLikedByScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "PostRepostedBy", getComponent: function () { return PostRepostedByScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "PostQuotes", getComponent: function () { return PostQuotesScreen; }, options: function (_a) { + var route = _a.route; + return ({ + title: title(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)), + }); + } }), _jsx(Stack.Screen, { name: "ProfileFeed", getComponent: function () { return ProfileFeedScreen; }, options: { title: title(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Feed"], ["Feed"])))) } }), _jsx(Stack.Screen, { name: "ProfileFeedLikedBy", getComponent: function () { return ProfileFeedLikedByScreen; }, options: { title: title(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Liked by"], ["Liked by"])))) } }), _jsx(Stack.Screen, { name: "ProfileLabelerLikedBy", getComponent: function () { return ProfileLabelerLikedByScreen; }, options: { title: title(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Liked by"], ["Liked by"])))) } }), _jsx(Stack.Screen, { name: "Debug", getComponent: function () { return StorybookScreen; }, options: { title: title(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Storybook"], ["Storybook"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "DebugMod", getComponent: function () { return DebugModScreen; }, options: { title: title(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Moderation states"], ["Moderation states"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "SharedPreferencesTester", getComponent: function () { return SharedPreferencesTesterScreen; }, options: { title: title(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Shared Preferences Tester"], ["Shared Preferences Tester"])))) } }), _jsx(Stack.Screen, { name: "Log", getComponent: function () { return LogScreen; }, options: { title: title(msg(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Log"], ["Log"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Support", getComponent: function () { return SupportScreen; }, options: { title: title(msg(templateObject_27 || (templateObject_27 = __makeTemplateObject(["Support"], ["Support"])))) } }), _jsx(Stack.Screen, { name: "PrivacyPolicy", getComponent: function () { return PrivacyPolicyScreen; }, options: { title: title(msg(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Privacy Policy"], ["Privacy Policy"])))) } }), _jsx(Stack.Screen, { name: "TermsOfService", getComponent: function () { return TermsOfServiceScreen; }, options: { title: title(msg(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Terms of Service"], ["Terms of Service"])))) } }), _jsx(Stack.Screen, { name: "CommunityGuidelines", getComponent: function () { return CommunityGuidelinesScreen; }, options: { title: title(msg(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Community Guidelines"], ["Community Guidelines"])))) } }), _jsx(Stack.Screen, { name: "CopyrightPolicy", getComponent: function () { return CopyrightPolicyScreen; }, options: { title: title(msg(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Copyright Policy"], ["Copyright Policy"])))) } }), _jsx(Stack.Screen, { name: "AppPasswords", getComponent: function () { return AppPasswordsScreen; }, options: { title: title(msg(templateObject_32 || (templateObject_32 = __makeTemplateObject(["App Passwords"], ["App Passwords"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "SavedFeeds", getComponent: function () { return SavedFeeds; }, options: { title: title(msg(templateObject_33 || (templateObject_33 = __makeTemplateObject(["Edit My Feeds"], ["Edit My Feeds"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "PreferencesFollowingFeed", getComponent: function () { return FollowingFeedPreferencesScreen; }, options: { + title: title(msg(templateObject_34 || (templateObject_34 = __makeTemplateObject(["Following Feed Preferences"], ["Following Feed Preferences"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "PreferencesThreads", getComponent: function () { return ThreadPreferencesScreen; }, options: { title: title(msg(templateObject_35 || (templateObject_35 = __makeTemplateObject(["Threads Preferences"], ["Threads Preferences"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "PreferencesExternalEmbeds", getComponent: function () { return ExternalMediaPreferencesScreen; }, options: { + title: title(msg(templateObject_36 || (templateObject_36 = __makeTemplateObject(["External Media Preferences"], ["External Media Preferences"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "AccessibilitySettings", getComponent: function () { return AccessibilitySettingsScreen; }, options: { + title: title(msg(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Accessibility Settings"], ["Accessibility Settings"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "AppearanceSettings", getComponent: function () { return AppearanceSettingsScreen; }, options: { + title: title(msg(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Appearance"], ["Appearance"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "AccountSettings", getComponent: function () { return AccountSettingsScreen; }, options: { + title: title(msg(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Account"], ["Account"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "PrivacyAndSecuritySettings", getComponent: function () { return PrivacyAndSecuritySettingsScreen; }, options: { + title: title(msg(templateObject_40 || (templateObject_40 = __makeTemplateObject(["Privacy and Security"], ["Privacy and Security"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "ActivityPrivacySettings", getComponent: function () { return ActivityPrivacySettingsScreen; }, options: { + title: title(msg(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Privacy and Security"], ["Privacy and Security"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "FindContactsSettings", getComponent: function () { return FindContactsSettingsScreen; }, options: { + title: title(msg(templateObject_42 || (templateObject_42 = __makeTemplateObject(["Find Contacts"], ["Find Contacts"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "NotificationSettings", getComponent: function () { return NotificationSettingsScreen; }, options: { title: title(msg(templateObject_43 || (templateObject_43 = __makeTemplateObject(["Notification settings"], ["Notification settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ReplyNotificationSettings", getComponent: function () { return ReplyNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_44 || (templateObject_44 = __makeTemplateObject(["Reply notifications"], ["Reply notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "MentionNotificationSettings", getComponent: function () { return MentionNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_45 || (templateObject_45 = __makeTemplateObject(["Mention notifications"], ["Mention notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "QuoteNotificationSettings", getComponent: function () { return QuoteNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_46 || (templateObject_46 = __makeTemplateObject(["Quote notifications"], ["Quote notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "LikeNotificationSettings", getComponent: function () { return LikeNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_47 || (templateObject_47 = __makeTemplateObject(["Like notifications"], ["Like notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "RepostNotificationSettings", getComponent: function () { return RepostNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_48 || (templateObject_48 = __makeTemplateObject(["Repost notifications"], ["Repost notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "NewFollowerNotificationSettings", getComponent: function () { return NewFollowerNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_49 || (templateObject_49 = __makeTemplateObject(["New follower notifications"], ["New follower notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "LikesOnRepostsNotificationSettings", getComponent: function () { return LikesOnRepostsNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_50 || (templateObject_50 = __makeTemplateObject(["Likes of your reposts notifications"], ["Likes of your reposts notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "RepostsOnRepostsNotificationSettings", getComponent: function () { return RepostsOnRepostsNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_51 || (templateObject_51 = __makeTemplateObject(["Reposts of your reposts notifications"], ["Reposts of your reposts notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "ActivityNotificationSettings", getComponent: function () { return ActivityNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_52 || (templateObject_52 = __makeTemplateObject(["Activity notifications"], ["Activity notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "MiscellaneousNotificationSettings", getComponent: function () { return MiscellaneousNotificationSettingsScreen; }, options: { + title: title(msg(templateObject_53 || (templateObject_53 = __makeTemplateObject(["Miscellaneous notifications"], ["Miscellaneous notifications"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "ContentAndMediaSettings", getComponent: function () { return ContentAndMediaSettingsScreen; }, options: { + title: title(msg(templateObject_54 || (templateObject_54 = __makeTemplateObject(["Content and Media"], ["Content and Media"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "InterestsSettings", getComponent: function () { return InterestsSettingsScreen; }, options: { + title: title(msg(templateObject_55 || (templateObject_55 = __makeTemplateObject(["Your interests"], ["Your interests"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "AboutSettings", getComponent: function () { return AboutSettingsScreen; }, options: { + title: title(msg(templateObject_56 || (templateObject_56 = __makeTemplateObject(["About"], ["About"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "AppIconSettings", getComponent: function () { return AppIconSettingsScreen; }, options: { + title: title(msg(templateObject_57 || (templateObject_57 = __makeTemplateObject(["App Icon"], ["App Icon"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "Hashtag", getComponent: function () { return HashtagScreen; }, options: { title: title(msg(templateObject_58 || (templateObject_58 = __makeTemplateObject(["Hashtag"], ["Hashtag"])))) } }), _jsx(Stack.Screen, { name: "Topic", getComponent: function () { return TopicScreen; }, options: { title: title(msg(templateObject_59 || (templateObject_59 = __makeTemplateObject(["Topic"], ["Topic"])))) } }), _jsx(Stack.Screen, { name: "MessagesConversation", getComponent: function () { return MessagesConversationScreen; }, options: { title: title(msg(templateObject_60 || (templateObject_60 = __makeTemplateObject(["Chat"], ["Chat"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "MessagesSettings", getComponent: function () { return MessagesSettingsScreen; }, options: { title: title(msg(templateObject_61 || (templateObject_61 = __makeTemplateObject(["Chat settings"], ["Chat settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "MessagesInbox", getComponent: function () { return MessagesInboxScreen; }, options: { title: title(msg(templateObject_62 || (templateObject_62 = __makeTemplateObject(["Chat request inbox"], ["Chat request inbox"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "NotificationsActivityList", getComponent: function () { return NotificationsActivityListScreen; }, options: { title: title(msg(templateObject_63 || (templateObject_63 = __makeTemplateObject(["Notifications"], ["Notifications"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "LegacyNotificationSettings", getComponent: function () { return LegacyNotificationSettingsScreen; }, options: { title: title(msg(templateObject_64 || (templateObject_64 = __makeTemplateObject(["Notification settings"], ["Notification settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Feeds", getComponent: function () { return FeedsScreen; }, options: { title: title(msg(templateObject_65 || (templateObject_65 = __makeTemplateObject(["Feeds"], ["Feeds"])))) } }), _jsx(Stack.Screen, { name: "StarterPack", getComponent: function () { return StarterPackScreen; }, options: { title: title(msg(templateObject_66 || (templateObject_66 = __makeTemplateObject(["Starter Pack"], ["Starter Pack"])))) } }), _jsx(Stack.Screen, { name: "StarterPackShort", getComponent: function () { return StarterPackScreenShort; }, options: { title: title(msg(templateObject_67 || (templateObject_67 = __makeTemplateObject(["Starter Pack"], ["Starter Pack"])))) } }), _jsx(Stack.Screen, { name: "StarterPackWizard", getComponent: function () { return Wizard; }, options: { title: title(msg(templateObject_68 || (templateObject_68 = __makeTemplateObject(["Create a starter pack"], ["Create a starter pack"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "StarterPackEdit", getComponent: function () { return Wizard; }, options: { title: title(msg(templateObject_69 || (templateObject_69 = __makeTemplateObject(["Edit your starter pack"], ["Edit your starter pack"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "VideoFeed", getComponent: function () { return VideoFeed; }, options: { + title: title(msg(templateObject_70 || (templateObject_70 = __makeTemplateObject(["Video Feed"], ["Video Feed"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "Bookmarks", getComponent: function () { return BookmarksScreen; }, options: { + title: title(msg(templateObject_71 || (templateObject_71 = __makeTemplateObject(["Saved Posts"], ["Saved Posts"])))), + requireAuth: true, + } }), _jsx(Stack.Screen, { name: "FindContactsFlow", getComponent: function () { return FindContactsFlowScreen; }, options: { + title: title(msg(templateObject_72 || (templateObject_72 = __makeTemplateObject(["Find Contacts"], ["Find Contacts"])))), + requireAuth: true, + gestureEnabled: false, + } })] })); +} +/** + * The TabsNavigator is used by native mobile to represent the routes + * in 3 distinct tab-stacks with a different root screen on each. + */ +function TabsNavigator(_a) { + var layout = _a.layout; + var tabBar = useCallback(function (props) { return (_jsx(BottomBar, __assign({}, props))); }, []); + return (_jsxs(Tab.Navigator, { initialRouteName: "HomeTab", backBehavior: "initialRoute", screenOptions: { headerShown: false, lazy: true }, tabBar: tabBar, layout: layout, children: [_jsx(Tab.Screen, { name: "HomeTab", getComponent: function () { return HomeTabNavigator; } }), _jsx(Tab.Screen, { name: "SearchTab", getComponent: function () { return SearchTabNavigator; } }), _jsx(Tab.Screen, { name: "MessagesTab", getComponent: function () { return MessagesTabNavigator; } }), _jsx(Tab.Screen, { name: "NotificationsTab", getComponent: function () { return NotificationsTabNavigator; } }), _jsx(Tab.Screen, { name: "MyProfileTab", getComponent: function () { return MyProfileTabNavigator; } })] })); +} +function screenOptions(t) { + return { + fullScreenGestureEnabled: true, + headerShown: false, + contentStyle: t.atoms.bg, + }; +} +function HomeTabNavigator() { + var t = useTheme(); + var BLURRED_SCROLL_EDGE_EFFECT = IS_LIQUID_GLASS + ? { + headerShown: true, + headerTransparent: true, + headerTitle: '', + scrollEdgeEffects: { + top: 'soft', + }, + } + : {}; + return (_jsxs(HomeTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Home", children: [_jsx(HomeTab.Screen, { name: "Home", getComponent: function () { return HomeScreen; }, options: BLURRED_SCROLL_EDGE_EFFECT }), _jsx(HomeTab.Screen, { name: "Start", getComponent: function () { return HomeScreen; }, options: BLURRED_SCROLL_EDGE_EFFECT }), commonScreens(HomeTab)] })); +} +function SearchTabNavigator() { + var t = useTheme(); + return (_jsxs(SearchTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Search", children: [_jsx(SearchTab.Screen, { name: "Search", getComponent: function () { return SearchScreen; } }), commonScreens(SearchTab)] })); +} +function NotificationsTabNavigator() { + var t = useTheme(); + return (_jsxs(NotificationsTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Notifications", children: [_jsx(NotificationsTab.Screen, { name: "Notifications", getComponent: function () { return NotificationsScreen; }, options: { requireAuth: true } }), commonScreens(NotificationsTab)] })); +} +function MyProfileTabNavigator() { + var t = useTheme(); + return (_jsxs(MyProfileTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "MyProfile", children: [_jsx(MyProfileTab.Screen + // MyProfile is not in AllNavigationParams - asserting as Profile at least + // gives us typechecking for initialParams -sfn + , { + // MyProfile is not in AllNavigationParams - asserting as Profile at least + // gives us typechecking for initialParams -sfn + name: 'MyProfile', getComponent: function () { return ProfileScreen; }, initialParams: { name: 'me', hideBackButton: true } }), commonScreens(MyProfileTab)] })); +} +function MessagesTabNavigator() { + var t = useTheme(); + return (_jsxs(MessagesTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Messages", children: [_jsx(MessagesTab.Screen, { name: "Messages", getComponent: function () { return MessagesScreen; }, options: function (_a) { + var _b, _c; + var route = _a.route; + return ({ + requireAuth: true, + animationTypeForReplace: (_c = (_b = route.params) === null || _b === void 0 ? void 0 : _b.animation) !== null && _c !== void 0 ? _c : 'push', + }); + } }), commonScreens(MessagesTab)] })); +} +/** + * The FlatNavigator is used by Web to represent the routes + * in a single ("flat") stack. + */ +var FlatNavigator = function (_a) { + var layout = _a.layout; + var t = useTheme(); + var numUnread = useUnreadNotifications(); + var screenListeners = useWebScrollRestoration(); + var title = function (page) { return bskyTitle(i18n._(page), numUnread); }; + return (_jsxs(Flat.Navigator, { layout: layout, screenListeners: screenListeners, screenOptions: screenOptions(t), children: [_jsx(Flat.Screen, { name: "Home", getComponent: function () { return HomeScreen; }, options: { title: title(msg(templateObject_73 || (templateObject_73 = __makeTemplateObject(["Home"], ["Home"])))) } }), _jsx(Flat.Screen, { name: "Search", getComponent: function () { return SearchScreen; }, options: { title: title(msg(templateObject_74 || (templateObject_74 = __makeTemplateObject(["Explore"], ["Explore"])))) } }), _jsx(Flat.Screen, { name: "Notifications", getComponent: function () { return NotificationsScreen; }, options: { title: title(msg(templateObject_75 || (templateObject_75 = __makeTemplateObject(["Notifications"], ["Notifications"])))), requireAuth: true } }), _jsx(Flat.Screen, { name: "Messages", getComponent: function () { return MessagesScreen; }, options: { title: title(msg(templateObject_76 || (templateObject_76 = __makeTemplateObject(["Messages"], ["Messages"])))), requireAuth: true } }), _jsx(Flat.Screen, { name: "Start", getComponent: function () { return HomeScreen; }, options: { title: title(msg(templateObject_77 || (templateObject_77 = __makeTemplateObject(["Home"], ["Home"])))) } }), commonScreens(Flat, numUnread)] })); +}; +/** + * The RoutesContainer should wrap all components which need access + * to the navigation context. + */ +var LINKING = { + // TODO figure out what we are going to use + // note: `bluesky://` is what is used in app.config.js + prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'], + getPathFromState: function (state) { + var _a, _b, _c, _d; + // find the current node in the navigation tree + var node = state.routes[state.index || 0]; + while (((_a = node.state) === null || _a === void 0 ? void 0 : _a.routes) && typeof ((_b = node.state) === null || _b === void 0 ? void 0 : _b.index) === 'number') { + node = (_c = node.state) === null || _c === void 0 ? void 0 : _c.routes[(_d = node.state) === null || _d === void 0 ? void 0 : _d.index]; + } + // build the path + var route = router.matchName(node.name); + if (typeof route === 'undefined') { + return '/'; // default to home + } + return route.build((node.params || {})); + }, + getStateFromPath: function (path) { + var _a = router.matchPath(path), name = _a[0], params = _a[1]; + // Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the + // intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid + // intent + // On web, there is no route state that's created by default, so we should initialize it as the home route. On + // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state + // since it will be created by react-navigation. + if (path.includes('intent/')) { + if (IS_NATIVE) + return; + return buildStateObject('Flat', 'Home', params); + } + if (IS_NATIVE) { + if (name === 'Search') { + return buildStateObject('SearchTab', 'Search', params); + } + if (name === 'Notifications') { + return buildStateObject('NotificationsTab', 'Notifications', params); + } + if (name === 'Home') { + return buildStateObject('HomeTab', 'Home', params); + } + if (name === 'Messages') { + return buildStateObject('MessagesTab', 'Messages', params); + } + // if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work + return buildStateObject('HomeTab', name, params, [ + { + name: 'Home', + params: {}, + }, + ]); + } + else { + var res = buildStateObject('Flat', name, params); + return res; + } + }, +}; +function RoutesContainer(_a) { + var children = _a.children; + var ax = useAnalytics(); + var notyLogger = ax.logger.useChild(ax.logger.Context.Notifications); + var theme = useColorSchemeStyle(DefaultTheme, DarkTheme); + var _b = useSession(), currentAccount = _b.currentAccount, accounts = _b.accounts; + var onPressSwitchAccount = useAccountSwitcher().onPressSwitchAccount; + var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut; + var previousScreen = useRef(undefined); + var emailDialogControl = useEmailDialogControl(); + var closeAllActiveElements = useCloseAllActiveElements(); + var linkingUrl = Linking.useLinkingURL(); + /** + * Handle navigation to a conversation, or prepares for account switch. + * + * Non-reactive because we need the latest data from some hooks + * after an async call - sfn + */ + var handleChatMessage = useNonReactiveCallback(function (payload) { + notyLogger.debug("handleChatMessage", { payload: payload }); + if (payload.recipientDid !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + // handled in useNotificationHandler after account switch finishes + storePayloadForAccountSwitch(payload); + closeAllActiveElements(); + var account = accounts.find(function (a) { return a.did === payload.recipientDid; }); + if (account) { + onPressSwitchAccount(account, 'Notification'); + } + else { + setShowLoggedOut(true); + } + } + else { + // @ts-expect-error nested navigators aren't typed -sfn + navigate('MessagesTab', { + screen: 'Messages', + params: { + pushToConversation: payload.convoId, + }, + }); + } + }); + function handlePushNotificationEntry() { + if (!IS_NATIVE) + return; + // intent urls are handled by `useIntentHandler` + if (linkingUrl) + return; + var notificationResponse = Notifications.getLastNotificationResponse(); + if (notificationResponse) { + notyLogger.debug("handlePushNotificationEntry: response", { + response: notificationResponse, + }); + // Clear the last notification response to ensure it's not used again + try { + Notifications.clearLastNotificationResponse(); + } + catch (error) { + notyLogger.error("handlePushNotificationEntry: error clearing notification response", { error: error }); + } + var payload = getNotificationPayload(notificationResponse.notification); + if (payload) { + ax.metric('notifications:openApp', { + reason: payload.reason, + causedBoot: true, + }); + if (payload.reason === 'chat-message') { + handleChatMessage(payload); + } + else { + var path = notificationToURL(payload); + if (path === '/notifications') { + resetToTab('NotificationsTab'); + notyLogger.debug("handlePushNotificationEntry: default navigate"); + } + else if (path) { + var _a = router.matchPath(path), screen_1 = _a[0], params = _a[1]; + // @ts-expect-error nested navigators aren't typed -sfn + navigate('HomeTab', { screen: screen_1, params: params }); + notyLogger.debug("handlePushNotificationEntry: navigate", { + screen: screen_1, + params: params, + }); + } + } + } + } + } + var onNavigationReady = useCallOnce(function () { + var currentScreen = getCurrentRouteName(); + setNavigationMetadata({ + previousScreen: currentScreen, + currentScreen: currentScreen, + }); + previousScreen.current = currentScreen; + handlePushNotificationEntry(); + ax.metric('router:navigate', {}); + if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { + emailDialogControl.open({ + id: EmailDialogScreenID.VerificationReminder, + }); + snoozeEmailConfirmationPrompt(); + } + ax.metric('init', { + initMs: Math.round( + // @ts-ignore Emitted by Metro in the bundle prelude + performance.now() - global.__BUNDLE_START_TIME__), + }); + if (IS_WEB) { + var referrerInfo = Referrer.getReferrerInfo(); + if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { + ax.metric('deepLink:referrerReceived', { + to: window.location.href, + referrer: referrerInfo === null || referrerInfo === void 0 ? void 0 : referrerInfo.referrer, + hostname: referrerInfo === null || referrerInfo === void 0 ? void 0 : referrerInfo.hostname, + }); + } + } + // temp, just testing + void ax.features.enabled(ax.features.AATest); + }); + return (_jsx(NavigationContainer, { ref: navigationRef, linking: LINKING, theme: theme, onStateChange: function () { + var currentScreen = getCurrentRouteName(); + // do this before metric + setNavigationMetadata({ + previousScreen: previousScreen.current, + currentScreen: currentScreen, + }); + ax.metric('router:navigate', { from: previousScreen.current }); + previousScreen.current = currentScreen; + }, onReady: onNavigationReady, + // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x + // However, there's a fair amount of places we do that, especially in when popping to the top of stacks. + // See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly. + // I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now. + // We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x + // -sfn + navigationInChildEnabled: true, children: children })); +} +function getCurrentRouteName() { + var _a; + if (navigationRef.isReady()) { + return (_a = navigationRef.getCurrentRoute()) === null || _a === void 0 ? void 0 : _a.name; + } + else { + return undefined; + } +} +/** + * These helpers can be used from outside of the RoutesContainer + * (eg in the state models). + */ +function navigate(name, params) { + if (navigationRef.isReady()) { + return Promise.race([ + new Promise(function (resolve) { + var handler = function () { + resolve(); + navigationRef.removeListener('state', handler); + }; + navigationRef.addListener('state', handler); + // @ts-ignore I dont know what would make typescript happy but I have a life -prf + navigationRef.navigate(name, params); + }), + timeout(1e3), + ]); + } + return Promise.resolve(); +} +function resetToTab(tabName) { + if (navigationRef.isReady()) { + navigate(tabName); + if (navigationRef.canGoBack()) { + navigationRef.dispatch(StackActions.popToTop()); //we need to check .canGoBack() before calling it + } + } +} +// returns a promise that resolves after the state reset is complete +function reset() { + if (navigationRef.isReady()) { + navigationRef.dispatch(CommonActions.reset({ + index: 0, + routes: [{ name: IS_NATIVE ? 'HomeTab' : 'Home' }], + })); + return Promise.race([ + timeout(1e3), + new Promise(function (resolve) { + var handler = function () { + resolve(); + navigationRef.removeListener('state', handler); + }; + navigationRef.addListener('state', handler); + }), + ]); + } + else { + return Promise.resolve(); + } +} +export { FlatNavigator, navigate, reset, resetToTab, RoutesContainer, TabsNavigator, }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26, templateObject_27, templateObject_28, templateObject_29, templateObject_30, templateObject_31, templateObject_32, templateObject_33, templateObject_34, templateObject_35, templateObject_36, templateObject_37, templateObject_38, templateObject_39, templateObject_40, templateObject_41, templateObject_42, templateObject_43, templateObject_44, templateObject_45, templateObject_46, templateObject_47, templateObject_48, templateObject_49, templateObject_50, templateObject_51, templateObject_52, templateObject_53, templateObject_54, templateObject_55, templateObject_56, templateObject_57, templateObject_58, templateObject_59, templateObject_60, templateObject_61, templateObject_62, templateObject_63, templateObject_64, templateObject_65, templateObject_66, templateObject_67, templateObject_68, templateObject_69, templateObject_70, templateObject_71, templateObject_72, templateObject_73, templateObject_74, templateObject_75, templateObject_76, templateObject_77; diff --git a/src/Splash.android.js b/src/Splash.android.js new file mode 100644 index 0000000000..a96e426acd --- /dev/null +++ b/src/Splash.android.js @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import * as SplashScreen from 'expo-splash-screen'; +export function Splash(_a) { + var isReady = _a.isReady, children = _a.children; + useEffect(function () { + if (isReady) { + SplashScreen.hideAsync(); + } + }, [isReady]); + if (isReady) { + return children; + } +} diff --git a/src/Splash.js b/src/Splash.js new file mode 100644 index 0000000000..085dc89235 --- /dev/null +++ b/src/Splash.js @@ -0,0 +1,144 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useCallback, useEffect } from 'react'; +import { AccessibilityInfo, Image as RNImage, StyleSheet, useColorScheme, View, } from 'react-native'; +import Animated, { Easing, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import Svg, { Path } from 'react-native-svg'; +import { Image } from 'expo-image'; +import * as SplashScreen from 'expo-splash-screen'; +import { Logotype } from '#/view/icons/Logotype'; +// @ts-ignore +import splashImagePointer from '../assets/splash/splash.png'; +// @ts-ignore +import darkSplashImagePointer from '../assets/splash/splash-dark.png'; +var splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri; +var darkSplashImageUri = RNImage.resolveAssetSource(darkSplashImagePointer).uri; +export var Logo = React.forwardRef(function LogoImpl(props, ref) { + var width = 1000; + var height = width * (67 / 64); + return (_jsx(Svg, { fill: "none", + // @ts-ignore it's fiiiiine + ref: ref, viewBox: "0 0 64 66", style: [{ width: width, height: height }, props.style], children: _jsx(Path, { fill: props.fill || '#fff', d: "M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z" }) })); +}); +export function Splash(props) { + 'use no memo'; + var insets = useSafeAreaInsets(); + var intro = useSharedValue(0); + var outroLogo = useSharedValue(0); + var outroApp = useSharedValue(0); + var outroAppOpacity = useSharedValue(0); + var _a = React.useState(false), isAnimationComplete = _a[0], setIsAnimationComplete = _a[1]; + var _b = React.useState(false), isImageLoaded = _b[0], setIsImageLoaded = _b[1]; + var _c = React.useState(false), isLayoutReady = _c[0], setIsLayoutReady = _c[1]; + var _d = React.useState(false), reduceMotion = _d[0], setReduceMotion = _d[1]; + var isReady = props.isReady && + isImageLoaded && + isLayoutReady && + reduceMotion !== undefined; + var colorScheme = useColorScheme(); + var isDarkMode = colorScheme === 'dark'; + var logoAnimation = useAnimatedStyle(function () { + return { + transform: [ + { + scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), + }, + { + scale: interpolate(outroLogo.get(), [0, 0.08, 1], [1, 0.8, 500], 'clamp'), + }, + ], + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), + }; + }); + var bottomLogoAnimation = useAnimatedStyle(function () { + return { + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), + }; + }); + var reducedLogoAnimation = useAnimatedStyle(function () { + return { + transform: [ + { + scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), + }, + ], + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), + }; + }); + var logoWrapperAnimation = useAnimatedStyle(function () { + return { + opacity: interpolate(outroAppOpacity.get(), [0, 0.1, 0.2, 1], [1, 1, 0, 0], 'clamp'), + }; + }); + var appAnimation = useAnimatedStyle(function () { + return { + transform: [ + { + scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'), + }, + ], + opacity: interpolate(outroAppOpacity.get(), [0, 0.1, 0.2, 1], [0, 0, 1, 1], 'clamp'), + }; + }); + var onFinish = useCallback(function () { return setIsAnimationComplete(true); }, []); + var onLayout = useCallback(function () { return setIsLayoutReady(true); }, []); + var onLoadEnd = useCallback(function () { return setIsImageLoaded(true); }, []); + useEffect(function () { + if (isReady) { + SplashScreen.hideAsync() + .then(function () { + intro.set(function () { + return withTiming(1, { duration: 400, easing: Easing.out(Easing.cubic) }, function () { + 'worklet'; + // set these values to check animation at specific point + outroLogo.set(function () { + return withTiming(1, { duration: 1200, easing: Easing.in(Easing.cubic) }, function () { + runOnJS(onFinish)(); + }); + }); + outroApp.set(function () { + return withTiming(1, { + duration: 1200, + easing: Easing.inOut(Easing.cubic), + }); + }); + outroAppOpacity.set(function () { + return withTiming(1, { + duration: 1200, + easing: Easing.in(Easing.cubic), + }); + }); + }); + }); + }) + .catch(function () { }); + } + }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady]); + useEffect(function () { + AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion); + }, []); + var logoAnimations = reduceMotion === true ? reducedLogoAnimation : logoAnimation; + // special off-spec color for dark mode + var logoBg = isDarkMode ? '#0F1824' : '#fff'; + return (_jsxs(View, { style: { flex: 1 }, onLayout: onLayout, children: [!isAnimationComplete && (_jsxs(View, { style: StyleSheet.absoluteFillObject, children: [_jsx(Image, { accessibilityIgnoresInvertColors: true, onLoadEnd: onLoadEnd, source: { uri: isDarkMode ? darkSplashImageUri : splashImageUri }, style: StyleSheet.absoluteFillObject }), _jsx(Animated.View, { style: [ + bottomLogoAnimation, + { + position: 'absolute', + bottom: insets.bottom + 40, + left: 0, + right: 0, + alignItems: 'center', + justifyContent: 'center', + opacity: 0, + }, + ], children: _jsx(Logotype, { fill: "#fff", width: 90 }) })] })), isReady && (_jsxs(_Fragment, { children: [_jsx(Animated.View, { style: [{ flex: 1 }, appAnimation], children: props.children }), !isAnimationComplete && (_jsx(Animated.View, { style: [ + StyleSheet.absoluteFillObject, + logoWrapperAnimation, + { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + transform: [{ translateY: -(insets.top / 2) }, { scale: 0.1 }], // scale from 1000px to 100px + }, + ], children: _jsx(Animated.View, { style: [logoAnimations], children: _jsx(Logo, { fill: logoBg }) }) }))] }))] })); +} diff --git a/src/Splash.web.js b/src/Splash.web.js new file mode 100644 index 0000000000..c389321b9e --- /dev/null +++ b/src/Splash.web.js @@ -0,0 +1,60 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +/* + * This is a reimplementation of what exists in our HTML template files + * already. Once the React tree mounts, this is what gets rendered first, until + * the app is ready to go. + */ +import { useEffect, useRef, useState } from 'react'; +import Svg, { Path } from 'react-native-svg'; +import { atoms as a, flatten } from '#/alf'; +var size = 100; +var ratio = 57 / 64; +export function Splash(_a) { + var isReady = _a.isReady, children = _a.children; + var _b = useState(false), isAnimationComplete = _b[0], setIsAnimationComplete = _b[1]; + var splashRef = useRef(null); + // hide the static one that's baked into the HTML - gets replaced by our React version below + useEffect(function () { + // double rAF ensures that the React version gets painted first + requestAnimationFrame(function () { + requestAnimationFrame(function () { + var splash = document.getElementById('splash'); + if (splash) { + splash.remove(); + } + }); + }); + }, []); + // when ready, we fade/scale out + useEffect(function () { + if (!isReady) + return; + var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + var node = splashRef.current; + if (!node || reduceMotion) { + setIsAnimationComplete(true); + return; + } + var animation = node.animate([ + { opacity: 1, transform: 'scale(1)' }, + { opacity: 0, transform: 'scale(1.5)' }, + ], { + duration: 300, + easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', + fill: 'forwards', + }); + animation.onfinish = function () { return setIsAnimationComplete(true); }; + return function () { + animation.cancel(); + }; + }, [isReady]); + return (_jsxs(_Fragment, { children: [isReady && children, !isAnimationComplete && (_jsx("div", { ref: splashRef, style: flatten([ + a.fixed, + a.inset_0, + a.flex, + a.align_center, + a.justify_center, + // to compensate for the `top: -50px` below + { transformOrigin: 'center calc(50% - 50px)' }, + ]), children: _jsx(Svg, { fill: "none", viewBox: "0 0 64 57", style: [a.relative, { width: size, height: size * ratio, top: -50 }], children: _jsx(Path, { fill: "#006AFF", d: "M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z" }) }) }))] })); +} diff --git a/src/ageAssurance/__mocks__/data.js b/src/ageAssurance/__mocks__/data.js new file mode 100644 index 0000000000..4af342a890 --- /dev/null +++ b/src/ageAssurance/__mocks__/data.js @@ -0,0 +1,3 @@ +export var prefetchAgeAssuranceData = function () { }; +export var setBirthdateForDid = function () { }; +export var setCreatedAtForDid = function () { }; diff --git a/src/ageAssurance/components/NoAccessScreen.js b/src/ageAssurance/components/NoAccessScreen.js new file mode 100644 index 0000000000..53c6dfb082 --- /dev/null +++ b/src/ageAssurance/components/NoAccessScreen.js @@ -0,0 +1,168 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useEffect } from 'react'; +import { ScrollView, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { SupportCode, useCreateSupportLink, } from '#/lib/hooks/useCreateSupportLink'; +import { dateDiff, useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { useIsBirthdateUpdateAllowed } from '#/state/birthdate'; +import { useSessionApi } from '#/state/session'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { AgeAssuranceAppealDialog } from '#/components/ageAssurance/AgeAssuranceAppealDialog'; +import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge'; +import { AgeAssuranceInitDialog } from '#/components/ageAssurance/AgeAssuranceInitDialog'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import * as Dialog from '#/components/Dialog'; +import { BirthDateSettingsDialog } from '#/components/dialogs/BirthDateSettings'; +import { DeviceLocationRequestDialog } from '#/components/dialogs/DeviceLocationRequestDialog'; +import { Full as Logo } from '#/components/icons/Logo'; +import { ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon } from '#/components/icons/Shield'; +import { createStaticClick, SimpleInlineLinkText } from '#/components/Link'; +import { Outlet as PortalOutlet } from '#/components/Portal'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { BottomSheetOutlet } from '#/../modules/bottom-sheet'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAgeAssuranceDataContext } from '#/ageAssurance/data'; +import { useComputeAgeAssuranceRegionAccess } from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'; +import { isLegacyBirthdateBug, useAgeAssuranceRegionConfig, } from '#/ageAssurance/util'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE, IS_WEB } from '#/env'; +import { useDeviceGeolocationApi } from '#/geolocation'; +var textStyles = [a.text_md, a.leading_snug]; +export function NoAccessScreen() { + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var gtPhone = useBreakpoints().gtPhone; + var insets = useSafeAreaInsets(); + var birthdateControl = useDialogControl(); + var data = useAgeAssuranceDataContext().data; + var region = useAgeAssuranceRegionConfig(); + var isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed(); + var logoutCurrentAccount = useSessionApi().logoutCurrentAccount; + var createSupportLink = useCreateSupportLink(); + var aa = useAgeAssurance(); + var isBlocked = aa.state.status === aa.Status.Blocked; + var isAARegion = !!region; + var hasDeclaredAge = (data === null || data === void 0 ? void 0 : data.declaredAge) !== undefined; + var canUpdateBirthday = isBirthdateUpdateAllowed || isLegacyBirthdateBug((data === null || data === void 0 ? void 0 : data.birthdate) || ''); + useEffect(function () { + // just counting overall hits here + ax.metric("blockedGeoOverlay:shown", {}); + ax.metric("ageAssurance:noAccessScreen:shown", { + accountCreatedAt: (data === null || data === void 0 ? void 0 : data.accountCreatedAt) || 'unknown', + isAARegion: isAARegion, + hasDeclaredAge: hasDeclaredAge, + canUpdateBirthday: canUpdateBirthday, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + var onPressLogout = useCallback(function () { + if (IS_WEB) { + // We're switching accounts, which remounts the entire app. + // On mobile, this gets us Home, but on the web we also need reset the URL. + // We can't change the URL via a navigate() call because the navigator + // itself is about to unmount, and it calls pushState() too late. + // So we change the URL ourselves. The navigator will pick it up on remount. + history.pushState(null, '', '/'); + } + logoutCurrentAccount('AgeAssuranceNoAccessScreen'); + }, [logoutCurrentAccount]); + var orgAdmonition = (_jsx(Admonition, { type: "tip", children: _jsx(Trans, { children: "For organizational accounts, use the birthdate of the person who is responsible for the account." }) })); + var birthdateUpdateText = canUpdateBirthday ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: [textStyles], children: _jsxs(Trans, { children: ["If you believe your birthdate is incorrect, you can update it by", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Click here to update your birthdate"], ["Click here to update your birthdate"])))), style: [textStyles] }, createStaticClick(function () { + ax.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {}); + birthdateControl.open(); + }), { children: "clicking here" })), "."] }) }), orgAdmonition] })) : (_jsx(Text, { style: [textStyles], children: _jsxs(Trans, { children: ["If you believe your birthdate is incorrect, please", ' ', _jsx(SimpleInlineLinkText, { to: createSupportLink({ code: SupportCode.AA_BIRTHDATE }), label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Click here to contact our support team"], ["Click here to contact our support team"])))), style: [textStyles], children: "contact our support team" }), "."] }) })); + return (_jsxs(_Fragment, { children: [_jsx(View, { style: [a.util_screen_outer, a.flex_1], children: _jsx(ScrollView, { contentContainerStyle: [ + a.px_2xl, + { + paddingTop: IS_WEB + ? a.p_5xl.padding + : insets.top + a.p_2xl.padding, + paddingBottom: 100, + }, + ], children: _jsxs(View, { style: [ + a.mx_auto, + a.w_full, + web({ + maxWidth: 380, + paddingTop: gtPhone ? '8vh' : undefined, + }), + { + gap: 32, + }, + ], children: [_jsx(View, { style: [a.align_start], children: _jsx(AgeAssuranceBadge, {}) }), hasDeclaredAge ? (_jsx(_Fragment, { children: isAARegion ? (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Hey there!" }) }), _jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "You are accessing Bluesky from a region that legally requires us to verify your age before allowing you to access the app." }) }), !aa.flags.isOverRegionMinAccessAge && (_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Unfortunately, your declared age indicates that you are not old enough to access Bluesky in your region." }) })), !isBlocked && birthdateUpdateText] }), aa.flags.isOverRegionMinAccessAge && _jsx(AccessSection, {})] })) : (_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Unfortunately, the birthdate you have saved to your profile makes you too young to access Bluesky." }) }), birthdateUpdateText] })) })) : (_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Hi there!" }) }), _jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "In order to provide an age-appropriate experience, we need to know your birthdate. This is a one-time thing, and your data will be kept private." }) }), _jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Set your birthdate below and we'll get you back to posting and exploring in no time!" }) }), _jsx(Button, { color: "primary", size: "large", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Click here to update your birthdate"], ["Click here to update your birthdate"])))), onPress: function () { return birthdateControl.open(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Add your birthdate" }) }) }), orgAdmonition] })), _jsxs(View, { style: [a.pt_lg, a.gap_xl], children: [_jsx(Logo, { width: 120, textFill: t.atoms.text.color }), _jsx(Text, { style: [a.text_sm, a.italic, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["To log out,", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Click here to log out"], ["Click here to log out"])))) }, createStaticClick(function () { + onPressLogout(); + }), { children: "click here" })), "."] }) })] })] }) }) }), _jsx(BirthDateSettingsDialog, { control: birthdateControl }), _jsx(BottomSheetOutlet, {}), _jsx(PortalOutlet, {})] })); +} +function AccessSection() { + var t = useTheme(); + var _a = useLingui(), _ = _a._, i18n = _a.i18n; + var ax = useAnalytics(); + var control = useDialogControl(); + var appealControl = Dialog.useDialogControl(); + var locationControl = Dialog.useDialogControl(); + var getTimeAgo = useGetTimeAgo(); + var setDeviceGeolocation = useDeviceGeolocationApi().setDeviceGeolocation; + var computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess(); + var aa = useAgeAssurance(); + var _b = aa.state, status = _b.status, lastInitiatedAt = _b.lastInitiatedAt; + var isBlocked = status === aa.Status.Blocked; + var hasInitiated = !!lastInitiatedAt; + var timeAgo = lastInitiatedAt + ? getTimeAgo(lastInitiatedAt, new Date()) + : null; + var diff = lastInitiatedAt + ? dateDiff(lastInitiatedAt, new Date(), 'down') + : null; + return (_jsxs(_Fragment, { children: [_jsx(AgeAssuranceInitDialog, { control: control }), _jsx(AgeAssuranceAppealDialog, { control: appealControl }), _jsxs(View, { style: [a.gap_xl], children: [isBlocked ? (_jsx(Admonition, { type: "warning", children: _jsxs(Trans, { children: ["You are currently unable to access Bluesky's Age Assurance flow. Please", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Contact our moderation team"], ["Contact our moderation team"])))) }, createStaticClick(function () { + appealControl.open(); + ax.metric('ageAssurance:appealDialogOpen', {}); + }), { children: "contact our moderation team" })), ' ', "if you believe this is an error."] }) })) : (_jsx(_Fragment, { children: _jsxs(View, { style: [a.gap_md], children: [_jsxs(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Verify now"], ["Verify now"])))), size: "large", color: hasInitiated ? 'secondary' : 'primary', onPress: function () { + control.open(); + ax.metric('ageAssurance:initDialogOpen', { + hasInitiatedPreviously: hasInitiated, + }); + }, children: [_jsx(ButtonIcon, { icon: ShieldIcon }), _jsx(ButtonText, { children: hasInitiated ? (_jsx(Trans, { children: "Verify again" })) : (_jsx(Trans, { children: "Verify now" })) })] }), lastInitiatedAt && timeAgo && diff ? (_jsx(Text, { style: [a.text_sm, a.italic, t.atoms.text_contrast_medium], title: i18n.date(lastInitiatedAt, { + dateStyle: 'medium', + timeStyle: 'medium', + }), children: diff.value === 0 ? (_jsx(Trans, { children: "Last initiated just now" })) : (_jsxs(Trans, { children: ["Last initiated ", timeAgo, " ago"] })) })) : (_jsx(Text, { style: [a.text_sm, a.italic, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Age assurance only takes a few minutes" }) }))] }) })), _jsx(View, { style: [a.gap_xs], children: IS_NATIVE && (_jsxs(_Fragment, { children: [_jsx(Admonition, { children: _jsxs(Trans, { children: ["Is your location not accurate?", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Confirm your location"], ["Confirm your location"])))) }, createStaticClick(function () { + locationControl.open(); + }), { children: "Tap here to confirm your location." })), ' '] }) }), _jsx(DeviceLocationRequestDialog, { control: locationControl, onLocationAcquired: function (props) { + var access = computeAgeAssuranceRegionAccess(props.geolocation); + if (access !== aa.Access.Full) { + props.disableDialogAction(); + props.setDialogError(_(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["We're sorry, but based on your device's location, you are currently located in a region that requires age assurance."], ["We're sorry, but based on your device's location, you are currently located in a region that requires age assurance."]))))); + } + else { + props.closeDialog(function () { + // set this after close! + setDeviceGeolocation(props.geolocation); + Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Thanks! You're all set."], ["Thanks! You're all set."])))), { + type: 'success', + }); + }); + } + } })] })) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/ageAssurance/components/RedirectOverlay.js b/src/ageAssurance/components/RedirectOverlay.js new file mode 100644 index 0000000000..038236b448 --- /dev/null +++ b/src/ageAssurance/components/RedirectOverlay.js @@ -0,0 +1,240 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; +import { Dimensions, View } from 'react-native'; +import * as Linking from 'expo-linking'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { retry } from '#/lib/async/retry'; +import { wait } from '#/lib/async/wait'; +import { parseLinkingUrl } from '#/lib/parseLinkingUrl'; +import { useAgent, useSession } from '#/state/session'; +import { atoms as a, platform, useBreakpoints, useTheme } from '#/alf'; +import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge'; +import { Button, ButtonText } from '#/components/Button'; +import { FullWindowOverlay } from '#/components/FullWindowOverlay'; +import { CheckThick_Stroke2_Corner0_Rounded as SuccessIcon } from '#/components/icons/Check'; +import { CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon } from '#/components/icons/CircleInfo'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { refetchAgeAssuranceServerState } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS, IS_WEB } from '#/env'; +/** + * Validate and parse the query parameters returned from the age assurance + * redirect. If not valid, returns `undefined` and the dialog will not open. + */ +export function parseRedirectOverlayState(state) { + if (state === void 0) { state = {}; } + var result = 'unknown'; + var actorDid = state.actorDid; + switch (state.result) { + case 'success': + result = 'success'; + break; + case 'unknown': + default: + result = 'unknown'; + break; + } + if (actorDid) { + return { + result: result, + actorDid: actorDid, + }; + } +} +var Context = createContext({ + isOpen: false, + open: function () { }, + close: function () { }, +}); +export function useRedirectOverlayContext() { + return useContext(Context); +} +export function Provider(_a) { + var children = _a.children; + var currentAccount = useSession().currentAccount; + var incomingUrl = Linking.useLinkingURL(); + var _b = useState(function () { + var _a, _b; + if (!incomingUrl) + return null; + var url = parseLinkingUrl(incomingUrl); + if (url.pathname !== '/intent/age-assurance') + return null; + var params = url.searchParams; + var state = parseRedirectOverlayState({ + result: (_a = params.get('result')) !== null && _a !== void 0 ? _a : undefined, + actorDid: (_b = params.get('actorDid')) !== null && _b !== void 0 ? _b : undefined, + }); + if (IS_WEB) { + // Clear the URL parameters so they don't re-trigger + history.pushState(null, '', '/'); + } + /* + * If we don't have an account or the account doesn't match, do + * nothing. By the time the user switches to their other account, AA + * state should be ready for them. + */ + if (state && currentAccount && state.actorDid === currentAccount.did) { + return state; + } + return null; + }), state = _b[0], setState = _b[1]; + var open = useCallback(function (state) { + setState(state); + }, []); + var close = useCallback(function () { + setState(null); + }, []); + return (_jsx(Context.Provider, { value: useMemo(function () { return ({ + isOpen: state !== null, + open: open, + close: close, + }); }, [state, open, close]), children: children })); +} +export function RedirectOverlay() { + var t = useTheme(); + var _ = useLingui()._; + var isOpen = useRedirectOverlayContext().isOpen; + var gtMobile = useBreakpoints().gtMobile; + return isOpen ? (_jsx(FullWindowOverlay, { children: _jsx(View, { style: [ + a.fixed, + a.inset_0, + // setting a zIndex when using FullWindowOverlay on iOS + // means the taps pass straight through to the underlying content (???) + // so don't set it on iOS. FullWindowOverlay already does the job. + !IS_IOS && { zIndex: 9999 }, + t.atoms.bg, + gtMobile ? a.p_2xl : a.p_xl, + a.align_center, + // @ts-ignore + platform({ + web: { + paddingTop: '35vh', + }, + default: { + paddingTop: Dimensions.get('window').height * 0.35, + }, + }), + ], children: _jsx(View, { role: "dialog", "aria-role": "dialog", "aria-label": _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Verifying your age assurance status"], ["Verifying your age assurance status"])))), children: _jsx(View, { style: [a.pb_3xl, { width: 300 }], children: _jsx(Inner, {}) }) }) }) })) : null; +} +function Inner() { + var _this = this; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var agent = useAgent(); + var polling = useRef(false); + var unmounted = useRef(false); + var _a = useState(false), error = _a[0], setError = _a[1]; + var _b = useState(false), success = _b[0], setSuccess = _b[1]; + var close = useRedirectOverlayContext().close; + useEffect(function () { + if (polling.current) + return; + polling.current = true; + ax.metric('ageAssurance:redirectDialogOpen', {}); + wait(3e3, retry(5, function () { return true; }, function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!agent.session) + return [2 /*return*/]; + if (unmounted.current) + return [2 /*return*/]; + return [4 /*yield*/, refetchAgeAssuranceServerState({ agent: agent })]; + case 1: + data = _a.sent(); + if ((data === null || data === void 0 ? void 0 : data.state.status) !== 'assured') { + throw new Error("Polling for age assurance state did not receive assured status"); + } + return [2 /*return*/, data]; + } + }); + }); }, 1e3)) + .then(function (data) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (!data) + return [2 /*return*/]; + if (!agent.session) + return [2 /*return*/]; + if (unmounted.current) + return [2 /*return*/]; + setSuccess(true); + ax.metric('ageAssurance:redirectDialogSuccess', {}); + return [2 /*return*/]; + }); + }); }) + .catch(function () { + if (unmounted.current) + return; + setError(true); + ax.metric('ageAssurance:redirectDialogFail', {}); + }); + return function () { + unmounted.current = true; + }; + }, [ax, agent]); + if (success) { + return (_jsx(_Fragment, { children: _jsxs(View, { style: [a.align_start, a.w_full], children: [_jsx(AgeAssuranceBadge, {}), _jsxs(View, { style: [ + a.flex_row, + a.justify_between, + a.align_center, + a.gap_sm, + a.pt_lg, + a.pb_md, + ], children: [_jsx(SuccessIcon, { size: "sm", fill: t.palette.positive_500 }), _jsx(Text, { style: [a.text_3xl, a.font_bold], children: _jsx(Trans, { children: "Success" }) })] }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "We've confirmed your age assurance status. You can now close this dialog." }) }), _jsx(View, { style: [a.w_full, a.pt_lg], children: _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", variant: "solid", color: "secondary", onPress: function () { return close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }) })] }) })); + } + return (_jsx(_Fragment, { children: _jsxs(View, { style: [a.align_start, a.w_full], children: [_jsx(AgeAssuranceBadge, {}), _jsxs(View, { style: [ + a.flex_row, + a.justify_between, + a.align_center, + a.gap_sm, + a.pt_lg, + a.pb_md, + ], children: [error && _jsx(ErrorIcon, { size: "lg", fill: t.palette.negative_500 }), _jsx(Text, { style: [a.text_3xl, a.font_bold], children: error ? _jsx(Trans, { children: "Connection issue" }) : _jsx(Trans, { children: "Verifying" }) }), !error && _jsx(Loader, { size: "lg" })] }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium, a.leading_snug], children: error ? (_jsx(Trans, { children: "We were unable to receive the verification due to a connection issue. It may arrive later. If it does, your account will update automatically." })) : (_jsx(Trans, { children: "We're confirming your age assurance status with our servers. This should only take a few seconds." })) }), error && (_jsx(View, { style: [a.w_full, a.pt_lg], children: _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", variant: "solid", color: "secondary", onPress: function () { return close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }) }))] }) })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/ageAssurance/data.js b/src/ageAssurance/data.js new file mode 100644 index 0000000000..4834d0dac5 --- /dev/null +++ b/src/ageAssurance/data.js @@ -0,0 +1,587 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; +import { AtpAgent, getAgeAssuranceRegionConfig, } from '@atproto/api'; +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; +import { focusManager, QueryClient, useQuery } from '@tanstack/react-query'; +import { persistQueryClient } from '@tanstack/react-query-persist-client'; +import debounce from 'lodash.debounce'; +import { networkRetry } from '#/lib/async/retry'; +import { PUBLIC_BSKY_SERVICE } from '#/lib/constants'; +import { createPersistedQueryStorage } from '#/lib/persisted-query-storage'; +import { getAge } from '#/lib/strings/time'; +import { hasSnoozedBirthdateUpdateForDid, snoozeBirthdateUpdateAllowedForDid, } from '#/state/birthdate'; +import { useAgent, useSession } from '#/state/session'; +import * as debug from '#/ageAssurance/debug'; +import { logger } from '#/ageAssurance/logger'; +import { getBirthdateStringFromAge, isLegacyBirthdateBug, } from '#/ageAssurance/util'; +import { IS_DEV } from '#/env'; +import { device } from '#/storage'; +/** + * Special query client for age assurance data so we can prefetch on app + * load without interfering with other queries. + */ +var qc = new QueryClient({ + defaultOptions: { + queries: { + /** + * We clear this manually, so disable automatic garbage collection. + * @see https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#how-it-works + */ + gcTime: Infinity, + }, + }, +}); +var persister = createAsyncStoragePersister({ + storage: createPersistedQueryStorage('age-assurance'), + key: 'age-assurance-query-client', +}); +var _a = persistQueryClient({ + queryClient: qc, + persister: persister, +}), cacheHydrationPromise = _a[1]; +function getDidFromAgentSession(agent) { + var sessionManager = agent.sessionManager; + if (!sessionManager || !sessionManager.did) + return; + return sessionManager.did; +} +/* + * Optimistic data + */ +var createdAtCache = new Map(); +export function setCreatedAtForDid(_a) { + var did = _a.did, createdAt = _a.createdAt; + createdAtCache.set(did, createdAt); +} +var birthdateCache = new Map(); +export function setBirthdateForDid(_a) { + var did = _a.did, birthdate = _a.birthdate; + birthdateCache.set(did, birthdate); +} +/* + * Config + */ +export var configQueryKey = ['config']; +export function getConfig() { + return __awaiter(this, void 0, void 0, function () { + var agent, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (debug.enabled) + return [2 /*return*/, debug.resolve(debug.config)]; + agent = new AtpAgent({ + service: PUBLIC_BSKY_SERVICE, + }); + return [4 /*yield*/, agent.app.bsky.ageassurance.getConfig()]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data]; + } + }); + }); +} +export function getConfigFromCache() { + return qc.getQueryData(configQueryKey); +} +var configPrefetchPromise; +export function prefetchConfig() { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + if (configPrefetchPromise) { + logger.debug("prefetchAgeAssuranceConfig: already in progress"); + return [2 /*return*/]; + } + configPrefetchPromise = new Promise(function (resolve) { return __awaiter(_this, void 0, void 0, function () { + var cached, res, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, cacheHydrationPromise]; + case 1: + _a.sent(); + cached = getConfigFromCache(); + if (!cached) return [3 /*break*/, 2]; + logger.debug("prefetchAgeAssuranceConfig: using cache"); + resolve(); + return [3 /*break*/, 6]; + case 2: + _a.trys.push([2, 4, 5, 6]); + logger.debug("prefetchAgeAssuranceConfig: resolving..."); + return [4 /*yield*/, networkRetry(3, function () { return getConfig(); })]; + case 3: + res = _a.sent(); + qc.setQueryData(configQueryKey, res); + return [3 /*break*/, 6]; + case 4: + e_1 = _a.sent(); + logger.warn("prefetchAgeAssuranceConfig: failed", { + safeMessage: e_1.message, + }); + return [3 /*break*/, 6]; + case 5: + resolve(); + return [7 /*endfinally*/]; + case 6: return [2 /*return*/]; + } + }); + }); }); + return [2 /*return*/]; + }); + }); +} +export function refetchConfig() { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + logger.debug("refetchConfig: fetching..."); + return [4 /*yield*/, getConfig()]; + case 1: + res = _a.sent(); + qc.setQueryData(configQueryKey, res); + return [2 /*return*/, res]; + } + }); + }); +} +export function useConfigQuery() { + return useQuery({ + /** + * Will re-fetch when stale, at most every hour (or 5s in dev for easier + * testing). + * + * @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat + */ + staleTime: IS_DEV ? 5e3 : 1000 * 60 * 60, + /** + * N.B. if prefetch failed above, we'll have no `initialData`, and this + * query will run on startup. + */ + initialData: getConfigFromCache(), + initialDataUpdatedAt: function () { var _a; return (_a = qc.getQueryState(configQueryKey)) === null || _a === void 0 ? void 0 : _a.dataUpdatedAt; }, + queryKey: configQueryKey, + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + logger.debug("useConfigQuery: fetching config"); + return [2 /*return*/, getConfig()]; + }); + }); + }, + }, qc); +} +/* + * Server state + */ +export function createServerStateQueryKey(_a) { + var did = _a.did; + return ['serverState', did]; +} +export function getServerState(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var geolocation, data, did; + var agent = _b.agent; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (debug.enabled && debug.serverState) + return [2 /*return*/, debug.resolve(debug.serverState)]; + geolocation = device.get(['mergedGeolocation']); + if (!geolocation || !geolocation.countryCode) { + logger.error("getServerState: missing geolocation countryCode"); + return [2 /*return*/]; + } + return [4 /*yield*/, agent.app.bsky.ageassurance.getState({ + countryCode: geolocation.countryCode, + regionCode: geolocation.regionCode, + })]; + case 1: + data = (_c.sent()).data; + did = getDidFromAgentSession(agent); + if (data && did && createdAtCache.has(did)) { + /* + * If account was just created, just use the local cache if available. On + * subsequent reloads, the server should have the correct value. + */ + data.metadata.accountCreatedAt = createdAtCache.get(did); + } + return [2 /*return*/, data !== null && data !== void 0 ? data : null]; + } + }); + }); +} +export function getServerStateFromCache(_a) { + var did = _a.did; + return qc.getQueryData(createServerStateQueryKey({ did: did })); +} +export function prefetchServerState(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var did, qk, cached, res, e_2; + var agent = _b.agent; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + did = getDidFromAgentSession(agent); + if (!did) + return [2 /*return*/]; + return [4 /*yield*/, cacheHydrationPromise]; + case 1: + _c.sent(); + qk = createServerStateQueryKey({ did: did }); + cached = getServerStateFromCache({ did: did }); + if (cached) { + logger.debug("prefetchServerState: using cache"); + return [2 /*return*/]; + } + _c.label = 2; + case 2: + _c.trys.push([2, 4, , 5]); + logger.debug("prefetchServerState: resolving..."); + return [4 /*yield*/, networkRetry(3, function () { return getServerState({ agent: agent }); })]; + case 3: + res = _c.sent(); + qc.setQueryData(qk, res); + return [3 /*break*/, 5]; + case 4: + e_2 = _c.sent(); + logger.warn("prefetchServerState: failed", { + safeMessage: e_2.message, + }); + return [3 /*break*/, 5]; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function refetchServerState(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var did, res; + var agent = _b.agent; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + did = getDidFromAgentSession(agent); + if (!did) + return [2 /*return*/]; + logger.debug("refetchServerState: fetching..."); + return [4 /*yield*/, networkRetry(3, function () { return getServerState({ agent: agent }); })]; + case 1: + res = _c.sent(); + qc.setQueryData(createServerStateQueryKey({ did: did }), res); + return [2 /*return*/, res]; + } + }); + }); +} +export function usePatchServerState() { + var _this = this; + var currentAccount = useSession().currentAccount; + return useCallback(function (next) { return __awaiter(_this, void 0, void 0, function () { + var did, prev, merged; + return __generator(this, function (_a) { + if (!currentAccount) + return [2 /*return*/]; + did = currentAccount.did; + prev = getServerStateFromCache({ did: did }); + merged = __assign(__assign({ metadata: {} }, (prev || {})), { state: next }); + qc.setQueryData(createServerStateQueryKey({ did: did }), merged); + return [2 /*return*/]; + }); + }); }, [currentAccount]); +} +export function useServerStateQuery() { + var _a, _b; + var agent = useAgent(); + var did = getDidFromAgentSession(agent); + var query = useQuery({ + enabled: !!did, + initialData: function () { + if (!did) + return; + return getServerStateFromCache({ did: did }); + }, + queryKey: createServerStateQueryKey({ did: did }), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, getServerState({ agent: agent })]; + }); + }); + }, + }, qc); + var refetch = useMemo(function () { return debounce(query.refetch, 100); }, [query.refetch]); + var isAssured = ((_b = (_a = query.data) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.status) === 'assured'; + /** + * `refetchOnWindowFocus` doesn't seem to want to work for this custom query + * client, so we manually subscribe to focus changes. + */ + useEffect(function () { + return focusManager.subscribe(function () { + var _a; + // logged out + if (!did) + return; + var isFocused = focusManager.isFocused(); + if (!isFocused) + return; + var config = getConfigFromCache(); + var geolocation = device.get(['mergedGeolocation']); + var isAArequired = Boolean(config && + geolocation && + !!getAgeAssuranceRegionConfig(config, { + countryCode: (_a = geolocation === null || geolocation === void 0 ? void 0 : geolocation.countryCode) !== null && _a !== void 0 ? _a : '', + regionCode: geolocation === null || geolocation === void 0 ? void 0 : geolocation.regionCode, + })); + // only refetch when needed + if (isAssured || !isAArequired) + return; + refetch(); + }); + }, [did, refetch, isAssured]); + return query; +} +export function createOtherRequiredDataQueryKey(_a) { + var did = _a.did; + return ['otherRequiredData', did]; +} +export function getOtherRequiredData(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var prefs, data, did; + var _c, _d, _e; + var agent = _b.agent; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + if (debug.enabled) + return [2 /*return*/, debug.resolve(debug.otherRequiredData)]; + return [4 /*yield*/, Promise.all([agent.getPreferences()])]; + case 1: + prefs = (_f.sent())[0]; + data = { + birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined, + }; + /** + * If we can't read a birthdate, it may be due to the user accessing the + * account via an app password. In that case, fall-back to declared age + * flags. + */ + if (!data.birthdate) { + if ((_c = prefs.declaredAge) === null || _c === void 0 ? void 0 : _c.isOverAge18) { + data.birthdate = getBirthdateStringFromAge(18); + } + else if ((_d = prefs.declaredAge) === null || _d === void 0 ? void 0 : _d.isOverAge16) { + data.birthdate = getBirthdateStringFromAge(16); + } + else if ((_e = prefs.declaredAge) === null || _e === void 0 ? void 0 : _e.isOverAge13) { + data.birthdate = getBirthdateStringFromAge(13); + } + } + did = getDidFromAgentSession(agent); + if (data && did && birthdateCache.has(did)) { + /* + * If birthdate was just set, use the local cache value. On subsequent + * reloads, the server should have the correct value. + */ + data.birthdate = birthdateCache.get(did); + } + /** + * If the user is under the minimum age, and the birthdate is not due to the + * legacy bug, AND we've not already snoozed their birthdate update, snooze + * further birthdate updates for this user. + * + * This is basically a migration step for this initial rollout. + */ + if (data.birthdate && + !isLegacyBirthdateBug(data.birthdate) && + !hasSnoozedBirthdateUpdateForDid(did)) { + snoozeBirthdateUpdateAllowedForDid(did); + } + return [2 /*return*/, data]; + } + }); + }); +} +export function getOtherRequiredDataFromCache(_a) { + var did = _a.did; + return qc.getQueryData(createOtherRequiredDataQueryKey({ did: did })); +} +export function prefetchOtherRequiredData(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var did, qk, cached, res, e_3; + var agent = _b.agent; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + did = getDidFromAgentSession(agent); + if (!did) + return [2 /*return*/]; + return [4 /*yield*/, cacheHydrationPromise]; + case 1: + _c.sent(); + qk = createOtherRequiredDataQueryKey({ did: did }); + cached = getOtherRequiredDataFromCache({ did: did }); + if (cached) { + logger.debug("prefetchOtherRequiredData: using cache"); + return [2 /*return*/]; + } + _c.label = 2; + case 2: + _c.trys.push([2, 4, , 5]); + logger.debug("prefetchOtherRequiredData: resolving..."); + return [4 /*yield*/, networkRetry(3, function () { return getOtherRequiredData({ agent: agent }); })]; + case 3: + res = _c.sent(); + qc.setQueryData(qk, res); + return [3 /*break*/, 5]; + case 4: + e_3 = _c.sent(); + logger.warn("prefetchOtherRequiredData: failed", { + safeMessage: e_3.message, + }); + return [3 /*break*/, 5]; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function usePatchOtherRequiredData() { + var _this = this; + var currentAccount = useSession().currentAccount; + return useCallback(function (next) { return __awaiter(_this, void 0, void 0, function () { + var did, prev, merged; + return __generator(this, function (_a) { + if (!currentAccount) + return [2 /*return*/]; + did = currentAccount.did; + prev = getOtherRequiredDataFromCache({ did: did }); + merged = __assign(__assign({}, (prev || {})), next); + qc.setQueryData(createOtherRequiredDataQueryKey({ did: did }), merged); + return [2 /*return*/]; + }); + }); }, [currentAccount]); +} +export function useOtherRequiredDataQuery() { + var agent = useAgent(); + var did = getDidFromAgentSession(agent); + return useQuery({ + enabled: !!did, + initialData: function () { + if (!did) + return; + return getOtherRequiredDataFromCache({ did: did }); + }, + queryKey: createOtherRequiredDataQueryKey({ did: did }), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, getOtherRequiredData({ agent: agent })]; + }); + }); + }, + }, qc); +} +/** + * Helper to prefetch all age assurance data. + */ +export function prefetchAgeAssuranceData(_a) { + var agent = _a.agent; + return Promise.allSettled([ + // config fetch initiated at the top of the App.platform.tsx files, awaited here + configPrefetchPromise, + prefetchServerState({ agent: agent }), + prefetchOtherRequiredData({ agent: agent }), + ]); +} +export function clearAgeAssuranceDataForDid(_a) { + var did = _a.did; + logger.debug("clearAgeAssuranceDataForDid: ".concat(did)); + qc.removeQueries({ queryKey: createServerStateQueryKey({ did: did }), exact: true }); + qc.removeQueries({ + queryKey: createOtherRequiredDataQueryKey({ did: did }), + exact: true, + }); +} +export function clearAgeAssuranceData() { + logger.debug("clearAgeAssuranceData"); + qc.clear(); +} +export var AgeAssuranceDataContext = createContext({ + config: undefined, + state: undefined, + data: { + accountCreatedAt: undefined, + declaredAge: undefined, + birthdate: undefined, + }, +}); +export function useAgeAssuranceDataContext() { + return useContext(AgeAssuranceDataContext); +} +export function AgeAssuranceDataProvider(_a) { + var children = _a.children; + var config = useConfigQuery().data; + var serverState = useServerStateQuery(); + var _b = serverState.data || {}, state = _b.state, metadata = _b.metadata; + var data = useOtherRequiredDataQuery().data; + var ctx = useMemo(function () { return ({ + config: config, + state: state, + data: { + accountCreatedAt: metadata === null || metadata === void 0 ? void 0 : metadata.accountCreatedAt, + declaredAge: (data === null || data === void 0 ? void 0 : data.birthdate) + ? getAge(new Date(data.birthdate)) + : undefined, + birthdate: data === null || data === void 0 ? void 0 : data.birthdate, + }, + }); }, [config, state, data, metadata]); + return (_jsx(AgeAssuranceDataContext.Provider, { value: ctx, children: children })); +} diff --git a/src/ageAssurance/debug.js b/src/ageAssurance/debug.js new file mode 100644 index 0000000000..b2b91c2521 --- /dev/null +++ b/src/ageAssurance/debug.js @@ -0,0 +1,106 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { ageAssuranceRuleIDs as ids, } from '@atproto/api'; +import { IS_DEV, IS_E2E } from '#/env'; +export var enabled = (IS_DEV && false) || IS_E2E; +export var geolocation = enabled + ? { + countryCode: 'AA', + regionCode: undefined, + } + : undefined; +var deviceGeolocationEnabled = false || IS_E2E; +export var deviceGeolocation = enabled && deviceGeolocationEnabled + ? { + countryCode: 'AA', + regionCode: undefined, + } + : undefined; +export var config = { + regions: [ + { + countryCode: 'AA', + regionCode: undefined, + minAccessAge: 13, + rules: [ + { + $type: ids.Default, + access: 'full', + }, + ], + }, + { + countryCode: 'BB', + regionCode: undefined, + minAccessAge: 16, + rules: [ + { + $type: ids.Default, + access: 'full', + }, + ], + }, + ], +}; +export var otherRequiredData = { + birthdate: new Date(2000, 1, 1).toISOString(), +}; +var serverStateEnabled = false; +export var serverState = serverStateEnabled + ? { + state: { + lastInitiatedAt: new Date(2025, 1, 1).toISOString(), + status: 'assured', + access: 'full', + }, + metadata: { + accountCreatedAt: new Date(2023, 1, 1).toISOString(), + }, + } + : undefined; +export function resolve(data) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, new Promise(function (y) { return setTimeout(y, 500); })]; // simulate network + case 1: + _a.sent(); // simulate network + return [2 /*return*/, data]; + } + }); + }); +} diff --git a/src/ageAssurance/index.js b/src/ageAssurance/index.js new file mode 100644 index 0000000000..c24be2918d --- /dev/null +++ b/src/ageAssurance/index.js @@ -0,0 +1,80 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; +import { useGetAndRegisterPushToken } from '#/lib/notifications/notifications'; +import { Provider as RedirectOverlayProvider } from '#/ageAssurance/components/RedirectOverlay'; +import { AgeAssuranceDataProvider } from '#/ageAssurance/data'; +import { useAgeAssuranceDataContext } from '#/ageAssurance/data'; +import { logger } from '#/ageAssurance/logger'; +import { useAgeAssuranceState, useOnAgeAssuranceAccessUpdate, } from '#/ageAssurance/state'; +import { AgeAssuranceAccess, AgeAssuranceStatus, } from '#/ageAssurance/types'; +import { isUnderAge, MIN_ACCESS_AGE, useAgeAssuranceRegionConfigWithFallback, } from '#/ageAssurance/util'; +export { prefetchConfig as prefetchAgeAssuranceConfig, prefetchAgeAssuranceData, refetchServerState as refetchAgeAssuranceServerState, usePatchOtherRequiredData as usePatchAgeAssuranceOtherRequiredData, usePatchServerState as usePatchAgeAssuranceServerState, } from '#/ageAssurance/data'; +export { logger } from '#/ageAssurance/logger'; +export { MIN_ACCESS_AGE } from '#/ageAssurance/util'; +var AgeAssuranceStateContext = createContext({ + Access: AgeAssuranceAccess, + Status: AgeAssuranceStatus, + state: { + lastInitiatedAt: undefined, + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.Full, + }, + flags: { + adultContentDisabled: false, + chatDisabled: false, + isOverRegionMinAccessAge: false, + isOverAppMinAccessAge: false, + }, +}); +/** + * THE MAIN AGE ASSURANCE CONTEXT HOOK + * + * Prefer this to using any of the lower-level data-provider hooks. + */ +export function useAgeAssurance() { + return useContext(AgeAssuranceStateContext); +} +export function Provider(_a) { + var children = _a.children; + return (_jsx(AgeAssuranceDataProvider, { children: _jsx(InnerProvider, { children: _jsx(RedirectOverlayProvider, { children: children }) }) })); +} +function InnerProvider(_a) { + var children = _a.children; + var state = useAgeAssuranceState(); + var data = useAgeAssuranceDataContext().data; + var config = useAgeAssuranceRegionConfigWithFallback(); + var getAndRegisterPushToken = useGetAndRegisterPushToken(); + var handleAccessUpdate = useCallback(function (s) { + getAndRegisterPushToken({ + isAgeRestricted: s.access !== AgeAssuranceAccess.Full, + }); + }, [getAndRegisterPushToken]); + useOnAgeAssuranceAccessUpdate(handleAccessUpdate); + useEffect(function () { + logger.debug("useAgeAssuranceState", { state: state }); + }, [state]); + return (_jsx(AgeAssuranceStateContext.Provider, { value: useMemo(function () { + var chatDisabled = state.access !== AgeAssuranceAccess.Full; + var isUnderAdultAge = (data === null || data === void 0 ? void 0 : data.birthdate) + ? isUnderAge(data.birthdate, 18) + : true; + var isOverRegionMinAccessAge = (data === null || data === void 0 ? void 0 : data.birthdate) + ? !isUnderAge(data.birthdate, config.minAccessAge) + : false; + var isOverAppMinAccessAge = (data === null || data === void 0 ? void 0 : data.birthdate) + ? !isUnderAge(data.birthdate, MIN_ACCESS_AGE) + : false; + var adultContentDisabled = state.access !== AgeAssuranceAccess.Full || isUnderAdultAge; + return { + Access: AgeAssuranceAccess, + Status: AgeAssuranceStatus, + state: state, + flags: { + adultContentDisabled: adultContentDisabled, + chatDisabled: chatDisabled, + isOverRegionMinAccessAge: isOverRegionMinAccessAge, + isOverAppMinAccessAge: isOverAppMinAccessAge, + }, + }; + }, [state, data, config]), children: children })); +} diff --git a/src/ageAssurance/logger.js b/src/ageAssurance/logger.js new file mode 100644 index 0000000000..c16f8081a0 --- /dev/null +++ b/src/ageAssurance/logger.js @@ -0,0 +1,2 @@ +import { Logger } from '#/logger'; +export var logger = Logger.create(Logger.Context.AgeAssurance); diff --git a/src/ageAssurance/state.js b/src/ageAssurance/state.js new file mode 100644 index 0000000000..d0c3c19cf3 --- /dev/null +++ b/src/ageAssurance/state.js @@ -0,0 +1,89 @@ +import { useEffect, useMemo, useState } from 'react'; +import { computeAgeAssuranceRegionAccess } from '@atproto/api'; +import { useSession } from '#/state/session'; +import { useAgeAssuranceDataContext } from '#/ageAssurance/data'; +import { logger } from '#/ageAssurance/logger'; +import { AgeAssuranceAccess, AgeAssuranceStatus, parseAccessFromString, parseStatusFromString, } from '#/ageAssurance/types'; +import { getAgeAssuranceRegionConfigWithFallback } from '#/ageAssurance/util'; +import { useGeolocation } from '#/geolocation'; +export function useAgeAssuranceState() { + var hasSession = useSession().hasSession; + var geolocation = useGeolocation(); + var _a = useAgeAssuranceDataContext(), config = _a.config, state = _a.state, data = _a.data; + return useMemo(function () { + /** + * This is where we control logged-out moderation prefs. It's all + * downstream of AA now. + */ + if (!hasSession) + return { + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.Safe, + }; + /** + * This can happen if the prefetch fails (such as due to network issues). + * The query handler will try it again, but if it continues to fail, of + * course we won't have config. + * + * In this case, fail open to avoid blocking users. + */ + if (!config) { + logger.warn('useAgeAssuranceState: missing config'); + return { + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.Safe, + error: 'config', + }; + } + var region = getAgeAssuranceRegionConfigWithFallback(config, geolocation); + var isAARequired = region.countryCode !== '*'; + var isTerminalState = (state === null || state === void 0 ? void 0 : state.status) === 'assured' || (state === null || state === void 0 ? void 0 : state.status) === 'blocked'; + /* + * If we are in a terminal state and AA is required for this region, + * we can trust the server state completely and avoid recomputing. + */ + if (isTerminalState && isAARequired) { + return { + lastInitiatedAt: state.lastInitiatedAt, + status: parseStatusFromString(state.status), + access: parseAccessFromString(state.access), + }; + } + /* + * Otherwise, we need to compute the access based on the latest data. For + * accounts with an accurate birthdate, our default fallback rules should + * ensure correct access. + */ + var result = computeAgeAssuranceRegionAccess(region, data); + var computed = { + lastInitiatedAt: state === null || state === void 0 ? void 0 : state.lastInitiatedAt, + // prefer server state + status: (state === null || state === void 0 ? void 0 : state.status) + ? parseStatusFromString(state === null || state === void 0 ? void 0 : state.status) + : AgeAssuranceStatus.Unknown, + // prefer server state + access: result + ? parseAccessFromString(result.access) + : AgeAssuranceAccess.Full, + }; + logger.debug('debug useAgeAssuranceState', { + region: region, + state: state, + data: data, + computed: computed, + }); + return computed; + }, [hasSession, geolocation, config, state, data]); +} +export function useOnAgeAssuranceAccessUpdate(cb) { + var state = useAgeAssuranceState(); + // start with null to ensure callback is called on first render + var _a = useState(null), prevAccess = _a[0], setPrevAccess = _a[1]; + useEffect(function () { + if (prevAccess !== state.access) { + setPrevAccess(state.access); + cb(state); + logger.debug("useOnAgeAssuranceAccessUpdate", { state: state }); + } + }, [cb, state, prevAccess]); +} diff --git a/src/ageAssurance/types.js b/src/ageAssurance/types.js new file mode 100644 index 0000000000..22c78bf5db --- /dev/null +++ b/src/ageAssurance/types.js @@ -0,0 +1,45 @@ +import { logger } from '#/ageAssurance/logger'; +export var AgeAssuranceAccess; +(function (AgeAssuranceAccess) { + AgeAssuranceAccess["Unknown"] = "unknown"; + AgeAssuranceAccess["None"] = "none"; + AgeAssuranceAccess["Safe"] = "safe"; + AgeAssuranceAccess["Full"] = "full"; +})(AgeAssuranceAccess || (AgeAssuranceAccess = {})); +export var AgeAssuranceStatus; +(function (AgeAssuranceStatus) { + AgeAssuranceStatus["Unknown"] = "unknown"; + AgeAssuranceStatus["Pending"] = "pending"; + AgeAssuranceStatus["Assured"] = "assured"; + AgeAssuranceStatus["Blocked"] = "blocked"; +})(AgeAssuranceStatus || (AgeAssuranceStatus = {})); +export function parseStatusFromString(raw) { + switch (raw) { + case 'unknown': + return AgeAssuranceStatus.Unknown; + case 'pending': + return AgeAssuranceStatus.Pending; + case 'assured': + return AgeAssuranceStatus.Assured; + case 'blocked': + return AgeAssuranceStatus.Blocked; + default: + logger.error("parseStatusFromString: unknown status value: ".concat(raw)); + return AgeAssuranceStatus.Unknown; + } +} +export function parseAccessFromString(raw) { + switch (raw) { + case 'unknown': + return AgeAssuranceAccess.Unknown; + case 'none': + return AgeAssuranceAccess.None; + case 'safe': + return AgeAssuranceAccess.Safe; + case 'full': + return AgeAssuranceAccess.Full; + default: + logger.error("parseAccessFromString: unknown access value: ".concat(raw)); + return AgeAssuranceAccess.Full; + } +} diff --git a/src/ageAssurance/useBeginAgeAssurance.js b/src/ageAssurance/useBeginAgeAssurance.js new file mode 100644 index 0000000000..35d1ad9fb3 --- /dev/null +++ b/src/ageAssurance/useBeginAgeAssurance.js @@ -0,0 +1,115 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { Platform } from 'react-native'; +import { AtpAgent } from '@atproto/api'; +import { useMutation } from '@tanstack/react-query'; +import { wait } from '#/lib/async/wait'; +import { DEV_ENV_APPVIEW, PUBLIC_APPVIEW, PUBLIC_APPVIEW_DID, } from '#/lib/constants'; +import { isNetworkError } from '#/lib/hooks/useCleanError'; +import { useAgent } from '#/state/session'; +import { usePatchAgeAssuranceServerState } from '#/ageAssurance'; +import { logger } from '#/ageAssurance/logger'; +import { useAnalytics } from '#/analytics'; +import { BLUESKY_PROXY_DID } from '#/env'; +import { useGeolocation } from '#/geolocation'; +var IS_DEV_ENV = BLUESKY_PROXY_DID !== PUBLIC_APPVIEW_DID; +var APPVIEW = IS_DEV_ENV ? DEV_ENV_APPVIEW : PUBLIC_APPVIEW; +export function useBeginAgeAssurance() { + var ax = useAnalytics(); + var agent = useAgent(); + var geolocation = useGeolocation(); + var patchAgeAssuranceStateResponse = usePatchAgeAssuranceServerState(); + return useMutation({ + mutationFn: function (props) { + return __awaiter(this, void 0, void 0, function () { + var countryCode, regionCode, token, appView, data; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + countryCode = (_a = geolocation === null || geolocation === void 0 ? void 0 : geolocation.countryCode) === null || _a === void 0 ? void 0 : _a.toUpperCase(); + regionCode = (_b = geolocation === null || geolocation === void 0 ? void 0 : geolocation.regionCode) === null || _b === void 0 ? void 0 : _b.toUpperCase(); + if (!countryCode) { + throw new Error("Geolocation not available, cannot init age assurance."); + } + return [4 /*yield*/, agent.com.atproto.server.getServiceAuth({ + aud: BLUESKY_PROXY_DID, + lxm: "app.bsky.ageassurance.begin", + })]; + case 1: + token = (_c.sent()).data.token; + appView = new AtpAgent({ service: APPVIEW }); + appView.sessionManager.session = __assign({}, agent.session); + appView.sessionManager.session.accessJwt = token; + appView.sessionManager.session.refreshJwt = ''; + ax.metric('ageAssurance:api:begin', { + platform: Platform.OS, + countryCode: countryCode, + regionCode: regionCode, + }); + return [4 /*yield*/, wait(2e3, appView.app.bsky.ageassurance.begin(__assign(__assign({}, props), { countryCode: countryCode, regionCode: regionCode }))) + // Just keeps this in sync, not necessarily used right now + ]; + case 2: + data = (_c.sent()).data; + // Just keeps this in sync, not necessarily used right now + patchAgeAssuranceStateResponse(data); + return [2 /*return*/]; + } + }); + }); + }, + onError: function (e) { + if (!isNetworkError(e)) { + logger.error("useBeginAgeAssurance failed", { + safeMessage: e, + }); + } + }, + }); +} diff --git a/src/ageAssurance/useComputeAgeAssuranceRegionAccess.js b/src/ageAssurance/useComputeAgeAssuranceRegionAccess.js new file mode 100644 index 0000000000..731f696aa9 --- /dev/null +++ b/src/ageAssurance/useComputeAgeAssuranceRegionAccess.js @@ -0,0 +1,20 @@ +import { useCallback } from 'react'; +import { computeAgeAssuranceRegionAccess } from '@atproto/api'; +import { useAgeAssuranceDataContext } from '#/ageAssurance/data'; +import { logger } from '#/ageAssurance/logger'; +import { AgeAssuranceAccess, parseAccessFromString } from '#/ageAssurance/types'; +import { getAgeAssuranceRegionConfigWithFallback } from '#/ageAssurance/util'; +export function useComputeAgeAssuranceRegionAccess() { + var _a = useAgeAssuranceDataContext(), config = _a.config, data = _a.data; + return useCallback(function (geolocation) { + if (!config) { + logger.warn('useComputeAgeAssuranceRegionAccess: missing config'); + return AgeAssuranceAccess.Unknown; + } + var region = getAgeAssuranceRegionConfigWithFallback(config, geolocation); + var result = computeAgeAssuranceRegionAccess(region, data); + return result + ? parseAccessFromString(result.access) + : AgeAssuranceAccess.Full; + }, [config, data]); +} diff --git a/src/ageAssurance/util.js b/src/ageAssurance/util.js new file mode 100644 index 0000000000..ac7d0b5bd5 --- /dev/null +++ b/src/ageAssurance/util.js @@ -0,0 +1,97 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { useMemo } from 'react'; +import { ageAssuranceRuleIDs as ids, getAgeAssuranceRegionConfig, } from '@atproto/api'; +import { getAge } from '#/lib/strings/time'; +import { DEFAULT_LOGGED_OUT_LABEL_PREFERENCES } from '#/state/queries/preferences/moderation'; +import { useAgeAssuranceDataContext } from '#/ageAssurance/data'; +import { AgeAssuranceAccess } from '#/ageAssurance/types'; +import { useGeolocation } from '#/geolocation'; +export var MIN_ACCESS_AGE = 13; +var FALLBACK_REGION_CONFIG = { + countryCode: '*', + regionCode: undefined, + minAccessAge: MIN_ACCESS_AGE, + rules: [ + { + $type: ids.IfDeclaredOverAge, + age: MIN_ACCESS_AGE, + access: AgeAssuranceAccess.Full, + }, + { + $type: ids.Default, + access: AgeAssuranceAccess.None, + }, + ], +}; +/** + * Get age assurance region config based on geolocation, with fallback to + * app defaults if no region config is found. + * + * See {@link getAgeAssuranceRegionConfig} for the generic option, which can + * return undefined if the geolocation does not match any AA region. + */ +export function getAgeAssuranceRegionConfigWithFallback(config, geolocation) { + var _a; + var region = getAgeAssuranceRegionConfig(config, { + countryCode: (_a = geolocation.countryCode) !== null && _a !== void 0 ? _a : '', + regionCode: geolocation.regionCode, + }); + return region || FALLBACK_REGION_CONFIG; +} +/** + * Hook to get the age assurance region config based on current geolocation. + * Does not fall-back to our app defaults. If no config is found, returns + * undefined, which indicates no regional age assurance rules apply. + */ +export function useAgeAssuranceRegionConfig() { + var geolocation = useGeolocation(); + var config = useAgeAssuranceDataContext().config; + return useMemo(function () { + var _a; + if (!config) + return; + // use generic helper, we want to potentially return undefined + return getAgeAssuranceRegionConfig(config, { + countryCode: (_a = geolocation.countryCode) !== null && _a !== void 0 ? _a : '', + regionCode: geolocation.regionCode, + }); + }, [config, geolocation]); +} +/** + * Hook to get the age assurance region config based on current geolocation. + * Falls back to our app defaults if no region config is found. + */ +export function useAgeAssuranceRegionConfigWithFallback() { + return useAgeAssuranceRegionConfig() || FALLBACK_REGION_CONFIG; +} +/** + * Some users may have erroneously set their birth date to the current date + * if one wasn't set on their account. We previously didn't do validation on + * the bday dialog, and it defaulted to the current date. This bug _has_ been + * seen in production, so we need to check for it where possible. + */ +export function isLegacyBirthdateBug(birthDate) { + return ['2025', '2024', '2023'].includes((birthDate || '').slice(0, 4)); +} +/** + * Returns whether the date (converted to an age as a whole integer) is under + * the provided minimum age. + */ +export function isUnderAge(birthDate, age) { + return getAge(new Date(birthDate)) < age; +} +export function getBirthdateStringFromAge(age) { + var today = new Date(); + return new Date(today.getFullYear() - age, today.getMonth(), today.getDate() - 1).toISOString(); +} +export var makeAgeRestrictedModerationPrefs = function (prefs) { return (__assign(__assign({}, prefs), { adultContentEnabled: false, labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES })); }; diff --git a/src/alf/atoms.js b/src/alf/atoms.js new file mode 100644 index 0000000000..92ffbd16ac --- /dev/null +++ b/src/alf/atoms.js @@ -0,0 +1,125 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { atoms as baseAtoms } from '@bsky.app/alf'; +import { CARD_ASPECT_RATIO } from '#/lib/constants'; +import { native, platform, web } from '#/alf/util/platform'; +import * as Layout from '#/components/Layout'; +var EXP_CURVE = 'cubic-bezier(0.16, 1, 0.3, 1)'; +export var atoms = __assign(__assign({}, baseAtoms), { h_full_vh: web({ + height: '100vh', + }), + /** + * Used for the outermost components on screens, to ensure that they can fill + * the screen and extend beyond. + */ + util_screen_outer: [ + web({ + minHeight: '100vh', + }), + native({ + height: '100%', + }), + ], + /* + * Theme-independent bg colors + */ + bg_transparent: { + backgroundColor: 'transparent', + }, + /** + * Aspect ratios + */ + aspect_square: { + aspectRatio: 1, + }, aspect_card: { + aspectRatio: CARD_ASPECT_RATIO, + }, + /* + * Transition + */ + transition_none: web({ + transitionProperty: 'none', + }), transition_timing_default: web({ + transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', + transitionDuration: '100ms', + }), transition_all: web({ + transitionProperty: 'all', + transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', + transitionDuration: '100ms', + }), transition_color: web({ + transitionProperty: 'color, background-color, border-color, text-decoration-color, fill, stroke', + transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', + transitionDuration: '100ms', + }), transition_opacity: web({ + transitionProperty: 'opacity', + transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', + transitionDuration: '100ms', + }), transition_transform: web({ + transitionProperty: 'transform', + transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', + transitionDuration: '100ms', + }), transition_delay_50ms: web({ + transitionDelay: '50ms', + }), + /* + * Animations + */ + fade_in: web({ + animation: 'fadeIn ease-out 0.15s', + }), fade_out: web({ + animation: 'fadeOut ease-out 0.15s', + animationFillMode: 'forwards', + }), zoom_in: web({ + animation: 'zoomIn ease-out 0.1s', + }), zoom_out: web({ + animation: 'zoomOut ease-out 0.1s', + }), slide_in_left: web({ + // exponential easing function + animation: 'slideInLeft cubic-bezier(0.16, 1, 0.3, 1) 0.5s', + }), slide_out_left: web({ + animation: 'slideOutLeft ease-in 0.15s', + animationFillMode: 'forwards', + }), + // special composite animation for dialogs + zoom_fade_in: web({ + animation: "zoomIn ".concat(EXP_CURVE, " 0.3s, fadeIn ").concat(EXP_CURVE, " 0.3s"), + }), + /** + * Visually hidden but available to screen readers (web). + * Use for live regions or off-screen labels (e.g. "Image 1 of 3"). + */ + sr_only: web({ + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0,0,0,0)', + whiteSpace: 'nowrap', + borderWidth: 0, + }), + /** + * {@link Layout.SCROLLBAR_OFFSET} + */ + scrollbar_offset: platform({ + web: { + transform: [ + { + translateX: Layout.SCROLLBAR_OFFSET, + }, + ], + }, + native: { + transform: [], + }, + }) }); diff --git a/src/alf/breakpoints.js b/src/alf/breakpoints.js new file mode 100644 index 0000000000..d55979ac72 --- /dev/null +++ b/src/alf/breakpoints.js @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; +import { useMediaQuery } from 'react-responsive'; +export function useBreakpoints() { + var gtPhone = useMediaQuery({ minWidth: 500 }); + var gtMobile = useMediaQuery({ minWidth: 800 }); + var gtTablet = useMediaQuery({ minWidth: 1300 }); + return useMemo(function () { + var active; + if (gtTablet) { + active = 'gtTablet'; + } + else if (gtMobile) { + active = 'gtMobile'; + } + else if (gtPhone) { + active = 'gtPhone'; + } + return { + activeBreakpoint: active, + gtPhone: gtPhone, + gtMobile: gtMobile, + gtTablet: gtTablet, + }; + }, [gtPhone, gtMobile, gtTablet]); +} +/** + * Fine-tuned breakpoints for the shell layout + */ +export function useLayoutBreakpoints() { + var rightNavVisible = useMediaQuery({ minWidth: 1100 }); + var centerColumnOffset = useMediaQuery({ minWidth: 1100, maxWidth: 1300 }); + var leftNavMinimal = useMediaQuery({ maxWidth: 1300 }); + return { + rightNavVisible: rightNavVisible, + centerColumnOffset: centerColumnOffset, + leftNavMinimal: leftNavMinimal, + }; +} diff --git a/src/alf/fonts.js b/src/alf/fonts.js new file mode 100644 index 0000000000..4e49fe33b2 --- /dev/null +++ b/src/alf/fonts.js @@ -0,0 +1,88 @@ +import { IS_ANDROID, IS_WEB } from '#/env'; +import { device } from '#/storage'; +var WEB_FONT_FAMILIES = "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\""; +var factor = 0.0625; // 1 - (15/16) +var fontScaleMultipliers = { + '-2': 1 - factor * 1, // unused + '-1': 1 - factor * 1, + '0': 1, // default + '1': 1 + factor * 1, + '2': 1 + factor * 1, // unused +}; +export function computeFontScaleMultiplier(scale) { + return fontScaleMultipliers[scale]; +} +export function getFontScale() { + var _a; + return (_a = device.get(['fontScale'])) !== null && _a !== void 0 ? _a : '0'; +} +export function setFontScale(fontScale) { + device.set(['fontScale'], fontScale); +} +export function getFontFamily() { + return device.get(['fontFamily']) || 'theme'; +} +export function setFontFamily(fontFamily) { + device.set(['fontFamily'], fontFamily); +} +/* + * Unused fonts are commented out, but the files are there if we need them. + */ +export function applyFonts(style, fontFamily) { + if (fontFamily === 'theme') { + if (IS_ANDROID) { + style.fontFamily = + { + 400: 'Inter-Regular', + 500: 'Inter-Medium', + 600: 'Inter-SemiBold', + 700: 'Inter-Bold', + 800: 'Inter-Bold', + 900: 'Inter-Bold', + }[String(style.fontWeight || '400')] || 'Inter-Regular'; + if (style.fontStyle === 'italic') { + if (style.fontFamily === 'Inter-Regular') { + style.fontFamily = 'Inter-Italic'; + } + else { + style.fontFamily += 'Italic'; + } + } + /* + * These are not supported on Android and actually break the styling. + */ + delete style.fontWeight; + delete style.fontStyle; + } + else { + style.fontFamily = 'InterVariable'; + if (style.fontStyle === 'italic') { + style.fontFamily += 'Italic'; + } + } + if (IS_WEB) { + // fallback families only supported on web + style.fontFamily += ", ".concat(WEB_FONT_FAMILIES); + } + /** + * Disable contextual alternates in Inter + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant} + */ + style.fontVariant = (style.fontVariant || []).concat('no-contextual'); + } + else { + // fallback families only supported on web + if (IS_WEB) { + style.fontFamily = style.fontFamily || WEB_FONT_FAMILIES; + } + /** + * Overridden to previous spacing for the `system` font option. + * https://github.com/bluesky-social/social-app/commit/2419096e2409008b7d71fd6b8f8d0dd5b016e267 + */ + style.letterSpacing = 0.25; + } +} +/** + * Here only for bundling purposes, not actually used. + */ +export { DO_NOT_USE } from '#/alf/util/unusedUseFonts'; diff --git a/src/alf/index.js b/src/alf/index.js new file mode 100644 index 0000000000..074075eff3 --- /dev/null +++ b/src/alf/index.js @@ -0,0 +1,79 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { computeFontScaleMultiplier, getFontFamily, getFontScale, setFontFamily as persistFontFamily, setFontScale as persistFontScale, } from '#/alf/fonts'; +import { themes } from '#/alf/themes'; +export { utils, } from '@bsky.app/alf'; +export { atoms } from '#/alf/atoms'; +export * from '#/alf/breakpoints'; +export * from '#/alf/fonts'; +export * as tokens from '#/alf/tokens'; +export * from '#/alf/util/flatten'; +export * from '#/alf/util/platform'; +export * from '#/alf/util/themeSelector'; +export * from '#/alf/util/useGutters'; +/* + * Context + */ +export var Context = React.createContext({ + themeName: 'light', + theme: themes.light, + themes: themes, + fonts: { + scale: getFontScale(), + scaleMultiplier: computeFontScaleMultiplier(getFontScale()), + family: getFontFamily(), + setFontScale: function () { }, + setFontFamily: function () { }, + }, + flags: {}, +}); +Context.displayName = 'AlfContext'; +export function ThemeProvider(_a) { + var children = _a.children, themeName = _a.theme; + var _b = React.useState(function () { + return getFontScale(); + }), fontScale = _b[0], setFontScale = _b[1]; + var _c = React.useState(function () { + return computeFontScaleMultiplier(fontScale); + }), fontScaleMultiplier = _c[0], setFontScaleMultiplier = _c[1]; + var setFontScaleAndPersist = React.useCallback(function (fs) { + setFontScale(fs); + persistFontScale(fs); + setFontScaleMultiplier(computeFontScaleMultiplier(fs)); + }, [setFontScale]); + var _d = React.useState(function () { return getFontFamily(); }), fontFamily = _d[0], setFontFamily = _d[1]; + var setFontFamilyAndPersist = React.useCallback(function (ff) { + setFontFamily(ff); + persistFontFamily(ff); + }, [setFontFamily]); + var value = React.useMemo(function () { return ({ + themes: themes, + themeName: themeName, + theme: themes[themeName], + fonts: { + scale: fontScale, + scaleMultiplier: fontScaleMultiplier, + family: fontFamily, + setFontScale: setFontScaleAndPersist, + setFontFamily: setFontFamilyAndPersist, + }, + flags: {}, + }); }, [ + themeName, + fontScale, + setFontScaleAndPersist, + fontFamily, + setFontFamilyAndPersist, + fontScaleMultiplier, + ]); + return _jsx(Context.Provider, { value: value, children: children }); +} +export function useAlf() { + return React.useContext(Context); +} +export function useTheme(theme) { + var alf = useAlf(); + return React.useMemo(function () { + return theme ? alf.themes[theme] : alf.theme; + }, [theme, alf]); +} diff --git a/src/alf/themes.js b/src/alf/themes.js new file mode 100644 index 0000000000..f875dd2ce5 --- /dev/null +++ b/src/alf/themes.js @@ -0,0 +1,37 @@ +import { createThemes, DEFAULT_PALETTE, DEFAULT_SUBDUED_PALETTE, } from '@bsky.app/alf'; +var DEFAULT_THEMES = createThemes({ + defaultPalette: DEFAULT_PALETTE, + subduedPalette: DEFAULT_SUBDUED_PALETTE, +}); +export var themes = { + lightPalette: DEFAULT_THEMES.light.palette, + darkPalette: DEFAULT_THEMES.dark.palette, + dimPalette: DEFAULT_THEMES.dim.palette, + light: DEFAULT_THEMES.light, + dark: DEFAULT_THEMES.dark, + dim: DEFAULT_THEMES.dim, +}; +/** + * @deprecated use ALF and access palette from `useTheme()` + */ +export var lightPalette = DEFAULT_THEMES.light.palette; +/** + * @deprecated use ALF and access palette from `useTheme()` + */ +export var darkPalette = DEFAULT_THEMES.dark.palette; +/** + * @deprecated use ALF and access palette from `useTheme()` + */ +export var dimPalette = DEFAULT_THEMES.dim.palette; +/** + * @deprecated use ALF and access theme from `useTheme()` + */ +export var light = DEFAULT_THEMES.light; +/** + * @deprecated use ALF and access theme from `useTheme()` + */ +export var dark = DEFAULT_THEMES.dark; +/** + * @deprecated use ALF and access theme from `useTheme()` + */ +export var dim = DEFAULT_THEMES.dim; diff --git a/src/alf/tokens.js b/src/alf/tokens.js new file mode 100644 index 0000000000..2556bb2ff6 --- /dev/null +++ b/src/alf/tokens.js @@ -0,0 +1,72 @@ +import { tokens } from '@bsky.app/alf'; +export * from '@bsky.app/alf/dist/tokens'; +export var color = { + temp_purple: tokens.labelerColor.purple, + temp_purple_dark: tokens.labelerColor.purple_dark, +}; +export var gradients = { + primary: { + values: [ + [0, '#054CFF'], + [0.4, '#1085FE'], + [0.6, '#1085FE'], + [1, '#59B9FF'], + ], + hover_value: '#1085FE', + }, + sky: { + values: [ + [0, '#0A7AFF'], + [1, '#59B9FF'], + ], + hover_value: '#0A7AFF', + }, + midnight: { + values: [ + [0, '#022C5E'], + [1, '#4079BC'], + ], + hover_value: '#022C5E', + }, + sunrise: { + values: [ + [0, '#4E90AE'], + [0.4, '#AEA3AB'], + [0.8, '#E6A98F'], + [1, '#F3A84C'], + ], + hover_value: '#AEA3AB', + }, + sunset: { + values: [ + [0, '#6772AF'], + [0.6, '#B88BB6'], + [1, '#FFA6AC'], + ], + hover_value: '#B88BB6', + }, + summer: { + values: [ + [0, '#FF6A56'], + [0.3, '#FF9156'], + [1, '#FFDD87'], + ], + hover_value: '#FF9156', + }, + nordic: { + values: [ + [0, '#083367'], + [1, '#9EE8C1'], + ], + hover_value: '#3A7085', + }, + bonfire: { + values: [ + [0, '#203E4E'], + [0.4, '#755B62'], + [0.8, '#CD7765'], + [1, '#EF956E'], + ], + hover_value: '#755B62', + }, +}; diff --git a/src/alf/typography.js b/src/alf/typography.js new file mode 100644 index 0000000000..8db8f828b3 --- /dev/null +++ b/src/alf/typography.js @@ -0,0 +1,74 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { createElement as _createElement } from "react"; +import { Children } from 'react'; +import { UITextView } from 'react-native-uitextview'; +import createEmojiRegex from 'emoji-regex'; +import { applyFonts, atoms, flatten } from '#/alf'; +import { IS_NATIVE } from '#/env'; +import { IS_IOS } from '#/env'; +/** + * Ensures that `lineHeight` defaults to a relative value of `1`, or applies + * other relative leading atoms. + * + * If the `lineHeight` value is > 2, we assume it's an absolute value and + * returns it as-is. + */ +export function normalizeTextStyles(styles, _a) { + var _b; + var fontScale = _a.fontScale, fontFamily = _a.fontFamily; + var s = (_b = flatten(styles)) !== null && _b !== void 0 ? _b : {}; + // should always be defined on these components + s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale; + if (s === null || s === void 0 ? void 0 : s.lineHeight) { + if (s.lineHeight !== 0 && s.lineHeight <= 2) { + s.lineHeight = Math.round(s.fontSize * s.lineHeight); + } + } + else if (!IS_NATIVE) { + s.lineHeight = s.fontSize; + } + applyFonts(s, fontFamily); + return s; +} +var EMOJI = createEmojiRegex(); +export function childHasEmoji(children) { + var hasEmoji = false; + Children.forEach(children, function (child) { + if (typeof child === 'string' && createEmojiRegex().test(child)) { + hasEmoji = true; + } + }); + return hasEmoji; +} +export function renderChildrenWithEmoji(children, props, emoji) { + if (props === void 0) { props = {}; } + if (!IS_IOS || !emoji) { + return children; + } + return Children.map(children, function (child) { + if (typeof child !== 'string') + return child; + var emojis = child.match(EMOJI); + if (emojis === null) { + return child; + } + return child.split(EMOJI).map(function (stringPart, index) { return [ + stringPart, + emojis[index] ? (_createElement(UITextView, __assign({}, props, { style: [props === null || props === void 0 ? void 0 : props.style, { fontFamily: 'System' }], key: index }), emojis[index])) : null, + ]; }); + }); +} +var SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u; +export function isOnlyEmoji(text) { + return text.length <= 15 && SINGLE_EMOJI_RE.test(text); +} diff --git a/src/alf/util/__tests__/colors.test.js b/src/alf/util/__tests__/colors.test.js new file mode 100644 index 0000000000..91cca2442a --- /dev/null +++ b/src/alf/util/__tests__/colors.test.js @@ -0,0 +1,32 @@ +import { jest } from '@jest/globals'; +import { transparentifyColor } from '../colorGeneration'; +describe('transparentifyColor', function () { + beforeEach(function () { + jest.clearAllMocks(); + }); + it('converts hsl() to hsla()', function () { + var result = transparentifyColor('hsl(120 100% 50%)', 0.5); + expect(result).toBe('hsla(120 100% 50%, 0.5)'); + }); + it('converts hsl() to hsla() - fully transparent', function () { + var result = transparentifyColor('hsl(120 100% 50%)', 0); + expect(result).toBe('hsla(120 100% 50%, 0)'); + }); + it('converts rgb() to rgba()', function () { + var result = transparentifyColor('rgb(255 0 0)', 0.75); + expect(result).toBe('rgba(255 0 0, 0.75)'); + }); + it('expands 3-digit hex and appends alpha channel', function () { + var result = transparentifyColor('#abc', 0.4); + expect(result).toBe('#aabbcc66'); + }); + it('appends alpha to 6-digit hex', function () { + var result = transparentifyColor('#aabbcc', 0.4); + expect(result).toBe('#aabbcc66'); + }); + it('returns the original string and warns for unsupported formats', function () { + var unsupported = 'blue'; + var result = transparentifyColor(unsupported, 0.5); + expect(result).toBe(unsupported); + }); +}); diff --git a/src/alf/util/colorGeneration.js b/src/alf/util/colorGeneration.js new file mode 100644 index 0000000000..0ebdb8897d --- /dev/null +++ b/src/alf/util/colorGeneration.js @@ -0,0 +1,6 @@ +import { utils } from '@bsky.app/alf'; +export var BLUE_HUE = 211; +/** + * @deprecated use `utils.alpha` from `@bsky.app/alf` instead + */ +export var transparentifyColor = utils.alpha; diff --git a/src/alf/util/flatten.js b/src/alf/util/flatten.js new file mode 100644 index 0000000000..e54859573d --- /dev/null +++ b/src/alf/util/flatten.js @@ -0,0 +1,2 @@ +import { StyleSheet } from 'react-native'; +export var flatten = StyleSheet.flatten; diff --git a/src/alf/util/platform.js b/src/alf/util/platform.js new file mode 100644 index 0000000000..1507e421c7 --- /dev/null +++ b/src/alf/util/platform.js @@ -0,0 +1 @@ +export { android, ios, native, platform, web } from '@bsky.app/alf'; diff --git a/src/alf/util/systemUI.js b/src/alf/util/systemUI.js new file mode 100644 index 0000000000..f95478329c --- /dev/null +++ b/src/alf/util/systemUI.js @@ -0,0 +1,19 @@ +import * as SystemUI from 'expo-system-ui'; +import { logger } from '#/logger'; +import { IS_ANDROID } from '#/env'; +export function setSystemUITheme(themeType, t) { + if (IS_ANDROID) { + try { + if (themeType === 'theme') { + SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor); + } + else { + SystemUI.setBackgroundColorAsync('black'); + } + } + catch (error) { + // Can reject with 'The current activity is no longer available' - no big deal + logger.debug('Could not set system UI theme', { safeMessage: error }); + } + } +} diff --git a/src/alf/util/themeSelector.js b/src/alf/util/themeSelector.js new file mode 100644 index 0000000000..76c598f844 --- /dev/null +++ b/src/alf/util/themeSelector.js @@ -0,0 +1,2 @@ +import { utils } from '@bsky.app/alf'; +export var select = utils.select; diff --git a/src/alf/util/unusedUseFonts.android.js b/src/alf/util/unusedUseFonts.android.js new file mode 100644 index 0000000000..c2754a6b23 --- /dev/null +++ b/src/alf/util/unusedUseFonts.android.js @@ -0,0 +1,20 @@ +import { useFonts } from 'expo-font'; +/* + * IMPORTANT: This is unused. Expo statically extracts these fonts. + * + * All used fonts MUST be configured here. Unused fonts can be commented out. + * + * This is used for both web fonts and native fonts. + */ +export function DO_NOT_USE() { + return useFonts({ + 'Inter-Regular': require('../../../assets/fonts/inter/Inter-Regular.otf'), + 'Inter-Italic': require('../../../assets/fonts/inter/Inter-Italic.otf'), + 'Inter-Medium': require('../../../assets/fonts/inter/Inter-Medium.otf'), + 'Inter-MediumItalic': require('../../../assets/fonts/inter/Inter-MediumItalic.otf'), + 'Inter-SemiBold': require('../../../assets/fonts/inter/Inter-SemiBold.otf'), + 'Inter-SemiBoldItalic': require('../../../assets/fonts/inter/Inter-SemiBoldItalic.otf'), + 'Inter-Bold': require('../../../assets/fonts/inter/Inter-Bold.otf'), + 'Inter-BoldItalic': require('../../../assets/fonts/inter/Inter-BoldItalic.otf'), + }); +} diff --git a/src/alf/util/unusedUseFonts.js b/src/alf/util/unusedUseFonts.js new file mode 100644 index 0000000000..4f4574d2ed --- /dev/null +++ b/src/alf/util/unusedUseFonts.js @@ -0,0 +1,14 @@ +import { useFonts } from 'expo-font'; +/* + * IMPORTANT: This is unused. Expo statically extracts these fonts. + * + * All used fonts MUST be configured here. Unused fonts can be commented out. + * + * This is used for both web fonts and native fonts. + */ +export function DO_NOT_USE() { + return useFonts({ + InterVariable: require('../../../assets/fonts/inter/InterVariable.woff2'), + 'InterVariable-Italic': require('../../../assets/fonts/inter/InterVariable-Italic.woff2'), + }); +} diff --git a/src/alf/util/useColorModeTheme.js b/src/alf/util/useColorModeTheme.js new file mode 100644 index 0000000000..e4b706c2b5 --- /dev/null +++ b/src/alf/util/useColorModeTheme.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { useColorScheme } from 'react-native'; +import { useThemePrefs } from '#/state/shell'; +import { dark, dim, light } from '#/alf/themes'; +import { IS_WEB } from '#/env'; +export function useColorModeTheme() { + var theme = useThemeName(); + React.useLayoutEffect(function () { + updateDocument(theme); + }, [theme]); + return theme; +} +export function useThemeName() { + var colorScheme = useColorScheme(); + var _a = useThemePrefs(), colorMode = _a.colorMode, darkTheme = _a.darkTheme; + return getThemeName(colorScheme, colorMode, darkTheme); +} +function getThemeName(colorScheme, colorMode, darkTheme) { + if ((colorMode === 'system' && colorScheme === 'light') || + colorMode === 'light') { + return 'light'; + } + else { + return darkTheme !== null && darkTheme !== void 0 ? darkTheme : 'dim'; + } +} +function updateDocument(theme) { + // @ts-ignore web only + if (IS_WEB && typeof window !== 'undefined') { + // @ts-ignore web only + var html = window.document.documentElement; + // @ts-ignore web only + var meta = window.document.querySelector('meta[name="theme-color"]'); + // remove any other color mode classes + html.className = html.className.replace(/(theme)--\w+/g, ''); + html.classList.add("theme--".concat(theme)); + // set color to 'theme-color' meta tag + meta === null || meta === void 0 ? void 0 : meta.setAttribute('content', getBackgroundColor(theme)); + window.localStorage.setItem('ALF_THEME', theme); + } +} +export function getBackgroundColor(theme) { + switch (theme) { + case 'light': + return light.atoms.bg.backgroundColor; + case 'dark': + return dark.atoms.bg.backgroundColor; + case 'dim': + return dim.atoms.bg.backgroundColor; + } +} diff --git a/src/alf/util/useGutters.js b/src/alf/util/useGutters.js new file mode 100644 index 0000000000..d5c299e6b4 --- /dev/null +++ b/src/alf/util/useGutters.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { useBreakpoints } from '#/alf/breakpoints'; +import * as tokens from '#/alf/tokens'; +var gutters = { + compact: { + default: tokens.space.sm, + gtPhone: tokens.space.sm, + gtMobile: tokens.space.md, + gtTablet: tokens.space.md, + }, + base: { + default: tokens.space.lg, + gtPhone: tokens.space.lg, + gtMobile: tokens.space.xl, + gtTablet: tokens.space.xl, + }, + wide: { + default: tokens.space.xl, + gtPhone: tokens.space.xl, + gtMobile: tokens.space._3xl, + gtTablet: tokens.space._3xl, + }, +}; +export function useGutters(_a) { + var top = _a[0], right = _a[1], bottom = _a[2], left = _a[3]; + var activeBreakpoint = useBreakpoints().activeBreakpoint; + if (right === undefined) { + right = bottom = left = top; + } + else if (bottom === undefined) { + bottom = top; + left = right; + } + return React.useMemo(function () { + return { + paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], + paddingRight: right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'], + paddingBottom: bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'], + paddingLeft: left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'], + }; + }, [activeBreakpoint, top, right, bottom, left]); +} diff --git a/src/analytics/PassiveAnalytics.js b/src/analytics/PassiveAnalytics.js new file mode 100644 index 0000000000..db4b242213 --- /dev/null +++ b/src/analytics/PassiveAnalytics.js @@ -0,0 +1,25 @@ +import { useEffect, useRef } from 'react'; +import { getCurrentState, onAppStateChange } from '#/lib/appState'; +import { useAnalytics } from '#/analytics'; +/** + * Tracks passive analytics like app foreground/background time. + */ +export function PassiveAnalytics() { + var ax = useAnalytics(); + var lastActive = useRef(getCurrentState() === 'active' ? performance.now() : null); + useEffect(function () { + var sub = onAppStateChange(function (state) { + if (state === 'active') { + lastActive.current = performance.now(); + ax.metric('state:foreground', {}); + } + else if (lastActive.current !== null) { + ax.metric('state:background', { + secondsActive: Math.round((performance.now() - lastActive.current) / 1e3), + }); + } + }); + return function () { return sub.remove(); }; + }, [ax]); + return null; +} diff --git a/src/analytics/features/index.js b/src/analytics/features/index.js new file mode 100644 index 0000000000..afc05d063e --- /dev/null +++ b/src/analytics/features/index.js @@ -0,0 +1,126 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { MMKV } from '@bsky.app/react-native-mmkv'; +import { setPolyfills } from '@growthbook/growthbook'; +import { GrowthBook } from '@growthbook/growthbook-react'; +import { getNavigationMetadata } from '#/analytics/metadata'; +import * as env from '#/env'; +export { Features } from '#/analytics/features/types'; +var CACHE = new MMKV({ id: 'bsky_features_cache' }); +setPolyfills({ + localStorage: { + getItem: function (key) { + var value = CACHE.getString(key); + return value != null ? JSON.parse(value) : null; + }, + setItem: function (key, value) { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + CACHE.set(key, value); + return [2 /*return*/]; + }); + }); }, + }, +}); +var TIMEOUT_INIT = 500; // TODO should base on p99 or something +var TIMEOUT_PREFER_LOW_LATENCY = 250; +var TIMEOUT_PREFER_FRESH_GATES = 1500; +export var features = new GrowthBook({ + apiHost: env.GROWTHBOOK_API_HOST, + clientKey: env.GROWTHBOOK_CLIENT_KEY, +}); +/** + * Initializer promise that must be awaited before using the GrowthBook + * instance or rendering the `AnalyticsFeaturesContext`. Note: this may not be + * fully initialized if it takes longer than `TIMEOUT_INIT` to initialize. In + * that case, we may see a flash of uncustomized content until the + * initialization completes. + */ +export var init = new Promise(function (y) { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, features.init({ timeout: TIMEOUT_INIT })]; + case 1: + _a.sent(); + y(); + return [2 /*return*/]; + } + }); +}); }); +/** + * Refresh feature gates from GrowthBook. Updates attributes based on the + * provided account, if any. + */ +export function refresh(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var strategy = _b.strategy; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, features.refreshFeatures({ + timeout: strategy === 'prefer-low-latency' + ? TIMEOUT_PREFER_LOW_LATENCY + : TIMEOUT_PREFER_FRESH_GATES, + })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +/** + * Converts our metadata into GrowthBook attributes and sets them. GrowthBook + * attributes are manually configured in the GrowthBook dashboard. So these + * values need to match exactly. Therefore, let's add them here manually to and + * not spread them to avoid mistakes. + */ +export function setAttributes(_a) { + var _b; + var base = _a.base, geolocation = _a.geolocation, session = _a.session, preferences = _a.preferences; + features.setAttributes({ + deviceId: base.deviceId, + sessionId: base.sessionId, + platform: base.platform, + appVersion: base.appVersion, + countryCode: geolocation.countryCode, + regionCode: geolocation.regionCode, + did: session === null || session === void 0 ? void 0 : session.did, + isBskyPds: session === null || session === void 0 ? void 0 : session.isBskyPds, + appLanguage: preferences === null || preferences === void 0 ? void 0 : preferences.appLanguage, + contentLanguages: preferences === null || preferences === void 0 ? void 0 : preferences.contentLanguages, + currentScreen: (_b = getNavigationMetadata()) === null || _b === void 0 ? void 0 : _b.currentScreen, + }); +} diff --git a/src/analytics/features/types.js b/src/analytics/features/types.js new file mode 100644 index 0000000000..01c5edb12b --- /dev/null +++ b/src/analytics/features/types.js @@ -0,0 +1,12 @@ +export var Features; +(function (Features) { + // core flags + Features["IsBskyTeam"] = "is_bsky_team"; + // debug flags + Features["DebugFeedContext"] = "debug_feed_context"; + // feature flags + Features["ImportContactsOnboardingDisable"] = "import_contacts:onboarding:disable"; + Features["ImportContactsSettingsDisable"] = "import_contacts:settings:disable"; + Features["LiveNowBetaDisable"] = "live_now_beta:disable"; + Features["AATest"] = "aa-test"; +})(Features || (Features = {})); diff --git a/src/analytics/identifiers/device.js b/src/analytics/identifiers/device.js new file mode 100644 index 0000000000..dfbbdd48c3 --- /dev/null +++ b/src/analytics/identifiers/device.js @@ -0,0 +1,68 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import uuid from 'react-native-uuid'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { device } from '#/storage'; +var LEGACY_STABLE_ID = 'STATSIG_LOCAL_STORAGE_STABLE_ID'; +export function getAndMigrateDeviceId() { + return __awaiter(this, void 0, void 0, function () { + var migrated, id; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + migrated = getDeviceId(); + if (migrated) + return [2 /*return*/, migrated]; + return [4 /*yield*/, AsyncStorage.getItem(LEGACY_STABLE_ID)]; + case 1: + id = (_a.sent()) || uuid.v4(); + device.set(['deviceId'], id); + return [2 /*return*/, id]; + } + }); + }); +} +export function getDeviceId() { + return device.get(['deviceId']); +} +export function getDeviceIdOrThrow() { + var id = device.get(['deviceId']); + if (!id) { + throw new Error("deviceId is not set, call getAndMigrateDeviceId first"); + } + return id; +} diff --git a/src/analytics/identifiers/index.js b/src/analytics/identifiers/index.js new file mode 100644 index 0000000000..567b008f71 --- /dev/null +++ b/src/analytics/identifiers/index.js @@ -0,0 +1,2 @@ +export * from '#/analytics/identifiers/device'; +export * from '#/analytics/identifiers/session'; diff --git a/src/analytics/identifiers/session.js b/src/analytics/identifiers/session.js new file mode 100644 index 0000000000..c789a7d7a5 --- /dev/null +++ b/src/analytics/identifiers/session.js @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; +import uuid from 'react-native-uuid'; +import { onAppStateChange } from '#/lib/appState'; +import { isSessionIdExpired } from '#/analytics/identifiers/util'; +import { device } from '#/storage'; +var sessionId = (function () { + var existing = device.get(['nativeSessionId']); + var lastEvent = device.get(['nativeSessionIdLastEventAt']); + var id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4(); + device.set(['nativeSessionId'], id); + device.set(['nativeSessionIdLastEventAt'], Date.now()); + return id; +})(); +export function getInitialSessionId() { + return sessionId; +} +/** + * Gets the current session ID. Freshness depends on `useSessionId` being + * mounted, which handles refreshing this value between foreground/background + * transitions. Since that's mounted in `analytics/index.tsx`, this value can + * generally be trusted to be up to date. + */ +export function getSessionId() { + return device.get(['nativeSessionId']); +} +export function useSessionId() { + var _a = useState(function () { return sessionId; }), id = _a[0], setId = _a[1]; + useEffect(function () { + var sub = onAppStateChange(function (state) { + if (state === 'active') { + var lastEvent = device.get(['nativeSessionIdLastEventAt']); + if (isSessionIdExpired(lastEvent)) { + sessionId = uuid.v4(); + device.set(['nativeSessionId'], sessionId); + setId(sessionId); + } + } + device.set(['nativeSessionIdLastEventAt'], Date.now()); + }); + return function () { return sub.remove(); }; + }, []); + return id; +} diff --git a/src/analytics/identifiers/session.test.js b/src/analytics/identifiers/session.test.js new file mode 100644 index 0000000000..f4f039a2df --- /dev/null +++ b/src/analytics/identifiers/session.test.js @@ -0,0 +1,67 @@ +jest.mock('#/storage', function () { return ({ + device: { + get: jest.fn(), + set: jest.fn(), + }, +}); }); +jest.mock('#/analytics/identifiers/util', function () { return ({ + isSessionIdExpired: jest.fn(), +}); }); +jest.mock('#/lib/appState', function () { return ({ + onAppStateChange: jest.fn(function () { return ({ remove: jest.fn() }); }), +}); }); +beforeEach(function () { + jest.resetModules(); + jest.clearAllMocks(); +}); +function getMocks() { + var device = require('#/storage').device; + var isSessionIdExpired = require('#/analytics/identifiers/util').isSessionIdExpired; + return { + device: jest.mocked(device), + isSessionIdExpired: jest.mocked(isSessionIdExpired), + }; +} +describe('session initialization', function () { + it('creates new session and sets timestamp when none exists', function () { + var _a = getMocks(), device = _a.device, isSessionIdExpired = _a.isSessionIdExpired; + device.get.mockReturnValue(undefined); + isSessionIdExpired.mockReturnValue(false); + var getInitialSessionId = require('./session').getInitialSessionId; + var id = getInitialSessionId(); + expect(id).toBeDefined(); + expect(typeof id).toBe('string'); + expect(device.set).toHaveBeenCalledWith(['nativeSessionId'], id); + expect(device.set).toHaveBeenCalledWith(['nativeSessionIdLastEventAt'], expect.any(Number)); + }); + it('reuses existing session when not expired', function () { + var _a = getMocks(), device = _a.device, isSessionIdExpired = _a.isSessionIdExpired; + var existingId = 'existing-session-id'; + device.get.mockImplementation(function (key) { + if (key[0] === 'nativeSessionId') + return existingId; + if (key[0] === 'nativeSessionIdLastEventAt') + return Date.now(); + return undefined; + }); + isSessionIdExpired.mockReturnValue(false); + var getInitialSessionId = require('./session').getInitialSessionId; + expect(getInitialSessionId()).toBe(existingId); + }); + it('creates new session when existing is expired', function () { + var _a = getMocks(), device = _a.device, isSessionIdExpired = _a.isSessionIdExpired; + var existingId = 'existing-session-id'; + device.get.mockImplementation(function (key) { + if (key[0] === 'nativeSessionId') + return existingId; + if (key[0] === 'nativeSessionIdLastEventAt') + return Date.now() - 999999; + return undefined; + }); + isSessionIdExpired.mockReturnValue(true); + var getInitialSessionId = require('./session').getInitialSessionId; + var id = getInitialSessionId(); + expect(id).not.toBe(existingId); + expect(device.set).toHaveBeenCalledWith(['nativeSessionId'], id); + }); +}); diff --git a/src/analytics/identifiers/session.web.js b/src/analytics/identifiers/session.web.js new file mode 100644 index 0000000000..51d03d53bb --- /dev/null +++ b/src/analytics/identifiers/session.web.js @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react'; +import uuid from 'react-native-uuid'; +import { onAppStateChange } from '#/lib/appState'; +import { isSessionIdExpired } from '#/analytics/identifiers/util'; +var SESSION_ID_KEY = 'bsky_session_id'; +var LAST_EVENT_KEY = 'bsky_session_id_last_event_at'; +var sessionId = (function () { + var existing = window.sessionStorage.getItem(SESSION_ID_KEY); + var lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY); + var lastEvent = lastEventStr ? Number(lastEventStr) : undefined; + var id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4(); + window.sessionStorage.setItem(SESSION_ID_KEY, id); + window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now())); + return id; +})(); +export function getInitialSessionId() { + return sessionId; +} +/** + * Gets the current session ID. Freshness depends on `useSessionId` being + * mounted, which handles refreshing this value between foreground/background + * transitions. Since that's mounted in `analytics/index.tsx`, this value can + * generally be trusted to be up to date. + */ +export function getSessionId() { + return window.sessionStorage.getItem(SESSION_ID_KEY); +} +export function useSessionId() { + var _a = useState(function () { return sessionId; }), id = _a[0], setId = _a[1]; + useEffect(function () { + var sub = onAppStateChange(function (state) { + if (state === 'active') { + var lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY); + var lastEvent = lastEventStr ? Number(lastEventStr) : undefined; + if (isSessionIdExpired(lastEvent)) { + sessionId = uuid.v4(); + window.sessionStorage.setItem(SESSION_ID_KEY, sessionId); + setId(sessionId); + } + } + window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now())); + }); + return function () { return sub.remove(); }; + }, []); + return id; +} diff --git a/src/analytics/identifiers/util.js b/src/analytics/identifiers/util.js new file mode 100644 index 0000000000..4273e0a171 --- /dev/null +++ b/src/analytics/identifiers/util.js @@ -0,0 +1,8 @@ +import * as env from '#/env'; +var ONE_MIN = 60 * 1e3; +var TTL = (env.IS_NATIVE ? 5 : 30) * ONE_MIN; // 5 min on native +export function isSessionIdExpired(since) { + if (since === undefined) + return false; + return Date.now() - since >= TTL; +} diff --git a/src/analytics/index.js b/src/analytics/index.js new file mode 100644 index 0000000000..606b992891 --- /dev/null +++ b/src/analytics/index.js @@ -0,0 +1,147 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var _a; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { Platform } from 'react-native'; +import { Logger } from '#/logger'; +import { Features, features as feats, init, refresh, setAttributes, } from '#/analytics/features'; +import { getAndMigrateDeviceId, getDeviceId, getInitialSessionId, useSessionId, } from '#/analytics/identifiers'; +import { getMetadataForLogger, getNavigationMetadata, } from '#/analytics/metadata'; +import { metrics } from '#/analytics/metrics'; +import * as refParams from '#/analytics/misc/refParams'; +import * as env from '#/env'; +import { useGeolocationServiceResponse } from '#/geolocation/service'; +import { device } from '#/storage'; +export * as utils from '#/analytics/utils'; +export var features = { init: init, refresh: refresh }; +export { Features } from '#/analytics/features'; +function createLogger(context, metadata) { + var logger = Logger.create(context, metadata); + return { + debug: logger.debug.bind(logger), + info: logger.info.bind(logger), + log: logger.log.bind(logger), + warn: logger.warn.bind(logger), + error: logger.error.bind(logger), + useChild: function (context) { + return useMemo(function () { return createLogger(context, metadata); }, [context, metadata]); + }, + Context: Logger.Context, + }; +} +var Context = createContext({ + logger: createLogger(Logger.Context.Default, {}), + metric: function (event, payload, metadata) { + if (metadata && '__meta' in metadata) { + delete metadata.__meta; + } + metrics.track(event, payload, __assign(__assign({}, metadata), { navigation: getNavigationMetadata() })); + }, + metadata: { + base: { + deviceId: (_a = getDeviceId()) !== null && _a !== void 0 ? _a : 'unknown', + sessionId: getInitialSessionId(), + platform: Platform.OS, + appVersion: env.APP_VERSION, + bundleIdentifier: env.BUNDLE_IDENTIFIER, + bundleDate: env.BUNDLE_DATE, + referrerSrc: refParams.src, + referrerUrl: refParams.url, + }, + geolocation: device.get(['geolocationServiceResponse']) || { + countryCode: '', + regionCode: '', + }, + }, +}); +/** + * Ensures that deviceId is set and migrated from legacy storage. Handled on + * startup in `App..tsx`. This must be awaited prior to the app + * booting up. + */ +export var setupDeviceId = getAndMigrateDeviceId(); +/** + * Analytics context provider. Decorates the parent analytics context with + * additional metadata. Nesting should be done carefully and sparingly. + */ +export function AnalyticsContext(_a) { + var children = _a.children, metadata = _a.metadata; + if (metadata) { + if (!('__meta' in metadata)) { + throw new Error('Use the useMeta() helper when passing metadata to AnalyticsContext'); + } + } + var sessionId = useSessionId(); + var geolocation = useGeolocationServiceResponse(); + var parentContext = useContext(Context); + var childContext = useMemo(function () { + var combinedMetadata = __assign(__assign(__assign({}, parentContext.metadata), metadata), { base: __assign(__assign({}, parentContext.metadata.base), { sessionId: sessionId }), geolocation: geolocation }); + var context = __assign(__assign({}, parentContext), { logger: createLogger(Logger.Context.Default, getMetadataForLogger(combinedMetadata)), metadata: combinedMetadata, metric: function (event, payload, extraMetadata) { + parentContext.metric(event, payload, __assign(__assign({}, combinedMetadata), extraMetadata)); + } }); + return context; + }, [sessionId, geolocation, parentContext, metadata]); + return _jsx(Context.Provider, { value: childContext, children: children }); +} +/** + * Feature gates provider. Decorates the parent analytics context with + * feature gate capabilities. Should be mounted within `AnalyticsContext`, + * and below the `` breaker in `App..tsx`. + */ +export function AnalyticsFeaturesContext(_a) { + var children = _a.children; + var parentContext = useContext(Context); + /** + * Side-effects: we need to synchronously set these during the same render + * cycle. These calls do not trigger re-renders, they just set properties on + * the singleton GrowthBook instance. + */ + setAttributes(parentContext.metadata); + feats.setTrackingCallback(function (experiment, result) { + parentContext.metric('experiment:viewed', { + experimentId: experiment.key, + variationId: result.key, + }); + }); + feats.setFeatureUsageCallback(function (feature, result) { + var _a, _b; + parentContext.metric('feature:viewed', { + featureId: feature, + featureResultValue: result.value, + experimentId: (_a = result.experiment) === null || _a === void 0 ? void 0 : _a.key, + variationId: (_b = result.experimentResult) === null || _b === void 0 ? void 0 : _b.key, + }); + }); + var childContext = useMemo(function () { + return __assign(__assign({}, parentContext), { features: __assign({ enabled: feats.isOn.bind(feats) }, Features) }); + }, [parentContext]); + return _jsx(Context.Provider, { value: childContext, children: children }); +} +/** + * Basic analytics context without feature gates. Should really only be used + * above the `AnalyticsFeaturesContext` provider. + */ +export function useAnalyticsBase() { + return useContext(Context); +} +/** + * The main analytics context, including feature gates. Use this everywhere you + * need metrics, features, or logging within the React tree. + */ +export function useAnalytics() { + var ctx = useContext(Context); + if (!('features' in ctx)) { + throw new Error('useAnalytics must be used within an AnalyticsFeaturesContext'); + } + return ctx; +} diff --git a/src/analytics/metadata.js b/src/analytics/metadata.js new file mode 100644 index 0000000000..67c46ea96b --- /dev/null +++ b/src/analytics/metadata.js @@ -0,0 +1,22 @@ +var navigationMetadata; +export function getNavigationMetadata() { + return navigationMetadata; +} +export function setNavigationMetadata(meta) { + navigationMetadata = meta; +} +/** + * We don't want or need to send all data to the logger + */ +export function getMetadataForLogger(_a) { + var base = _a.base, geolocation = _a.geolocation, session = _a.session; + return { + deviceId: base.deviceId, + sessionId: base.sessionId, + platform: base.platform, + appVersion: base.appVersion, + countryCode: geolocation.countryCode, + regionCode: geolocation.regionCode, + isBskyPds: (session === null || session === void 0 ? void 0 : session.isBskyPds) || 'anonymous', + }; +} diff --git a/src/analytics/metrics/client.js b/src/analytics/metrics/client.js new file mode 100644 index 0000000000..e0f1f94d92 --- /dev/null +++ b/src/analytics/metrics/client.js @@ -0,0 +1,156 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { onAppStateChange } from '#/lib/appState'; +import { isNetworkError } from '#/lib/strings/errors'; +import { Logger } from '#/logger'; +import * as env from '#/env'; +var TRACKING_ENDPOINT = env.METRICS_API_HOST + '/t'; +var logger = Logger.create(Logger.Context.Metric, {}); +var MetricsClient = /** @class */ (function () { + function MetricsClient() { + this.maxBatchSize = 100; + this.started = false; + this.queue = []; + this.failedQueue = []; + this.flushInterval = null; + } + MetricsClient.prototype.start = function () { + var _this = this; + if (this.started) + return; + this.started = true; + this.flushInterval = setInterval(function () { + _this.flush(); + }, 10000); + onAppStateChange(function (state) { + if (state === 'active') { + _this.retryFailedLogs(); + } + else { + _this.flush(); + } + }); + }; + MetricsClient.prototype.track = function (event, payload, metadata) { + if (metadata === void 0) { metadata = {}; } + this.start(); + var e = { + source: 'app', + time: Date.now(), + event: event, + payload: payload, + metadata: metadata, + }; + this.queue.push(e); + logger.debug("event: ".concat(e.event), e); + if (this.queue.length > this.maxBatchSize) { + this.flush(); + } + }; + MetricsClient.prototype.flush = function () { + if (!this.queue.length) + return; + var events = this.queue.splice(0, this.queue.length); + this.sendBatch(events); + }; + MetricsClient.prototype.sendBatch = function (events_1) { + return __awaiter(this, arguments, void 0, function (events, isRetry) { + var body, success, res, error, e_1; + var _a; + if (isRetry === void 0) { isRetry = false; } + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + logger.debug("sendBatch: ".concat(events.length), { + isRetry: isRetry, + }); + _b.label = 1; + case 1: + _b.trys.push([1, 6, , 7]); + body = JSON.stringify({ events: events }); + if (!(env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon)) return [3 /*break*/, 2]; + success = navigator.sendBeacon(TRACKING_ENDPOINT, new Blob([body], { type: 'application/json' })); + if (!success) { + // construct a "network error" for `isNetworkError` to work + throw new Error("Failed to fetch: sendBeacon returned false"); + } + return [3 /*break*/, 5]; + case 2: return [4 /*yield*/, fetch(TRACKING_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ events: events }), + keepalive: true, + })]; + case 3: + res = _b.sent(); + if (!!res.ok) return [3 /*break*/, 5]; + return [4 /*yield*/, res.text().catch(function () { return 'Unknown error'; }) + // construct a "network error" for `isNetworkError` to work + ]; + case 4: + error = _b.sent(); + // construct a "network error" for `isNetworkError` to work + throw new Error("".concat(res.status, " Failed to fetch \u2014 ").concat(error)); + case 5: return [3 /*break*/, 7]; + case 6: + e_1 = _b.sent(); + if (isNetworkError(e_1)) { + if (isRetry) + return [2 /*return*/]; // retry once + (_a = this.failedQueue).push.apply(_a, events); + return [2 /*return*/]; + } + logger.error("Failed to send metrics", { + safeMessage: e_1.toString(), + }); + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); + }; + MetricsClient.prototype.retryFailedLogs = function () { + if (!this.failedQueue.length) + return; + var events = this.failedQueue.splice(0, this.failedQueue.length); + this.sendBatch(events, true); + }; + return MetricsClient; +}()); +export { MetricsClient }; diff --git a/src/analytics/metrics/client.test.js b/src/analytics/metrics/client.test.js new file mode 100644 index 0000000000..71d42478ae --- /dev/null +++ b/src/analytics/metrics/client.test.js @@ -0,0 +1,235 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { MetricsClient } from './client'; +var appStateCallback; +jest.mock('#/lib/appState', function () { return ({ + onAppStateChange: jest.fn(function (cb) { + appStateCallback = cb; + return { remove: jest.fn() }; + }), +}); }); +jest.mock('#/logger', function () { return ({ + Logger: { + create: function () { return ({ + info: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }); }, + Context: { Metric: 'metric' }, + }, +}); }); +jest.mock('#/env', function () { return ({ + METRICS_API_HOST: 'https://test.metrics.api', + IS_WEB: false, +}); }); +describe('MetricsClient', function () { + var fetchMock; + var fetchRequests; + beforeEach(function () { + jest.useFakeTimers({ advanceTimers: true }); + fetchRequests = []; + fetchMock = jest.fn().mockImplementation(function (_url, options) { return __awaiter(void 0, void 0, void 0, function () { + var body; + return __generator(this, function (_a) { + body = JSON.parse(options.body); + fetchRequests.push({ body: body }); + return [2 /*return*/, { ok: true, status: 200 }]; + }); + }); }); + global.fetch = fetchMock; + }); + afterEach(function () { + jest.useRealTimers(); + jest.clearAllMocks(); + }); + it('flushes events on interval', function () { return __awaiter(void 0, void 0, void 0, function () { + var client; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + client = new MetricsClient(); + client.track('click', { button: 'submit' }); + client.track('view', { screen: 'home' }); + expect(fetchRequests).toHaveLength(0); + // Advance past the 10 second interval + return [4 /*yield*/, jest.advanceTimersByTimeAsync(10000)]; + case 1: + // Advance past the 10 second interval + _a.sent(); + expect(fetchRequests).toHaveLength(1); + expect(fetchRequests[0].body.events).toHaveLength(2); + expect(fetchRequests[0].body.events[0].event).toBe('click'); + expect(fetchRequests[0].body.events[1].event).toBe('view'); + return [2 /*return*/]; + } + }); + }); }); + it('flushes when maxBatchSize is exceeded', function () { return __awaiter(void 0, void 0, void 0, function () { + var client, i; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + client = new MetricsClient(); + client.maxBatchSize = 5; + // Add events up to maxBatchSize (should not flush yet) + for (i = 0; i < 5; i++) { + client.track('click', { button: "btn-".concat(i) }); + } + expect(fetchRequests).toHaveLength(0); + // One more event should trigger flush (> maxBatchSize) + client.track('click', { button: 'btn-trigger' }); + // Allow microtasks to run + return [4 /*yield*/, jest.advanceTimersByTimeAsync(0)]; + case 1: + // Allow microtasks to run + _a.sent(); + expect(fetchRequests).toHaveLength(1); + expect(fetchRequests[0].body.events).toHaveLength(6); + return [2 /*return*/]; + } + }); + }); }); + it('retries failed events once on 500 response', function () { return __awaiter(void 0, void 0, void 0, function () { + var requestCount, client; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + requestCount = 0; + fetchMock.mockImplementation(function (_url, options) { return __awaiter(void 0, void 0, void 0, function () { + var body; + return __generator(this, function (_a) { + requestCount++; + body = JSON.parse(options.body); + if (requestCount === 1) { + // First request fails with 500 - "Failed to fetch" triggers isNetworkError + return [2 /*return*/, { + ok: false, + status: 500, + text: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/, 'Internal Server Error']; + }); }); }, + }]; + } + // Retry succeeds + fetchRequests.push({ body: body }); + return [2 /*return*/, { ok: true, status: 200 }]; + }); + }); }); + client = new MetricsClient(); + client.track('click', { button: 'submit' }); + // Trigger flush via interval + return [4 /*yield*/, jest.advanceTimersByTimeAsync(10000)]; + case 1: + // Trigger flush via interval + _a.sent(); + expect(requestCount).toBe(1); + expect(fetchRequests).toHaveLength(0); + // Simulate app coming to foreground to trigger retry + appStateCallback('active'); + return [4 /*yield*/, jest.advanceTimersByTimeAsync(0)]; + case 2: + _a.sent(); + expect(requestCount).toBe(2); + expect(fetchRequests).toHaveLength(1); + expect(fetchRequests[0].body.events).toHaveLength(1); + expect(fetchRequests[0].body.events[0].event).toBe('click'); + return [2 /*return*/]; + } + }); + }); }); + it('does not retry more than once', function () { return __awaiter(void 0, void 0, void 0, function () { + var requestCount, client; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + requestCount = 0; + fetchMock.mockImplementation(function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + requestCount++; + // Always fail with network-like error + return [2 /*return*/, { + ok: false, + status: 500, + text: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/, 'Internal Server Error']; + }); }); }, + }]; + }); + }); }); + client = new MetricsClient(); + client.track('click', { button: 'submit' }); + // First flush fails + return [4 /*yield*/, jest.advanceTimersByTimeAsync(10000)]; + case 1: + // First flush fails + _a.sent(); + expect(requestCount).toBe(1); + // Retry also fails + appStateCallback('active'); + return [4 /*yield*/, jest.advanceTimersByTimeAsync(0)]; + case 2: + _a.sent(); + expect(requestCount).toBe(2); + // Another foreground event should not retry again (events are dropped) + appStateCallback('active'); + return [4 /*yield*/, jest.advanceTimersByTimeAsync(0)]; + case 3: + _a.sent(); + expect(requestCount).toBe(2); // No additional requests + return [2 /*return*/]; + } + }); + }); }); + it('flushes when app goes to background', function () { return __awaiter(void 0, void 0, void 0, function () { + var client; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + client = new MetricsClient(); + client.track('click', { button: 'submit' }); + expect(fetchRequests).toHaveLength(0); + // Simulate app going to background + appStateCallback('background'); + return [4 /*yield*/, jest.advanceTimersByTimeAsync(0)]; + case 1: + _a.sent(); + expect(fetchRequests).toHaveLength(1); + return [2 /*return*/]; + } + }); + }); }); +}); diff --git a/src/analytics/metrics/index.js b/src/analytics/metrics/index.js new file mode 100644 index 0000000000..8f410403f7 --- /dev/null +++ b/src/analytics/metrics/index.js @@ -0,0 +1,3 @@ +import { MetricsClient } from '#/analytics/metrics/client'; +export * from '#/analytics/metrics/utils'; +export var metrics = new MetricsClient(); diff --git a/src/analytics/metrics/types.js b/src/analytics/metrics/types.js new file mode 100644 index 0000000000..84b434d454 --- /dev/null +++ b/src/analytics/metrics/types.js @@ -0,0 +1,4 @@ +/* + * Do not import runtime code into this file + */ +export {}; diff --git a/src/analytics/metrics/utils.js b/src/analytics/metrics/utils.js new file mode 100644 index 0000000000..1a44a2c357 --- /dev/null +++ b/src/analytics/metrics/utils.js @@ -0,0 +1,8 @@ +export function toClout(n) { + if (n == null) { + return undefined; + } + else { + return Math.max(0, Math.round(Math.log(n))); + } +} diff --git a/src/analytics/misc/refParams.js b/src/analytics/misc/refParams.js new file mode 100644 index 0000000000..b97dade168 --- /dev/null +++ b/src/analytics/misc/refParams.js @@ -0,0 +1,16 @@ +/** + * This is used for our own Bluesky post embeds, and maybe other things. + * + * In the case of our embeds, `ref_src=embed`. Not sure if `ref_url` is used. + */ +var _a, _b; +import * as env from '#/env'; +var refSrc = ''; +var refUrl = ''; +if (env.IS_WEB) { + var params = new URLSearchParams(window.location.search); + refSrc = (_a = params.get('ref_src')) !== null && _a !== void 0 ? _a : ''; + refUrl = decodeURIComponent((_b = params.get('ref_url')) !== null && _b !== void 0 ? _b : ''); +} +export var src = refSrc; +export var url = refUrl; diff --git a/src/analytics/utils.js b/src/analytics/utils.js new file mode 100644 index 0000000000..1e5c78abc6 --- /dev/null +++ b/src/analytics/utils.js @@ -0,0 +1,25 @@ +import { useMemo } from 'react'; +import { BSKY_SERVICE } from '#/lib/constants'; +/** + * Thin `useMemo` wrapper that marks the metadata as memoized and provides a + * type guard. + */ +export function useMeta(metadata) { + var m = useMemo(function () { return metadata; }, [metadata]); + if (!m) + return; + // @ts-ignore + m.__meta = true; + return m; +} +export function accountToSessionMetadata(account) { + if (!account) { + return; + } + else { + return { + did: account.did, + isBskyPds: account.service.startsWith(BSKY_SERVICE), + }; + } +} diff --git a/src/components/AccountList.js b/src/components/AccountList.js new file mode 100644 index 0000000000..381b914674 --- /dev/null +++ b/src/components/AccountList.js @@ -0,0 +1,104 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React, { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { isJwtExpired } from '#/lib/jwt'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useProfilesQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { CheckThick_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon } from '#/components/icons/Chevron'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +import { useActorStatus } from '#/features/liveNow'; +export function AccountList(_a) { + var onSelectAccount = _a.onSelectAccount, onSelectOther = _a.onSelectOther, otherLabel = _a.otherLabel, pendingDid = _a.pendingDid; + var _b = useSession(), currentAccount = _b.currentAccount, accounts = _b.accounts; + var t = useTheme(); + var _ = useLingui()._; + var profiles = useProfilesQuery({ + handles: accounts.map(function (acc) { return acc.did; }), + }).data; + var onPressAddAccount = useCallback(function () { + onSelectOther(); + }, [onSelectOther]); + return (_jsxs(View, { pointerEvents: pendingDid ? 'none' : 'auto', style: [ + a.rounded_lg, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + ], children: [accounts.map(function (account) { return (_jsxs(React.Fragment, { children: [_jsx(AccountItem, { profile: profiles === null || profiles === void 0 ? void 0 : profiles.profiles.find(function (p) { return p.did === account.did; }), account: account, onSelect: onSelectAccount, isCurrentAccount: account.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did), isPendingAccount: account.did === pendingDid }), _jsx(View, { style: [a.border_b, t.atoms.border_contrast_low] })] }, account.did)); }), _jsx(Button, { testID: "chooseAddAccountBtn", style: [a.flex_1], onPress: pendingDid ? undefined : onPressAddAccount, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sign in to account that is not listed"], ["Sign in to account that is not listed"])))), children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.align_center, + a.p_lg, + a.gap_sm, + (hovered || pressed) && t.atoms.bg_contrast_25, + ], children: [_jsx(View, { style: [ + t.atoms.bg_contrast_25, + a.rounded_full, + { width: 48, height: 48 }, + a.justify_center, + a.align_center, + (hovered || pressed) && t.atoms.bg_contrast_50, + ], children: _jsx(PlusIcon, { style: [t.atoms.text_contrast_low], size: "md" }) }), _jsx(Text, { style: [a.flex_1, a.leading_tight, a.text_md, a.font_medium], children: otherLabel !== null && otherLabel !== void 0 ? otherLabel : _jsx(Trans, { children: "Other account" }) }), _jsx(ChevronIcon, { size: "md", style: [t.atoms.text_contrast_low] })] })); + } })] })); +} +function AccountItem(_a) { + var profile = _a.profile, account = _a.account, onSelect = _a.onSelect, isCurrentAccount = _a.isCurrentAccount, isPendingAccount = _a.isPendingAccount; + var t = useTheme(); + var _ = useLingui()._; + var verification = useSimpleVerificationState({ profile: profile }); + var live = useActorStatus(profile).isActive; + var onPress = useCallback(function () { + onSelect(account); + }, [account, onSelect]); + var isLoggedOut = !account.refreshJwt || isJwtExpired(account.refreshJwt); + return (_jsx(Button, { testID: "chooseAccountBtn-".concat(account.handle), style: [a.w_full], onPress: onPress, label: isCurrentAccount + ? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Continue as ", " (currently signed in)"], ["Continue as ", " (currently signed in)"])), account.handle)) + : _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Sign in as ", ""], ["Sign in as ", ""])), account.handle)), children: function (_a) { + var _b; + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.align_center, + a.p_lg, + a.gap_sm, + (hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25, + ], children: [_jsx(UserAvatar, { avatar: profile === null || profile === void 0 ? void 0 : profile.avatar, size: 48, type: ((_b = profile === null || profile === void 0 ? void 0 : profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', live: live, hideLiveBadge: true }), _jsxs(View, { style: [a.flex_1, a.gap_2xs, a.pr_2xl], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(Text, { emoji: true, style: [a.font_medium, a.leading_tight, a.text_md], numberOfLines: 1, children: sanitizeDisplayName((profile === null || profile === void 0 ? void 0 : profile.displayName) || (profile === null || profile === void 0 ? void 0 : profile.handle) || account.handle) }), verification.showBadge && (_jsx(View, { children: _jsx(VerificationCheck, { width: 12, verifier: verification.role === 'verifier' }) }))] }), _jsx(Text, { style: [ + a.leading_tight, + t.atoms.text_contrast_medium, + a.text_sm, + ], children: sanitizeHandle(account.handle, '@') }), isLoggedOut && (_jsx(Text, { style: [ + a.leading_tight, + a.text_xs, + a.italic, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Logged out" }) }))] }), isCurrentAccount ? (_jsx(View, { style: [ + { + width: 20, + height: 20, + backgroundColor: t.palette.positive_500, + }, + a.rounded_full, + a.justify_center, + a.align_center, + ], children: _jsx(CheckIcon, { size: "xs", style: [{ color: t.palette.white }] }) })) : (_jsx(ChevronIcon, { size: "md", style: [t.atoms.text_contrast_low] }))] })); + } }, account.did)); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/Admonition.js b/src/components/Admonition.js new file mode 100644 index 0000000000..1b07e13d7d --- /dev/null +++ b/src/components/Admonition.js @@ -0,0 +1,96 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { createContext, useContext } from 'react'; +import { View } from 'react-native'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button as BaseButton } from '#/components/Button'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon } from '#/components/icons/CircleInfo'; +import { CircleX_Stroke2_Corner0_Rounded as CircleXIcon } from '#/components/icons/CircleX'; +import { Warning_Stroke2_Corner0_Rounded as WarningIcon } from '#/components/icons/Warning'; +import { Text as BaseText } from '#/components/Typography'; +import { EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon } from './icons/Emoji'; +var Context = createContext({ + type: 'info', +}); +Context.displayName = 'AdmonitionContext'; +export function Icon() { + var t = useTheme(); + var type = useContext(Context).type; + var Icon = { + info: CircleInfoIcon, + tip: CircleInfoIcon, + warning: WarningIcon, + error: CircleXIcon, + apology: EmojiSadIcon, + }[type]; + var fill = { + info: t.atoms.text_contrast_medium.color, + tip: t.palette.primary_500, + warning: t.palette.yellow, + error: t.palette.negative_500, + apology: t.atoms.text_contrast_medium.color, + }[type]; + return _jsx(Icon, { fill: fill, size: "md" }); +} +export function Content(_a) { + var children = _a.children, style = _a.style, rest = __rest(_a, ["children", "style"]); + return (_jsx(View, __assign({ style: [a.gap_sm, a.flex_1, { minHeight: 20 }, a.justify_center, style] }, rest, { children: children }))); +} +export function Text(_a) { + var children = _a.children, style = _a.style, rest = __rest(_a, ["children", "style"]); + return (_jsx(BaseText, __assign({}, rest, { style: [a.text_sm, a.leading_snug, a.pr_md, style], children: children }))); +} +export function Button(_a) { + var children = _a.children, props = __rest(_a, ["children"]); + return (_jsx(BaseButton, __assign({ size: "tiny" }, props, { children: children }))); +} +export function Row(_a) { + var children = _a.children, style = _a.style; + return (_jsx(View, { style: [a.w_full, a.flex_row, a.align_start, a.gap_sm, style], children: children })); +} +export function Outer(_a) { + var children = _a.children, _b = _a.type, type = _b === void 0 ? 'info' : _b, style = _a.style; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var borderColor = { + info: t.atoms.border_contrast_high.borderColor, + tip: t.palette.primary_500, + warning: t.palette.yellow, + error: t.palette.negative_500, + apology: t.atoms.border_contrast_high.borderColor, + }[type]; + return (_jsx(Context.Provider, { value: { type: type }, children: _jsx(View, { style: [ + gtMobile ? a.p_md : a.p_sm, + a.p_md, + a.rounded_sm, + a.border, + t.atoms.bg, + { borderColor: borderColor }, + style, + ], children: children }) })); +} +export function Admonition(_a) { + var children = _a.children, type = _a.type, style = _a.style; + return (_jsx(Outer, { type: type, style: style, children: _jsxs(Row, { children: [_jsx(Icon, {}), _jsx(Content, { children: _jsx(Text, { children: children }) })] }) })); +} diff --git a/src/components/AppLanguageDropdown.js b/src/components/AppLanguageDropdown.js new file mode 100644 index 0000000000..5cada04aeb --- /dev/null +++ b/src/components/AppLanguageDropdown.js @@ -0,0 +1,65 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { sanitizeAppLanguageSetting } from '#/locale/helpers'; +import { APP_LANGUAGES } from '#/locale/languages'; +import { useLanguagePrefs, useLanguagePrefsApi } from '#/state/preferences'; +import { resetPostsFeedQueries } from '#/state/queries/post-feed'; +import { atoms as a, platform, useTheme } from '#/alf'; +import * as Select from '#/components/Select'; +import { Button } from './Button'; +export function AppLanguageDropdown() { + var t = useTheme(); + var _ = useLingui()._; + var queryClient = useQueryClient(); + var langPrefs = useLanguagePrefs(); + var setLangPrefs = useLanguagePrefsApi(); + var sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage); + var onChangeAppLanguage = React.useCallback(function (value) { + if (!value) + return; + if (sanitizedLang !== value) { + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)); + } + // reset feeds to refetch content + resetPostsFeedQueries(queryClient); + }, [sanitizedLang, setLangPrefs, queryClient]); + return (_jsxs(Select.Root, { value: sanitizeAppLanguageSetting(langPrefs.appLanguage), onValueChange: onChangeAppLanguage, children: [_jsx(Select.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Change app language"], ["Change app language"])))), children: function (_a) { + var props = _a.props; + return (_jsxs(Button, __assign({}, props, { label: props.accessibilityLabel, size: platform({ + web: 'tiny', + native: 'small', + }), variant: "ghost", color: "secondary", shape: "rectangular", style: [ + a.pr_xs, + a.pl_sm, + platform({ + web: [{ alignSelf: 'flex-start' }, a.gap_sm], + native: [a.gap_xs], + }), + ], children: [_jsx(Select.ValueText, { placeholder: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Select an app language"], ["Select an app language"])))), style: [t.atoms.text_contrast_medium] }), _jsx(Select.Icon, { style: [t.atoms.text_contrast_medium] })] }))); + } }), _jsx(Select.Content, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Select language"], ["Select language"])))), renderItem: function (_a) { + var label = _a.label, value = _a.value; + return (_jsxs(Select.Item, { value: value, label: label, children: [_jsx(Select.ItemIndicator, {}), _jsx(Select.ItemText, { children: label })] })); + }, items: APP_LANGUAGES.map(function (l) { return ({ + label: l.name, + value: l.code2, + }); }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/AvatarStack.js b/src/components/AvatarStack.js new file mode 100644 index 0000000000..d3ac3a6327 --- /dev/null +++ b/src/components/AvatarStack.js @@ -0,0 +1,60 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { moderateProfile } from '@atproto/api'; +import { logger } from '#/logger'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useProfilesQuery } from '#/state/queries/profile'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme } from '#/alf'; +export function AvatarStack(_a) { + var profiles = _a.profiles, _b = _a.size, size = _b === void 0 ? 26 : _b, numPending = _a.numPending, backgroundColor = _a.backgroundColor; + var translation = size / 3; // overlap by 1/3 + var t = useTheme(); + var moderationOpts = useModerationOpts(); + var isPending = (numPending && profiles.length === 0) || !moderationOpts; + var items = isPending + ? Array.from({ length: numPending !== null && numPending !== void 0 ? numPending : profiles.length }).map(function (_, i) { return ({ + key: i, + profile: null, + moderation: null, + }); }) + : profiles.map(function (item) { return ({ + key: item.did, + profile: item, + moderation: moderateProfile(item, moderationOpts), + }); }); + return (_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.relative, + { width: size + (items.length - 1) * (size - translation) }, + ], children: items.map(function (item, i) { + var _a; + return (_jsx(View, { style: [ + t.atoms.bg_contrast_25, + a.relative, + { + width: size, + height: size, + left: i * -translation, + borderWidth: 1, + borderColor: backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : t.atoms.bg.backgroundColor, + borderRadius: 999, + zIndex: 3 - i, + }, + ], children: item.profile && (_jsx(UserAvatar, { size: size - 2, avatar: item.profile.avatar, type: ((_a = item.profile.associated) === null || _a === void 0 ? void 0 : _a.labeler) ? 'labeler' : 'user', moderation: item.moderation.ui('avatar') })) }, item.key)); + }) })); +} +export function AvatarStackWithFetch(_a) { + var profiles = _a.profiles, size = _a.size, backgroundColor = _a.backgroundColor; + var _b = useProfilesQuery({ handles: profiles }), data = _b.data, error = _b.error; + if (error) { + if (error.name !== 'AbortError') { + logger.error('Error fetching profiles for AvatarStack', { + safeMessage: error, + }); + } + return null; + } + return (_jsx(AvatarStack, { numPending: profiles.length, profiles: (data === null || data === void 0 ? void 0 : data.profiles) || [], size: size, backgroundColor: backgroundColor })); +} diff --git a/src/components/Button.js b/src/components/Button.js new file mode 100644 index 0000000000..c6dbe58151 --- /dev/null +++ b/src/components/Button.js @@ -0,0 +1,752 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Pressable, View, } from 'react-native'; +import { atoms as a, flatten, select, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +var Context = React.createContext({ + hovered: false, + focused: false, + pressed: false, + disabled: false, +}); +Context.displayName = 'ButtonContext'; +export function useButtonContext() { + return React.useContext(Context); +} +export var Button = React.forwardRef(function (_a, ref) { + var children = _a.children, variant = _a.variant, color = _a.color, size = _a.size, _b = _a.shape, shape = _b === void 0 ? 'default' : _b, label = _a.label, _c = _a.disabled, disabled = _c === void 0 ? false : _c, style = _a.style, hoverStyleProp = _a.hoverStyle, _d = _a.PressableComponent, PressableComponent = _d === void 0 ? Pressable : _d, onPressInOuter = _a.onPressIn, onPressOutOuter = _a.onPressOut, onHoverInOuter = _a.onHoverIn, onHoverOutOuter = _a.onHoverOut, onFocusOuter = _a.onFocus, onBlurOuter = _a.onBlur, rest = __rest(_a, ["children", "variant", "color", "size", "shape", "label", "disabled", "style", "hoverStyle", "PressableComponent", "onPressIn", "onPressOut", "onHoverIn", "onHoverOut", "onFocus", "onBlur"]); + /** + * The `variant` prop is deprecated in favor of simply specifying `color`. + * If a `color` is set, then we want to use the existing codepaths for + * "solid" buttons. This is to maintain backwards compatibility. + */ + if (!variant && color) { + variant = 'solid'; + } + var t = useTheme(); + var _e = React.useState({ + pressed: false, + hovered: false, + focused: false, + }), state = _e[0], setState = _e[1]; + var onPressIn = React.useCallback(function (e) { + setState(function (s) { return (__assign(__assign({}, s), { pressed: true })); }); + onPressInOuter === null || onPressInOuter === void 0 ? void 0 : onPressInOuter(e); + }, [setState, onPressInOuter]); + var onPressOut = React.useCallback(function (e) { + setState(function (s) { return (__assign(__assign({}, s), { pressed: false })); }); + onPressOutOuter === null || onPressOutOuter === void 0 ? void 0 : onPressOutOuter(e); + }, [setState, onPressOutOuter]); + var onHoverIn = React.useCallback(function (e) { + setState(function (s) { return (__assign(__assign({}, s), { hovered: true })); }); + onHoverInOuter === null || onHoverInOuter === void 0 ? void 0 : onHoverInOuter(e); + }, [setState, onHoverInOuter]); + var onHoverOut = React.useCallback(function (e) { + setState(function (s) { return (__assign(__assign({}, s), { hovered: false })); }); + onHoverOutOuter === null || onHoverOutOuter === void 0 ? void 0 : onHoverOutOuter(e); + }, [setState, onHoverOutOuter]); + var onFocus = React.useCallback(function (e) { + setState(function (s) { return (__assign(__assign({}, s), { focused: true })); }); + onFocusOuter === null || onFocusOuter === void 0 ? void 0 : onFocusOuter(e); + }, [setState, onFocusOuter]); + var onBlur = React.useCallback(function (e) { + setState(function (s) { return (__assign(__assign({}, s), { focused: false })); }); + onBlurOuter === null || onBlurOuter === void 0 ? void 0 : onBlurOuter(e); + }, [setState, onBlurOuter]); + var _f = React.useMemo(function () { + var baseStyles = []; + var hoverStyles = []; + /* + * This is the happy path for new button styles, following the + * deprecation of `variant` prop. This redundant `variant` check is here + * just to make this handling easier to understand. + */ + if (variant === 'solid') { + if (color === 'primary') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.primary_500, + }); + hoverStyles.push({ + backgroundColor: t.palette.primary_600, + }); + } + else { + baseStyles.push({ + backgroundColor: t.palette.primary_200, + }); + } + } + else if (color === 'secondary') { + if (!disabled) { + baseStyles.push(t.atoms.bg_contrast_50); + hoverStyles.push(t.atoms.bg_contrast_100); + } + else { + baseStyles.push(t.atoms.bg_contrast_50); + } + } + else if (color === 'secondary_inverted') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.contrast_900, + }); + hoverStyles.push({ + backgroundColor: t.palette.contrast_975, + }); + } + else { + baseStyles.push({ + backgroundColor: t.palette.contrast_600, + }); + } + } + else if (color === 'negative') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.negative_500, + }); + hoverStyles.push({ + backgroundColor: t.palette.negative_600, + }); + } + else { + baseStyles.push({ + backgroundColor: t.palette.negative_700, + }); + } + } + else if (color === 'primary_subtle') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.primary_50, + }); + hoverStyles.push({ + backgroundColor: t.palette.primary_100, + }); + } + else { + baseStyles.push({ + backgroundColor: t.palette.primary_50, + }); + } + } + else if (color === 'negative_subtle') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.negative_50, + }); + hoverStyles.push({ + backgroundColor: t.palette.negative_100, + }); + } + else { + baseStyles.push({ + backgroundColor: t.palette.negative_50, + }); + } + } + } + else { + /* + * BEGIN DEPRECATED STYLES + */ + if (color === 'primary') { + if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }); + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.primary_500, + }); + hoverStyles.push(a.border, { + backgroundColor: t.palette.primary_50, + }); + } + else { + baseStyles.push(a.border, { + borderColor: t.palette.primary_200, + }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg); + hoverStyles.push({ + backgroundColor: t.palette.primary_100, + }); + } + } + } + else if (color === 'secondary') { + if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }); + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_300, + }); + hoverStyles.push(t.atoms.bg_contrast_50); + } + else { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_200, + }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg); + hoverStyles.push({ + backgroundColor: t.palette.contrast_50, + }); + } + } + } + else if (color === 'secondary_inverted') { + if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }); + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_300, + }); + hoverStyles.push(t.atoms.bg_contrast_50); + } + else { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_200, + }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg); + hoverStyles.push({ + backgroundColor: t.palette.contrast_50, + }); + } + } + } + else if (color === 'negative') { + if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }); + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.negative_500, + }); + hoverStyles.push(a.border, { + backgroundColor: t.palette.negative_50, + }); + } + else { + baseStyles.push(a.border, { + borderColor: t.palette.negative_200, + }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg); + hoverStyles.push({ + backgroundColor: t.palette.negative_100, + }); + } + } + } + else if (color === 'negative_subtle') { + if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }); + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.negative_500, + }); + hoverStyles.push(a.border, { + backgroundColor: t.palette.negative_50, + }); + } + else { + baseStyles.push(a.border, { + borderColor: t.palette.negative_200, + }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg); + hoverStyles.push({ + backgroundColor: t.palette.negative_100, + }); + } + } + } + /* + * END DEPRECATED STYLES + */ + } + if (shape === 'default') { + if (size === 'large') { + baseStyles.push(a.rounded_full, { + paddingVertical: 12, + paddingHorizontal: 24, + gap: 6, + }); + } + else if (size === 'small') { + baseStyles.push(a.rounded_full, { + paddingVertical: 8, + paddingHorizontal: 14, + gap: 5, + }); + } + else if (size === 'tiny') { + baseStyles.push(a.rounded_full, { + paddingVertical: 5, + paddingHorizontal: 10, + gap: 3, + }); + } + } + else if (shape === 'rectangular') { + if (size === 'large') { + baseStyles.push({ + paddingVertical: 12, + paddingHorizontal: 25, + borderRadius: 10, + gap: 3, + }); + } + else if (size === 'small') { + baseStyles.push({ + paddingVertical: 8, + paddingHorizontal: 13, + borderRadius: 8, + gap: 3, + }); + } + else if (size === 'tiny') { + baseStyles.push({ + paddingVertical: 5, + paddingHorizontal: 9, + borderRadius: 6, + gap: 2, + }); + } + } + else if (shape === 'round' || shape === 'square') { + /* + * These sizes match the actual rendered size on screen, based on + * Chrome's web inspector + */ + if (size === 'large') { + if (shape === 'round') { + baseStyles.push({ height: 44, width: 44 }); + } + else { + baseStyles.push({ height: 44, width: 44 }); + } + } + else if (size === 'small') { + if (shape === 'round') { + baseStyles.push({ height: 33, width: 33 }); + } + else { + baseStyles.push({ height: 33, width: 33 }); + } + } + else if (size === 'tiny') { + if (shape === 'round') { + baseStyles.push({ height: 25, width: 25 }); + } + else { + baseStyles.push({ height: 25, width: 25 }); + } + } + if (shape === 'round') { + baseStyles.push(a.rounded_full); + } + else if (shape === 'square') { + if (size === 'tiny') { + baseStyles.push({ + borderRadius: 6, + }); + } + else { + baseStyles.push(a.rounded_sm); + } + } + } + return { + baseStyles: baseStyles, + hoverStyles: hoverStyles, + }; + }, [t, variant, color, size, shape, disabled]), baseStyles = _f.baseStyles, hoverStyles = _f.hoverStyles; + var context = React.useMemo(function () { return (__assign(__assign({}, state), { variant: variant, color: color, size: size, shape: shape, disabled: disabled || false })); }, [state, variant, color, size, shape, disabled]); + return (_jsx(PressableComponent, __assign({ role: "button", accessibilityHint: undefined }, rest, { + // @ts-ignore - this will always be a pressable + ref: ref, "aria-label": label, "aria-pressed": state.pressed, accessibilityLabel: label, disabled: disabled || false, accessibilityState: { + disabled: disabled || false, + }, style: __spreadArray([ + a.flex_row, + a.align_center, + a.justify_center, + a.curve_continuous, + baseStyles, + style + ], (state.hovered || state.pressed + ? [hoverStyles, hoverStyleProp] + : []), true), onPressIn: onPressIn, onPressOut: onPressOut, onHoverIn: onHoverIn, onHoverOut: onHoverOut, onFocus: onFocus, onBlur: onBlur, children: _jsx(Context.Provider, { value: context, children: typeof children === 'function' ? children(context) : children }) }))); +}); +Button.displayName = 'Button'; +export function useSharedButtonTextStyles() { + var t = useTheme(); + var _a = useButtonContext(), color = _a.color, variant = _a.variant, disabled = _a.disabled, size = _a.size; + return React.useMemo(function () { + var baseStyles = []; + /* + * This is the happy path for new button styles, following the + * deprecation of `variant` prop. This redundant `variant` check is here + * just to make this handling easier to understand. + */ + if (variant === 'solid') { + if (color === 'primary') { + if (!disabled) { + baseStyles.push({ color: t.palette.white }); + } + else { + baseStyles.push({ + color: select(t.name, { + light: t.palette.white, + dim: t.atoms.text_inverted.color, + dark: t.atoms.text_inverted.color, + }), + }); + } + } + else if (color === 'secondary') { + if (!disabled) { + baseStyles.push(t.atoms.text_contrast_medium); + } + else { + baseStyles.push({ + color: t.palette.contrast_300, + }); + } + } + else if (color === 'secondary_inverted') { + if (!disabled) { + baseStyles.push(t.atoms.text_inverted); + } + else { + baseStyles.push({ + color: t.palette.contrast_300, + }); + } + } + else if (color === 'negative') { + if (!disabled) { + baseStyles.push({ color: t.palette.white }); + } + else { + baseStyles.push({ color: t.palette.negative_300 }); + } + } + else if (color === 'primary_subtle') { + if (!disabled) { + baseStyles.push({ + color: t.palette.primary_600, + }); + } + else { + baseStyles.push({ + color: t.palette.primary_200, + }); + } + } + else if (color === 'negative_subtle') { + if (!disabled) { + baseStyles.push({ + color: t.palette.negative_600, + }); + } + else { + baseStyles.push({ + color: t.palette.negative_200, + }); + } + } + } + else { + /* + * BEGIN DEPRECATED STYLES + */ + if (color === 'primary') { + if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ + color: t.palette.primary_600, + }); + } + else { + baseStyles.push({ color: t.palette.primary_600, opacity: 0.5 }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({ color: t.palette.primary_600 }); + } + else { + baseStyles.push({ color: t.palette.primary_600, opacity: 0.5 }); + } + } + } + else if (color === 'secondary') { + if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ + color: t.palette.contrast_600, + }); + } + else { + baseStyles.push({ + color: t.palette.contrast_300, + }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({ + color: t.palette.contrast_600, + }); + } + else { + baseStyles.push({ + color: t.palette.contrast_300, + }); + } + } + } + else if (color === 'secondary_inverted') { + if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ + color: t.palette.contrast_600, + }); + } + else { + baseStyles.push({ + color: t.palette.contrast_300, + }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({ + color: t.palette.contrast_600, + }); + } + else { + baseStyles.push({ + color: t.palette.contrast_300, + }); + } + } + } + else if (color === 'negative') { + if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ color: t.palette.negative_400 }); + } + else { + baseStyles.push({ color: t.palette.negative_400, opacity: 0.5 }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({ color: t.palette.negative_400 }); + } + else { + baseStyles.push({ color: t.palette.negative_400, opacity: 0.5 }); + } + } + } + else if (color === 'negative_subtle') { + if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ color: t.palette.negative_400 }); + } + else { + baseStyles.push({ color: t.palette.negative_400, opacity: 0.5 }); + } + } + else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({ color: t.palette.negative_400 }); + } + else { + baseStyles.push({ color: t.palette.negative_400, opacity: 0.5 }); + } + } + } + /* + * END DEPRECATED STYLES + */ + } + if (size === 'large') { + baseStyles.push(a.text_md, a.leading_snug, a.font_medium); + } + else if (size === 'small') { + baseStyles.push(a.text_sm, a.leading_snug, a.font_medium); + } + else if (size === 'tiny') { + baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold); + } + return flatten(baseStyles); + }, [t, variant, color, size, disabled]); +} +export function ButtonText(_a) { + var children = _a.children, style = _a.style, rest = __rest(_a, ["children", "style"]); + var textStyles = useSharedButtonTextStyles(); + return (_jsx(Text, __assign({}, rest, { style: [a.text_center, textStyles, style], children: children }))); +} +export function ButtonIcon(_a) { + var Comp = _a.icon, size = _a.size; + var _b = useButtonContext(), buttonSize = _b.size, buttonShape = _b.shape; + var textStyles = useSharedButtonTextStyles(); + var _c = React.useMemo(function () { + /** + * Pre-set icon sizes for different button sizes + */ + var iconSizeShorthand = size !== null && size !== void 0 ? size : ({ + large: 'md', + small: 'sm', + tiny: 'xs', + }[buttonSize || 'small'] || 'sm'); + /* + * Copied here from icons/common.tsx so we can tweak if we need to, but + * also so that we can calculate transforms. + */ + var iconSize = { + xs: 12, + sm: 16, + md: 18, + lg: 24, + xl: 28, + '2xs': 8, + '2xl': 32, + '3xl': 40, + }[iconSizeShorthand]; + /* + * Goal here is to match rendered text size so that different size icons + * don't increase button size + */ + var iconContainerSize = { + large: 20, + small: 17, + tiny: 15, + }[buttonSize || 'small']; + /* + * The icon needs to be closer to the edge of the button than the text. Therefore + * we make the gap slightly too large, and then pull in the sides using negative margins. + */ + var iconNegativeMargin = 0; + if (buttonShape === 'default') { + iconNegativeMargin = { + large: -2, + small: -2, + tiny: -1, + }[buttonSize || 'small']; + } + return { + iconSize: iconSize, + iconContainerSize: iconContainerSize, + iconNegativeMargin: iconNegativeMargin, + }; + }, [buttonSize, buttonShape, size]), iconSize = _c.iconSize, iconContainerSize = _c.iconContainerSize, iconNegativeMargin = _c.iconNegativeMargin; + return (_jsx(View, { style: [ + a.z_20, + { + width: size === '2xs' ? 10 : iconContainerSize, + height: iconContainerSize, + marginLeft: iconNegativeMargin, + marginRight: iconNegativeMargin, + }, + ], children: _jsx(View, { style: [ + a.absolute, + { + width: iconSize, + height: iconSize, + top: '50%', + left: '50%', + transform: [ + { + translateX: (iconSize / 2) * -1, + }, + { + translateY: (iconSize / 2) * -1, + }, + ], + }, + ], children: _jsx(Comp, { width: iconSize, style: [ + { + color: textStyles.color, + pointerEvents: 'none', + }, + ] }) }) })); +} +export function StackedButton(_a) { + var children = _a.children, props = __rest(_a, ["children"]); + return (_jsx(Button, __assign({}, props, { size: "tiny", style: [ + a.flex_col, + { + height: 72, + paddingHorizontal: 16, + borderRadius: 20, + gap: 4, + }, + props.style, + ], children: _jsx(StackedButtonInnerText, { icon: props.icon, children: children }) }))); +} +function StackedButtonInnerText(_a) { + var children = _a.children, Icon = _a.icon; + var textStyles = useSharedButtonTextStyles(); + return (_jsxs(_Fragment, { children: [_jsx(Icon, { width: 24, fill: textStyles.color }), _jsx(ButtonText, { children: children })] })); +} diff --git a/src/components/ContextMenu/Backdrop.ios.js b/src/components/ContextMenu/Backdrop.ios.js new file mode 100644 index 0000000000..9230e227e8 --- /dev/null +++ b/src/components/ContextMenu/Backdrop.ios.js @@ -0,0 +1,51 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { Pressable } from 'react-native'; +import Animated, { Extrapolation, interpolate, useAnimatedProps, useAnimatedStyle, } from 'react-native-reanimated'; +import { BlurView } from 'expo-blur'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +import { useContextMenuContext } from './context'; +var AnimatedBlurView = Animated.createAnimatedComponent(BlurView); +export function Backdrop(props) { + var mode = useContextMenuContext().mode; + switch (mode) { + case 'full': + return _jsx(BlurredBackdrop, __assign({}, props)); + case 'auxiliary-only': + return _jsx(OpacityBackdrop, __assign({}, props)); + } +} +function BlurredBackdrop(_a) { + var animation = _a.animation, _b = _a.intensity, intensity = _b === void 0 ? 50 : _b, onPress = _a.onPress; + var _ = useLingui()._; + var animatedProps = useAnimatedProps(function () { return ({ + intensity: interpolate(animation.get(), [0, 1], [0, intensity], Extrapolation.CLAMP), + }); }); + return (_jsx(AnimatedBlurView, { animatedProps: animatedProps, style: [a.absolute, a.inset_0], tint: "systemMaterialDark", children: _jsx(Pressable, { style: a.flex_1, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close menu"], ["Close menu"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Tap to close context menu"], ["Tap to close context menu"])))), onPress: onPress }) })); +} +function OpacityBackdrop(_a) { + var animation = _a.animation, onPress = _a.onPress; + var t = useTheme(); + var _ = useLingui()._; + var animatedStyle = useAnimatedStyle(function () { return ({ + opacity: interpolate(animation.get(), [0, 1], [0, 0.05], Extrapolation.CLAMP), + }); }); + return (_jsx(Animated.View, { style: [a.absolute, a.inset_0, t.atoms.bg_contrast_975, animatedStyle], children: _jsx(Pressable, { style: a.flex_1, accessibilityLabel: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Close menu"], ["Close menu"])))), accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Tap to close context menu"], ["Tap to close context menu"])))), onPress: onPress }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/ContextMenu/Backdrop.js b/src/components/ContextMenu/Backdrop.js new file mode 100644 index 0000000000..49ffac95cb --- /dev/null +++ b/src/components/ContextMenu/Backdrop.js @@ -0,0 +1,24 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { Pressable } from 'react-native'; +import Animated, { Extrapolation, interpolate, useAnimatedStyle, } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +import { useContextMenuContext } from './context'; +export function Backdrop(_a) { + var animation = _a.animation, _b = _a.intensity, intensity = _b === void 0 ? 50 : _b, onPress = _a.onPress; + var t = useTheme(); + var _ = useLingui()._; + var mode = useContextMenuContext().mode; + var reduced = mode === 'auxiliary-only'; + var target = reduced ? 0.05 : intensity / 100; + var animatedStyle = useAnimatedStyle(function () { return ({ + opacity: interpolate(animation.get(), [0, 1], [0, target], Extrapolation.CLAMP), + }); }); + return (_jsx(Animated.View, { style: [a.absolute, a.inset_0, t.atoms.bg_contrast_975, animatedStyle], children: _jsx(Pressable, { style: a.flex_1, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close menu"], ["Close menu"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Tap to close context menu"], ["Tap to close context menu"])))), onPress: onPress }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/ContextMenu/context.js b/src/components/ContextMenu/context.js new file mode 100644 index 0000000000..cc46fac3be --- /dev/null +++ b/src/components/ContextMenu/context.js @@ -0,0 +1,28 @@ +import { createContext, useContext } from 'react'; +export var Context = createContext(null); +Context.displayName = 'ContextMenuContext'; +export var MenuContext = createContext(null); +MenuContext.displayName = 'ContextMenuMenuContext'; +export var ItemContext = createContext(null); +ItemContext.displayName = 'ContextMenuItemContext'; +export function useContextMenuContext() { + var context = useContext(Context); + if (!context) { + throw new Error('useContextMenuContext must be used within a Context.Provider'); + } + return context; +} +export function useContextMenuMenuContext() { + var context = useContext(MenuContext); + if (!context) { + throw new Error('useContextMenuMenuContext must be used within a Context.Provider'); + } + return context; +} +export function useContextMenuItemContext() { + var context = useContext(ItemContext); + if (!context) { + throw new Error('useContextMenuItemContext must be used within a Context.Provider'); + } + return context; +} diff --git a/src/components/ContextMenu/index.js b/src/components/ContextMenu/index.js new file mode 100644 index 0000000000..39bfc9363c --- /dev/null +++ b/src/components/ContextMenu/index.js @@ -0,0 +1,714 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react'; +import { BackHandler, Keyboard, Pressable, useWindowDimensions, View, } from 'react-native'; +import { Gesture, GestureDetector, } from 'react-native-gesture-handler'; +import { KeyboardEvents } from 'react-native-keyboard-controller'; +import Animated, { clamp, interpolate, runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; +import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; +import { captureRef } from 'react-native-view-shot'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useIsFocused } from '@react-navigation/native'; +import flattenReactChildren from 'react-keyed-flatten-children'; +import { HITSLOP_10 } from '#/lib/constants'; +import { useHaptics } from '#/lib/haptics'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { logger } from '#/logger'; +import { atoms as a, platform, tokens, useTheme } from '#/alf'; +import { Context, ItemContext, MenuContext, useContextMenuContext, useContextMenuItemContext, useContextMenuMenuContext, } from '#/components/ContextMenu/context'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { createPortalGroup } from '#/components/Portal'; +import { Text } from '#/components/Typography'; +import { IS_ANDROID, IS_IOS } from '#/env'; +import { Backdrop } from './Backdrop'; +export { useDialogControl as useContextMenuControl, } from '#/components/Dialog'; +var _a = createPortalGroup(), PortalProvider = _a.Provider, Outlet = _a.Outlet, Portal = _a.Portal; +var SPRING_IN = { + mass: 0.75, + damping: 300, + stiffness: 1200, + restDisplacementThreshold: 0.01, +}; +var SPRING_OUT = { + mass: IS_IOS ? 1.25 : 0.75, + damping: 150, + stiffness: 1000, + restDisplacementThreshold: 0.01, +}; +/** + * Needs placing near the top of the provider stack, but BELOW the theme provider. + */ +export function Provider(_a) { + var children = _a.children; + return (_jsxs(PortalProvider, { children: [children, _jsx(Outlet, {})] })); +} +export function Root(_a) { + var children = _a.children; + var playHaptic = useHaptics(); + var _b = useState('full'), mode = _b[0], setMode = _b[1]; + var _c = useState(null), measurement = _c[0], setMeasurement = _c[1]; + var returnLocationSV = useSharedValue(null); + var animationSV = useSharedValue(0); + var translationSV = useSharedValue(0); + var isFocused = useIsFocused(); + var hoverables = useRef(new Map()); + var hoverablesSV = useSharedValue({}); + var syncHoverablesThrottleRef = useRef(undefined); + var _d = useState(null), hoveredMenuItem = _d[0], setHoveredMenuItem = _d[1]; + var onHoverableTouchUp = useCallback(function (id) { + var hoverable = hoverables.current.get(id); + if (!hoverable) { + logger.warn("No such hoverable with id ".concat(id)); + return; + } + hoverable.onTouchUp(); + }, []); + var onCompletedClose = useCallback(function () { + hoverables.current.clear(); + setMeasurement(null); + }, []); + var context = useMemo(function () { + return ({ + isOpen: !!measurement && isFocused, + measurement: measurement, + returnLocationSV: returnLocationSV, + animationSV: animationSV, + translationSV: translationSV, + mode: mode, + open: function (evt, mode) { + setMeasurement(evt); + setMode(mode); + animationSV.set(withSpring(1, SPRING_IN)); + // reset return location + returnLocationSV.set(null); + }, + close: function () { + animationSV.set(withSpring(0, SPRING_OUT, function (finished) { + if (finished) { + hoverablesSV.set({}); + translationSV.set(0); + // note: return location has to be reset on open, + // rather than on close, otherwise there's a flicker + // where the reanimated update is faster than the react render + runOnJS(onCompletedClose)(); + } + })); + }, + registerHoverable: function (id, rect, onTouchUp) { + hoverables.current.set(id, { id: id, rect: rect, onTouchUp: onTouchUp }); + // we need this data on the UI thread, but we want to limit cross-thread communication + // and this function will be called in quick succession, so we need to throttle it + if (syncHoverablesThrottleRef.current) + clearTimeout(syncHoverablesThrottleRef.current); + syncHoverablesThrottleRef.current = setTimeout(function () { + syncHoverablesThrottleRef.current = undefined; + hoverablesSV.set(Object.fromEntries( + // eslint-ignore + __spreadArray([], hoverables.current.entries(), true).map(function (_a) { + var id = _a[0], rect = _a[1].rect; + return [ + id, + { id: id, rect: rect }, + ]; + }))); + }, 1); + }, + hoverablesSV: hoverablesSV, + onTouchUpMenuItem: onHoverableTouchUp, + hoveredMenuItem: hoveredMenuItem, + setHoveredMenuItem: function (item) { + if (item) + playHaptic('Light'); + setHoveredMenuItem(item); + }, + }); + }, [ + measurement, + returnLocationSV, + setMeasurement, + onCompletedClose, + isFocused, + animationSV, + translationSV, + hoverablesSV, + onHoverableTouchUp, + hoveredMenuItem, + setHoveredMenuItem, + playHaptic, + mode, + ]); + useEffect(function () { + if (IS_ANDROID && context.isOpen) { + var listener_1 = BackHandler.addEventListener('hardwareBackPress', function () { + context.close(); + return true; + }); + return function () { return listener_1.remove(); }; + } + }, [context]); + return _jsx(Context.Provider, { value: context, children: children }); +} +export function Trigger(_a) { + var _this = this; + var children = _a.children, label = _a.label, contentLabel = _a.contentLabel, style = _a.style; + var context = useContextMenuContext(); + var playHaptic = useHaptics(); + var insets = useSafeAreaInsets(); + var ref = useRef(null); + var isFocused = useIsFocused(); + var _b = useState(null), image = _b[0], setImage = _b[1]; + var _c = useState(null), pendingMeasurement = _c[0], setPendingMeasurement = _c[1]; + var open = useNonReactiveCallback(function (mode) { return __awaiter(_this, void 0, void 0, function () { + var _a, measurement, capture; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + playHaptic(); + return [4 /*yield*/, Promise.all([ + measureView(ref.current, insets), + captureRef(ref, { result: 'data-uri' }).catch(function (err) { + logger.error(err instanceof Error ? err : String(err), { + message: 'Failed to capture image of context menu trigger', + }); + // will cause the image to fail to load, but it will get handled gracefully + return ''; + }), + ])]; + case 1: + _a = _b.sent(), measurement = _a[0], capture = _a[1]; + Keyboard.dismiss(); + setImage(capture); + if (measurement) { + setPendingMeasurement({ measurement: measurement, mode: mode }); + } + return [2 /*return*/]; + } + }); + }); }); + // after keyboard hides, the position might change - set a return location + useEffect(function () { + if (context.isOpen && context.measurement) { + var hide_1 = KeyboardEvents.addListener('keyboardDidHide', function () { + measureView(ref.current, insets) + .then(function (newMeasurement) { + if (!newMeasurement || !context.measurement) + return; + if (newMeasurement.x !== context.measurement.x || + newMeasurement.y !== context.measurement.y) { + context.returnLocationSV.set({ + x: newMeasurement.x, + y: newMeasurement.y, + }); + } + }) + .catch(function () { }); + }); + return function () { + hide_1.remove(); + }; + } + }, [context, insets]); + var doubleTapGesture = useMemo(function () { + return Gesture.Tap() + .numberOfTaps(2) + .hitSlop(HITSLOP_10) + .onEnd(function () { return void open('auxiliary-only'); }) + .runOnJS(true); + }, [open]); + var hoverablesSV = context.hoverablesSV, setHoveredMenuItem = context.setHoveredMenuItem, onTouchUpMenuItem = context.onTouchUpMenuItem, translationSV = context.translationSV, animationSV = context.animationSV; + var hoveredItemSV = useSharedValue(null); + useAnimatedReaction(function () { return hoveredItemSV.get(); }, function (hovered, prev) { + if (hovered !== prev) { + runOnJS(setHoveredMenuItem)(hovered); + } + }); + var pressAndHoldGesture = useMemo(function () { + return Gesture.Pan() + .activateAfterLongPress(500) + .cancelsTouchesInView(false) + .averageTouches(true) + .onStart(function () { + 'worklet'; + runOnJS(open)('full'); + }) + .onUpdate(function (evt) { + 'worklet'; + var item = getHoveredHoverable(evt, hoverablesSV, translationSV); + hoveredItemSV.set(item); + }) + .onEnd(function () { + 'worklet'; + // don't recalculate hovered item - if they haven't moved their finger from + // the initial press, it's jarring to then select the item underneath + // as the menu may have slid into place beneath their finger + var item = hoveredItemSV.get(); + if (item) { + runOnJS(onTouchUpMenuItem)(item); + } + }); + }, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV]); + var composedGestures = Gesture.Exclusive(doubleTapGesture, pressAndHoldGesture); + var measurement = context.measurement || (pendingMeasurement === null || pendingMeasurement === void 0 ? void 0 : pendingMeasurement.measurement); + return (_jsxs(_Fragment, { children: [_jsx(GestureDetector, { gesture: composedGestures, children: _jsx(View, { ref: ref, style: [{ opacity: context.isOpen ? 0 : 1 }, style], children: children({ + IS_NATIVE: true, + control: { isOpen: context.isOpen, open: open }, + state: { + pressed: false, + hovered: false, + focused: false, + }, + props: { + ref: null, + onPress: null, + onFocus: null, + onBlur: null, + onPressIn: null, + onPressOut: null, + accessibilityHint: null, + accessibilityLabel: label, + accessibilityRole: null, + }, + }) }) }), isFocused && image && measurement && (_jsx(Portal, { children: _jsx(TriggerClone, { label: contentLabel, translation: translationSV, animation: animationSV, image: image, measurement: measurement, returnLocation: context.returnLocationSV, onDisplay: function () { + if (pendingMeasurement) { + context.open(pendingMeasurement.measurement, pendingMeasurement.mode); + setPendingMeasurement(null); + } + } }) }))] })); +} +/** + * an image of the underlying trigger with a grow animation + */ +function TriggerClone(_a) { + var translation = _a.translation, animation = _a.animation, image = _a.image, measurement = _a.measurement, returnLocation = _a.returnLocation, onDisplay = _a.onDisplay, label = _a.label; + var _ = useLingui()._; + var animatedStyles = useAnimatedStyle(function () { + var anim = animation.get(); + var ret = returnLocation.get(); + var returnOffsetX = ret + ? interpolate(anim, [0, 1], [ret.x - measurement.x, 0]) + : 0; + var returnOffsetY = ret + ? interpolate(anim, [0, 1], [ret.y - measurement.y, 0]) + : 0; + return { + transform: [ + { translateX: returnOffsetX }, + { translateY: translation.get() * anim + returnOffsetY }, + ], + }; + }); + var handleError = useCallback(function (evt) { + logger.error('Context menu image load error', { message: evt.error }); + onDisplay(); + }, [onDisplay]); + return (_jsx(Animated.View, { style: [ + a.absolute, + { + top: measurement.y, + left: measurement.x, + width: measurement.width, + height: measurement.height, + }, + a.z_10, + a.pointer_events_none, + animatedStyles, + ], children: _jsx(Image, { onDisplay: onDisplay, onError: handleError, source: image, style: { + width: measurement.width, + height: measurement.height, + }, accessibilityLabel: label, accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["The subject of the context menu"], ["The subject of the context menu"])))), accessibilityIgnoresInvertColors: false }) })); +} +export function AuxiliaryView(_a) { + var children = _a.children, _b = _a.align, align = _b === void 0 ? 'left' : _b; + var context = useContextMenuContext(); + var screenWidth = useWindowDimensions().width; + var topInset = useSafeAreaInsets().top; + var ensureOnScreenTranslationSV = useSharedValue(0); + var isOpen = context.isOpen, mode = context.mode, measurement = context.measurement, translationSV = context.translationSV, animationSV = context.animationSV; + var animatedStyle = useAnimatedStyle(function () { + return { + opacity: clamp(animationSV.get(), 0, 1), + transform: [ + { + translateY: (ensureOnScreenTranslationSV.get() || translationSV.get()) * + animationSV.get(), + }, + { scale: interpolate(animationSV.get(), [0, 1], [0.2, 1]) }, + ], + }; + }); + var menuContext = useMemo(function () { return ({ align: align }); }, [align]); + var onLayout = useCallback(function () { + if (!measurement) + return; + var translation = 0; + // vibes based, just assuming it'll fit within this space. revisit if we use + // AuxiliaryView for something tall + var TOP_INSET = topInset + 80; + var distanceMessageFromTop = measurement.y - TOP_INSET; + if (distanceMessageFromTop < 0) { + translation = -distanceMessageFromTop; + } + // normally, the context menu is responsible for measuring itself and moving everything into the right place + // however, in auxiliary-only mode, that doesn't happen, so we need to do it ourselves here + if (mode === 'auxiliary-only') { + translationSV.set(translation); + ensureOnScreenTranslationSV.set(0); + } + // however, we also need to make sure that for super tall triggers, we don't go off the screen + // so we have an additional cap on the standard transform every other element has + // note: this breaks the press-and-hold gesture for the reaction items. unfortunately I think + // we'll just have to live with it for now, fixing it would be possible but be a large complexity + // increase for an edge case + else { + ensureOnScreenTranslationSV.set(translation); + } + }, [mode, measurement, translationSV, topInset, ensureOnScreenTranslationSV]); + if (!isOpen || !measurement) + return null; + return (_jsx(Portal, { children: _jsx(Context.Provider, { value: context, children: _jsx(MenuContext.Provider, { value: menuContext, children: _jsx(Animated.View, { onLayout: onLayout, style: [ + a.absolute, + { + top: measurement.y, + transformOrigin: align === 'left' ? 'bottom left' : 'bottom right', + }, + align === 'left' + ? { left: measurement.x } + : { right: screenWidth - measurement.x - measurement.width }, + animatedStyle, + a.z_20, + ], children: children }) }) }) })); +} +var MENU_WIDTH = 240; +export function Outer(_a) { + var children = _a.children, style = _a.style, _b = _a.align, align = _b === void 0 ? 'left' : _b; + var t = useTheme(); + var context = useContextMenuContext(); + var insets = useSafeAreaInsets(); + var frame = useSafeAreaFrame(); + var screenWidth = useWindowDimensions().width; + var animationSV = context.animationSV, translationSV = context.translationSV; + var animatedContainerStyle = useAnimatedStyle(function () { return ({ + transform: [{ translateY: translationSV.get() * animationSV.get() }], + }); }); + var animatedStyle = useAnimatedStyle(function () { return ({ + opacity: clamp(animationSV.get(), 0, 1), + transform: [{ scale: interpolate(animationSV.get(), [0, 1], [0.2, 1]) }], + }); }); + var onLayout = useCallback(function (evt) { + if (!context.measurement) + return; // should not happen + var translation = 0; + // pure vibes based + var TOP_INSET = insets.top + 80; + var BOTTOM_INSET_IOS = insets.bottom + 20; + var BOTTOM_INSET_ANDROID = insets.bottom + 12; + var height = evt.nativeEvent.layout.height; + var topPosition = context.measurement.y + context.measurement.height + tokens.space.xs; + var bottomPosition = topPosition + height; + var safeAreaBottomLimit = frame.height - + platform({ + ios: BOTTOM_INSET_IOS, + android: BOTTOM_INSET_ANDROID, + default: 0, + }); + var diff = bottomPosition - safeAreaBottomLimit; + if (diff > 0) { + translation = -diff; + } + else { + var distanceMessageFromTop = context.measurement.y - TOP_INSET; + if (distanceMessageFromTop < 0) { + translation = -Math.max(distanceMessageFromTop, diff); + } + } + if (translation !== 0) { + translationSV.set(translation); + } + }, [context.measurement, frame.height, insets, translationSV]); + var menuContext = useMemo(function () { return ({ align: align }); }, [align]); + if (!context.isOpen || !context.measurement) + return null; + return (_jsx(Portal, { children: _jsx(Context.Provider, { value: context, children: _jsxs(MenuContext.Provider, { value: menuContext, children: [_jsx(Backdrop, { animation: animationSV, onPress: context.close }), context.mode === 'full' && ( + /* containing element - stays the same size, so we measure it + to determine if a translation is necessary. also has the positioning */ + _jsx(Animated.View, { onLayout: onLayout, style: [ + a.absolute, + a.z_10, + a.mt_xs, + { + width: MENU_WIDTH, + top: context.measurement.y + context.measurement.height, + }, + align === 'left' + ? { left: context.measurement.x } + : { + right: screenWidth - + context.measurement.x - + context.measurement.width, + }, + animatedContainerStyle, + ], children: _jsx(Animated.View, { style: [ + a.rounded_md, + a.shadow_md, + t.atoms.bg_contrast_25, + a.w_full, + // @ts-ignore react-native-web expects string, and this file is platform-split -sfn + // note: above @ts-ignore cannot be a @ts-expect-error because this does not cause an error + // in the typecheck CI - presumably because of RNW overriding the types + { + transformOrigin: + // "top right" doesn't seem to work on android, so set explicitly in pixels + align === 'left' ? [0, 0, 0] : [MENU_WIDTH, 0, 0], + }, + animatedStyle, + style, + ], children: _jsx(View, { style: [ + a.flex_1, + a.rounded_md, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + ], children: flattenReactChildren(children).map(function (child, i) { + return React.isValidElement(child) && + (child.type === Item || child.type === Divider) ? (_jsxs(React.Fragment, { children: [i > 0 ? (_jsx(View, { style: [a.border_b, t.atoms.border_contrast_low] })) : null, React.cloneElement(child, { + // @ts-expect-error not typed + style: { + borderRadius: 0, + borderWidth: 0, + }, + })] }, i)) : null; + }) }) }) }))] }) }) })); +} +export function Item(_a) { + var children = _a.children, label = _a.label, unstyled = _a.unstyled, style = _a.style, onPress = _a.onPress, position = _a.position, rest = __rest(_a, ["children", "label", "unstyled", "style", "onPress", "position"]); + var t = useTheme(); + var context = useContextMenuContext(); + var playHaptic = useHaptics(); + var _b = useInteractionState(), focused = _b.state, onFocus = _b.onIn, onBlur = _b.onOut; + var _c = useInteractionState(), pressed = _c.state, onPressIn = _c.onIn, onPressOut = _c.onOut; + var id = useId(); + var align = useContextMenuMenuContext().align; + var close = context.close, measurement = context.measurement, registerHoverable = context.registerHoverable; + var handleLayout = useCallback(function (evt) { + if (!measurement) + return; // should be impossible + var layout = evt.nativeEvent.layout; + var yOffset = position + ? position.y + : measurement.y + measurement.height + tokens.space.xs; + var xOffset = position + ? position.x + : align === 'left' + ? measurement.x + : measurement.x + measurement.width - layout.width; + registerHoverable(id, { + width: layout.width, + height: layout.height, + y: yOffset + layout.y, + x: xOffset + layout.x, + }, function () { + close(); + onPress(); + }); + }, [id, measurement, registerHoverable, close, onPress, align, position]); + var itemContext = useMemo(function () { return ({ disabled: Boolean(rest.disabled) }); }, [rest.disabled]); + return (_jsx(Pressable, __assign({}, rest, { onLayout: handleLayout, accessibilityHint: "", accessibilityLabel: label, onFocus: onFocus, onBlur: onBlur, onPress: function (e) { + close(); + onPress === null || onPress === void 0 ? void 0 : onPress(e); + }, onPressIn: function (e) { + var _a; + onPressIn(); + (_a = rest.onPressIn) === null || _a === void 0 ? void 0 : _a.call(rest, e); + playHaptic('Light'); + }, onPressOut: function (e) { + var _a; + onPressOut(); + (_a = rest.onPressOut) === null || _a === void 0 ? void 0 : _a.call(rest, e); + }, style: [ + !unstyled && [ + a.flex_row, + a.align_center, + a.gap_sm, + a.px_md, + a.rounded_md, + a.border, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + { minHeight: 44, paddingVertical: 10 }, + (focused || pressed || context.hoveredMenuItem === id) && + !rest.disabled && + t.atoms.bg_contrast_50, + ], + style, + ], children: _jsx(ItemContext.Provider, { value: itemContext, children: typeof children === 'function' + ? children((focused || pressed || context.hoveredMenuItem === id) && + !rest.disabled) + : children }) }))); +} +export function ItemText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + var disabled = useContextMenuItemContext().disabled; + return (_jsx(Text, { numberOfLines: 2, ellipsizeMode: "middle", style: [ + a.flex_1, + a.text_md, + a.font_semi_bold, + t.atoms.text_contrast_high, + { paddingTop: 3 }, + style, + disabled && t.atoms.text_contrast_low, + ], children: children })); +} +export function ItemIcon(_a) { + var Comp = _a.icon; + var t = useTheme(); + var disabled = useContextMenuItemContext().disabled; + return (_jsx(Comp, { size: "lg", fill: disabled + ? t.atoms.text_contrast_low.color + : t.atoms.text_contrast_medium.color })); +} +export function ItemRadio(_a) { + var selected = _a.selected; + var t = useTheme(); + return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + t.atoms.border_contrast_high, + { + borderWidth: 1, + height: 20, + width: 20, + }, + ], children: selected ? (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + { height: 14, width: 14 }, + selected ? { backgroundColor: t.palette.primary_500 } : {}, + ] })) : null })); +} +export function LabelText(_a) { + var children = _a.children; + var t = useTheme(); + return (_jsx(Text, { style: [ + a.font_semi_bold, + t.atoms.text_contrast_medium, + { marginBottom: -8 }, + ], children: children })); +} +export function Divider() { + var t = useTheme(); + return (_jsx(View, { style: [t.atoms.border_contrast_low, a.flex_1, { borderTopWidth: 3 }] })); +} +function measureView(view, insets) { + if (!view) + return Promise.resolve(null); + return new Promise(function (resolve) { + view === null || view === void 0 ? void 0 : view.measureInWindow(function (x, y, width, height) { + return resolve({ + x: x, + y: y + + platform({ + default: 0, + android: insets.top, // not included in measurement + }), + width: width, + height: height, + }); + }); + }); +} +function getHoveredHoverable(evt, hoverables, translation) { + 'worklet'; + var x = evt.absoluteX; + var y = evt.absoluteY; + var yOffset = translation.get(); + var rects = Object.values(hoverables.get()); + for (var _i = 0, rects_1 = rects; _i < rects_1.length; _i++) { + var _a = rects_1[_i], id = _a.id, rect = _a.rect; + var isWithinLeftBound = x >= rect.x; + var isWithinRightBound = x <= rect.x + rect.width; + var isWithinTopBound = y >= rect.y + yOffset; + var isWithinBottomBound = y <= rect.y + rect.height + yOffset; + if (isWithinLeftBound && + isWithinRightBound && + isWithinTopBound && + isWithinBottomBound) { + return id; + } + } + return null; +} +var templateObject_1; diff --git a/src/components/ContextMenu/index.web.js b/src/components/ContextMenu/index.web.js new file mode 100644 index 0000000000..34dc0f14e8 --- /dev/null +++ b/src/components/ContextMenu/index.web.js @@ -0,0 +1,9 @@ +export * from '#/components/Menu'; +export function Provider(_a) { + var children = _a.children; + return children; +} +// native only +export function AuxiliaryView(_a) { + return null; +} diff --git a/src/components/ContextMenu/types.js b/src/components/ContextMenu/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/ContextMenu/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/DebugFieldDisplay.js b/src/components/DebugFieldDisplay.js new file mode 100644 index 0000000000..8d25683c4e --- /dev/null +++ b/src/components/DebugFieldDisplay.js @@ -0,0 +1,41 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { TouchableWithoutFeedback, View } from 'react-native'; +import * as Clipboard from 'expo-clipboard'; +import { atoms as a, useTheme } from '#/alf'; +import * as Prompt from '#/components/Prompt'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useDevMode } from '#/storage/hooks/dev-mode'; +/** + * Internal-use component to display debug information supplied by the appview. + * The `debug` field only exists on some API views, and is only visible for + * internal users in dev mode. As such, none of these strings need to be + * translated. + * + * This component can be removed at any time if we don't find it useful. + */ +export function DebugFieldDisplay(_a) { + var subject = _a.subject; + var t = useTheme(); + var devMode = useDevMode()[0]; + var prompt = Prompt.usePromptControl(); + if (!devMode) + return; + if (!subject.debug) + return; + return (_jsxs(_Fragment, { children: [_jsx(Prompt.Basic, { control: prompt, title: "Debug", description: JSON.stringify(subject.debug, null, 2), cancelButtonCta: "Close", confirmButtonCta: "Copy", onConfirm: function () { + Clipboard.setStringAsync(JSON.stringify(subject.debug, null, 2)); + Toast.show('Copied to clipboard', { type: 'success' }); + } }), _jsx(TouchableWithoutFeedback, { accessibilityRole: "button", onPress: function (e) { + e.preventDefault(); + e.stopPropagation(); + prompt.open(); + return false; + }, children: _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs, a.pt_sm, a.pb_xs], children: [_jsx(View, { style: [a.py_xs, a.px_sm, a.rounded_sm, t.atoms.bg_contrast_25], children: _jsx(Text, { style: [a.font_bold, a.text_xs, t.atoms.text_contrast_medium], children: "Debug" }) }), _jsx(Text, { numberOfLines: 1, style: [ + a.flex_1, + a.text_xs, + a.leading_tight, + { fontFamily: 'monospace' }, + t.atoms.text_contrast_low, + ], children: JSON.stringify(subject.debug) })] }) })] })); +} diff --git a/src/components/Dialog/context.js b/src/components/Dialog/context.js new file mode 100644 index 0000000000..7bc700a8a7 --- /dev/null +++ b/src/components/Dialog/context.js @@ -0,0 +1,57 @@ +import { createContext, useContext, useEffect, useId, useMemo, useRef, } from 'react'; +import { useDialogStateContext } from '#/state/dialogs'; +import { IS_DEV } from '#/env'; +import { BottomSheetSnapPoint } from '../../../modules/bottom-sheet/src/BottomSheet.types'; +export var Context = createContext({ + close: function () { }, + isNativeDialog: false, + nativeSnapPoint: BottomSheetSnapPoint.Hidden, + disableDrag: false, + setDisableDrag: function () { }, + isWithinDialog: false, +}); +Context.displayName = 'DialogContext'; +export function useDialogContext() { + return useContext(Context); +} +export function useDialogControl() { + var id = useId(); + var control = useRef({ + open: function () { }, + close: function () { }, + }); + var activeDialogs = useDialogStateContext().activeDialogs; + useEffect(function () { + activeDialogs.current.set(id, control); + return function () { + // eslint-disable-next-line react-hooks/exhaustive-deps + activeDialogs.current.delete(id); + }; + }, [id, activeDialogs]); + return useMemo(function () { return ({ + id: id, + ref: control, + open: function () { + if (control.current) { + control.current.open(); + } + else { + if (IS_DEV) { + console.warn('Attemped to open a dialog control that was not attached to a dialog!\n' + + 'Please ensure that the Dialog is mounted when calling open/close'); + } + } + }, + close: function (cb) { + if (control.current) { + control.current.close(cb); + } + else { + if (IS_DEV) { + console.warn('Attemped to close a dialog control that was not attached to a dialog!\n' + + 'Please ensure that the Dialog is mounted when calling open/close'); + } + } + }, + }); }, [id, control]); +} diff --git a/src/components/Dialog/index.js b/src/components/Dialog/index.js new file mode 100644 index 0000000000..e8e6f17ddb --- /dev/null +++ b/src/components/Dialog/index.js @@ -0,0 +1,282 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React, { useImperativeHandle } from 'react'; +import { Pressable, ScrollView, TextInput, View, } from 'react-native'; +import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller'; +import Animated, { runOnJS, useAnimatedStyle, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { ScrollProvider } from '#/lib/ScrollContext'; +import { logger } from '#/logger'; +import { useA11y } from '#/state/a11y'; +import { useDialogStateControlContext } from '#/state/dialogs'; +import { List } from '#/view/com/util/List'; +import { android, atoms as a, ios, platform, tokens, useTheme } from '#/alf'; +import { useThemeName } from '#/alf/util/useColorModeTheme'; +import { Context, useDialogContext } from '#/components/Dialog/context'; +import { createInput } from '#/components/forms/TextField'; +import { IS_ANDROID, IS_IOS, IS_LIQUID_GLASS } from '#/env'; +import { BottomSheet, BottomSheetSnapPoint } from '../../../modules/bottom-sheet'; +export { useDialogContext, useDialogControl } from '#/components/Dialog/context'; +export * from '#/components/Dialog/shared'; +export * from '#/components/Dialog/types'; +export * from '#/components/Dialog/utils'; +export var Input = createInput(TextInput); +export function Outer(_a) { + var children = _a.children, control = _a.control, onClose = _a.onClose, nativeOptions = _a.nativeOptions, testID = _a.testID; + var themeName = useThemeName(); + var t = useTheme(themeName); + var ref = React.useRef(null); + var closeCallbacks = React.useRef([]); + var _b = useDialogStateControlContext(), setDialogIsOpen = _b.setDialogIsOpen, setFullyExpandedCount = _b.setFullyExpandedCount; + var prevSnapPoint = React.useRef(BottomSheetSnapPoint.Hidden); + var _c = React.useState(false), disableDrag = _c[0], setDisableDrag = _c[1]; + var _d = React.useState(BottomSheetSnapPoint.Partial), snapPoint = _d[0], setSnapPoint = _d[1]; + var callQueuedCallbacks = React.useCallback(function () { + for (var _i = 0, _a = closeCallbacks.current; _i < _a.length; _i++) { + var cb = _a[_i]; + try { + cb(); + } + catch (e) { + logger.error(e || 'Error running close callback'); + } + } + closeCallbacks.current = []; + }, []); + var open = React.useCallback(function () { + var _a; + // Run any leftover callbacks that might have been queued up before calling `.open()` + callQueuedCallbacks(); + setDialogIsOpen(control.id, true); + (_a = ref.current) === null || _a === void 0 ? void 0 : _a.present(); + }, [setDialogIsOpen, control.id, callQueuedCallbacks]); + // This is the function that we call when we want to dismiss the dialog. + var close = React.useCallback(function (cb) { + var _a; + if (typeof cb === 'function') { + closeCallbacks.current.push(cb); + } + (_a = ref.current) === null || _a === void 0 ? void 0 : _a.dismiss(); + }, []); + // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to + // happen before we run this. It is passed to the `BottomSheet` component. + var onCloseAnimationComplete = React.useCallback(function () { + // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this + // tells us that we need to toggle the accessibility overlay setting + setDialogIsOpen(control.id, false); + callQueuedCallbacks(); + onClose === null || onClose === void 0 ? void 0 : onClose(); + }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]); + var onSnapPointChange = function (e) { + var snapPoint = e.nativeEvent.snapPoint; + setSnapPoint(snapPoint); + if (snapPoint === BottomSheetSnapPoint.Full && + prevSnapPoint.current !== BottomSheetSnapPoint.Full) { + setFullyExpandedCount(function (c) { return c + 1; }); + } + else if (snapPoint !== BottomSheetSnapPoint.Full && + prevSnapPoint.current === BottomSheetSnapPoint.Full) { + setFullyExpandedCount(function (c) { return c - 1; }); + } + prevSnapPoint.current = snapPoint; + }; + var onStateChange = function (e) { + if (e.nativeEvent.state === 'closed') { + onCloseAnimationComplete(); + if (prevSnapPoint.current === BottomSheetSnapPoint.Full) { + setFullyExpandedCount(function (c) { return c - 1; }); + } + prevSnapPoint.current = BottomSheetSnapPoint.Hidden; + } + }; + useImperativeHandle(control.ref, function () { return ({ + open: open, + close: close, + }); }, [open, close]); + var context = React.useMemo(function () { return ({ + close: close, + isNativeDialog: true, + nativeSnapPoint: snapPoint, + disableDrag: disableDrag, + setDisableDrag: setDisableDrag, + isWithinDialog: true, + }); }, [close, snapPoint, disableDrag, setDisableDrag]); + return (_jsx(BottomSheet, __assign({ ref: ref, + // device-bezel radius when undefined + cornerRadius: IS_LIQUID_GLASS ? undefined : 20, backgroundColor: t.atoms.bg.backgroundColor }, nativeOptions, { onSnapPointChange: onSnapPointChange, onStateChange: onStateChange, disableDrag: disableDrag, children: _jsx(Context.Provider, { value: context, children: _jsx(View, { testID: testID, style: [a.relative], children: children }) }) }))); +} +/** + * @deprecated use `Dialog.ScrollableInner` instead + */ +export function Inner(_a) { + var children = _a.children, style = _a.style, header = _a.header; + var insets = useSafeAreaInsets(); + return (_jsxs(_Fragment, { children: [header, _jsx(View, { style: [ + a.pt_2xl, + a.px_xl, + IS_LIQUID_GLASS + ? a.pb_2xl + : { paddingBottom: insets.bottom + insets.top }, + style, + ], children: children })] })); +} +export var ScrollableInner = React.forwardRef(function ScrollableInner(_a, ref) { + var children = _a.children, contentContainerStyle = _a.contentContainerStyle, header = _a.header, props = __rest(_a, ["children", "contentContainerStyle", "header"]); + var _b = useDialogContext(), nativeSnapPoint = _b.nativeSnapPoint, disableDrag = _b.disableDrag, setDisableDrag = _b.setDisableDrag; + var insets = useSafeAreaInsets(); + var isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full; + var paddingBottom = 0; + if (IS_IOS) { + paddingBottom = tokens.space._2xl; + } + else { + paddingBottom = + Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl; + if (isAtMaxSnapPoint) { + paddingBottom += insets.top; + } + } + var onScroll = function (e) { + if (!IS_ANDROID) { + return; + } + var contentOffset = e.nativeEvent.contentOffset; + if (contentOffset.y > 0 && !disableDrag) { + setDisableDrag(true); + } + else if (contentOffset.y <= 1 && disableDrag) { + setDisableDrag(false); + } + }; + return (_jsxs(ScrollView, __assign({ contentContainerStyle: [ + a.pt_2xl, + IS_LIQUID_GLASS ? a.px_2xl : a.px_xl, + { paddingBottom: paddingBottom }, + contentContainerStyle, + ], ref: ref, showsVerticalScrollIndicator: IS_ANDROID ? false : undefined, contentInsetAdjustmentBehavior: isAtMaxSnapPoint ? 'automatic' : 'never', automaticallyAdjustKeyboardInsets: isAtMaxSnapPoint }, props, { bounces: isAtMaxSnapPoint, scrollEventThrottle: 50, onScroll: IS_ANDROID ? onScroll : undefined, keyboardShouldPersistTaps: "handled", + // TODO: figure out why this positions the header absolutely (rather than stickily) + // on Android. fine to disable for now, because we don't have any + // dialogs that use this that actually scroll -sfn + stickyHeaderIndices: ios(header ? [0] : undefined), children: [header, children] }))); +}); +export var InnerFlatList = React.forwardRef(function InnerFlatList(_a, ref) { + var headerOffset = _a.headerOffset, footer = _a.footer, style = _a.style, contentContainerStyle = _a.contentContainerStyle, props = __rest(_a, ["headerOffset", "footer", "style", "contentContainerStyle"]); + var insets = useSafeAreaInsets(); + var _b = useDialogContext(), nativeSnapPoint = _b.nativeSnapPoint, disableDrag = _b.disableDrag, setDisableDrag = _b.setDisableDrag; + var isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full; + var onScroll = function (e) { + 'worklet'; + if (!IS_ANDROID) { + return; + } + var contentOffset = e.contentOffset; + if (contentOffset.y > 0 && !disableDrag) { + runOnJS(setDisableDrag)(true); + } + else if (contentOffset.y <= 1 && disableDrag) { + runOnJS(setDisableDrag)(false); + } + }; + return (_jsxs(ScrollProvider, { onScroll: onScroll, children: [_jsx(List, __assign({ keyboardShouldPersistTaps: "handled", contentInsetAdjustmentBehavior: isAtMaxSnapPoint ? 'automatic' : 'never', automaticallyAdjustKeyboardInsets: isAtMaxSnapPoint, scrollIndicatorInsets: { top: headerOffset }, bounces: isAtMaxSnapPoint, ref: ref, showsVerticalScrollIndicator: IS_ANDROID ? false : undefined }, props, { style: [a.h_full, style], contentContainerStyle: [ + { paddingTop: headerOffset }, + android({ + paddingBottom: insets.top + insets.bottom + tokens.space.xl, + }), + contentContainerStyle, + ] })), footer] })); +}); +export function FlatListFooter(_a) { + var children = _a.children, onLayout = _a.onLayout; + var t = useTheme(); + var _b = useSafeAreaInsets(), top = _b.top, bottom = _b.bottom; + var height = useReanimatedKeyboardAnimation().height; + var animatedStyle = useAnimatedStyle(function () { + if (!IS_IOS) + return {}; + return { + transform: [{ translateY: Math.min(0, height.get() + bottom - 10) }], + }; + }); + return (_jsx(Animated.View, { onLayout: onLayout, style: [ + a.absolute, + a.bottom_0, + a.w_full, + a.z_10, + a.border_t, + t.atoms.bg, + t.atoms.border_contrast_low, + a.px_lg, + a.pt_md, + { + paddingBottom: platform({ + ios: tokens.space.md + bottom + (IS_LIQUID_GLASS ? top : 0), + android: tokens.space.md + bottom + top, + }), + }, + // TODO: had to admit defeat here, but we should + // try and get this to work for Android as well -sfn + ios(animatedStyle), + ], children: children })); +} +export function Handle(_a) { + var _b = _a.difference, difference = _b === void 0 ? false : _b, fill = _a.fill; + var t = useTheme(); + var _ = useLingui()._; + var screenReaderEnabled = useA11y().screenReaderEnabled; + var close = useDialogContext().close; + return (_jsx(View, { style: [a.absolute, a.w_full, a.align_center, a.z_10, { height: 20 }], children: _jsx(Pressable, { accessible: screenReaderEnabled, onPress: function () { return close(); }, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Dismiss"], ["Dismiss"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Double tap to close the dialog"], ["Double tap to close the dialog"])))), children: _jsx(View, { style: [ + a.rounded_sm, + { + top: tokens.space._2xl / 2 - 2.5, + width: 35, + height: 5, + alignSelf: 'center', + }, + difference + ? { + // TODO: mixBlendMode is only available on the new architecture -sfn + // backgroundColor: t.palette.white, + // mixBlendMode: 'difference', + backgroundColor: t.palette.white, + opacity: 0.75, + } + : { + backgroundColor: fill || t.palette.contrast_975, + opacity: 0.5, + }, + ] }) }) })); +} +export function Close() { + return null; +} +export function Backdrop() { + return null; +} +var templateObject_1, templateObject_2; diff --git a/src/components/Dialog/index.web.js b/src/components/Dialog/index.web.js new file mode 100644 index 0000000000..bdb3d81ee2 --- /dev/null +++ b/src/components/Dialog/index.web.js @@ -0,0 +1,232 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useImperativeHandle } from 'react'; +import { FlatList, Pressable, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { DismissableLayer, FocusGuards, FocusScope } from 'radix-ui/internal'; +import { RemoveScrollBar } from 'react-remove-scroll-bar'; +import { logger } from '#/logger'; +import { useA11y } from '#/state/a11y'; +import { useDialogStateControlContext } from '#/state/dialogs'; +import { atoms as a, flatten, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { Context } from '#/components/Dialog/context'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Portal } from '#/components/Portal'; +export { useDialogContext, useDialogControl } from '#/components/Dialog/context'; +export * from '#/components/Dialog/shared'; +export * from '#/components/Dialog/types'; +export * from '#/components/Dialog/utils'; +export { Input } from '#/components/forms/TextField'; +// 100 minus 10vh of paddingVertical +export var WEB_DIALOG_HEIGHT = '80vh'; +var stopPropagation = function (e) { return e.stopPropagation(); }; +var preventDefault = function (e) { return e.preventDefault(); }; +export function Outer(_a) { + var _this = this; + var children = _a.children, control = _a.control, onClose = _a.onClose, webOptions = _a.webOptions; + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var _b = React.useState(false), isOpen = _b[0], setIsOpen = _b[1]; + var setDialogIsOpen = useDialogStateControlContext().setDialogIsOpen; + var open = React.useCallback(function () { + setDialogIsOpen(control.id, true); + setIsOpen(true); + }, [setIsOpen, setDialogIsOpen, control.id]); + var close = React.useCallback(function (cb) { + setDialogIsOpen(control.id, false); + setIsOpen(false); + try { + if (cb && typeof cb === 'function') { + // This timeout ensures that the callback runs at the same time as it would on native. I.e. + // console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2') + // This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output + // 'Step 1', 'Step 3', 'Step 2'. + setTimeout(cb); + } + } + catch (e) { + logger.error("Dialog closeCallback failed", { + message: e.message, + }); + } + onClose === null || onClose === void 0 ? void 0 : onClose(); + }, [control.id, onClose, setDialogIsOpen]); + var handleBackgroundPress = React.useCallback(function (e) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + (webOptions === null || webOptions === void 0 ? void 0 : webOptions.onBackgroundPress) ? webOptions.onBackgroundPress(e) : close(); + return [2 /*return*/]; + }); + }); }, [webOptions, close]); + useImperativeHandle(control.ref, function () { return ({ + open: open, + close: close, + }); }, [close, open]); + var context = React.useMemo(function () { return ({ + close: close, + isNativeDialog: false, + nativeSnapPoint: 0, + disableDrag: false, + setDisableDrag: function () { }, + isWithinDialog: true, + }); }, [close]); + return (_jsx(_Fragment, { children: isOpen && (_jsx(Portal, { children: _jsxs(Context.Provider, { value: context, children: [_jsx(RemoveScrollBar, {}), _jsx(Pressable, { accessibilityHint: undefined, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close active dialog"], ["Close active dialog"])))), onPress: handleBackgroundPress, children: _jsxs(View, { style: [ + web(a.fixed), + a.inset_0, + a.z_10, + a.px_xl, + (webOptions === null || webOptions === void 0 ? void 0 : webOptions.alignCenter) ? a.justify_center : undefined, + a.align_center, + { + overflowY: 'auto', + paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop, + }, + ], children: [_jsx(Backdrop, {}), _jsx(View, { style: [ + a.w_full, + a.z_20, + a.align_center, + web({ minHeight: '60vh', position: 'static' }), + ], children: children })] }) })] }) })) })); +} +export function Inner(_a) { + var children = _a.children, style = _a.style, label = _a.label, accessibilityLabelledBy = _a.accessibilityLabelledBy, accessibilityDescribedBy = _a.accessibilityDescribedBy, header = _a.header, contentContainerStyle = _a.contentContainerStyle; + var t = useTheme(); + var close = React.useContext(Context).close; + var gtMobile = useBreakpoints().gtMobile; + var reduceMotionEnabled = useA11y().reduceMotionEnabled; + FocusGuards.useFocusGuards(); + return (_jsx(FocusScope.FocusScope, { loop: true, asChild: true, trapped: true, children: _jsx(View, { role: "dialog", "aria-role": "dialog", "aria-label": label, "aria-labelledby": accessibilityLabelledBy, "aria-describedby": accessibilityDescribedBy, + // @ts-expect-error web only -prf + onClick: stopPropagation, onStartShouldSetResponder: function (_) { return true; }, onTouchEnd: stopPropagation, + // note: flatten is required for some reason -sfn + style: flatten([ + a.relative, + a.rounded_md, + a.w_full, + a.border, + t.atoms.bg, + { + maxWidth: 600, + borderColor: t.palette.contrast_200, + shadowColor: t.palette.black, + shadowOpacity: t.name === 'light' ? 0.1 : 0.4, + shadowRadius: 30, + }, + !reduceMotionEnabled && a.zoom_fade_in, + style, + ]), children: _jsxs(DismissableLayer.DismissableLayer, { onInteractOutside: preventDefault, onFocusOutside: preventDefault, onDismiss: close, style: { height: '100%', display: 'flex', flexDirection: 'column' }, children: [header, _jsx(View, { style: [gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle], children: children })] }) }) })); +} +export var ScrollableInner = Inner; +export var InnerFlatList = React.forwardRef(function InnerFlatList(_a, ref) { + var label = _a.label, style = _a.style, webInnerStyle = _a.webInnerStyle, webInnerContentContainerStyle = _a.webInnerContentContainerStyle, footer = _a.footer, props = __rest(_a, ["label", "style", "webInnerStyle", "webInnerContentContainerStyle", "footer"]); + var gtMobile = useBreakpoints().gtMobile; + return (_jsxs(Inner, { label: label, style: [ + a.overflow_hidden, + a.px_0, + web({ maxHeight: WEB_DIALOG_HEIGHT }), + webInnerStyle, + ], contentContainerStyle: [a.h_full, a.px_0, webInnerContentContainerStyle], children: [_jsx(FlatList, __assign({ ref: ref, style: [a.h_full, gtMobile ? a.px_2xl : a.px_xl, style] }, props)), footer] })); +}); +export function FlatListFooter(_a) { + var children = _a.children, onLayout = _a.onLayout; + var t = useTheme(); + return (_jsx(View, { onLayout: onLayout, style: [ + a.absolute, + a.bottom_0, + a.w_full, + a.z_10, + t.atoms.bg, + a.border_t, + t.atoms.border_contrast_low, + a.px_lg, + a.py_md, + ], children: children })); +} +export function Close() { + var _ = useLingui()._; + var close = React.useContext(Context).close; + return (_jsx(View, { style: [ + a.absolute, + a.z_10, + { + top: a.pt_md.paddingTop, + right: a.pr_md.paddingRight, + }, + ], children: _jsx(Button, { size: "small", variant: "ghost", color: "secondary", shape: "round", onPress: function () { return close(); }, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close active dialog"], ["Close active dialog"])))), children: _jsx(ButtonIcon, { icon: X, size: "md" }) }) })); +} +export function Handle() { + return null; +} +export function Backdrop() { + var t = useTheme(); + var reduceMotionEnabled = useA11y().reduceMotionEnabled; + return (_jsx(View, { style: { opacity: 0.8 }, children: _jsx(View, { style: [ + a.fixed, + a.inset_0, + { backgroundColor: t.palette.black }, + !reduceMotionEnabled && a.fade_in, + ] }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/Dialog/shared.js b/src/components/Dialog/shared.js new file mode 100644 index 0000000000..3964090889 --- /dev/null +++ b/src/components/Dialog/shared.js @@ -0,0 +1,30 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View, } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +import { IS_LIQUID_GLASS } from '#/env'; +export function Header(_a) { + var renderLeft = _a.renderLeft, renderRight = _a.renderRight, children = _a.children, style = _a.style, onLayout = _a.onLayout; + var t = useTheme(); + return (_jsxs(View, { onLayout: onLayout, style: [ + a.sticky, + a.top_0, + a.relative, + a.w_full, + a.py_sm, + a.flex_row, + a.justify_center, + a.align_center, + { minHeight: IS_LIQUID_GLASS ? 64 : 50 }, + a.border_b, + t.atoms.border_contrast_medium, + t.atoms.bg, + { borderTopLeftRadius: a.rounded_md.borderRadius }, + { borderTopRightRadius: a.rounded_md.borderRadius }, + style, + ], children: [renderLeft && (_jsx(View, { style: [a.absolute, { left: IS_LIQUID_GLASS ? 12 : 6 }], children: renderLeft() })), children, renderRight && (_jsx(View, { style: [a.absolute, { right: IS_LIQUID_GLASS ? 12 : 6 }], children: renderRight() }))] })); +} +export function HeaderText(_a) { + var children = _a.children, style = _a.style; + return (_jsx(Text, { style: [a.text_lg, a.text_center, a.font_semi_bold, style], children: children })); +} diff --git a/src/components/Dialog/sheet-wrapper.js b/src/components/Dialog/sheet-wrapper.js new file mode 100644 index 0000000000..31cca220f9 --- /dev/null +++ b/src/components/Dialog/sheet-wrapper.js @@ -0,0 +1,67 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import { SystemBars } from 'react-native-edge-to-edge'; +import { IS_IOS, IS_LIQUID_GLASS } from '#/env'; +/** + * If we're calling a system API like the image picker that opens a sheet + * wrap it in this function to make sure the status bar is the correct color. + */ +export function useSheetWrapper() { + var _this = this; + return useCallback(function (promise) { return __awaiter(_this, void 0, void 0, function () { + var entry, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(IS_IOS && !IS_LIQUID_GLASS)) return [3 /*break*/, 2]; + entry = SystemBars.pushStackEntry({ + style: { + statusBar: 'light', + }, + }); + return [4 /*yield*/, promise]; + case 1: + res = _a.sent(); + SystemBars.popStackEntry(entry); + return [2 /*return*/, res]; + case 2: return [4 /*yield*/, promise]; + case 3: return [2 /*return*/, _a.sent()]; + } + }); + }); }, []); +} diff --git a/src/components/Dialog/types.js b/src/components/Dialog/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/Dialog/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/Dialog/utils.js b/src/components/Dialog/utils.js new file mode 100644 index 0000000000..35e503b9c8 --- /dev/null +++ b/src/components/Dialog/utils.js @@ -0,0 +1,16 @@ +import React from 'react'; +export function useAutoOpen(control, showTimeout) { + React.useEffect(function () { + if (showTimeout) { + var timeout_1 = setTimeout(function () { + control.open(); + }, showTimeout); + return function () { + clearTimeout(timeout_1); + }; + } + else { + control.open(); + } + }, [control, showTimeout]); +} diff --git a/src/components/Divider.js b/src/components/Divider.js new file mode 100644 index 0000000000..71c6daf317 --- /dev/null +++ b/src/components/Divider.js @@ -0,0 +1,8 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +export function Divider(_a) { + var style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [a.w_full, a.border_t, t.atoms.border_contrast_low, style] })); +} diff --git a/src/components/DraggableList/index.js b/src/components/DraggableList/index.js new file mode 100644 index 0000000000..c80725b5d4 --- /dev/null +++ b/src/components/DraggableList/index.js @@ -0,0 +1,320 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useLayoutEffect, useRef } from 'react'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { measure, runOnJS, scrollTo, useAnimatedRef, useAnimatedStyle, useFrameCallback, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated'; +import { useHaptics } from '#/lib/haptics'; +import { atoms as a, useTheme, web } from '#/alf'; +import { DotGrid2x3_Stroke2_Corner0_Rounded as GripIcon } from '#/components/icons/DotGrid'; +import { IS_IOS } from '#/env'; +var AUTO_SCROLL_THRESHOLD = 50; +var AUTO_SCROLL_SPEED = 4; +export function SortableList(_a) { + var data = _a.data, keyExtractor = _a.keyExtractor, renderItem = _a.renderItem, onReorder = _a.onReorder, onDragStart = _a.onDragStart, onDragEnd = _a.onDragEnd, itemHeight = _a.itemHeight, scrollRef = _a.scrollRef, scrollOffset = _a.scrollOffset; + var t = useTheme(); + var state = useSharedValue({ + slots: Object.fromEntries(data.map(function (item, i) { return [keyExtractor(item), i]; })), + activeKey: '', + dragStartSlot: -1, + }); + var dragY = useSharedValue(0); + // Auto-scroll shared values + var scrollCompensation = useSharedValue(0); + var isGestureActive = useSharedValue(false); + // We track scroll position ourselves because scrollOffset.get() lags + // by one frame after scrollTo(), causing a feedback loop where the + // frame callback keeps thinking the item is at the edge. + var trackedScrollY = useSharedValue(0); + // For measuring list position within scroll content + var listRef = useAnimatedRef(); + var listContentOffset = useSharedValue(0); + var viewportHeight = useSharedValue(0); + var measureDone = useSharedValue(false); + // Sync slots when data changes externally (e.g. pin/unpin). + // Skip after our own reorder — the worklet already set correct slots + // on the UI thread, and a redundant JS-side set() would be wasteful. + var skipNextSync = useRef(false); + var currentKeys = data.map(function (item) { return keyExtractor(item); }).join(','); + useLayoutEffect(function () { + if (skipNextSync.current) { + skipNextSync.current = false; + return; + } + var nextSlots = {}; + data.forEach(function (item, i) { + nextSlots[keyExtractor(item)] = i; + }); + state.set({ slots: nextSlots, activeKey: '', dragStartSlot: -1 }); + dragY.set(0); + }, [currentKeys, data, keyExtractor, state, dragY]); + var handleReorder = function (sortedKeys) { + skipNextSync.current = true; + var byKey = new Map(data.map(function (item) { return [keyExtractor(item), item]; })); + onReorder(sortedKeys.map(function (key) { return byKey.get(key); })); + onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(); + }; + // Auto-scroll: runs every frame while a gesture is active. + useFrameCallback(function () { + if (!isGestureActive.get()) + return; + if (!scrollRef || !scrollOffset) + return; + var s = state.get(); + if (s.activeKey === '') + return; + // Measure list and scroll view on first frame of drag. + // Use scrollOffset here (only once) since no lag has occurred yet. + if (!measureDone.get()) { + var scrollM = measure(scrollRef); + var listM = measure(listRef); + if (!scrollM || !listM) + return; + trackedScrollY.set(scrollOffset.get()); + listContentOffset.set(listM.pageY - scrollM.pageY + trackedScrollY.get()); + viewportHeight.set(scrollM.height); + measureDone.set(true); + } + var startSlot = s.dragStartSlot; + var currentDragY = dragY.get(); + // Use trackedScrollY (not scrollOffset) to avoid the one-frame lag + // after scrollTo() that causes a feedback loop. + var scrollY = trackedScrollY.get(); + // Item position relative to scroll viewport top. + var itemContentY = listContentOffset.get() + startSlot * itemHeight + currentDragY; + var itemViewportY = itemContentY - scrollY; + var itemBottomViewportY = itemViewportY + itemHeight; + var scrollDelta = 0; + if (itemViewportY < AUTO_SCROLL_THRESHOLD) { + scrollDelta = -AUTO_SCROLL_SPEED; + } + else if (itemBottomViewportY > + viewportHeight.get() - AUTO_SCROLL_THRESHOLD) { + scrollDelta = AUTO_SCROLL_SPEED; + } + if (scrollDelta === 0) + return; + // Don't scroll if the item is already at a list boundary. + var effectiveSlotPos = (startSlot * itemHeight + currentDragY) / itemHeight; + if (scrollDelta < 0 && effectiveSlotPos <= 0) + return; + if (scrollDelta > 0 && effectiveSlotPos >= data.length - 1) + return; + // Don't scroll past the top. + if (scrollDelta < 0 && scrollY <= 0) + return; + var newScrollY = Math.max(0, scrollY + scrollDelta); + scrollTo(scrollRef, 0, newScrollY, false); + trackedScrollY.set(newScrollY); + scrollCompensation.set(scrollCompensation.get() + (newScrollY - scrollY)); + }); + // Render in stable key order so React never reorders native views. + // On Android, native ViewGroup child reordering causes a visual flash. + var sortedData = __spreadArray([], data, true).sort(function (a, b) { + var ka = keyExtractor(a); + var kb = keyExtractor(b); + return ka < kb ? -1 : ka > kb ? 1 : 0; + }); + return (_jsx(Animated.View, { ref: listRef, style: [{ height: data.length * itemHeight }, t.atoms.bg_contrast_25], children: sortedData.map(function (item) { + var key = keyExtractor(item); + return (_jsx(SortableItem, { item: item, itemKey: key, itemCount: data.length, itemHeight: itemHeight, state: state, dragY: dragY, scrollCompensation: scrollCompensation, isGestureActive: isGestureActive, measureDone: measureDone, renderItem: renderItem, onCommitReorder: handleReorder, onDragStart: onDragStart, onDragEnd: onDragEnd }, key)); + }) })); +} +function SortableItem(_a) { + var item = _a.item, itemKey = _a.itemKey, itemCount = _a.itemCount, itemHeight = _a.itemHeight, state = _a.state, dragY = _a.dragY, scrollCompensation = _a.scrollCompensation, isGestureActive = _a.isGestureActive, measureDone = _a.measureDone, renderItem = _a.renderItem, onCommitReorder = _a.onCommitReorder, onDragStart = _a.onDragStart, onDragEnd = _a.onDragEnd; + var t = useTheme(); + var playHaptic = useHaptics(); + var lastHapticSlot = useSharedValue(-1); + var gesture = Gesture.Pan() + .onStart(function () { + 'worklet'; + var s = state.get(); + var mySlot = s.slots[itemKey]; + state.set(__assign(__assign({}, s), { activeKey: itemKey, dragStartSlot: mySlot })); + dragY.set(0); + scrollCompensation.set(0); + isGestureActive.set(true); + measureDone.set(false); + lastHapticSlot.set(mySlot); + if (onDragStart) { + runOnJS(onDragStart)(); + } + runOnJS(playHaptic)(); + }) + .onChange(function (e) { + 'worklet'; + var startSlot = state.get().dragStartSlot; + var minY = -startSlot * itemHeight; + var maxY = (itemCount - 1 - startSlot) * itemHeight; + // Include scroll compensation so the item tracks with auto-scroll. + var effectiveY = e.translationY + scrollCompensation.get(); + var clampedY = Math.max(minY, Math.min(effectiveY, maxY)); + dragY.set(clampedY); + var currentSlot = Math.round((startSlot * itemHeight + clampedY) / itemHeight); + var clampedSlot = Math.max(0, Math.min(currentSlot, itemCount - 1)); + if (IS_IOS && clampedSlot !== lastHapticSlot.get()) { + lastHapticSlot.set(clampedSlot); + runOnJS(playHaptic)('Light'); + } + }) + .onEnd(function () { + 'worklet'; + // Stop auto-scroll BEFORE the snap animation. + isGestureActive.set(false); + var startSlot = state.get().dragStartSlot; + var rawNewSlot = Math.round((startSlot * itemHeight + dragY.get()) / itemHeight); + var newSlot = Math.max(0, Math.min(rawNewSlot, itemCount - 1)); + var snapOffset = (newSlot - startSlot) * itemHeight; + // Animate to the target slot, then commit. + dragY.set(withTiming(snapOffset, { duration: 200 }, function (finished) { + if (finished) { + if (newSlot !== startSlot) { + // Compute new slots on the UI thread so animated styles + // reflect final positions before React re-renders. + var cur = state.get(); + var sorted = new Array(itemCount); + for (var key in cur.slots) { + sorted[cur.slots[key]] = key; + } + var movedKey = sorted[startSlot]; + sorted.splice(startSlot, 1); + sorted.splice(newSlot, 0, movedKey); + var nextSlots = {}; + for (var i = 0; i < sorted.length; i++) { + nextSlots[sorted[i]] = i; + } + state.set({ + slots: nextSlots, + activeKey: '', + dragStartSlot: -1, + }); + dragY.set(0); + runOnJS(onCommitReorder)(sorted); + } + else { + var s = state.get(); + state.set(__assign(__assign({}, s), { activeKey: '', dragStartSlot: -1 })); + dragY.set(0); + if (onDragEnd) { + runOnJS(onDragEnd)(); + } + } + } + })); + }) + // Reset if the gesture is cancelled without onEnd firing. + .onFinalize(function () { + 'worklet'; + isGestureActive.set(false); + if (state.get().activeKey === itemKey && dragY.get() === 0) { + var s = state.get(); + state.set(__assign(__assign({}, s), { activeKey: '', dragStartSlot: -1 })); + if (onDragEnd) { + runOnJS(onDragEnd)(); + } + } + }); + // All vertical positioning is via translateY (no `top`). This avoids + // discrete jumps when slots change — Reanimated smoothly animates from + // the current translateY to the new target on every state transition. + // On first mount we skip the animation so items appear instantly. + var isFirstRender = useSharedValue(true); + var animatedStyle = useAnimatedStyle(function () { + var s = state.get(); + var mySlot = s.slots[itemKey]; + if (mySlot === undefined) { + return {}; + } + var baseY = mySlot * itemHeight; + // Active item: follow the finger with a slight scale-up and shadow. + if (s.activeKey === itemKey) { + return __assign({ transform: [ + { translateY: s.dragStartSlot * itemHeight + dragY.get() }, + { scale: withSpring(1.03) }, + ], zIndex: 999 }, (IS_IOS + ? { + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: withSpring(0.08), + shadowRadius: withSpring(4), + } + : { + elevation: withSpring(3), + })); + } + // Reset for non-active states. Without this, shadow props + // set during dragging linger on the native view. + var inactive = __assign({}, (IS_IOS + ? { + shadowOpacity: withSpring(0), + shadowRadius: withSpring(0), + } + : { + elevation: withSpring(0), + })); + // Another item is being dragged — shift to make room. + if (s.activeKey !== '') { + isFirstRender.set(false); + var currentDragPos = Math.round((s.dragStartSlot * itemHeight + dragY.get()) / itemHeight); + var clampedPos = Math.max(0, Math.min(currentDragPos, itemCount - 1)); + var offset = 0; + if (s.dragStartSlot < clampedPos && + mySlot > s.dragStartSlot && + mySlot <= clampedPos) { + offset = -itemHeight; + } + else if (s.dragStartSlot > clampedPos && + mySlot < s.dragStartSlot && + mySlot >= clampedPos) { + offset = itemHeight; + } + return __assign({ transform: [ + { translateY: withTiming(baseY + offset, { duration: 200 }) }, + { scale: withSpring(1) }, + ], zIndex: 0 }, inactive); + } + // Idle: sit at our slot. On first render use a direct value so items + // don't animate from y=0. After any drag, use withTiming so the + // shift→idle transition is smooth (no discrete jump). + if (isFirstRender.get()) { + isFirstRender.set(false); + return __assign({ transform: [{ translateY: baseY }, { scale: 1 }], zIndex: 0 }, inactive); + } + return __assign({ transform: [{ translateY: withTiming(baseY, { duration: 200 }) }, { scale: 1 }], zIndex: 0 }, inactive); + }); + var dragHandle = (_jsx(GestureDetector, { gesture: gesture, children: _jsx(Animated.View, { style: [ + a.justify_center, + a.align_center, + a.px_sm, + a.py_md, + web({ cursor: 'grab' }), + ], hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, children: _jsx(GripIcon, { size: "lg", fill: t.atoms.text_contrast_medium.color, style: web({ pointerEvents: 'none' }) }) }) })); + return (_jsx(Animated.View, { style: [ + { + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: itemHeight, + }, + animatedStyle, + ], children: renderItem(item, dragHandle) })); +} diff --git a/src/components/DraggableList/index.web.js b/src/components/DraggableList/index.web.js new file mode 100644 index 0000000000..5c6d10a92e --- /dev/null +++ b/src/components/DraggableList/index.web.js @@ -0,0 +1,121 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { useTheme } from '#/alf'; +import { DotGrid2x3_Stroke2_Corner0_Rounded as GripIcon } from '#/components/icons/DotGrid'; +export function SortableList(_a) { + var data = _a.data, keyExtractor = _a.keyExtractor, renderItem = _a.renderItem, onReorder = _a.onReorder, onDragStart = _a.onDragStart, onDragEnd = _a.onDragEnd, itemHeight = _a.itemHeight; + var t = useTheme(); + var _b = useState(null), dragState = _b[0], setDragState = _b[1]; + var getNewPosition = function (state) { + var translationY = state.currentY - state.startY; + var rawNewPos = Math.round((state.activeIndex * itemHeight + translationY) / itemHeight); + return Math.max(0, Math.min(rawNewPos, data.length - 1)); + }; + var handlePointerMove = function (e) { + if (!dragState) + return; + e.preventDefault(); + setDragState(function (prev) { return (prev ? __assign(__assign({}, prev), { currentY: e.clientY }) : null); }); + }; + var handlePointerUp = function () { + if (!dragState) + return; + var newPos = getNewPosition(dragState); + if (newPos !== dragState.activeIndex) { + var next = __spreadArray([], data, true); + var moved = next.splice(dragState.activeIndex, 1)[0]; + next.splice(newPos, 0, moved); + onReorder(next); + } + setDragState(null); + onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(); + }; + var handlePointerDown = function (e, index) { + e.preventDefault(); + e.target.setPointerCapture(e.pointerId); + setDragState({ activeIndex: index, currentY: e.clientY, startY: e.clientY }); + onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(); + }; + var newPos = dragState ? getNewPosition(dragState) : -1; + return (_jsx(View, { style: [ + { height: data.length * itemHeight, position: 'relative' }, + t.atoms.bg_contrast_25, + ], + // @ts-expect-error web-only pointer events + onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerUp, children: data.map(function (item, index) { + var isActive = (dragState === null || dragState === void 0 ? void 0 : dragState.activeIndex) === index; + // Clamp translation so the item stays within list bounds. + var rawTranslationY = isActive + ? dragState.currentY - dragState.startY + : 0; + var translationY = isActive + ? Math.max(-index * itemHeight, Math.min(rawTranslationY, (data.length - 1 - index) * itemHeight)) + : 0; + // Non-dragged items shift to make room for the dragged item. + var offset = 0; + if (dragState && !isActive) { + var orig = dragState.activeIndex; + if (orig < newPos && index > orig && index <= newPos) { + offset = -itemHeight; + } + else if (orig > newPos && index < orig && index >= newPos) { + offset = itemHeight; + } + } + var dragHandle = (_jsx("div", { onPointerDown: function (e) { + return handlePointerDown(e, index); + }, style: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + paddingLeft: 8, + paddingRight: 8, + paddingTop: 12, + paddingBottom: 12, + cursor: isActive ? 'grabbing' : 'grab', + touchAction: 'none', + userSelect: 'none', + }, children: _jsx(GripIcon, { size: "lg", fill: t.atoms.text_contrast_medium.color, style: { pointerEvents: 'none' } }) })); + return (_jsx(View, { style: [ + { + position: 'absolute', + top: index * itemHeight, + left: 0, + right: 0, + height: itemHeight, + transform: [{ translateY: isActive ? translationY : offset }], + scale: isActive ? 1.03 : 1, + zIndex: isActive ? 999 : 0, + boxShadow: isActive ? '0 2px 12px rgba(0,0,0,0.06)' : 'none', + // Animate scale/shadow on pickup, and transform for + // non-dragged items shifting into place. + transition: isActive + ? 'box-shadow 200ms ease, scale 200ms ease' + : dragState + ? 'transform 200ms ease' + : 'none', + }, + ], children: renderItem(item, dragHandle) }, keyExtractor(item))); + }) })); +} diff --git a/src/components/Error.js b/src/components/Error.js new file mode 100644 index 0000000000..cc81d3a3e8 --- /dev/null +++ b/src/components/Error.js @@ -0,0 +1,36 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useGoBack } from '#/lib/hooks/useGoBack'; +import { CenteredView } from '#/view/com/util/Views'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { Text } from '#/components/Typography'; +export function Error(_a) { + var title = _a.title, message = _a.message, onRetry = _a.onRetry, onGoBack = _a.onGoBack, hideBackButton = _a.hideBackButton, _b = _a.sideBorders, sideBorders = _b === void 0 ? true : _b; + var _ = useLingui()._; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var goBack = useGoBack(onGoBack); + return (_jsxs(CenteredView, { style: [ + a.h_full_vh, + a.align_center, + a.gap_5xl, + !gtMobile && a.justify_between, + t.atoms.border_contrast_low, + { paddingTop: 175, paddingBottom: 110 }, + ], sideBorders: sideBorders, children: [_jsxs(View, { style: [a.w_full, a.align_center, a.gap_lg], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_3xl], children: title }), _jsx(Text, { style: [ + a.text_md, + a.text_center, + t.atoms.text_contrast_high, + { lineHeight: 1.4 }, + gtMobile ? { width: 450 } : [a.w_full, a.px_lg], + ], children: message })] }), _jsxs(View, { style: [a.gap_md, gtMobile ? { width: 350 } : [a.w_full, a.px_lg]], children: [onRetry && (_jsx(Button, { variant: "solid", color: "primary", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Press to retry"], ["Press to retry"])))), onPress: onRetry, size: "large", style: [a.rounded_sm, a.overflow_hidden, { paddingVertical: 10 }], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }) })), !hideBackButton && (_jsx(Button, { variant: "solid", color: onRetry ? 'secondary' : 'primary', label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Return to previous page"], ["Return to previous page"])))), onPress: goBack, size: "large", style: [a.rounded_sm, a.overflow_hidden, { paddingVertical: 10 }], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go Back" }) }) }))] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/FeedCard.js b/src/components/FeedCard.js new file mode 100644 index 0000000000..9d638cf310 --- /dev/null +++ b/src/components/FeedCard.js @@ -0,0 +1,267 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useMemo } from 'react'; +import { View } from 'react-native'; +import { AtUri, RichText as RichTextApi, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { precacheFeedFromGeneratorView } from '#/state/queries/feed'; +import { useAddSavedFeedsMutation, usePreferencesQuery, useRemoveFeedMutation, } from '#/state/queries/preferences'; +import { useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, select, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText, } from '#/components/Button'; +import { Live_Stroke2_Corner0_Rounded as LiveIcon } from '#/components/icons/Live'; +import { Pin_Stroke2_Corner0_Rounded as PinIcon } from '#/components/icons/Pin'; +import { Link as InternalLink } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { useActiveLiveEventFeedUris } from '#/features/liveEvents/context'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from './icons/Trash'; +export function Default(props) { + var view = props.view; + return (_jsx(Link, __assign({}, props, { children: _jsxs(Outer, { children: [_jsxs(Header, { children: [_jsx(Avatar, { src: view.avatar }), _jsx(TitleAndByline, { title: view.displayName, creator: view.creator, uri: view.uri }), _jsx(SaveButton, { view: view, pin: true })] }), _jsx(Description, { description: view.description }), _jsx(Likes, { count: view.likeCount || 0 })] }) }))); +} +export function Link(_a) { + var view = _a.view, children = _a.children, props = __rest(_a, ["view", "children"]); + var queryClient = useQueryClient(); + var href = React.useMemo(function () { + return createProfileFeedHref({ feed: view }); + }, [view]); + React.useEffect(function () { + precacheFeedFromGeneratorView(queryClient, view); + }, [view, queryClient]); + return (_jsx(InternalLink, __assign({ label: view.displayName, to: href, style: [a.flex_col] }, props, { children: children }))); +} +export function Outer(_a) { + var children = _a.children; + return _jsx(View, { style: [a.w_full, a.gap_sm], children: children }); +} +export function Header(_a) { + var children = _a.children; + return _jsx(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: children }); +} +export function Avatar(_a) { + var src = _a.src, _b = _a.size, size = _b === void 0 ? 40 : _b; + return _jsx(UserAvatar, { type: "algo", size: size, avatar: src }); +} +export function AvatarPlaceholder(_a) { + var _b = _a.size, size = _b === void 0 ? 40 : _b; + var t = useTheme(); + return (_jsx(View, { style: [ + t.atoms.bg_contrast_25, + { + width: size, + height: size, + borderRadius: 8, + }, + ] })); +} +export function TitleAndByline(_a) { + var title = _a.title, creator = _a.creator, uri = _a.uri; + var t = useTheme(); + var activeLiveEvents = useActiveLiveEventFeedUris(); + var liveColor = useMemo(function () { + return select(t.name, { + dark: t.palette.negative_600, + dim: t.palette.negative_600, + light: t.palette.negative_500, + }); + }, [t]); + return (_jsxs(View, { style: [a.flex_1], children: [uri && activeLiveEvents.has(uri) && (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_2xs], children: [_jsx(LiveIcon, { size: "xs", fill: liveColor }), _jsx(Text, { style: [ + a.text_2xs, + a.font_medium, + a.leading_snug, + { color: liveColor }, + ], children: _jsx(Trans, { children: "Happening now" }) })] })), _jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_snug], numberOfLines: 1, children: title }), creator && (_jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_medium], numberOfLines: 1, children: _jsxs(Trans, { children: ["Feed by ", sanitizeHandle(creator.handle, '@')] }) }))] })); +} +export function TitleAndBylinePlaceholder(_a) { + var creator = _a.creator; + var t = useTheme(); + return (_jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsx(View, { style: [ + a.rounded_xs, + t.atoms.bg_contrast_50, + { + width: '60%', + height: 14, + }, + ] }), creator && (_jsx(View, { style: [ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + width: '40%', + height: 10, + }, + ] }))] })); +} +export function Description(_a) { + var description = _a.description, rest = __rest(_a, ["description"]); + var rt = React.useMemo(function () { + if (!description) + return; + var rt = new RichTextApi({ text: description || '' }); + rt.detectFacetsWithoutResolution(); + return rt; + }, [description]); + if (!rt) + return null; + return _jsx(RichText, __assign({ value: rt, disableLinks: true }, rest)); +} +export function DescriptionPlaceholder() { + var t = useTheme(); + return (_jsxs(View, { style: [a.gap_xs], children: [_jsx(View, { style: [a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, { height: 12 }] }), _jsx(View, { style: [a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, { height: 12 }] }), _jsx(View, { style: [ + a.rounded_xs, + a.w_full, + t.atoms.bg_contrast_50, + { height: 12, width: 100 }, + ] })] })); +} +export function Likes(_a) { + var count = _a.count; + var t = useTheme(); + return (_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium, a.font_semi_bold], children: _jsxs(Trans, { children: ["Liked by ", _jsx(Plural, { value: count || 0, one: "# user", other: "# users" })] }) })); +} +export function SaveButton(_a) { + var view = _a.view, pin = _a.pin, props = __rest(_a, ["view", "pin"]); + var hasSession = useSession().hasSession; + if (!hasSession) + return null; + return _jsx(SaveButtonInner, __assign({ view: view, pin: pin }, props)); +} +function SaveButtonInner(_a) { + var _this = this; + var view = _a.view, pin = _a.pin, _b = _a.text, text = _b === void 0 ? true : _b, buttonProps = __rest(_a, ["view", "pin", "text"]); + var _ = useLingui()._; + var preferences = usePreferencesQuery().data; + var _c = useAddSavedFeedsMutation(), isAddSavedFeedPending = _c.isPending, saveFeeds = _c.mutateAsync; + var _d = useRemoveFeedMutation(), isRemovePending = _d.isPending, removeFeed = _d.mutateAsync; + var uri = view.uri; + var type = view.uri.includes('app.bsky.feed.generator') ? 'feed' : 'list'; + var savedFeedConfig = React.useMemo(function () { + var _a; + return (_a = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds) === null || _a === void 0 ? void 0 : _a.find(function (feed) { return feed.value === uri; }); + }, [preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds, uri]); + var removePromptControl = Prompt.usePromptControl(); + var isPending = isAddSavedFeedPending || isRemovePending; + var toggleSave = React.useCallback(function (e) { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + e.preventDefault(); + e.stopPropagation(); + _a.label = 1; + case 1: + _a.trys.push([1, 6, , 7]); + if (!savedFeedConfig) return [3 /*break*/, 3]; + return [4 /*yield*/, removeFeed(savedFeedConfig)]; + case 2: + _a.sent(); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, saveFeeds([ + { + type: type, + value: uri, + pinned: pin || false, + }, + ])]; + case 4: + _a.sent(); + _a.label = 5; + case 5: + Toast.show(_(msg({ message: 'Feeds updated!', context: 'toast' }))); + return [3 /*break*/, 7]; + case 6: + err_1 = _a.sent(); + logger.error(err_1, { message: "FeedCard: failed to update feeds", pin: pin }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to update feeds"], ["Failed to update feeds"])))), 'xmark'); + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); }, [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type]); + var onPrompRemoveFeed = React.useCallback(function (e) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + e.preventDefault(); + e.stopPropagation(); + removePromptControl.open(); + return [2 /*return*/]; + }); + }); }, [removePromptControl]); + return (_jsxs(_Fragment, { children: [_jsx(Button, __assign({ disabled: isPending, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Add this feed to your feeds"], ["Add this feed to your feeds"])))), size: "small", variant: "solid", color: savedFeedConfig ? 'secondary' : 'primary', onPress: savedFeedConfig ? onPrompRemoveFeed : toggleSave }, buttonProps, { children: savedFeedConfig ? (_jsxs(_Fragment, { children: [isPending ? (_jsx(ButtonIcon, { size: "md", icon: Loader })) : (!text && _jsx(ButtonIcon, { size: "md", icon: TrashIcon })), text && (_jsx(ButtonText, { children: _jsx(Trans, { children: "Unpin Feed" }) }))] })) : (_jsxs(_Fragment, { children: [_jsx(ButtonIcon, { size: "md", icon: isPending ? Loader : PinIcon }), text && (_jsx(ButtonText, { children: _jsx(Trans, { children: "Pin Feed" }) }))] })) })), _jsx(Prompt.Basic, { control: removePromptControl, title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Remove from your feeds?"], ["Remove from your feeds?"])))), description: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Are you sure you want to remove this from your feeds?"], ["Are you sure you want to remove this from your feeds?"])))), onConfirm: toggleSave, confirmButtonCta: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Remove"], ["Remove"])))), confirmButtonColor: "negative" })] })); +} +export function createProfileFeedHref(_a) { + var feed = _a.feed; + var urip = new AtUri(feed.uri); + var handleOrDid = feed.creator.handle || feed.creator.did; + return "/profile/".concat(handleOrDid, "/feed/").concat(urip.rkey); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/FeedInterstitials.js b/src/components/FeedInterstitials.js new file mode 100644 index 0000000000..fdfe4b0630 --- /dev/null +++ b/src/components/FeedInterstitials.js @@ -0,0 +1,563 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ScrollView, View } from 'react-native'; +import Animated, { Easing, FadeIn, FadeOut, LayoutAnimationConfig, LinearTransition, } from 'react-native-reanimated'; +import { AtUri } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useGetPopularFeedsQuery } from '#/state/queries/feed'; +import { useProfilesQuery } from '#/state/queries/profile'; +import { useSuggestedFollowsByActorQuery, useSuggestedFollowsQuery, } from '#/state/queries/suggested-follows'; +import { useSession } from '#/state/session'; +import * as userActionHistory from '#/state/userActionHistory'; +import { BlockDrawerGesture } from '#/view/shell/BlockDrawerGesture'; +import { atoms as a, native, useBreakpoints, useTheme, web, } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import * as FeedCard from '#/components/FeedCard'; +import { ArrowRight_Stroke2_Corner0_Rounded as ArrowRight } from '#/components/icons/Arrow'; +import { Hashtag_Stroke2_Corner0_Rounded as Hashtag } from '#/components/icons/Hashtag'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { InlineLinkText } from '#/components/Link'; +import * as ProfileCard from '#/components/ProfileCard'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS } from '#/env'; +import { FollowDialogWithoutGuide } from './ProgressGuide/FollowDialog'; +import { ProgressGuideList } from './ProgressGuide/List'; +var DISMISS_ANIMATION_DURATION = 200; +var MOBILE_CARD_WIDTH = 165; +var FINAL_CARD_WIDTH = 120; +function CardOuter(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + return (_jsx(View, { testID: "CardOuter", style: [ + a.flex_1, + a.w_full, + a.p_md, + a.rounded_lg, + a.border, + t.atoms.bg, + t.atoms.shadow_sm, + t.atoms.border_contrast_low, + !gtMobile && { + width: MOBILE_CARD_WIDTH, + }, + style, + ], children: children })); +} +export function SuggestedFollowPlaceholder() { + return (_jsx(CardOuter, { children: _jsxs(ProfileCard.Outer, { children: [_jsxs(View, { style: [a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto], children: [_jsx(ProfileCard.AvatarPlaceholder, { size: 88 }), _jsx(ProfileCard.NamePlaceholder, {}), _jsx(View, { style: [a.w_full], children: _jsx(ProfileCard.DescriptionPlaceholder, { numberOfLines: 2 }) })] }), _jsx(ProfileCard.FollowButtonPlaceholder, {})] }) })); +} +export function SuggestedFeedsCardPlaceholder() { + return (_jsxs(CardOuter, { style: [a.gap_sm], children: [_jsxs(FeedCard.Header, { children: [_jsx(FeedCard.AvatarPlaceholder, {}), _jsx(FeedCard.TitleAndBylinePlaceholder, { creator: true })] }), _jsx(FeedCard.DescriptionPlaceholder, {})] })); +} +function getRank(seenPost) { + var _a, _b, _c; + var tier; + if (seenPost.feedContext === 'popfriends') { + tier = 'a'; + } + else if ((_a = seenPost.feedContext) === null || _a === void 0 ? void 0 : _a.startsWith('cluster')) { + tier = 'b'; + } + else if (seenPost.feedContext === 'popcluster') { + tier = 'c'; + } + else if ((_b = seenPost.feedContext) === null || _b === void 0 ? void 0 : _b.startsWith('ntpc')) { + tier = 'd'; + } + else if ((_c = seenPost.feedContext) === null || _c === void 0 ? void 0 : _c.startsWith('t-')) { + tier = 'e'; + } + else if (seenPost.feedContext === 'nettop') { + tier = 'f'; + } + else { + tier = 'g'; + } + var score = Math.round(Math.log(1 + seenPost.likeCount + seenPost.repostCount + seenPost.replyCount)); + if (seenPost.isFollowedBy || Math.random() > 0.9) { + score *= 2; + } + var rank = 100 - score; + return "".concat(tier, "-").concat(rank); +} +function sortSeenPosts(postA, postB) { + var rankA = getRank(postA); + var rankB = getRank(postB); + // Yes, we're comparing strings here. + // The "larger" string means a worse rank. + if (rankA > rankB) { + return 1; + } + else if (rankA < rankB) { + return -1; + } + else { + return 0; + } +} +function useExperimentalSuggestedUsersQuery() { + var currentAccount = useSession().currentAccount; + var userActionSnapshot = userActionHistory.useActionHistorySnapshot(); + var dids = useMemo(function () { + var likes = userActionSnapshot.likes, follows = userActionSnapshot.follows, followSuggestions = userActionSnapshot.followSuggestions, seen = userActionSnapshot.seen; + var likeDids = likes + .map(function (l) { return new AtUri(l); }) + .map(function (uri) { return uri.host; }) + .filter(function (did) { return !follows.includes(did); }); + var suggestedDids = []; + if (followSuggestions.length > 0) { + suggestedDids = [ + // It's ok if these will pick the same item (weighed by its frequency) + followSuggestions[Math.floor(Math.random() * followSuggestions.length)], + followSuggestions[Math.floor(Math.random() * followSuggestions.length)], + followSuggestions[Math.floor(Math.random() * followSuggestions.length)], + followSuggestions[Math.floor(Math.random() * followSuggestions.length)], + ]; + } + var seenDids = seen + .sort(sortSeenPosts) + .map(function (l) { return new AtUri(l.uri); }) + .map(function (uri) { return uri.host; }); + return __spreadArray([], new Set(__spreadArray(__spreadArray(__spreadArray([], suggestedDids, true), likeDids, true), seenDids, true)), true).filter(function (did) { return did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); + }, [userActionSnapshot, currentAccount]); + var _a = useProfilesQuery({ + handles: dids.slice(0, 16), + }), data = _a.data, isLoading = _a.isLoading, error = _a.error; + var profiles = data + ? data.profiles.filter(function (profile) { + var _a; + return !((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following); + }) + : []; + return { + isLoading: isLoading, + error: error, + profiles: profiles.slice(0, 6), + }; +} +export function SuggestedFollows(_a) { + var feed = _a.feed; + var currentAccount = useSession().currentAccount; + var _b = feed.split('|'), feedType = _b[0], feedUriOrDid = _b[1]; + if (feedType === 'author') { + if ((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === feedUriOrDid) { + return null; + } + else { + return _jsx(SuggestedFollowsProfile, { did: feedUriOrDid }); + } + } + else { + return _jsx(SuggestedFollowsHome, {}); + } +} +export function SuggestedFollowsProfile(_a) { + var did = _a.did; + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var maxLength = gtMobile ? 4 : 6; + var _b = useSuggestedFollowsByActorQuery({ + did: did, + }), isSuggestionsLoading = _b.isLoading, data = _b.data, error = _b.error; + var _c = useSuggestedFollowsQuery({ limit: 25 }), moreSuggestions = _c.data, fetchNextPage = _c.fetchNextPage, hasNextPage = _c.hasNextPage, isFetchingNextPage = _c.isFetchingNextPage; + var _d = useState(new Set()), dismissedDids = _d[0], setDismissedDids = _d[1]; + var onDismiss = useCallback(function (dismissedDid) { + setDismissedDids(function (prev) { return new Set(prev).add(dismissedDid); }); + }, []); + // Combine profiles from the actor-specific query with fallback suggestions + var allProfiles = useMemo(function () { + var _a, _b; + var actorProfiles = (_a = data === null || data === void 0 ? void 0 : data.suggestions) !== null && _a !== void 0 ? _a : []; + var fallbackProfiles = (_b = moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages.flatMap(function (page) { + return page.actors.map(function (actor) { return ({ actor: actor, recId: page.recId }); }); + })) !== null && _b !== void 0 ? _b : []; + // Dedupe by did, preferring actor-specific profiles + var seen = new Set(); + var combined = []; + for (var _i = 0, actorProfiles_1 = actorProfiles; _i < actorProfiles_1.length; _i++) { + var profile = actorProfiles_1[_i]; + if (!seen.has(profile.did)) { + seen.add(profile.did); + combined.push({ actor: profile, recId: data === null || data === void 0 ? void 0 : data.recId }); + } + } + for (var _c = 0, fallbackProfiles_1 = fallbackProfiles; _c < fallbackProfiles_1.length; _c++) { + var profile = fallbackProfiles_1[_c]; + if (!seen.has(profile.actor.did) && profile.actor.did !== did) { + seen.add(profile.actor.did); + combined.push(profile); + } + } + return combined; + }, [data === null || data === void 0 ? void 0 : data.suggestions, moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages, did, data === null || data === void 0 ? void 0 : data.recId]); + var filteredProfiles = useMemo(function () { + return allProfiles.filter(function (p) { return !dismissedDids.has(p.actor.did); }); + }, [allProfiles, dismissedDids]); + // Fetch more when running low + useEffect(function () { + if (moderationOpts && + filteredProfiles.length < maxLength && + hasNextPage && + !isFetchingNextPage) { + void fetchNextPage(); + } + }, [ + filteredProfiles.length, + maxLength, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + moderationOpts, + ]); + return (_jsx(ProfileGrid, { isSuggestionsLoading: isSuggestionsLoading, profiles: filteredProfiles, totalProfileCount: allProfiles.length, error: error, viewContext: "profile", onDismiss: onDismiss })); +} +export function SuggestedFollowsHome() { + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var maxLength = gtMobile ? 4 : 6; + var _a = useExperimentalSuggestedUsersQuery(), isSuggestionsLoading = _a.isLoading, experimentalProfiles = _a.profiles, experimentalError = _a.error; + var _b = useSuggestedFollowsQuery({ limit: 25 }), moreSuggestions = _b.data, fetchNextPage = _b.fetchNextPage, hasNextPage = _b.hasNextPage, isFetchingNextPage = _b.isFetchingNextPage, suggestionsError = _b.error; + var _c = useState(new Set()), dismissedDids = _c[0], setDismissedDids = _c[1]; + var onDismiss = useCallback(function (did) { + setDismissedDids(function (prev) { return new Set(prev).add(did); }); + }, []); + // Combine profiles from experimental query with paginated suggestions + var allProfiles = useMemo(function () { + var _a; + var fallbackProfiles = (_a = moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages.flatMap(function (page) { + return page.actors.map(function (actor) { return ({ actor: actor, recId: page.recId }); }); + })) !== null && _a !== void 0 ? _a : []; + // Dedupe by did, preferring experimental profiles + var seen = new Set(); + var combined = []; + for (var _i = 0, experimentalProfiles_1 = experimentalProfiles; _i < experimentalProfiles_1.length; _i++) { + var profile = experimentalProfiles_1[_i]; + if (!seen.has(profile.did)) { + seen.add(profile.did); + combined.push({ actor: profile, recId: undefined }); + } + } + for (var _b = 0, fallbackProfiles_2 = fallbackProfiles; _b < fallbackProfiles_2.length; _b++) { + var profile = fallbackProfiles_2[_b]; + if (!seen.has(profile.actor.did)) { + seen.add(profile.actor.did); + combined.push(profile); + } + } + return combined; + }, [experimentalProfiles, moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages]); + var filteredProfiles = useMemo(function () { + return allProfiles.filter(function (p) { return !dismissedDids.has(p.actor.did); }); + }, [allProfiles, dismissedDids]); + // Fetch more when running low + useEffect(function () { + if (moderationOpts && + filteredProfiles.length < maxLength && + hasNextPage && + !isFetchingNextPage) { + void fetchNextPage(); + } + }, [ + filteredProfiles.length, + maxLength, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + moderationOpts, + ]); + return (_jsx(ProfileGrid, { isSuggestionsLoading: isSuggestionsLoading, profiles: filteredProfiles, totalProfileCount: allProfiles.length, error: experimentalError || suggestionsError, viewContext: "feed", onDismiss: onDismiss })); +} +export function ProfileGrid(_a) { + var isSuggestionsLoading = _a.isSuggestionsLoading, error = _a.error, profiles = _a.profiles, totalProfileCount = _a.totalProfileCount, _b = _a.viewContext, viewContext = _b === void 0 ? 'feed' : _b, onDismiss = _a.onDismiss, _c = _a.isVisible, isVisible = _c === void 0 ? true : _c; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + var gtMobile = useBreakpoints().gtMobile; + var followDialogControl = useDialogControl(); + var isLoading = isSuggestionsLoading || !moderationOpts; + var isProfileHeaderContext = viewContext === 'profileHeader'; + var isFeedContext = viewContext === 'feed'; + var maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6; + var minLength = gtMobile ? 3 : 4; + // Track seen profiles + var seenProfilesRef = useRef(new Set()); + var containerRef = useRef(null); + var hasTrackedRef = useRef(false); + var logContext = isFeedContext + ? 'InterstitialDiscover' + : isProfileHeaderContext + ? 'Profile' + : 'InterstitialProfile'; + // Callback to fire seen events + var fireSeen = useCallback(function () { + if (isLoading || error || !profiles.length) + return; + if (hasTrackedRef.current) + return; + hasTrackedRef.current = true; + var profilesToShow = profiles.slice(0, maxLength); + profilesToShow.forEach(function (profile, index) { + if (!seenProfilesRef.current.has(profile.actor.did)) { + seenProfilesRef.current.add(profile.actor.did); + ax.metric('suggestedUser:seen', { + logContext: logContext, + recId: profile.recId, + position: index, + suggestedDid: profile.actor.did, + category: null, + }); + } + }); + }, [ax, isLoading, error, profiles, maxLength, logContext]); + // For profile header, fire when isVisible becomes true + useEffect(function () { + if (isProfileHeaderContext) { + if (!isVisible) { + hasTrackedRef.current = false; + return; + } + fireSeen(); + } + }, [isVisible, isProfileHeaderContext, fireSeen]); + // For feed interstitials, use IntersectionObserver to detect actual visibility + useEffect(function () { + if (isProfileHeaderContext) + return; // handled above + if (isLoading || error || !profiles.length) + return; + var node = containerRef.current; + if (!node) + return; + // Use IntersectionObserver on web to detect when actually visible + if (typeof IntersectionObserver !== 'undefined') { + var observer_1 = new IntersectionObserver(function (entries) { + var _a; + if ((_a = entries[0]) === null || _a === void 0 ? void 0 : _a.isIntersecting) { + fireSeen(); + observer_1.disconnect(); + } + }, { threshold: 0.5 }); + // @ts-ignore - web only + observer_1.observe(node); + return function () { return observer_1.disconnect(); }; + } + else { + // On native, delay slightly to account for layout shifts during hydration + var timeout_1 = setTimeout(function () { + fireSeen(); + }, 500); + return function () { return clearTimeout(timeout_1); }; + } + }, [isProfileHeaderContext, isLoading, error, profiles.length, fireSeen]); + var content = isLoading + ? Array(maxLength) + .fill(0) + .map(function (_, i) { return (_jsx(View, { style: [ + a.flex_1, + gtMobile && + web([ + a.flex_0, + a.flex_grow, + { width: "calc(30% - ".concat(a.gap_md.gap / 2, "px)") }, + ]), + ], children: _jsx(SuggestedFollowPlaceholder, {}) }, i)); }) + : error || !profiles.length + ? null + : profiles.slice(0, maxLength).map(function (profile, index) { return (_jsx(Animated.View, { layout: native(LinearTransition.delay(DISMISS_ANIMATION_DURATION).easing(Easing.out(Easing.exp))), exiting: FadeOut.duration(DISMISS_ANIMATION_DURATION), + // for web, as the cards are static, not in a list + entering: web(FadeIn.delay(DISMISS_ANIMATION_DURATION * 2)), style: [ + a.flex_1, + gtMobile && + web([ + a.flex_0, + a.flex_grow, + { width: "calc(30% - ".concat(a.gap_md.gap / 2, "px)") }, + ]), + ], children: _jsx(ProfileCard.Link, { profile: profile.actor, onPress: function () { + ax.metric('suggestedUser:press', { + logContext: isFeedContext + ? 'InterstitialDiscover' + : 'InterstitialProfile', + recId: profile.recId, + position: index, + suggestedDid: profile.actor.did, + category: null, + }); + }, style: [a.flex_1], children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(CardOuter, { style: [ + (hovered || pressed) && t.atoms.border_contrast_high, + ], children: _jsxs(ProfileCard.Outer, { children: [onDismiss && (_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Dismiss this suggestion"], ["Dismiss this suggestion"])))), onPress: function (e) { + e.preventDefault(); + onDismiss(profile.actor.did); + ax.metric('suggestedUser:dismiss', { + logContext: isFeedContext + ? 'InterstitialDiscover' + : 'InterstitialProfile', + position: index, + suggestedDid: profile.actor.did, + recId: profile.recId, + }); + }, style: [ + a.absolute, + a.z_10, + a.p_xs, + { top: -4, right: -4 }, + ], children: function (_a) { + var dismissHovered = _a.hovered, dismissPressed = _a.pressed; + return (_jsx(X, { size: "xs", fill: dismissHovered || dismissPressed + ? t.atoms.text.color + : t.atoms.text_contrast_medium.color })); + } })), _jsxs(View, { style: [ + a.flex_col, + a.align_center, + a.gap_sm, + a.pb_sm, + a.mb_auto, + ], children: [_jsx(ProfileCard.Avatar, { profile: profile.actor, moderationOpts: moderationOpts, disabledPreview: true, size: 88 }), _jsxs(View, { style: [a.flex_col, a.align_center, a.max_w_full], children: [_jsx(ProfileCard.Name, { profile: profile.actor, moderationOpts: moderationOpts }), _jsx(ProfileCard.Description, { profile: profile.actor, numberOfLines: 2, style: [ + t.atoms.text_contrast_medium, + a.text_center, + a.text_xs, + ] })] })] }), _jsx(ProfileCard.FollowButton, { profile: profile.actor, moderationOpts: moderationOpts, logContext: "FeedInterstitial", withIcon: false, style: [a.rounded_sm], onFollow: function () { + ax.metric('suggestedUser:follow', { + logContext: isFeedContext + ? 'InterstitialDiscover' + : 'InterstitialProfile', + location: 'Card', + recId: profile.recId, + position: index, + suggestedDid: profile.actor.did, + category: null, + }); + } })] }) })); + } }) }, profile.actor.did)); }); + // Use totalProfileCount (before dismissals) for minLength check on initial render. + var profileCountForMinCheck = totalProfileCount !== null && totalProfileCount !== void 0 ? totalProfileCount : profiles.length; + if (error || (!isLoading && profileCountForMinCheck < minLength)) { + ax.logger.debug("Not enough profiles to show suggested follows"); + return null; + } + return (_jsxs(View, { ref: containerRef, style: [ + !isProfileHeaderContext && a.border_t, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + ], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: [_jsxs(View, { style: [ + a.px_lg, + a.pt_md, + a.flex_row, + a.align_center, + a.justify_between, + ], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: [_jsx(Text, { style: [a.text_sm, a.font_semi_bold, t.atoms.text], children: _jsx(Trans, { children: "Suggested for you" }) }), !isProfileHeaderContext && (_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["See more suggested profiles"], ["See more suggested profiles"])))), onPress: function () { + followDialogControl.open(); + ax.metric('suggestedUser:seeMore', { + logContext: isFeedContext ? 'Explore' : 'Profile', + }); + }, children: function (_a) { + var hovered = _a.hovered; + return (_jsx(Text, { style: [ + a.text_sm, + { color: t.palette.primary_500 }, + hovered && + web({ + textDecorationLine: 'underline', + textDecorationColor: t.palette.primary_500, + }), + ], children: _jsx(Trans, { children: "See more" }) })); + } }))] }), _jsx(FollowDialogWithoutGuide, { control: followDialogControl }), _jsx(LayoutAnimationConfig, { skipExiting: true, skipEntering: true, children: gtMobile ? (_jsx(View, { style: [a.p_lg, a.pt_md], children: _jsx(View, { style: [a.flex_1, a.flex_row, a.flex_wrap, a.gap_md], children: content }) })) : (_jsx(BlockDrawerGesture, { children: _jsxs(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: [a.p_lg, a.pt_md, a.flex_row, a.gap_md], snapToInterval: MOBILE_CARD_WIDTH + a.gap_md.gap, decelerationRate: "fast", children: [content, !isProfileHeaderContext && (_jsx(SeeMoreSuggestedProfilesCard, { onPress: function () { + followDialogControl.open(); + ax.metric('suggestedUser:seeMore', { + logContext: 'Explore', + }); + } }))] }) })) })] })); +} +function SeeMoreSuggestedProfilesCard(_a) { + var onPress = _a.onPress; + var _ = useLingui()._; + return (_jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Browse more accounts"], ["Browse more accounts"])))), onPress: onPress, style: [ + a.flex_col, + a.align_center, + a.justify_center, + a.gap_sm, + a.p_md, + a.rounded_lg, + { width: FINAL_CARD_WIDTH }, + ], children: [_jsx(ButtonIcon, { icon: ArrowRight, size: "lg" }), _jsx(ButtonText, { style: [a.text_md, a.font_medium, a.leading_snug, a.text_center], children: _jsx(Trans, { children: "See more" }) })] })); +} +var numFeedsToDisplay = 3; +export function SuggestedFeeds() { + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var _a = useGetPopularFeedsQuery({ + limit: numFeedsToDisplay, + }), data = _a.data, isLoading = _a.isLoading, error = _a.error; + var navigation = useNavigation(); + var gtMobile = useBreakpoints().gtMobile; + var feeds = useMemo(function () { + var items = []; + if (!data) + return items; + for (var _i = 0, _a = data.pages; _i < _a.length; _i++) { + var page = _a[_i]; + for (var _b = 0, _c = page.feeds; _b < _c.length; _b++) { + var feed = _c[_b]; + items.push(feed); + } + } + return items; + }, [data]); + var content = isLoading ? (Array(numFeedsToDisplay) + .fill(0) + .map(function (_, i) { return _jsx(SuggestedFeedsCardPlaceholder, {}, i); })) : error || !feeds ? null : (_jsx(_Fragment, { children: feeds.slice(0, numFeedsToDisplay).map(function (feed) { return (_jsx(FeedCard.Link, { view: feed, onPress: function () { + ax.metric('feed:interstitial:feedCard:press', {}); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(CardOuter, { style: [(hovered || pressed) && t.atoms.border_contrast_high], children: _jsxs(FeedCard.Outer, { children: [_jsxs(FeedCard.Header, { children: [_jsx(FeedCard.Avatar, { src: feed.avatar }), _jsx(FeedCard.TitleAndByline, { title: feed.displayName, creator: feed.creator, uri: feed.uri })] }), _jsx(FeedCard.Description, { description: feed.description, numberOfLines: 3 })] }) })); + } }, feed.uri)); }) })); + return error ? null : (_jsxs(View, { style: [a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25], children: [_jsxs(View, { style: [a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs], children: [_jsx(Text, { style: [ + a.flex_1, + a.text_lg, + a.font_semi_bold, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Some other feeds you might like" }) }), _jsx(Hashtag, { fill: t.atoms.text_contrast_low.color })] }), gtMobile ? (_jsxs(View, { style: [a.flex_1, a.px_lg, a.pt_md, a.pb_xl, a.gap_md], children: [content, _jsxs(View, { style: [ + a.flex_row, + a.justify_end, + a.align_center, + a.pt_xs, + a.gap_md, + ], children: [_jsx(InlineLinkText, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Browse more suggestions"], ["Browse more suggestions"])))), to: "/search", style: [t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Browse more suggestions" }) }), _jsx(ArrowRight, { size: "sm", fill: t.atoms.text_contrast_medium.color })] })] })) : (_jsx(BlockDrawerGesture, { children: _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, snapToInterval: MOBILE_CARD_WIDTH + a.gap_md.gap, decelerationRate: "fast", children: _jsxs(View, { style: [a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md], children: [content, _jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Browse more feeds on the Explore page"], ["Browse more feeds on the Explore page"])))), onPress: function () { + navigation.navigate('SearchTab'); + }, style: [a.flex_col], children: _jsx(CardOuter, { children: _jsx(View, { style: [a.flex_1, a.justify_center], children: _jsxs(View, { style: [a.flex_row, a.px_lg], children: [_jsx(Text, { style: [a.pr_xl, a.flex_1, a.leading_snug], children: _jsx(Trans, { children: "Browse more suggestions on the Explore page" }) }), _jsx(ArrowRight, { size: "xl" })] }) }) }) })] }) }) }))] })); +} +export function ProgressGuide() { + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + return (_jsx(View, { style: [ + t.atoms.border_contrast_low, + a.px_lg, + a.py_lg, + !gtMobile && { marginTop: 4 }, + ], children: _jsx(ProgressGuideList, {}) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/Fill.js b/src/components/Fill.js new file mode 100644 index 0000000000..360a22b17b --- /dev/null +++ b/src/components/Fill.js @@ -0,0 +1,7 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a } from '#/alf'; +export function Fill(_a) { + var children = _a.children, style = _a.style; + return _jsx(View, { style: [a.absolute, a.inset_0, style], children: children }); +} diff --git a/src/components/FocusScope/index.js b/src/components/FocusScope/index.js new file mode 100644 index 0000000000..e2b726fc15 --- /dev/null +++ b/src/components/FocusScope/index.js @@ -0,0 +1,103 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { Children, cloneElement, isValidElement, useCallback, useEffect, useMemo, useRef, } from 'react'; +import { AccessibilityInfo, findNodeHandle, Pressable, Text, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useA11y } from '#/state/a11y'; +/** + * Conditionally wraps children in a `FocusTrap` component based on whether + * screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when + * no better option is available. + */ +export function FocusScope(_a) { + var children = _a.children; + var screenReaderEnabled = useA11y().screenReaderEnabled; + return screenReaderEnabled ? _jsx(FocusTrap, { children: children }) : children; +} +/** + * `FocusTrap` is intended as a last-ditch effort to ensure that users keep + * focus within a certain section of the app, like an overlay. + * + * It works by placing "guards" at the start and end of the active content. + * Then when the user reaches either of those guards, it will announce that + * they have reached the start or end of the content and tell them how to + * remain within the active content section. + */ +function FocusTrap(_a) { + var children = _a.children; + var _ = useLingui()._; + var child = useRef(null); + /* + * Here we add a ref to the first child of this component. This currently + * overrides any ref already on that first child, so we throw an error here + * to prevent us from ever accidentally doing this. + */ + var decoratedChildren = useMemo(function () { + return Children.toArray(children).map(function (node, i) { + if (i === 0 && isValidElement(node)) { + var n = node; + if (n.props.ref !== undefined) { + throw new Error('FocusScope needs to override the ref on its first child.'); + } + return cloneElement(n, __assign(__assign({}, n.props), { ref: child })); + } + return node; + }); + }, [children]); + var focusNode = useCallback(function (ref) { + if (!ref) + return; + var node = findNodeHandle(ref); + if (node) { + AccessibilityInfo.setAccessibilityFocus(node); + } + }, []); + useEffect(function () { + setTimeout(function () { + focusNode(child.current); + }, 1e3); + }, [focusNode]); + return (_jsxs(_Fragment, { children: [_jsx(Pressable, { accessible: true, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You've reached the start of the active content."], ["You've reached the start of the active content."])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please go back, or activate this element to return to the start of the active content."], ["Please go back, or activate this element to return to the start of the active content."])))), accessibilityActions: [{ name: 'activate', label: 'activate' }], onAccessibilityAction: function (event) { + switch (event.nativeEvent.actionName) { + case 'activate': { + focusNode(child.current); + } + } + }, children: _jsx(Noop, {}) }), _jsx(View + /** + * This property traps focus effectively on iOS, but not on Android. + */ + , { + /** + * This property traps focus effectively on iOS, but not on Android. + */ + accessibilityViewIsModal: true, children: decoratedChildren }), _jsx(Pressable, { accessibilityLabel: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You've reached the end of the active content."], ["You've reached the end of the active content."])))), accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Please go back, or activate this element to return to the start of the active content."], ["Please go back, or activate this element to return to the start of the active content."])))), accessibilityActions: [{ name: 'activate', label: 'activate' }], onAccessibilityAction: function (event) { + switch (event.nativeEvent.actionName) { + case 'activate': { + focusNode(child.current); + } + } + }, children: _jsx(Noop, {}) })] })); +} +function Noop() { + return (_jsx(Text, { accessible: false, style: { + height: 1, + opacity: 0, + }, children: ' ' })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/FocusScope/index.web.js b/src/components/FocusScope/index.web.js new file mode 100644 index 0000000000..353657263c --- /dev/null +++ b/src/components/FocusScope/index.web.js @@ -0,0 +1,11 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { FocusScope as RadixFocusScope } from 'radix-ui/internal'; +/* + * The web version of the FocusScope component is a proper implementation, we + * use this in Dialogs and such already. It's here as a convenient counterpart + * to the hacky native solution. + */ +export function FocusScope(_a) { + var children = _a.children; + return (_jsx(RadixFocusScope.FocusScope, { loop: true, asChild: true, trapped: true, children: children })); +} diff --git a/src/components/FullWindowOverlay.ios.js b/src/components/FullWindowOverlay.ios.js new file mode 100644 index 0000000000..b928508a50 --- /dev/null +++ b/src/components/FullWindowOverlay.ios.js @@ -0,0 +1 @@ +export { FullWindowOverlay } from 'react-native-screens'; diff --git a/src/components/FullWindowOverlay.js b/src/components/FullWindowOverlay.js new file mode 100644 index 0000000000..43b6fc93d1 --- /dev/null +++ b/src/components/FullWindowOverlay.js @@ -0,0 +1 @@ +export { Fragment as FullWindowOverlay } from 'react'; diff --git a/src/components/GradientFill.js b/src/components/GradientFill.js new file mode 100644 index 0000000000..9fc6efeb34 --- /dev/null +++ b/src/components/GradientFill.js @@ -0,0 +1,10 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { LinearGradient } from 'expo-linear-gradient'; +import { atoms as a } from '#/alf'; +export function GradientFill(_a) { + var gradient = _a.gradient, style = _a.style; + if (gradient.values.length < 2) { + throw new Error('Gradient must have at least 2 colors'); + } + return (_jsx(LinearGradient, { colors: gradient.values.map(function (c) { return c[1]; }), locations: gradient.values.map(function (c) { return c[0]; }), start: { x: 0, y: 0 }, end: { x: 1, y: 1 }, style: [a.absolute, a.inset_0, style] })); +} diff --git a/src/components/Grid.js b/src/components/Grid.js new file mode 100644 index 0000000000..b3d8980fb3 --- /dev/null +++ b/src/components/Grid.js @@ -0,0 +1,33 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { View } from 'react-native'; +import { atoms as a } from '#/alf'; +var Context = createContext({ + gap: 0, +}); +Context.displayName = 'GridContext'; +export function Row(_a) { + var children = _a.children, _b = _a.gap, gap = _b === void 0 ? 0 : _b, style = _a.style; + return (_jsx(Context.Provider, { value: useMemo(function () { return ({ gap: gap }); }, [gap]), children: _jsx(View, { style: [ + a.flex_row, + a.flex_1, + { + marginLeft: -gap / 2, + marginRight: -gap / 2, + }, + style, + ], children: children }) })); +} +export function Col(_a) { + var children = _a.children, _b = _a.width, width = _b === void 0 ? 1 : _b, style = _a.style; + var gap = useContext(Context).gap; + return (_jsx(View, { style: [ + a.flex_col, + { + paddingLeft: gap / 2, + paddingRight: gap / 2, + width: "".concat(width * 100, "%"), + }, + style, + ], children: children })); +} diff --git a/src/components/IconCircle.js b/src/components/IconCircle.js new file mode 100644 index 0000000000..1bf186042a --- /dev/null +++ b/src/components/IconCircle.js @@ -0,0 +1,18 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme, } from '#/alf'; +export function IconCircle(_a) { + var Icon = _a.icon, _b = _a.size, size = _b === void 0 ? 'xl' : _b, style = _a.style, iconStyle = _a.iconStyle; + var t = useTheme(); + return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + { + width: size === 'lg' ? 52 : 64, + height: size === 'lg' ? 52 : 64, + backgroundColor: t.palette.primary_50, + }, + style, + ], children: _jsx(Icon, { size: size, style: [{ color: t.palette.primary_500 }, iconStyle] }) })); +} diff --git a/src/components/InterestTabs.js b/src/components/InterestTabs.js new file mode 100644 index 0000000000..e297faddb8 --- /dev/null +++ b/src/components/InterestTabs.js @@ -0,0 +1,260 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect, useRef, useState } from 'react'; +import { View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { DraggableScrollView } from '#/view/com/pager/DraggableScrollView'; +import { atoms as a, tokens, useTheme, web } from '#/alf'; +import { transparentifyColor } from '#/alf/util/colorGeneration'; +import { Button, ButtonIcon } from '#/components/Button'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft, ArrowRight_Stroke2_Corner0_Rounded as ArrowRight, } from '#/components/icons/Arrow'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +/** + * Tab component that automatically scrolls the selected tab into view - used for interests + * in the Find Follows dialog, Explore screen, etc. + */ +export function InterestTabs(_c) { + var onSelectTab = _c.onSelectTab, interests = _c.interests, selectedInterest = _c.selectedInterest, disabled = _c.disabled, interestsDisplayNames = _c.interestsDisplayNames, _d = _c.TabComponent, TabComponent = _d === void 0 ? Tab : _d, contentContainerStyle = _c.contentContainerStyle, _e = _c.gutterWidth, gutterWidth = _e === void 0 ? tokens.space.lg : _e; + var t = useTheme(); + var _ = useLingui()._; + var listRef = useRef(null); + var _f = useState(0), totalWidth = _f[0], setTotalWidth = _f[1]; + var _g = useState(0), scrollX = _g[0], setScrollX = _g[1]; + var _h = useState(0), contentWidth = _h[0], setContentWidth = _h[1]; + var pendingTabOffsets = useRef([]); + var _j = useState([]), tabOffsets = _j[0], setTabOffsets = _j[1]; + var onInitialLayout = useNonReactiveCallback(function () { + var index = interests.indexOf(selectedInterest); + scrollIntoViewIfNeeded(index); + }); + useEffect(function () { + if (tabOffsets) { + onInitialLayout(); + } + }, [tabOffsets, onInitialLayout]); + function scrollIntoViewIfNeeded(index) { + var _c; + var btnLayout = tabOffsets[index]; + if (!btnLayout) + return; + (_c = listRef.current) === null || _c === void 0 ? void 0 : _c.scrollTo({ + // centered + x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2), + animated: true, + }); + } + function handleSelectTab(index) { + var tab = interests[index]; + onSelectTab(tab); + scrollIntoViewIfNeeded(index); + } + function handleTabLayout(index, x, width) { + if (!tabOffsets.length) { + pendingTabOffsets.current[index] = { x: x, width: width }; + // not only do we check if the length is equal to the number of interests, + // but we also need to ensure that the array isn't sparse. `.filter()` + // removes any empty slots from the array + if (pendingTabOffsets.current.filter(function (o) { return !!o; }).length === interests.length) { + setTabOffsets(pendingTabOffsets.current); + } + } + } + var canScrollLeft = scrollX > 0; + var canScrollRight = Math.ceil(scrollX) < contentWidth - totalWidth; + var cleanupRef = useRef(null); + function scrollLeft() { + if (isContinuouslyScrollingRef.current) { + return; + } + if (listRef.current && canScrollLeft) { + var newScrollX = Math.max(0, scrollX - 200); + listRef.current.scrollTo({ x: newScrollX, animated: true }); + } + } + function scrollRight() { + if (isContinuouslyScrollingRef.current) { + return; + } + if (listRef.current && canScrollRight) { + var maxScroll = contentWidth - totalWidth; + var newScrollX = Math.min(maxScroll, scrollX + 200); + listRef.current.scrollTo({ x: newScrollX, animated: true }); + } + } + var isContinuouslyScrollingRef = useRef(false); + function startContinuousScroll(direction) { + // Clear any existing continuous scroll + if (cleanupRef.current) { + cleanupRef.current(); + } + var holdTimeout = null; + var animationFrame = null; + var isActive = true; + isContinuouslyScrollingRef.current = false; + var cleanup = function () { + isActive = false; + if (holdTimeout) + clearTimeout(holdTimeout); + if (animationFrame) + cancelAnimationFrame(animationFrame); + cleanupRef.current = null; + // Reset flag after a delay to prevent onPress from firing + setTimeout(function () { + isContinuouslyScrollingRef.current = false; + }, 100); + }; + cleanupRef.current = cleanup; + // Start continuous scrolling after hold delay + holdTimeout = setTimeout(function () { + if (!isActive) + return; + isContinuouslyScrollingRef.current = true; + var currentScrollPosition = scrollX; + var scroll = function () { + if (!isActive || !listRef.current) + return; + var scrollAmount = 3; + var maxScroll = contentWidth - totalWidth; + var newScrollX; + var canContinue = false; + if (direction === 'left' && currentScrollPosition > 0) { + newScrollX = Math.max(0, currentScrollPosition - scrollAmount); + canContinue = newScrollX > 0; + } + else if (direction === 'right' && currentScrollPosition < maxScroll) { + newScrollX = Math.min(maxScroll, currentScrollPosition + scrollAmount); + canContinue = newScrollX < maxScroll; + } + else { + return; + } + currentScrollPosition = newScrollX; + listRef.current.scrollTo({ x: newScrollX, animated: false }); + if (canContinue && isActive) { + animationFrame = requestAnimationFrame(scroll); + } + }; + scroll(); + }, 500); + } + function stopContinuousScroll() { + if (cleanupRef.current) { + cleanupRef.current(); + } + } + useEffect(function () { + return function () { + if (cleanupRef.current) { + cleanupRef.current(); + } + }; + }, []); + return (_jsxs(View, { style: [a.relative, a.flex_row], children: [_jsx(DraggableScrollView, { ref: listRef, contentContainerStyle: [ + a.gap_sm, + { paddingHorizontal: gutterWidth }, + contentContainerStyle, + ], showsHorizontalScrollIndicator: false, decelerationRate: "fast", snapToOffsets: tabOffsets.filter(function (o) { return !!o; }).length === interests.length + ? tabOffsets.map(function (o) { return o.x - tokens.space.xl; }) + : undefined, onLayout: function (evt) { return setTotalWidth(evt.nativeEvent.layout.width); }, onContentSizeChange: function (width) { return setContentWidth(width); }, onScroll: function (evt) { + var newScrollX = evt.nativeEvent.contentOffset.x; + setScrollX(newScrollX); + }, scrollEventThrottle: 16, children: interests.map(function (interest, i) { + var active = interest === selectedInterest && !disabled; + return (_jsx(TabComponent, { onSelectTab: handleSelectTab, active: active, index: i, interest: interest, interestsDisplayName: interestsDisplayNames[interest], onLayout: handleTabLayout }, interest)); + }) }), IS_WEB && canScrollLeft && (_jsx(View, { style: [ + a.absolute, + a.top_0, + a.left_0, + a.bottom_0, + a.justify_center, + { paddingLeft: gutterWidth }, + a.pr_md, + a.z_10, + web({ + background: "linear-gradient(to right, ".concat(t.atoms.bg.backgroundColor, " 0%, ").concat(t.atoms.bg.backgroundColor, " 70%, ").concat(transparentifyColor(t.atoms.bg.backgroundColor, 0), " 100%)"), + }), + ], children: _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Scroll left"], ["Scroll left"])))), onPress: scrollLeft, onPressIn: function () { return startContinuousScroll('left'); }, onPressOut: stopContinuousScroll, color: "secondary", size: "small", style: [ + a.border, + t.atoms.border_contrast_low, + t.atoms.bg, + a.h_full, + a.aspect_square, + a.rounded_full, + ], children: _jsx(ButtonIcon, { icon: ArrowLeft }) }) })), IS_WEB && canScrollRight && (_jsx(View, { style: [ + a.absolute, + a.top_0, + a.right_0, + a.bottom_0, + a.justify_center, + { paddingRight: gutterWidth }, + a.pl_md, + a.z_10, + web({ + background: "linear-gradient(to left, ".concat(t.atoms.bg.backgroundColor, " 0%, ").concat(t.atoms.bg.backgroundColor, " 70%, ").concat(transparentifyColor(t.atoms.bg.backgroundColor, 0), " 100%)"), + }), + ], children: _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Scroll right"], ["Scroll right"])))), onPress: scrollRight, onPressIn: function () { return startContinuousScroll('right'); }, onPressOut: stopContinuousScroll, color: "secondary", size: "small", style: [ + a.border, + t.atoms.border_contrast_low, + t.atoms.bg, + a.h_full, + a.aspect_square, + a.rounded_full, + ], children: _jsx(ButtonIcon, { icon: ArrowRight }) }) }))] })); +} +function Tab(_c) { + var onSelectTab = _c.onSelectTab, interest = _c.interest, active = _c.active, index = _c.index, interestsDisplayName = _c.interestsDisplayName, onLayout = _c.onLayout; + var t = useTheme(); + var _ = useLingui()._; + var label = active + ? _(msg({ + message: "\"".concat(interestsDisplayName, "\" category (active)"), + comment: 'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is currently selected.', + })) + : _(msg({ + message: "Select \"".concat(interestsDisplayName, "\" category"), + comment: 'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is not currently active and can be selected.', + })); + return (_jsx(View, { onLayout: function (e) { + return onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width); + }, children: _jsx(Button, { label: label, onPress: function () { return onSelectTab(index); }, + // disable focus ring, we handle it + style: web({ outline: 'none' }), children: function (_c) { + var hovered = _c.hovered, pressed = _c.pressed, focused = _c.focused; + return (_jsx(View, { style: [ + a.rounded_full, + a.px_lg, + a.py_sm, + a.border, + active || hovered || pressed + ? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium] + : focused + ? { + borderColor: t.palette.primary_300, + backgroundColor: t.palette.primary_25, + } + : [t.atoms.bg, t.atoms.border_contrast_low], + ], children: _jsx(Text, { style: [ + a.font_medium, + active || hovered || pressed + ? t.atoms.text + : t.atoms.text_contrast_medium, + ], children: interestsDisplayName }) })); + } }) }, interest)); +} +export function boostInterests(boosts) { + return function (_a, _b) { + var _c, _d; + var indexA = (_c = boosts === null || boosts === void 0 ? void 0 : boosts.indexOf(_a)) !== null && _c !== void 0 ? _c : -1; + var indexB = (_d = boosts === null || boosts === void 0 ? void 0 : boosts.indexOf(_b)) !== null && _d !== void 0 ? _d : -1; + var rankA = indexA === -1 ? Infinity : indexA; + var rankB = indexB === -1 ? Infinity : indexB; + return rankA - rankB; + }; +} +var templateObject_1, templateObject_2; diff --git a/src/components/InternationalPhoneCodeSelect.js b/src/components/InternationalPhoneCodeSelect.js new file mode 100644 index 0000000000..cdf7739887 --- /dev/null +++ b/src/components/InternationalPhoneCodeSelect.js @@ -0,0 +1,80 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { Fragment, useMemo } from 'react'; +import { Text as RNText } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { getDefaultCountry, INTERNATIONAL_TELEPHONE_CODES, } from '#/lib/international-telephone-codes'; +import { regionName } from '#/locale/helpers'; +import { atoms as a, web } from '#/alf'; +import * as Select from '#/components/Select'; +import { IS_WEB } from '#/env'; +import { useGeolocation } from '#/geolocation'; +/** + * Country picker for a phone number input + * + * Pro tip: you can use `location?.countryCode` from `useGeolocationStatus()` + * to set a default value. + */ +export function InternationalPhoneCodeSelect(_a) { + var value = _a.value, onChange = _a.onChange; + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var location = useGeolocation(); + var defaultCountry = useMemo(function () { + return getDefaultCountry(location); + }, [location]); + var items = useMemo(function () { + return (Object.entries(INTERNATIONAL_TELEPHONE_CODES) + .map(function (_a) { + var value = _a[0], _b = _a[1], code = _b.code, unicodeFlag = _b.unicodeFlag, svgFlag = _b.svgFlag; + var name = regionName(value, i18n.locale); + return { + value: value, + name: name, + code: code, + label: "".concat(name, " ").concat(code), + unicodeFlag: unicodeFlag, + svgFlag: svgFlag, + }; + }) + // boost the default value to the top, then sort by name + .sort(function (a, b) { + if (a.value === defaultCountry) + return -1; + if (b.value === defaultCountry) + return 1; + return a.name.localeCompare(b.name); + })); + }, [i18n.locale, defaultCountry]); + var selected = useMemo(function () { + return items.find(function (item) { return item.value === value; }); + }, [value, items]); + return (_jsxs(Select.Root, { value: value, onValueChange: onChange, children: [_jsxs(Select.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select telephone code"], ["Select telephone code"])))), children: [_jsx(Select.ValueText, { placeholder: "+...", webOverrideValue: selected, children: function (selected) { return (_jsxs(_Fragment, { children: [_jsx(Flag, __assign({}, selected)), selected.code] })); } }), _jsx(Select.Icon, {})] }), _jsx(Select.Content, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Country code"], ["Country code"])))), items: items, renderItem: function (item) { return (_jsxs(Fragment, { children: [_jsxs(Select.Item, { value: item.value, label: item.label, children: [_jsx(Select.ItemIndicator, {}), _jsxs(Select.ItemText, { style: [a.flex_1], emoji: true, children: [IS_WEB ? _jsx(Flag, __assign({}, item)) : item.unicodeFlag + ' ', item.name] }), _jsxs(Select.ItemText, { style: [a.text_right], children: [' ', item.code] })] }), item.value === defaultCountry && _jsx(Select.Separator, {})] }, item.value)); } })] })); +} +function Flag(_a) { + var unicodeFlag = _a.unicodeFlag, svgFlag = _a.svgFlag; + if (IS_WEB) { + return (_jsx(Image, { source: svgFlag, style: [ + a.rounded_2xs, + { height: 13, aspectRatio: 4 / 3, marginRight: 6 }, + web({ verticalAlign: 'bottom' }), + ], accessibilityIgnoresInvertColors: true })); + } + return _jsx(RNText, { style: [{ lineHeight: 21 }], children: unicodeFlag + ' ' }); +} +var templateObject_1, templateObject_2; diff --git a/src/components/KeepAwake.js b/src/components/KeepAwake.js new file mode 100644 index 0000000000..2f68ab69cb --- /dev/null +++ b/src/components/KeepAwake.js @@ -0,0 +1,29 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useId } from 'react'; +import { useKeepAwake } from 'expo-keep-awake'; +import { useIsFocused } from '@react-navigation/native'; +/** + * Stops the screen from sleeping. Only applies to the current screen. + * + * Note: Expo keeps the screen permanently awake when in dev mode, so + * you'll only see this do anything when in production. + * + * @platform ios, android + */ +export function KeepAwake(_a) { + var _b = _a.enabled, enabled = _b === void 0 ? true : _b; + var isFocused = useIsFocused(); + if (enabled && isFocused) { + return _jsx(KeepAwakeInner, {}); + } + else { + return null; + } +} +function KeepAwakeInner() { + var id = useId(); + // if you don't pass an explicit ID, any `useKeepAwake` hook unmounting disables them all. + // very strange behaviour, but easily fixed by passing a unique ID -sfn + useKeepAwake(id); + return null; +} diff --git a/src/components/KeepAwake.web.js b/src/components/KeepAwake.web.js new file mode 100644 index 0000000000..807f3706ab --- /dev/null +++ b/src/components/KeepAwake.web.js @@ -0,0 +1,3 @@ +export function KeepAwake() { + return null; +} diff --git a/src/components/KnownFollowers.js b/src/components/KnownFollowers.js new file mode 100644 index 0000000000..1209161d1f --- /dev/null +++ b/src/components/KnownFollowers.js @@ -0,0 +1,139 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { moderateProfile, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { makeProfileLink } from '#/lib/routes/links'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme } from '#/alf'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +var AVI_SIZE = 30; +var AVI_SIZE_SMALL = 20; +var AVI_BORDER = 1; +/** + * Shared logic to determine if `KnownFollowers` should be shown. + * + * Checks the # of actual returned users instead of the `count` value, because + * `count` includes blocked users and `followers` does not. + */ +export function shouldShowKnownFollowers(knownFollowers) { + return knownFollowers && knownFollowers.followers.length > 0; +} +export function KnownFollowers(_a) { + var _b; + var profile = _a.profile, moderationOpts = _a.moderationOpts, onLinkPress = _a.onLinkPress, minimal = _a.minimal, showIfEmpty = _a.showIfEmpty; + var cache = React.useRef(new Map()); + /* + * Results for `knownFollowers` are not sorted consistently, so when + * revalidating we can see a flash of this data updating. This cache prevents + * this happening for screens that remain in memory. When pushing a new + * screen, or once this one is popped, this cache is empty, so new data is + * displayed. + */ + if (((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.knownFollowers) && !cache.current.has(profile.did)) { + cache.current.set(profile.did, profile.viewer.knownFollowers); + } + var cachedKnownFollowers = cache.current.get(profile.did); + if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) { + return (_jsx(KnownFollowersInner, { profile: profile, cachedKnownFollowers: cachedKnownFollowers, moderationOpts: moderationOpts, onLinkPress: onLinkPress, minimal: minimal, showIfEmpty: showIfEmpty })); + } + return _jsx(EmptyFallback, { show: showIfEmpty }); +} +function KnownFollowersInner(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, cachedKnownFollowers = _a.cachedKnownFollowers, onLinkPress = _a.onLinkPress, minimal = _a.minimal, showIfEmpty = _a.showIfEmpty; + var t = useTheme(); + var _ = useLingui()._; + var textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]; + var slice = cachedKnownFollowers.followers.slice(0, 3).map(function (f) { + var moderation = moderateProfile(f, moderationOpts); + return { + profile: __assign(__assign({}, f), { displayName: sanitizeDisplayName(f.displayName || f.handle, moderation.ui('displayName')) }), + moderation: moderation, + }; + }); + // Does not have blocks applied. Always >= slices.length + var serverCount = cachedKnownFollowers.count; + /* + * We check above too, but here for clarity and a reminder to _check for + * valid indices_ + */ + if (slice.length === 0) + return _jsx(EmptyFallback, { show: showIfEmpty }); + var SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE; + return (_jsx(Link, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Press to view followers of this account that you also follow"], ["Press to view followers of this account that you also follow"])))), onPress: onLinkPress, to: makeProfileLink(profile, 'known-followers'), style: [ + a.max_w_full, + a.flex_row, + minimal ? a.gap_sm : a.gap_md, + a.align_center, + { marginLeft: -AVI_BORDER }, + ], children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.flex_row, + { + height: SIZE, + }, + pressed && { + opacity: 0.5, + }, + ], children: slice.map(function (_a, i) { + var _b; + var prof = _a.profile, moderation = _a.moderation; + return (_jsx(View, { style: [ + a.rounded_full, + { + borderWidth: AVI_BORDER, + borderColor: t.atoms.bg.backgroundColor, + width: SIZE + AVI_BORDER * 2, + height: SIZE + AVI_BORDER * 2, + zIndex: AVI_BORDER - i, + marginLeft: i > 0 ? -8 : 0, + }, + ], children: _jsx(UserAvatar, { size: SIZE, avatar: prof.avatar, moderation: moderation.ui('avatar'), type: ((_b = prof.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', noBorder: true }) }, prof.did)); + }) }), _jsx(Text, { style: [ + a.flex_shrink, + textStyle, + hovered && { + textDecorationLine: 'underline', + textDecorationColor: t.atoms.text_contrast_medium.color, + }, + pressed && { + opacity: 0.5, + }, + ], numberOfLines: 2, children: slice.length >= 2 ? ( + // 2-n followers, including blocks + serverCount > 2 ? (_jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did), ",", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[1].profile.displayName }, slice[1].profile.did), ", and", ' ', _jsx(Plural, { value: serverCount - 2, one: "# other", other: "# others" })] }) // only 2 + ) : (_jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did), ' ', "and", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[1].profile.displayName }, slice[1].profile.did)] }))) : serverCount > 1 ? ( + // 1-n followers, including blocks + _jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did), ' ', "and", ' ', _jsx(Plural, { value: serverCount - 1, one: "# other", other: "# others" })] })) : ( + // only 1 + _jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did)] })) })] })); + } })); +} +function EmptyFallback(_a) { + var show = _a.show; + var t = useTheme(); + if (!show) + return null; + return (_jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Not followed by anyone you're following" }) })); +} +var templateObject_1; diff --git a/src/components/LabelingServiceCard/index.js b/src/components/LabelingServiceCard/index.js new file mode 100644 index 0000000000..4b6a25f615 --- /dev/null +++ b/src/components/LabelingServiceCard/index.js @@ -0,0 +1,105 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { getLabelingServiceTitle } from '#/lib/moderation'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useLabelerInfoQuery } from '#/state/queries/labeler'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme } from '#/alf'; +import { Flag_Stroke2_Corner0_Rounded as Flag } from '#/components/icons/Flag'; +import { Link as InternalLink } from '#/components/Link'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRight } from '../icons/Chevron'; +export function Outer(_a) { + var children = _a.children, style = _a.style; + return (_jsx(View, { style: [ + a.flex_row, + a.gap_md, + a.w_full, + a.p_lg, + a.pr_md, + a.overflow_hidden, + style, + ], children: children })); +} +export function Avatar(_a) { + var avatar = _a.avatar; + return _jsx(UserAvatar, { type: "labeler", size: 40, avatar: avatar }); +} +export function Title(_a) { + var value = _a.value; + return (_jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_tight], children: value })); +} +export function Description(_a) { + var value = _a.value, handle = _a.handle; + var _ = useLingui()._; + return value ? (_jsx(Text, { numberOfLines: 2, children: _jsx(RichText, { value: value }) })) : (_jsx(Text, { emoji: true, style: [a.leading_snug], children: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["By ", ""], ["By ", ""])), sanitizeHandle(handle, '@'))) })); +} +export function RegionalNotice() { + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.gap_xs, + a.pt_2xs, + { marginLeft: -2 }, + ], children: [_jsx(Flag, { fill: t.atoms.text_contrast_low.color, size: "sm" }), _jsx(Text, { style: [a.italic, a.leading_snug], children: _jsx(Trans, { children: "Required in your region" }) })] })); +} +export function LikeCount(_a) { + var likeCount = _a.likeCount; + var t = useTheme(); + return (_jsx(Text, { style: [ + a.mt_sm, + a.text_sm, + t.atoms.text_contrast_medium, + { fontWeight: '600' }, + ], children: _jsxs(Trans, { children: ["Liked by ", _jsx(Plural, { value: likeCount, one: "# user", other: "# users" })] }) })); +} +export function Content(_a) { + var children = _a.children; + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.gap_md, + a.align_center, + a.justify_between, + ], children: [_jsx(View, { style: [a.gap_2xs, a.flex_1], children: children }), _jsx(ChevronRight, { size: "md", style: [a.z_10, t.atoms.text_contrast_low] })] })); +} +/** + * The canonical view for a labeling service. Use this or compose your own. + */ +export function Default(_a) { + var labeler = _a.labeler, style = _a.style; + return (_jsxs(Outer, { style: style, children: [_jsx(Avatar, { avatar: labeler.creator.avatar }), _jsxs(Content, { children: [_jsx(Title, { value: getLabelingServiceTitle({ + displayName: labeler.creator.displayName, + handle: labeler.creator.handle, + }) }), _jsx(Description, { value: labeler.creator.description, handle: labeler.creator.handle }), labeler.likeCount ? _jsx(LikeCount, { likeCount: labeler.likeCount }) : null] })] })); +} +export function Link(_a) { + var children = _a.children, labeler = _a.labeler; + var _ = useLingui()._; + return (_jsx(InternalLink, { to: { + screen: 'Profile', + params: { + name: labeler.creator.did, + }, + }, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["View the labeling service provided by @", ""], ["View the labeling service provided by @", ""])), labeler.creator.handle)), children: children })); +} +// TODO not finished yet +export function DefaultSkeleton() { + return (_jsx(View, { children: _jsx(Text, { children: "Loading" }) })); +} +export function Loader(_a) { + var did = _a.did, _b = _a.loading, LoadingComponent = _b === void 0 ? DefaultSkeleton : _b, ErrorComponent = _a.error, Component = _a.component; + var _c = useLabelerInfoQuery({ did: did }), isLoading = _c.isLoading, data = _c.data, error = _c.error; + return isLoading ? (LoadingComponent ? (_jsx(LoadingComponent, {})) : null) : error || !data ? (ErrorComponent ? (_jsx(ErrorComponent, { error: (error === null || error === void 0 ? void 0 : error.message) || 'Unknown error' })) : null) : (_jsx(Component, { labeler: data })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/LanguageSelect.js b/src/components/LanguageSelect.js new file mode 100644 index 0000000000..a5944b45cd --- /dev/null +++ b/src/components/LanguageSelect.js @@ -0,0 +1,28 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { sanitizeAppLanguageSetting } from '#/locale/helpers'; +import { APP_LANGUAGES } from '#/locale/languages'; +import * as Select from '#/components/Select'; +export function LanguageSelect(_a) { + var value = _a.value, onChange = _a.onChange, _b = _a.items, items = _b === void 0 ? APP_LANGUAGES.map(function (l) { return ({ + label: l.name, + value: l.code2, + }); }) : _b, label = _a.label; + var _ = useLingui()._; + var handleOnChange = React.useCallback(function (value) { + if (!value) + return; + onChange(sanitizeAppLanguageSetting(value)); + }, [onChange]); + return (_jsxs(Select.Root, { value: value ? sanitizeAppLanguageSetting(value) : undefined, onValueChange: handleOnChange, children: [_jsxs(Select.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select language"], ["Select language"])))), children: [_jsx(Select.ValueText, { placeholder: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Select language"], ["Select language"])))) }), _jsx(Select.Icon, {})] }), _jsx(Select.Content, { label: label, renderItem: function (_a) { + var label = _a.label, value = _a.value; + return (_jsxs(Select.Item, { value: value, label: label, children: [_jsx(Select.ItemIndicator, {}), _jsx(Select.ItemText, { children: label })] })); + }, items: items })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/Layout/Header/index.js b/src/components/Layout/Header/index.js new file mode 100644 index 0000000000..be04a076d2 --- /dev/null +++ b/src/components/Layout/Header/index.js @@ -0,0 +1,146 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext } from 'react'; +import { Keyboard, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { HITSLOP_30 } from '#/lib/constants'; +import { useSetDrawerOpen } from '#/state/shell'; +import { atoms as a, platform, useBreakpoints, useGutters, useLayoutBreakpoints, useTheme, web, } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft } from '#/components/icons/Arrow'; +import { Menu_Stroke2_Corner0_Rounded as Menu } from '#/components/icons/Menu'; +import { BUTTON_VISUAL_ALIGNMENT_OFFSET, CENTER_COLUMN_OFFSET, HEADER_SLOT_SIZE, SCROLLBAR_OFFSET, } from '#/components/Layout/const'; +import { ScrollbarOffsetContext } from '#/components/Layout/context'; +import { Text } from '#/components/Typography'; +import { IS_IOS } from '#/env'; +export function Outer(_a) { + var _b; + var children = _a.children, noBottomBorder = _a.noBottomBorder, headerRef = _a.headerRef, _c = _a.sticky, sticky = _c === void 0 ? true : _c; + var t = useTheme(); + var gutters = useGutters([0, 'base']); + var gtMobile = useBreakpoints().gtMobile; + var isWithinOffsetView = useContext(ScrollbarOffsetContext).isWithinOffsetView; + var centerColumnOffset = useLayoutBreakpoints().centerColumnOffset; + return (_jsx(View, { ref: headerRef, style: [ + a.w_full, + !noBottomBorder && a.border_b, + a.flex_row, + a.align_center, + a.gap_sm, + sticky && web([a.sticky, { top: 0 }, a.z_10, t.atoms.bg]), + gutters, + platform({ + native: [a.pb_xs, { minHeight: 48 }], + web: [a.py_xs, { minHeight: 52 }], + }), + t.atoms.border_contrast_low, + gtMobile && [a.mx_auto, { maxWidth: 600 }], + !isWithinOffsetView && { + transform: [ + { translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0 }, + { translateX: (_b = web(SCROLLBAR_OFFSET)) !== null && _b !== void 0 ? _b : 0 }, + ], + }, + ], children: children })); +} +var AlignmentContext = createContext('platform'); +AlignmentContext.displayName = 'AlignmentContext'; +export function Content(_a) { + var children = _a.children, _b = _a.align, align = _b === void 0 ? 'platform' : _b; + return (_jsx(View, { style: [ + a.flex_1, + a.justify_center, + IS_IOS && align === 'platform' && a.align_center, + { minHeight: HEADER_SLOT_SIZE }, + ], children: _jsx(AlignmentContext.Provider, { value: align, children: children }) })); +} +export function Slot(_a) { + var children = _a.children; + return _jsx(View, { style: [a.z_50, { width: HEADER_SLOT_SIZE }], children: children }); +} +export function BackButton(_a) { + var onPress = _a.onPress, style = _a.style, props = __rest(_a, ["onPress", "style"]); + var _ = useLingui()._; + var navigation = useNavigation(); + var onPressBack = useCallback(function (evt) { + onPress === null || onPress === void 0 ? void 0 : onPress(evt); + if (evt.defaultPrevented) + return; + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }, [onPress, navigation]); + return (_jsx(Slot, { children: _jsx(Button, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go back"], ["Go back"])))), size: "small", variant: "ghost", color: "secondary", shape: "round", onPress: onPressBack, hitSlop: HITSLOP_30, style: [ + { marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET }, + a.bg_transparent, + style, + ] }, props, { children: _jsx(ButtonIcon, { icon: ArrowLeft, size: "lg" }) })) })); +} +export function MenuButton() { + var _ = useLingui()._; + var setDrawerOpen = useSetDrawerOpen(); + var gtMobile = useBreakpoints().gtMobile; + var onPress = useCallback(function () { + Keyboard.dismiss(); + setDrawerOpen(true); + }, [setDrawerOpen]); + return gtMobile ? null : (_jsx(Slot, { children: _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Open drawer menu"], ["Open drawer menu"])))), size: "small", variant: "ghost", color: "secondary", shape: "square", onPress: onPress, hitSlop: HITSLOP_30, style: [ + { marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET }, + a.bg_transparent, + ], children: _jsx(ButtonIcon, { icon: Menu, size: "lg" }) }) })); +} +export function TitleText(_a) { + var children = _a.children, style = _a.style; + var gtMobile = useBreakpoints().gtMobile; + var align = useContext(AlignmentContext); + return (_jsx(Text, { style: [ + a.text_lg, + a.font_semi_bold, + a.leading_tight, + IS_IOS && align === 'platform' && a.text_center, + gtMobile && a.text_xl, + style, + ], numberOfLines: 2, emoji: true, children: children })); +} +export function SubtitleText(_a) { + var children = _a.children; + var t = useTheme(); + var align = useContext(AlignmentContext); + return (_jsx(Text, { style: [ + a.text_sm, + a.leading_snug, + IS_IOS && align === 'platform' && a.text_center, + t.atoms.text_contrast_medium, + ], numberOfLines: 2, children: children })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/Layout/const.js b/src/components/Layout/const.js new file mode 100644 index 0000000000..93dd6cf66f --- /dev/null +++ b/src/components/Layout/const.js @@ -0,0 +1,16 @@ +export var SCROLLBAR_OFFSET = 'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)'; +export var SCROLLBAR_OFFSET_POSITIVE = 'calc(var(--removed-body-scroll-bar-size, 0px) / 2)'; +/** + * Useful for visually aligning icons within header buttons with the elements + * below them on the screen. Apply positively or negatively depending on side + * of the screen you're on. + */ +export var BUTTON_VISUAL_ALIGNMENT_OFFSET = 3; +/** + * Corresponds to the width of a small square or round button + */ +export var HEADER_SLOT_SIZE = 33; +/** + * How far to shift the center column when in the tablet breakpoint + */ +export var CENTER_COLUMN_OFFSET = -105; diff --git a/src/components/Layout/context.js b/src/components/Layout/context.js new file mode 100644 index 0000000000..55eec6fec9 --- /dev/null +++ b/src/components/Layout/context.js @@ -0,0 +1,5 @@ +import React from 'react'; +export var ScrollbarOffsetContext = React.createContext({ + isWithinOffsetView: false, +}); +ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext'; diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js new file mode 100644 index 0000000000..7ba7ec2c4e --- /dev/null +++ b/src/components/Layout/index.js @@ -0,0 +1,151 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { forwardRef, memo, useContext, useMemo } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { KeyboardAwareScrollView, } from 'react-native-keyboard-controller'; +import Animated, { useAnimatedProps, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useShellLayout } from '#/state/shell/shell-layout'; +import { atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme, web, } from '#/alf'; +import { useDialogContext } from '#/components/Dialog'; +import { CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET } from '#/components/Layout/const'; +import { ScrollbarOffsetContext } from '#/components/Layout/context'; +import { IS_WEB } from '#/env'; +export * from '#/components/Layout/const'; +export * as Header from '#/components/Layout/Header'; +/** + * Outermost component of every screen + */ +export var Screen = memo(function Screen(_a) { + var style = _a.style, noInsetTop = _a.noInsetTop, props = __rest(_a, ["style", "noInsetTop"]); + var top = useSafeAreaInsets().top; + return (_jsxs(_Fragment, { children: [IS_WEB && _jsx(WebCenterBorders, {}), _jsx(View, __assign({ style: [a.util_screen_outer, { paddingTop: noInsetTop ? 0 : top }, style] }, props))] })); +}); +/** + * Default scroll view for simple pages + */ +export var Content = memo(forwardRef(function Content(_a, ref) { + var children = _a.children, style = _a.style, contentContainerStyle = _a.contentContainerStyle, ignoreTabletLayoutOffset = _a.ignoreTabletLayoutOffset, props = __rest(_a, ["children", "style", "contentContainerStyle", "ignoreTabletLayoutOffset"]); + var t = useTheme(); + var footerHeight = useShellLayout().footerHeight; + var animatedProps = useAnimatedProps(function () { + return { + scrollIndicatorInsets: { + bottom: footerHeight.get(), + top: 0, + right: 1, + }, + }; + }); + return (_jsx(Animated.ScrollView, __assign({ ref: ref, id: "content", automaticallyAdjustsScrollIndicatorInsets: false, indicatorStyle: t.scheme === 'dark' ? 'white' : 'black', + // sets the scroll inset to the height of the footer + animatedProps: animatedProps, style: [scrollViewStyles.common, style], contentContainerStyle: [ + scrollViewStyles.contentContainer, + contentContainerStyle, + ] }, props, { children: IS_WEB ? (_jsx(Center, { ignoreTabletLayoutOffset: ignoreTabletLayoutOffset, children: children })) : (children) }))); +})); +var scrollViewStyles = StyleSheet.create({ + common: { + width: '100%', + }, + contentContainer: { + paddingBottom: 100, + }, +}); +/** + * Default scroll view for simple pages. + * + * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment. + */ +export var KeyboardAwareContent = memo(function LayoutKeyboardAwareContent(_a) { + var children = _a.children, style = _a.style, contentContainerStyle = _a.contentContainerStyle, props = __rest(_a, ["children", "style", "contentContainerStyle"]); + return (_jsx(KeyboardAwareScrollView, __assign({ style: [scrollViewStyles.common, style], contentContainerStyle: [ + scrollViewStyles.contentContainer, + contentContainerStyle, + ], keyboardShouldPersistTaps: "handled" }, props, { children: IS_WEB ? _jsx(Center, { children: children }) : children }))); +}); +/** + * Utility component to center content within the screen + */ +export var Center = memo(function LayoutCenter(_a) { + var _b; + var children = _a.children, style = _a.style, ignoreTabletLayoutOffset = _a.ignoreTabletLayoutOffset, props = __rest(_a, ["children", "style", "ignoreTabletLayoutOffset"]); + var isWithinOffsetView = useContext(ScrollbarOffsetContext).isWithinOffsetView; + var gtMobile = useBreakpoints().gtMobile; + var centerColumnOffset = useLayoutBreakpoints().centerColumnOffset; + var isWithinDialog = useDialogContext().isWithinDialog; + var ctx = useMemo(function () { return ({ isWithinOffsetView: true }); }, []); + return (_jsx(View, __assign({ style: [ + a.w_full, + a.mx_auto, + gtMobile && { + maxWidth: 600, + }, + !isWithinOffsetView && { + transform: [ + { + translateX: centerColumnOffset && + !ignoreTabletLayoutOffset && + !isWithinDialog + ? CENTER_COLUMN_OFFSET + : 0, + }, + { translateX: (_b = web(SCROLLBAR_OFFSET)) !== null && _b !== void 0 ? _b : 0 }, + ], + }, + style, + ] }, props, { children: _jsx(ScrollbarOffsetContext.Provider, { value: ctx, children: children }) }))); +}); +/** + * Only used within `Layout.Screen`, not for reuse + */ +var WebCenterBorders = memo(function LayoutWebCenterBorders() { + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var centerColumnOffset = useLayoutBreakpoints().centerColumnOffset; + return gtMobile ? (_jsx(View, { style: [ + a.fixed, + a.inset_0, + a.border_l, + a.border_r, + t.atoms.border_contrast_low, + web({ + width: 602, + left: '50%', + transform: __spreadArray([ + { translateX: '-50%' }, + { translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0 } + ], a.scrollbar_offset.transform, true), + }), + ] })) : null; +}); diff --git a/src/components/LikedByList.js b/src/components/LikedByList.js new file mode 100644 index 0000000000..1b4ae9e0fe --- /dev/null +++ b/src/components/LikedByList.js @@ -0,0 +1,126 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { cleanError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { useLikedByQuery } from '#/state/queries/post-liked-by'; +import { useResolveUriQuery } from '#/state/queries/resolve-uri'; +import { ProfileCardWithFollowBtn } from '#/view/com/profile/ProfileCard'; +import { List } from '#/view/com/util/List'; +import { ListFooter, ListMaybePlaceholder } from '#/components/Lists'; +function renderItem(_a) { + var item = _a.item, index = _a.index; + return (_jsx(ProfileCardWithFollowBtn, { profile: item.actor, noBorder: index === 0 }, item.actor.did)); +} +function keyExtractor(item) { + return item.actor.did; +} +export function LikedByList(_a) { + var _this = this; + var uri = _a.uri; + var _ = useLingui()._; + var initialNumToRender = useInitialNumToRender(); + var _b = React.useState(false), isPTRing = _b[0], setIsPTRing = _b[1]; + var _c = useResolveUriQuery(uri), resolvedUri = _c.data, resolveError = _c.error, isUriLoading = _c.isLoading; + var _d = useLikedByQuery(resolvedUri === null || resolvedUri === void 0 ? void 0 : resolvedUri.uri), data = _d.data, isLikedByLoading = _d.isLoading, isFetchingNextPage = _d.isFetchingNextPage, hasNextPage = _d.hasNextPage, fetchNextPage = _d.fetchNextPage, likedByError = _d.error, refetch = _d.refetch; + var error = resolveError || likedByError; + var isError = !!resolveError || !!likedByError; + var likes = React.useMemo(function () { + if (data === null || data === void 0 ? void 0 : data.pages) { + return data.pages.flatMap(function (page) { return page.likes; }); + } + return []; + }, [data]); + var onRefresh = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTRing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, refetch()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + logger.error('Failed to refresh likes', { message: err_1 }); + return [3 /*break*/, 4]; + case 4: + setIsPTRing(false); + return [2 /*return*/]; + } + }); + }); }, [refetch, setIsPTRing]); + var onEndReached = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isFetchingNextPage || !hasNextPage || isError) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPage()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_2 = _a.sent(); + logger.error('Failed to load more likes', { message: err_2 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]); + if (likes.length < 1) { + return (_jsx(ListMaybePlaceholder, { isLoading: isUriLoading || isLikedByLoading, isError: isError, emptyType: "results", emptyTitle: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["No likes yet"], ["No likes yet"])))), emptyMessage: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Nobody has liked this yet. Maybe you should be the first!"], ["Nobody has liked this yet. Maybe you should be the first!"])))), errorMessage: cleanError(resolveError || error), onRetry: isError ? refetch : undefined, topBorder: false, sideBorders: false })); + } + return (_jsx(List, { data: likes, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTRing, onRefresh: onRefresh, onEndReached: onEndReached, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage }), onEndReachedThreshold: 3, initialNumToRender: initialNumToRender, windowSize: 11, sideBorders: false })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/LinearGradientBackground.js b/src/components/LinearGradientBackground.js new file mode 100644 index 0000000000..22b904e1d0 --- /dev/null +++ b/src/components/LinearGradientBackground.js @@ -0,0 +1,14 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { LinearGradient } from 'expo-linear-gradient'; +import { gradients } from '#/alf/tokens'; +export function LinearGradientBackground(_a) { + var style = _a.style, _b = _a.gradient, gradient = _b === void 0 ? 'sky' : _b, children = _a.children, start = _a.start, end = _a.end; + var colors = gradients[gradient].values.map(function (_a) { + var _ = _a[0], color = _a[1]; + return color; + }); + if (gradient.length < 2) { + throw new Error('Gradient must have at least 2 colors'); + } + return (_jsx(LinearGradient, { colors: colors, style: style, start: start, end: end, children: children })); +} diff --git a/src/components/Link.js b/src/components/Link.js new file mode 100644 index 0000000000..ad642c1d2d --- /dev/null +++ b/src/components/Link.js @@ -0,0 +1,385 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React, { useMemo } from 'react'; +import { Linking } from 'react-native'; +import { sanitizeUrl } from '@braintree/sanitize-url'; +import { StackActions, } from '@react-navigation/native'; +import { BSKY_DOWNLOAD_URL } from '#/lib/constants'; +import { useNavigationDeduped } from '#/lib/hooks/useNavigationDeduped'; +import { useOpenLink } from '#/lib/hooks/useOpenLink'; +import { shareUrl } from '#/lib/sharing'; +import { convertBskyAppUrlIfNeeded, createProxiedUrl, isBskyDownloadUrl, isExternalUrl, linkRequiresWarning, } from '#/lib/strings/url-helpers'; +import { useModalControls } from '#/state/modals'; +import { atoms as a, flatten, useTheme, web } from '#/alf'; +import { Button } from '#/components/Button'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE, IS_WEB } from '#/env'; +import { router } from '#/routes'; +import { useGlobalDialogsControlContext } from './dialogs/Context'; +/** + * Only available within a `Link`, since that inherits from `Button`. + * `InlineLink` provides no context. + */ +export { useButtonContext as useLinkContext } from '#/components/Button'; +export function useLink(_a) { + var to = _a.to, displayText = _a.displayText, _b = _a.action, action = _b === void 0 ? 'push' : _b, disableMismatchWarning = _a.disableMismatchWarning, outerOnPress = _a.onPress, outerOnLongPress = _a.onLongPress, shareOnLongPress = _a.shareOnLongPress, overridePresentation = _a.overridePresentation, shouldProxy = _a.shouldProxy; + var navigation = useNavigationDeduped(); + var href = useMemo(function () { + var _a; + return typeof to === 'string' + ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) + : to.screen + ? (_a = router.matchName(to.screen)) === null || _a === void 0 ? void 0 : _a.build(to.params) + : to.href + ? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href)) + : undefined; + }, [to]); + if (!href) { + throw new Error('Could not resolve screen. Link `to` prop must be a string or an object with `screen` and `params` properties'); + } + var isExternal = isExternalUrl(href); + var closeModal = useModalControls().closeModal; + var linkWarningDialogControl = useGlobalDialogsControlContext().linkWarningDialogControl; + var openLink = useOpenLink(); + var onPress = React.useCallback(function (e) { + var _a, _b; + var exitEarlyIfFalse = outerOnPress === null || outerOnPress === void 0 ? void 0 : outerOnPress(e); + if (exitEarlyIfFalse === false) + return; + var requiresWarning = Boolean(!disableMismatchWarning && + displayText && + isExternal && + linkRequiresWarning(href, displayText)); + if (IS_WEB) { + e.preventDefault(); + } + if (requiresWarning) { + linkWarningDialogControl.open({ + displayText: displayText, + href: href, + }); + } + else { + if (isExternal) { + openLink(href, overridePresentation, shouldProxy); + } + else { + var shouldOpenInNewTab = shouldClickOpenNewTab(e); + if (isBskyDownloadUrl(href)) { + shareUrl(BSKY_DOWNLOAD_URL); + } + else if (shouldOpenInNewTab || + href.startsWith('http') || + href.startsWith('mailto')) { + openLink(href); + } + else { + closeModal(); // close any active modals + var _c = router.matchPath(href), screen_1 = _c[0], params = _c[1]; + // does not apply to web's flat navigator + if (IS_NATIVE && screen_1 !== 'NotFound') { + var state = navigation.getState(); + // if screen is not in the current navigator, it means it's + // most likely a tab screen. note: state can be undefined + if (!((_b = (_a = state === null || state === void 0 ? void 0 : state.routeNames) === null || _a === void 0 ? void 0 : _a.includes) === null || _b === void 0 ? void 0 : _b.call(_a, screen_1))) { + var parent_1 = navigation.getParent(); + if (parent_1 && + parent_1.getState().routeNames.includes("".concat(screen_1, "Tab"))) { + // yep, it's a tab screen. i.e. SearchTab + // thus we need to navigate to the child screen + // via the parent navigator + // see https://reactnavigation.org/docs/upgrading-from-6.x/#changes-to-the-navigate-action + // TODO: can we support the other kinds of actions? push/replace -sfn + // @ts-expect-error include does not narrow the type unfortunately + parent_1.navigate("".concat(screen_1, "Tab"), { screen: screen_1, params: params }); + return; + } + else { + // will probably fail, but let's try anyway + } + } + } + if (action === 'push') { + navigation.dispatch(StackActions.push(screen_1, params)); + } + else if (action === 'replace') { + navigation.dispatch(StackActions.replace(screen_1, params)); + } + else if (action === 'navigate') { + // @ts-expect-error not typed + navigation.navigate(screen_1, params, { pop: true }); + } + else { + throw Error('Unsupported navigator action.'); + } + } + } + } + }, [ + outerOnPress, + disableMismatchWarning, + displayText, + isExternal, + href, + openLink, + closeModal, + action, + navigation, + overridePresentation, + shouldProxy, + linkWarningDialogControl, + ]); + var handleLongPress = React.useCallback(function () { + var requiresWarning = Boolean(!disableMismatchWarning && + displayText && + isExternal && + linkRequiresWarning(href, displayText)); + if (requiresWarning) { + linkWarningDialogControl.open({ + displayText: displayText, + href: href, + share: true, + }); + } + else { + shareUrl(href); + } + }, [ + disableMismatchWarning, + displayText, + href, + isExternal, + linkWarningDialogControl, + ]); + var onLongPress = React.useCallback(function (e) { + var exitEarlyIfFalse = outerOnLongPress === null || outerOnLongPress === void 0 ? void 0 : outerOnLongPress(e); + if (exitEarlyIfFalse === false) + return; + return IS_NATIVE && shareOnLongPress ? handleLongPress() : undefined; + }, [outerOnLongPress, handleLongPress, shareOnLongPress]); + return { + isExternal: isExternal, + href: href, + onPress: onPress, + onLongPress: onLongPress, + }; +} +/** + * A interactive element that renders as a `` tag on the web. On mobile it + * will translate the `href` to navigator screens and params and dispatch a + * navigation action. + * + * Intended to behave as a web anchor tag. For more complex routing, use a + * `Button`. + */ +export function Link(_a) { + var children = _a.children, to = _a.to, _b = _a.action, action = _b === void 0 ? 'push' : _b, outerOnPress = _a.onPress, outerOnLongPress = _a.onLongPress, download = _a.download, shouldProxy = _a.shouldProxy, overridePresentation = _a.overridePresentation, rest = __rest(_a, ["children", "to", "action", "onPress", "onLongPress", "download", "shouldProxy", "overridePresentation"]); + var _c = useLink({ + to: to, + displayText: typeof children === 'string' ? children : '', + action: action, + onPress: outerOnPress, + onLongPress: outerOnLongPress, + shouldProxy: shouldProxy, + overridePresentation: overridePresentation, + }), href = _c.href, isExternal = _c.isExternal, onPress = _c.onPress, onLongPress = _c.onLongPress; + return (_jsx(Button, __assign({}, rest, { style: [a.justify_start, rest.style], role: "link", accessibilityRole: "link", href: href, onPress: download ? undefined : onPress, onLongPress: onLongPress }, web({ + hrefAttrs: { + target: download ? undefined : isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + download: download, + }, + dataSet: { + // no underline, only `InlineLink` has underlines + noUnderline: '1', + }, + }), { children: children }))); +} +export function InlineLinkText(_a) { + var _b; + var children = _a.children, to = _a.to, _c = _a.action, action = _c === void 0 ? 'push' : _c, disableMismatchWarning = _a.disableMismatchWarning, style = _a.style, outerOnPress = _a.onPress, outerOnLongPress = _a.onLongPress, download = _a.download, selectable = _a.selectable, label = _a.label, shareOnLongPress = _a.shareOnLongPress, disableUnderline = _a.disableUnderline, overridePresentation = _a.overridePresentation, shouldProxy = _a.shouldProxy, rest = __rest(_a, ["children", "to", "action", "disableMismatchWarning", "style", "onPress", "onLongPress", "download", "selectable", "label", "shareOnLongPress", "disableUnderline", "overridePresentation", "shouldProxy"]); + var t = useTheme(); + var stringChildren = typeof children === 'string'; + var _d = useLink({ + to: to, + displayText: stringChildren ? children : '', + action: action, + disableMismatchWarning: disableMismatchWarning, + onPress: outerOnPress, + onLongPress: outerOnLongPress, + shareOnLongPress: shareOnLongPress, + overridePresentation: overridePresentation, + shouldProxy: shouldProxy, + }), href = _d.href, isExternal = _d.isExternal, onPress = _d.onPress, onLongPress = _d.onLongPress; + var _e = useInteractionState(), hovered = _e.state, onHoverIn = _e.onIn, onHoverOut = _e.onOut; + var flattenedStyle = flatten(style) || {}; + return (_jsx(Text, __assign({ selectable: selectable, accessibilityHint: "", accessibilityLabel: label }, rest, { style: [ + { color: t.palette.primary_500 }, + hovered && + !disableUnderline && __assign({}, web({ + outline: 0, + textDecorationLine: 'underline', + textDecorationColor: (_b = flattenedStyle.color) !== null && _b !== void 0 ? _b : t.palette.primary_500, + })), + flattenedStyle, + ], role: "link", onPress: download ? undefined : onPress, onLongPress: onLongPress, onMouseEnter: onHoverIn, onMouseLeave: onHoverOut, accessibilityRole: "link", href: href }, web({ + hrefAttrs: { + target: download ? undefined : isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + download: download, + }, + dataSet: { + // default to no underline, apply this ourselves + noUnderline: '1', + }, + }), { children: children }))); +} +/** + * A barebones version of `InlineLinkText`, for use outside a + * `react-navigation` context. + */ +export function SimpleInlineLinkText(_a) { + var _b; + var children = _a.children, to = _a.to, style = _a.style, download = _a.download, selectable = _a.selectable, label = _a.label, disableUnderline = _a.disableUnderline, shouldProxy = _a.shouldProxy, outerOnPress = _a.onPress, rest = __rest(_a, ["children", "to", "style", "download", "selectable", "label", "disableUnderline", "shouldProxy", "onPress"]); + var t = useTheme(); + var _c = useInteractionState(), hovered = _c.state, onHoverIn = _c.onIn, onHoverOut = _c.onOut; + var flattenedStyle = flatten(style) || {}; + var isExternal = isExternalUrl(to); + var href = to; + if (shouldProxy) { + href = createProxiedUrl(href); + } + var onPress = function (e) { + var exitEarlyIfFalse = outerOnPress === null || outerOnPress === void 0 ? void 0 : outerOnPress(e); + if (exitEarlyIfFalse === false) + return; + Linking.openURL(href); + }; + return (_jsx(Text, __assign({ selectable: selectable, accessibilityHint: "", accessibilityLabel: label }, rest, { style: [ + { color: t.palette.primary_500 }, + hovered && + !disableUnderline && __assign({}, web({ + outline: 0, + textDecorationLine: 'underline', + textDecorationColor: (_b = flattenedStyle.color) !== null && _b !== void 0 ? _b : t.palette.primary_500, + })), + flattenedStyle, + ], role: "link", onPress: onPress, onMouseEnter: onHoverIn, onMouseLeave: onHoverOut, accessibilityRole: "link", href: href }, web({ + hrefAttrs: { + target: download ? undefined : isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + download: download, + }, + dataSet: { + // default to no underline, apply this ourselves + noUnderline: '1', + }, + }), { children: children }))); +} +export function WebOnlyInlineLinkText(_a) { + var children = _a.children, to = _a.to, onPress = _a.onPress, props = __rest(_a, ["children", "to", "onPress"]); + return IS_WEB ? (_jsx(InlineLinkText, __assign({}, props, { to: to, onPress: onPress, children: children }))) : (_jsx(Text, __assign({}, props, { children: children }))); +} +/** + * Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI + * + * Example: + * ` {...})} />` + */ +export function createStaticClick(onPressHandler) { + return { + to: '#', + onPress: function (e) { + e.preventDefault(); + onPressHandler(e); + return false; + }, + }; +} +/** + * Utility to create a static `onPress` handler for a `Link`, but only if the + * click was not modified in some way e.g. `Cmd` or a middle click. + * + * On native, this behaves the same as `createStaticClick` because there are no + * options to "modify" the click in this sense. + * + * Example: + * ` {...})} />` + */ +export function createStaticClickIfUnmodified(onPressHandler) { + return { + onPress: function (e) { + if (!IS_WEB || !isModifiedClickEvent(e)) { + e.preventDefault(); + onPressHandler(e); + return false; + } + }, + }; +} +/** + * Determines if the click event has a meta key pressed, indicating the user + * intends to deviate from default behavior. + */ +export function isClickEventWithMetaKey(e) { + if (!IS_WEB) + return false; + var event = e; + return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey; +} +/** + * Determines if the web click target is anything other than `_self` + */ +export function isClickTargetExternal(e) { + if (!IS_WEB) + return false; + var event = e; + var el = event.currentTarget; + return el && el.target && el.target !== '_self'; +} +/** + * Determines if a click event has been modified in some way from its default + * behavior, e.g. `Cmd` or a middle click. + * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} + */ +export function isModifiedClickEvent(e) { + if (!IS_WEB) + return false; + var event = e; + var isPrimaryButton = event.button === 0; + return (isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton); +} +/** + * Determines if a click event has been modified in a way that should indiciate + * that the user intends to open a new tab. + * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} + */ +export function shouldClickOpenNewTab(e) { + if (!IS_WEB) + return false; + var event = e; + var isMiddleClick = IS_WEB && event.button === 1; + return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick; +} diff --git a/src/components/ListCard.js b/src/components/ListCard.js new file mode 100644 index 0000000000..ab991169c5 --- /dev/null +++ b/src/components/ListCard.js @@ -0,0 +1,85 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { AtUri, moderateUserList, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { precacheList } from '#/state/queries/feed'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { Avatar, Description, Header, Outer, SaveButton, } from '#/components/FeedCard'; +import { Link as InternalLink } from '#/components/Link'; +import * as Hider from '#/components/moderation/Hider'; +import { Text } from '#/components/Typography'; +/* + * This component is based on `FeedCard` and is tightly coupled with that + * component. Please refer to `FeedCard` for more context. + */ +export { Avatar, AvatarPlaceholder, Description, Header, Outer, SaveButton, TitleAndBylinePlaceholder, } from '#/components/FeedCard'; +var CURATELIST = 'app.bsky.graph.defs#curatelist'; +var MODLIST = 'app.bsky.graph.defs#modlist'; +export function Default(props) { + var view = props.view, showPinButton = props.showPinButton; + var moderationOpts = useModerationOpts(); + var moderation = moderationOpts + ? moderateUserList(view, moderationOpts) + : undefined; + return (_jsx(Link, __assign({}, props, { children: _jsxs(Outer, { children: [_jsxs(Header, { children: [_jsx(Avatar, { src: view.avatar }), _jsx(TitleAndByline, { title: view.name, creator: view.creator, purpose: view.purpose, modUi: moderation === null || moderation === void 0 ? void 0 : moderation.ui('contentView') }), showPinButton && view.purpose === CURATELIST && (_jsx(SaveButton, { view: view, pin: true }))] }), _jsx(Description, { description: view.description })] }) }))); +} +export function Link(_a) { + var view = _a.view, children = _a.children, props = __rest(_a, ["view", "children"]); + var queryClient = useQueryClient(); + var href = React.useMemo(function () { + return createProfileListHref({ list: view }); + }, [view]); + React.useEffect(function () { + precacheList(queryClient, view); + }, [view, queryClient]); + return (_jsx(InternalLink, __assign({ label: view.name, to: href }, props, { children: children }))); +} +export function TitleAndByline(_a) { + var title = _a.title, creator = _a.creator, _b = _a.purpose, purpose = _b === void 0 ? CURATELIST : _b, modUi = _a.modUi; + var t = useTheme(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + return (_jsxs(View, { style: [a.flex_1], children: [_jsxs(Hider.Outer, { modui: modUi, isContentVisibleInitialState: creator && (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === creator.did, allowOverride: creator && (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === creator.did, children: [_jsx(Hider.Mask, { children: _jsx(Text, { style: [a.text_md, a.font_semi_bold, a.leading_snug, a.italic], numberOfLines: 1, children: _jsx(Trans, { children: "Hidden list" }) }) }), _jsx(Hider.Content, { children: _jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_snug], numberOfLines: 1, children: title }) })] }), creator && (_jsx(Text, { emoji: true, style: [a.leading_snug, t.atoms.text_contrast_medium], numberOfLines: 1, children: purpose === MODLIST + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Moderation list by ", ""], ["Moderation list by ", ""])), sanitizeHandle(creator.handle, '@'))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["List by ", ""], ["List by ", ""])), sanitizeHandle(creator.handle, '@'))) }))] })); +} +export function createProfileListHref(_a) { + var list = _a.list; + var urip = new AtUri(list.uri); + var handleOrDid = list.creator.handle || list.creator.did; + return "/profile/".concat(handleOrDid, "/lists/").concat(urip.rkey); +} +var templateObject_1, templateObject_2; diff --git a/src/components/Lists.js b/src/components/Lists.js new file mode 100644 index 0000000000..6eaeeecc11 --- /dev/null +++ b/src/components/Lists.js @@ -0,0 +1,85 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { memo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { cleanError } from '#/lib/strings/errors'; +import { EmptyState, } from '#/view/com/util/EmptyState'; +import { CenteredView } from '#/view/com/util/Views'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { Error } from '#/components/Error'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +export function ListFooter(_a) { + var isFetchingNextPage = _a.isFetchingNextPage, hasNextPage = _a.hasNextPage, error = _a.error, onRetry = _a.onRetry, height = _a.height, style = _a.style, _b = _a.showEndMessage, showEndMessage = _b === void 0 ? false : _b, endMessageText = _a.endMessageText, renderEndMessage = _a.renderEndMessage; + var t = useTheme(); + return (_jsx(View, { style: [ + a.w_full, + a.align_center, + a.border_t, + a.pb_lg, + t.atoms.border_contrast_low, + { height: height !== null && height !== void 0 ? height : 180, paddingTop: 30 }, + style, + ], children: isFetchingNextPage ? (_jsx(Loader, { size: "xl" })) : error ? (_jsx(ListFooterMaybeError, { error: error, onRetry: onRetry })) : !hasNextPage && showEndMessage ? (renderEndMessage ? (renderEndMessage()) : (_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_low], children: endMessageText !== null && endMessageText !== void 0 ? endMessageText : _jsx(Trans, { children: "You have reached the end" }) }))) : null })); +} +function ListFooterMaybeError(_a) { + var error = _a.error, onRetry = _a.onRetry; + var t = useTheme(); + var _ = useLingui()._; + if (!error) + return null; + return (_jsx(View, { style: [a.w_full, a.px_lg], children: _jsxs(View, { style: [ + a.flex_row, + a.gap_md, + a.p_md, + a.rounded_sm, + a.align_center, + t.atoms.bg_contrast_25, + ], children: [_jsx(Text, { style: [a.flex_1, a.text_sm, t.atoms.text_contrast_medium], numberOfLines: 2, children: error ? (cleanError(error)) : (_jsx(Trans, { children: "Oops, something went wrong!" })) }), _jsx(Button, { variant: "solid", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Press to retry"], ["Press to retry"])))), style: [ + a.align_center, + a.justify_center, + a.rounded_sm, + a.overflow_hidden, + a.px_md, + a.py_sm, + ], onPress: onRetry, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }) })] }) })); +} +var ListMaybePlaceholder = function (_a) { + var isLoading = _a.isLoading, noEmpty = _a.noEmpty, isError = _a.isError, emptyTitle = _a.emptyTitle, emptyMessage = _a.emptyMessage, errorTitle = _a.errorTitle, errorMessage = _a.errorMessage, _b = _a.emptyType, emptyType = _b === void 0 ? 'page' : _b, onRetry = _a.onRetry, onGoBack = _a.onGoBack, hideBackButton = _a.hideBackButton, sideBorders = _a.sideBorders, _c = _a.topBorder, topBorder = _c === void 0 ? false : _c, emptyStateIcon = _a.emptyStateIcon, emptyStateButton = _a.emptyStateButton, _d = _a.useEmptyState, useEmptyState = _d === void 0 ? false : _d; + var t = useTheme(); + var _ = useLingui()._; + var _e = useBreakpoints(), gtMobile = _e.gtMobile, gtTablet = _e.gtTablet; + if (isLoading) { + return (_jsx(CenteredView, { style: [ + a.h_full_vh, + a.align_center, + !gtMobile ? a.justify_between : a.gap_5xl, + t.atoms.border_contrast_low, + { paddingTop: 175, paddingBottom: 110 }, + ], sideBorders: sideBorders !== null && sideBorders !== void 0 ? sideBorders : gtMobile, topBorder: topBorder && !gtTablet, children: _jsx(View, { style: [a.w_full, a.align_center, { top: 100 }], children: _jsx(Loader, { size: "xl" }) }) })); + } + if (isError) { + return (_jsx(Error, { title: errorTitle !== null && errorTitle !== void 0 ? errorTitle : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Oops!"], ["Oops!"])))), message: errorMessage !== null && errorMessage !== void 0 ? errorMessage : _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Something went wrong!"], ["Something went wrong!"])))), onRetry: onRetry, onGoBack: onGoBack, sideBorders: sideBorders, hideBackButton: hideBackButton })); + } + if (useEmptyState) { + return (_jsx(CenteredView, { style: [t.atoms.border_contrast_low], sideBorders: sideBorders !== null && sideBorders !== void 0 ? sideBorders : gtMobile, children: _jsx(EmptyState, { icon: emptyStateIcon, message: emptyMessage !== null && emptyMessage !== void 0 ? emptyMessage : (emptyType === 'results' + ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["No results found"], ["No results found"])))) + : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Page not found"], ["Page not found"]))))), button: emptyStateButton }) })); + } + if (!noEmpty) { + return (_jsx(Error, { title: emptyTitle !== null && emptyTitle !== void 0 ? emptyTitle : (emptyType === 'results' + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["No results found"], ["No results found"])))) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Page not found"], ["Page not found"]))))), message: emptyMessage !== null && emptyMessage !== void 0 ? emptyMessage : _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["We're sorry! We can't find the page you were looking for."], ["We're sorry! We can't find the page you were looking for."])))), onRetry: onRetry, onGoBack: onGoBack, hideBackButton: hideBackButton, sideBorders: sideBorders })); + } + return null; +}; +ListMaybePlaceholder = memo(ListMaybePlaceholder); +export { ListMaybePlaceholder }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/components/Loader.js b/src/components/Loader.js new file mode 100644 index 0000000000..4b2a83c231 --- /dev/null +++ b/src/components/Loader.js @@ -0,0 +1,37 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming, } from 'react-native-reanimated'; +import { atoms as a, useTheme } from '#/alf'; +import { useCommonSVGProps } from '#/components/icons/common'; +import { Loader_Stroke2_Corner0_Rounded as Icon } from '#/components/icons/Loader'; +export function Loader(props) { + var t = useTheme(); + var common = useCommonSVGProps(props); + var rotation = useSharedValue(0); + var animatedStyles = useAnimatedStyle(function () { return ({ + transform: [{ rotate: rotation.get() + 'deg' }], + }); }); + React.useEffect(function () { + rotation.set(function () { + return withRepeat(withTiming(360, { duration: 500, easing: Easing.linear }), -1); + }); + }, [rotation]); + return (_jsx(Animated.View, { style: [ + a.relative, + a.justify_center, + a.align_center, + { width: common.size, height: common.size }, + animatedStyles, + ], children: _jsx(Icon, __assign({}, props, { style: [a.absolute, a.inset_0, t.atoms.text_contrast_high, props.style] })) })); +} diff --git a/src/components/Loader.web.js b/src/components/Loader.web.js new file mode 100644 index 0000000000..ea75d5bc48 --- /dev/null +++ b/src/components/Loader.web.js @@ -0,0 +1,31 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +import { useCommonSVGProps } from '#/components/icons/common'; +import { Loader_Stroke2_Corner0_Rounded as Icon } from '#/components/icons/Loader'; +export function Loader(props) { + var t = useTheme(); + var common = useCommonSVGProps(props); + return (_jsx(View, { style: [ + a.relative, + a.justify_center, + a.align_center, + { width: common.size, height: common.size }, + ], children: _jsx("div", { className: "rotate-500ms", children: _jsx(Icon, __assign({}, props, { style: [ + a.absolute, + a.inset_0, + t.atoms.text_contrast_high, + props.style, + ] })) }) })); +} diff --git a/src/components/LockScroll/index.js b/src/components/LockScroll/index.js new file mode 100644 index 0000000000..21ce57d263 --- /dev/null +++ b/src/components/LockScroll/index.js @@ -0,0 +1,3 @@ +export function LockScroll() { + return null; +} diff --git a/src/components/LockScroll/index.web.js b/src/components/LockScroll/index.web.js new file mode 100644 index 0000000000..86856581ca --- /dev/null +++ b/src/components/LockScroll/index.web.js @@ -0,0 +1,2 @@ +import { RemoveScrollBar } from 'react-remove-scroll-bar'; +export var LockScroll = RemoveScrollBar; diff --git a/src/components/MediaInsetBorder.js b/src/components/MediaInsetBorder.js new file mode 100644 index 0000000000..76c9833ea1 --- /dev/null +++ b/src/components/MediaInsetBorder.js @@ -0,0 +1,36 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { StyleSheet } from 'react-native'; +import { atoms as a, platform, useTheme } from '#/alf'; +import { Fill } from '#/components/Fill'; +import { IS_HIGH_DPI } from '#/env'; +/** + * Applies and thin border within a bounding box. Used to contrast media from + * bg of the container. + */ +export function MediaInsetBorder(_a) { + var children = _a.children, style = _a.style, opaque = _a.opaque; + var t = useTheme(); + var isLight = t.name === 'light'; + return (_jsx(Fill, { style: [ + a.rounded_md, + { + borderWidth: platform({ + native: StyleSheet.hairlineWidth, + // while we generally use hairlineWidth (aka 1px), + // we make an exception here for high DPI screens + // as the 1px border is very noticeable -sfn + web: IS_HIGH_DPI ? 0.5 : StyleSheet.hairlineWidth, + }), + }, + opaque + ? [t.atoms.border_contrast_low] + : [ + isLight + ? t.atoms.border_contrast_low + : t.atoms.border_contrast_high, + { opacity: 0.6 }, + ], + a.pointer_events_none, + style, + ], children: children })); +} diff --git a/src/components/MediaPreview.js b/src/components/MediaPreview.js new file mode 100644 index 0000000000..c6e58a0f7a --- /dev/null +++ b/src/components/MediaPreview.js @@ -0,0 +1,83 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { StyleSheet, View } from 'react-native'; +import { Image } from 'expo-image'; +import { Trans } from '@lingui/react/macro'; +import { isTenorGifUri } from '#/lib/strings/embed-player'; +import { atoms as a, useTheme } from '#/alf'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { Text } from '#/components/Typography'; +import { PlayButtonIcon } from '#/components/video/PlayButtonIcon'; +import * as bsky from '#/types/bsky'; +/** + * Streamlined MediaPreview component which just handles images, gifs, and videos + */ +export function Embed(_a) { + var embed = _a.embed, style = _a.style; + var e = bsky.post.parseEmbed(embed); + if (!e) + return null; + if (e.type === 'images') { + return (_jsx(Outer, { style: style, children: e.view.images.map(function (image) { return (_jsx(ImageItem, { thumbnail: image.thumb, alt: image.alt }, image.thumb)); }) })); + } + else if (e.type === 'link') { + if (!e.view.external.thumb) + return null; + if (!isTenorGifUri(e.view.external.uri)) + return null; + return (_jsx(Outer, { style: style, children: _jsx(GifItem, { thumbnail: e.view.external.thumb, alt: e.view.external.title }) })); + } + else if (e.type === 'video') { + return (_jsx(Outer, { style: style, children: e.view.presentation === 'gif' ? (_jsx(GifItem, { thumbnail: e.view.thumbnail, alt: e.view.alt })) : (_jsx(VideoItem, { thumbnail: e.view.thumbnail, alt: e.view.alt })) })); + } + else if (e.type === 'post_with_media' && + // ignore further "nested" RecordWithMedia + e.media.type !== 'post_with_media' && + // ignore any unknowns + e.media.view !== null) { + return _jsx(Embed, { embed: e.media.view, style: style }); + } + return null; +} +export function Outer(_a) { + var children = _a.children, style = _a.style; + return _jsx(View, { style: [a.flex_row, a.gap_xs, style], children: children }); +} +export function ImageItem(_a) { + var thumbnail = _a.thumbnail, alt = _a.alt, children = _a.children; + var t = useTheme(); + if (!thumbnail) { + return (_jsx(View, { style: [ + { backgroundColor: 'black' }, + a.flex_1, + a.aspect_square, + { maxWidth: 100 }, + a.rounded_xs, + ], accessibilityLabel: alt, accessibilityHint: "", children: children })); + } + return (_jsxs(View, { style: [a.relative, a.flex_1, a.aspect_square, { maxWidth: 100 }], children: [_jsx(Image, { source: { uri: thumbnail }, alt: alt, style: [a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25], contentFit: "cover", accessible: true, accessibilityIgnoresInvertColors: true }, thumbnail), _jsx(MediaInsetBorder, { style: [a.rounded_xs] }), children] })); +} +export function GifItem(_a) { + var thumbnail = _a.thumbnail, alt = _a.alt; + return (_jsxs(ImageItem, { thumbnail: thumbnail, alt: alt, children: [_jsx(View, { style: [a.absolute, a.inset_0, a.justify_center, a.align_center], children: _jsx(PlayButtonIcon, { size: 24 }) }), _jsx(View, { style: styles.altContainer, children: _jsx(Text, { style: styles.alt, children: _jsx(Trans, { children: "GIF" }) }) })] })); +} +export function VideoItem(_a) { + var thumbnail = _a.thumbnail, alt = _a.alt; + return (_jsx(ImageItem, { thumbnail: thumbnail, alt: alt, children: _jsx(View, { style: [a.absolute, a.inset_0, a.justify_center, a.align_center], children: _jsx(PlayButtonIcon, { size: 24 }) }) })); +} +var styles = StyleSheet.create({ + altContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 6, + paddingVertical: 3, + position: 'absolute', + left: 5, + bottom: 5, + zIndex: 2, + }, + alt: { + color: 'white', + fontSize: 7, + fontWeight: '600', + }, +}); diff --git a/src/components/Menu/context.js b/src/components/Menu/context.js new file mode 100644 index 0000000000..7a5b69879f --- /dev/null +++ b/src/components/Menu/context.js @@ -0,0 +1,19 @@ +import React from 'react'; +export var Context = React.createContext(null); +Context.displayName = 'MenuContext'; +export var ItemContext = React.createContext(null); +ItemContext.displayName = 'MenuItemContext'; +export function useMenuContext() { + var context = React.useContext(Context); + if (!context) { + throw new Error('useMenuContext must be used within a Context.Provider'); + } + return context; +} +export function useMenuItemContext() { + var context = React.useContext(ItemContext); + if (!context) { + throw new Error('useMenuItemContext must be used within a Context.Provider'); + } + return context; +} diff --git a/src/components/Menu/index.js b/src/components/Menu/index.js new file mode 100644 index 0000000000..a8da64fe41 --- /dev/null +++ b/src/components/Menu/index.js @@ -0,0 +1,277 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { cloneElement, Fragment, isValidElement, useMemo } from 'react'; +import { Pressable, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import flattenReactChildren from 'react-keyed-flatten-children'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Context, ItemContext, useMenuContext, useMenuItemContext, } from '#/components/Menu/context'; +import { Text } from '#/components/Typography'; +import { IS_ANDROID, IS_IOS, IS_NATIVE } from '#/env'; +export { useDialogControl as useMenuControl, } from '#/components/Dialog'; +export { useMenuContext }; +export function Root(_a) { + var children = _a.children, control = _a.control; + var defaultControl = Dialog.useDialogControl(); + var context = useMemo(function () { return ({ + control: control || defaultControl, + }); }, [control, defaultControl]); + return _jsx(Context.Provider, { value: context, children: children }); +} +export function Trigger(_a) { + var children = _a.children, label = _a.label, _b = _a.role, role = _b === void 0 ? 'button' : _b, hint = _a.hint; + var context = useMenuContext(); + var _c = useInteractionState(), focused = _c.state, onFocus = _c.onIn, onBlur = _c.onOut; + var _d = useInteractionState(), pressed = _d.state, onPressIn = _d.onIn, onPressOut = _d.onOut; + return children({ + IS_NATIVE: true, + control: context.control, + state: { + hovered: false, + focused: focused, + pressed: pressed, + }, + props: { + ref: null, + onPress: context.control.open, + onFocus: onFocus, + onBlur: onBlur, + onPressIn: onPressIn, + onPressOut: onPressOut, + accessibilityHint: hint, + accessibilityLabel: label, + accessibilityRole: role, + }, + }); +} +export function Outer(_a) { + var children = _a.children, showCancel = _a.showCancel; + var context = useMenuContext(); + var _ = useLingui()._; + return (_jsxs(Dialog.Outer, { control: context.control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(Context.Provider, { value: context, children: _jsx(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Menu"], ["Menu"])))), children: _jsxs(View, { style: [a.gap_lg], children: [children, IS_NATIVE && showCancel && _jsx(Cancel, {})] }) }) })] })); +} +export function Item(_a) { + var _this = this; + var children = _a.children, label = _a.label, style = _a.style, onPress = _a.onPress, rest = __rest(_a, ["children", "label", "style", "onPress"]); + var t = useTheme(); + var context = useMenuContext(); + var _b = useInteractionState(), focused = _b.state, onFocus = _b.onIn, onBlur = _b.onOut; + var _c = useInteractionState(), pressed = _c.state, onPressIn = _c.onIn, onPressOut = _c.onOut; + return (_jsx(Pressable, __assign({}, rest, { accessibilityHint: "", accessibilityLabel: label, onFocus: onFocus, onBlur: onBlur, onPress: function (e) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (IS_ANDROID) { + /** + * Below fix for iOS doesn't work for Android, this does. + */ + onPress === null || onPress === void 0 ? void 0 : onPress(e); + context.control.close(); + } + else if (IS_IOS) { + /** + * Fixes a subtle bug on iOS + * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127} + */ + context.control.close(function () { + onPress === null || onPress === void 0 ? void 0 : onPress(e); + }); + } + return [2 /*return*/]; + }); + }); }, onPressIn: function (e) { + var _a; + onPressIn(); + (_a = rest.onPressIn) === null || _a === void 0 ? void 0 : _a.call(rest, e); + }, onPressOut: function (e) { + var _a; + onPressOut(); + (_a = rest.onPressOut) === null || _a === void 0 ? void 0 : _a.call(rest, e); + }, style: [ + a.flex_row, + a.align_center, + a.gap_sm, + a.px_md, + a.rounded_md, + a.overflow_hidden, + a.border, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + { minHeight: 44, paddingVertical: 10 }, + style, + (focused || pressed) && !rest.disabled && [t.atoms.bg_contrast_50], + ], children: _jsx(ItemContext.Provider, { value: { disabled: Boolean(rest.disabled) }, children: children }) }))); +} +export function ItemText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + var disabled = useMenuItemContext().disabled; + return (_jsx(Text, { numberOfLines: 1, ellipsizeMode: "middle", style: [ + a.flex_1, + a.text_md, + a.font_semi_bold, + t.atoms.text_contrast_high, + style, + disabled && t.atoms.text_contrast_low, + ], children: children })); +} +export function ItemIcon(_a) { + var Comp = _a.icon, fill = _a.fill; + var t = useTheme(); + var disabled = useMenuItemContext().disabled; + return (_jsx(Comp, { size: "lg", fill: fill + ? fill({ disabled: disabled }) + : disabled + ? t.atoms.text_contrast_low.color + : t.atoms.text_contrast_medium.color })); +} +export function ItemRadio(_a) { + var selected = _a.selected; + var t = useTheme(); + return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + t.atoms.border_contrast_high, + { + borderWidth: 1, + height: 20, + width: 20, + }, + ], children: selected ? (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + { height: 14, width: 14 }, + selected + ? { + backgroundColor: t.palette.primary_500, + } + : {}, + ] })) : null })); +} +/** + * NATIVE ONLY - for adding non-pressable items to the menu + * + * @platform ios, android + */ +export function ContainerItem(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.gap_sm, + a.px_md, + a.rounded_lg, + a.curve_continuous, + a.border, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + { paddingVertical: 10 }, + style, + ], children: children })); +} +export function LabelText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + return (_jsx(Text, { style: [ + a.font_semi_bold, + t.atoms.text_contrast_medium, + { marginBottom: -8 }, + style, + ], children: children })); +} +export function Group(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.rounded_lg, + a.curve_continuous, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + style, + ], children: flattenReactChildren(children).map(function (child, i) { + return isValidElement(child) && + (child.type === Item || child.type === ContainerItem) ? (_jsxs(Fragment, { children: [i > 0 ? (_jsx(View, { style: [a.border_b, t.atoms.border_contrast_low] })) : null, cloneElement(child, { + // @ts-expect-error cloneElement is not aware of the types + style: { + borderRadius: 0, + borderWidth: 0, + }, + })] }, i)) : null; + }) })); +} +function Cancel() { + var _ = useLingui()._; + var context = useMenuContext(); + return (_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close this dialog"], ["Close this dialog"])))), size: "small", variant: "ghost", color: "secondary", onPress: function () { return context.control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })); +} +export function Divider() { + return null; +} +var templateObject_1, templateObject_2; diff --git a/src/components/Menu/index.web.js b/src/components/Menu/index.web.js new file mode 100644 index 0000000000..3829e54109 --- /dev/null +++ b/src/components/Menu/index.web.js @@ -0,0 +1,248 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { forwardRef, useCallback, useId, useMemo, useState } from 'react'; +import { Pressable, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { DropdownMenu } from 'radix-ui'; +import { useA11y } from '#/state/a11y'; +import { atoms as a, flatten, useTheme, web } from '#/alf'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Context, ItemContext, useMenuContext, useMenuItemContext, } from '#/components/Menu/context'; +import { Portal } from '#/components/Portal'; +import { Text } from '#/components/Typography'; +export { useMenuContext }; +export function useMenuControl() { + var id = useId(); + var _a = useState(false), isOpen = _a[0], setIsOpen = _a[1]; + return useMemo(function () { return ({ + id: id, + ref: { current: null }, + isOpen: isOpen, + open: function () { + setIsOpen(true); + }, + close: function () { + setIsOpen(false); + }, + }); }, [id, isOpen, setIsOpen]); +} +export function Root(_a) { + var children = _a.children, control = _a.control; + var _ = useLingui()._; + var defaultControl = useMenuControl(); + var context = useMemo(function () { return ({ + control: control || defaultControl, + }); }, [control, defaultControl]); + var onOpenChange = useCallback(function (open) { + if (context.control.isOpen && !open) { + context.control.close(); + } + else if (!context.control.isOpen && open) { + context.control.open(); + } + }, [context.control]); + return (_jsxs(Context.Provider, { value: context, children: [context.control.isOpen && (_jsx(Portal, { children: _jsx(Pressable, { style: [a.fixed, a.inset_0, a.z_50], onPress: function () { return context.control.close(); }, accessibilityHint: "", accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Context menu backdrop, click to close the menu."], ["Context menu backdrop, click to close the menu."])))) }) })), _jsx(DropdownMenu.Root, { open: context.control.isOpen, onOpenChange: onOpenChange, children: children })] })); +} +var RadixTriggerPassThrough = forwardRef(function (props, ref) { + // @ts-expect-error Radix provides no types of this stuff + return props.children(__assign(__assign({}, props), { ref: ref })); +}); +RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'; +export function Trigger(_a) { + var children = _a.children, label = _a.label, _b = _a.role, role = _b === void 0 ? 'button' : _b, hint = _a.hint; + var control = useMenuContext().control; + var _c = useInteractionState(), hovered = _c.state, onMouseEnter = _c.onIn, onMouseLeave = _c.onOut; + var _d = useInteractionState(), focused = _d.state, onFocus = _d.onIn, onBlur = _d.onOut; + return (_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx(RadixTriggerPassThrough, { children: function (props) { + return children({ + IS_NATIVE: false, + control: control, + state: { + hovered: hovered, + focused: focused, + pressed: false, + }, + props: __assign(__assign({}, props), { + // No-op override to prevent false positive that interprets mobile scroll as a tap. + // This requires the custom onPress handler below to compensate. + // https://github.com/radix-ui/primitives/issues/1912 + onPointerDown: undefined, onPress: function () { + if (window.event instanceof KeyboardEvent) { + // The onPointerDown hack above is not relevant to this press, so don't do anything. + return; + } + // Compensate for the disabled onPointerDown above by triggering it manually. + if (control.isOpen) { + control.close(); + } + else { + control.open(); + } + }, onFocus: onFocus, onBlur: onBlur, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, accessibilityHint: hint, accessibilityLabel: label, accessibilityRole: role }), + }); + } }) })); +} +export function Outer(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + var reduceMotionEnabled = useA11y().reduceMotionEnabled; + return (_jsx(DropdownMenu.Portal, { children: _jsx(DropdownMenu.Content, { sideOffset: 5, collisionPadding: { left: 5, right: 5, bottom: 5 }, loop: true, "aria-label": "Test", className: "dropdown-menu-transform-origin dropdown-menu-constrain-size", children: _jsx(View, { style: [ + a.rounded_sm, + a.p_xs, + a.border, + t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25, + t.atoms.shadow_md, + t.atoms.border_contrast_low, + a.overflow_auto, + !reduceMotionEnabled && a.zoom_fade_in, + style, + ], children: children }) }) })); +} +export function Item(_a) { + var children = _a.children, label = _a.label, onPress = _a.onPress, style = _a.style, rest = __rest(_a, ["children", "label", "onPress", "style"]); + var t = useTheme(); + var control = useMenuContext().control; + var _b = useInteractionState(), hovered = _b.state, onMouseEnter = _b.onIn, onMouseLeave = _b.onOut; + var _c = useInteractionState(), focused = _c.state, onFocus = _c.onIn, onBlur = _c.onOut; + return (_jsx(DropdownMenu.Item, { asChild: true, children: _jsx(Pressable, __assign({}, rest, { className: "radix-dropdown-item", accessibilityHint: "", accessibilityLabel: label, onPress: function (e) { + onPress(e); + /** + * Ported forward from Radix + * @see https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item + */ + if (!e.defaultPrevented) { + control.close(); + } + }, onFocus: onFocus, onBlur: onBlur, + // need `flatten` here for Radix compat + style: flatten([ + a.flex_row, + a.align_center, + a.gap_lg, + a.py_sm, + a.rounded_xs, + a.overflow_hidden, + { minHeight: 32, paddingHorizontal: 10 }, + web({ outline: 0 }), + (hovered || focused) && + !rest.disabled && [ + web({ outline: '0 !important' }), + t.name === 'light' + ? t.atoms.bg_contrast_25 + : t.atoms.bg_contrast_50, + ], + style, + ]) }, web({ + onMouseEnter: onMouseEnter, + onMouseLeave: onMouseLeave, + }), { children: _jsx(ItemContext.Provider, { value: { disabled: Boolean(rest.disabled) }, children: children }) })) })); +} +export function ItemText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + var disabled = useMenuItemContext().disabled; + return (_jsx(Text, { style: [ + a.flex_1, + a.font_semi_bold, + t.atoms.text_contrast_high, + style, + disabled && t.atoms.text_contrast_low, + ], children: children })); +} +export function ItemIcon(_a) { + var Comp = _a.icon, _b = _a.position, position = _b === void 0 ? 'left' : _b, fill = _a.fill; + var t = useTheme(); + var disabled = useMenuItemContext().disabled; + return (_jsx(View, { style: [ + position === 'left' && { + marginLeft: -2, + }, + position === 'right' && { + marginRight: -2, + marginLeft: 12, + }, + ], children: _jsx(Comp, { size: "md", fill: fill + ? fill({ disabled: disabled }) + : disabled + ? t.atoms.text_contrast_low.color + : t.atoms.text_contrast_medium.color }) })); +} +export function ItemRadio(_a) { + var selected = _a.selected; + var t = useTheme(); + return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + t.atoms.border_contrast_high, + { + borderWidth: 1, + height: 20, + width: 20, + }, + ], children: selected ? (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + { height: 14, width: 14 }, + selected + ? { + backgroundColor: t.palette.primary_500, + } + : {}, + ] })) : null })); +} +export function LabelText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + return (_jsx(Text, { style: [ + a.font_semi_bold, + a.p_sm, + t.atoms.text_contrast_low, + a.leading_snug, + { paddingHorizontal: 10 }, + style, + ], children: children })); +} +export function Group(_a) { + var children = _a.children; + return children; +} +export function Divider() { + var t = useTheme(); + return (_jsx(DropdownMenu.Separator, { style: flatten([ + a.my_xs, + t.atoms.bg_contrast_100, + a.flex_shrink_0, + { height: 1 }, + ]) })); +} +export function ContainerItem() { + return null; +} +var templateObject_1; diff --git a/src/components/Menu/types.js b/src/components/Menu/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/Menu/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/NewskieDialog.js b/src/components/NewskieDialog.js new file mode 100644 index 0000000000..f12f9d2f8d --- /dev/null +++ b/src/components/NewskieDialog.js @@ -0,0 +1,94 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { moderateProfile } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { differenceInSeconds } from 'date-fns'; +import { HITSLOP_10 } from '#/lib/constants'; +import { useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useDialogControl } from '#/components/Dialog'; +import { Newskie } from '#/components/icons/Newskie'; +import * as StarterPackCard from '#/components/StarterPack/StarterPackCard'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +export function NewskieDialog(_a) { + var profile = _a.profile, disabled = _a.disabled; + var _ = useLingui()._; + var control = useDialogControl(); + var createdAt = profile.createdAt; + var now = useState(function () { return Date.now(); })[0]; + var daysOld = useMemo(function () { + if (!createdAt) + return Infinity; + return differenceInSeconds(now, new Date(createdAt)) / 86400; + }, [createdAt, now]); + if (!createdAt || daysOld > 7) + return null; + return (_jsxs(View, { style: [a.pr_2xs], children: [_jsx(Button, { disabled: disabled, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["This user is new here. Press for more info about when they joined."], ["This user is new here. Press for more info about when they joined."])))), hitSlop: HITSLOP_10, onPress: control.open, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(Newskie, { size: "lg", fill: "#FFC404", style: { + opacity: hovered || pressed ? 0.5 : 1, + } })); + } }), _jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { profile: profile, createdAt: createdAt, now: now })] })] })); +} +function DialogInner(_a) { + var profile = _a.profile, createdAt = _a.createdAt, now = _a.now; + var control = Dialog.useDialogContext(); + var _ = useLingui()._; + var t = useTheme(); + var moderationOpts = useModerationOpts(); + var currentAccount = useSession().currentAccount; + var timeAgo = useGetTimeAgo(); + var isMe = profile.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var profileName = useMemo(function () { + if (!moderationOpts) + return profile.displayName || profile.handle; + var moderation = moderateProfile(profile, moderationOpts); + return sanitizeDisplayName(profile.displayName || profile.handle, moderation.ui('displayName')); + }, [moderationOpts, profile]); + var getJoinMessage = function () { + var timeAgoString = timeAgo(createdAt, now, { format: 'long' }); + if (isMe) { + if (profile.joinedViaStarterPack) { + return _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You joined Bluesky using a starter pack ", " ago"], ["You joined Bluesky using a starter pack ", " ago"])), timeAgoString)); + } + else { + return _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You joined Bluesky ", " ago"], ["You joined Bluesky ", " ago"])), timeAgoString)); + } + } + else { + if (profile.joinedViaStarterPack) { + return _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["", " joined Bluesky using a starter pack ", " ago"], ["", " joined Bluesky using a starter pack ", " ago"])), profileName, timeAgoString)); + } + else { + return _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["", " joined Bluesky ", " ago"], ["", " joined Bluesky ", " ago"])), profileName, timeAgoString)); + } + } + }; + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["New user info dialog"], ["New user info dialog"])))), style: web({ maxWidth: 400 }), children: [_jsxs(View, { style: [a.gap_md], children: [_jsxs(View, { style: [a.align_center], children: [_jsx(View, { style: [ + { + height: 60, + width: 64, + }, + ], children: _jsx(Newskie, { width: 64, height: 64, fill: "#FFC404", style: [a.absolute, a.inset_0] }) }), _jsx(Text, { style: [a.font_semi_bold, a.text_xl], children: isMe ? _jsx(Trans, { children: "Welcome, friend!" }) : _jsx(Trans, { children: "Say hello!" }) })] }), _jsx(Text, { style: [a.text_md, a.text_center, a.leading_snug], children: getJoinMessage() }), profile.joinedViaStarterPack ? (_jsx(StarterPackCard.Link, { starterPack: profile.joinedViaStarterPack, onPress: function () { return control.close(); }, children: _jsx(View, { style: [ + a.w_full, + a.mt_sm, + a.p_lg, + a.border, + a.rounded_sm, + t.atoms.border_contrast_low, + ], children: _jsx(StarterPackCard.Card, { starterPack: profile.joinedViaStarterPack }) }) })) : null, IS_NATIVE && (_jsx(Button, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Close"], ["Close"])))), color: "secondary", size: "small", style: [a.mt_sm], onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }))] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/Pills.js b/src/components/Pills.js new file mode 100644 index 0000000000..0cb502a4e3 --- /dev/null +++ b/src/components/Pills.js @@ -0,0 +1,104 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { BSKY_LABELER_DID } from '@atproto/api'; +import { Trans } from '@lingui/react/macro'; +import { useModerationCauseDescription } from '#/lib/moderation/useModerationCauseDescription'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog'; +import { Text } from '#/components/Typography'; +export function Row(_a) { + var children = _a.children, style = _a.style, _b = _a.size, size = _b === void 0 ? 'sm' : _b; + var styles = React.useMemo(function () { + switch (size) { + case 'lg': + return [{ gap: 5 }]; + case 'sm': + default: + return [{ gap: 3 }]; + } + }, [size]); + return (_jsx(View, { style: [a.flex_row, a.flex_wrap, a.gap_xs, styles, style], children: children })); +} +export function Label(_a) { + var cause = _a.cause, _b = _a.size, size = _b === void 0 ? 'sm' : _b, disableDetailsDialog = _a.disableDetailsDialog, noBg = _a.noBg; + var t = useTheme(); + var control = useModerationDetailsDialogControl(); + var desc = useModerationCauseDescription(cause); + var isLabeler = Boolean(desc.sourceType && desc.sourceDid); + var isBlueskyLabel = desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID; + var _c = React.useMemo(function () { + switch (size) { + case 'lg': { + return { + outer: [ + t.atoms.bg_contrast_25, + { + gap: 5, + paddingHorizontal: 5, + paddingVertical: 5, + }, + ], + avi: 16, + text: [a.text_sm], + }; + } + case 'sm': + default: { + return { + outer: [ + !noBg && t.atoms.bg_contrast_25, + { + gap: 3, + paddingHorizontal: 3, + paddingVertical: 3, + }, + ], + avi: 12, + text: [a.text_xs], + }; + } + } + }, [t, size, noBg]), outer = _c.outer, avi = _c.avi, text = _c.text; + return (_jsxs(_Fragment, { children: [_jsx(Button, { disabled: disableDetailsDialog, label: desc.name, onPress: function (e) { + e.preventDefault(); + e.stopPropagation(); + control.open(); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.rounded_full, + outer, + (hovered || pressed) && t.atoms.bg_contrast_50, + ], children: [isBlueskyLabel || !isLabeler ? (_jsx(desc.icon, { width: avi, fill: t.atoms.text_contrast_medium.color })) : (_jsx(UserAvatar, { avatar: desc.sourceAvi, type: "user", size: avi })), _jsx(Text, { emoji: true, style: [ + text, + a.font_semi_bold, + a.leading_tight, + t.atoms.text_contrast_medium, + { paddingRight: 3 }, + ], children: desc.name })] })); + } }), !disableDetailsDialog && (_jsx(ModerationDetailsDialog, { control: control, modcause: cause }))] })); +} +export function FollowsYou(_a) { + var _b = _a.size, size = _b === void 0 ? 'sm' : _b; + var t = useTheme(); + var variantStyles = React.useMemo(function () { + switch (size) { + case 'sm': + case 'lg': + default: + return [ + { + paddingHorizontal: 6, + paddingVertical: 3, + borderRadius: 4, + }, + ]; + } + }, [size]); + return (_jsx(View, { style: [variantStyles, a.justify_center, t.atoms.bg_contrast_50], children: _jsx(Text, { style: [a.text_xs, a.leading_tight], children: _jsx(Trans, { children: "Follows You" }) }) })); +} diff --git a/src/components/PolicyUpdateOverlay/Badge.js b/src/components/PolicyUpdateOverlay/Badge.js new file mode 100644 index 0000000000..11c2a0ca1b --- /dev/null +++ b/src/components/PolicyUpdateOverlay/Badge.js @@ -0,0 +1,26 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { Logo } from '#/view/icons/Logo'; +import { atoms as a, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +export function Badge() { + var t = useTheme(); + return (_jsx(View, { style: [a.align_start], children: _jsxs(View, { style: [ + a.pl_md, + a.pr_lg, + a.py_sm, + a.rounded_full, + a.flex_row, + a.align_center, + a.gap_xs, + { + backgroundColor: t.palette.primary_25, + }, + ], children: [_jsx(Logo, { fill: t.palette.primary_600, width: 14 }), _jsx(Text, { style: [ + a.font_semi_bold, + { + color: t.palette.primary_600, + }, + ], children: _jsx(Trans, { children: "Announcement" }) })] }) })); +} diff --git a/src/components/PolicyUpdateOverlay/Overlay.js b/src/components/PolicyUpdateOverlay/Overlay.js new file mode 100644 index 0000000000..4b708cc61b --- /dev/null +++ b/src/components/PolicyUpdateOverlay/Overlay.js @@ -0,0 +1,70 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { ScrollView, View } from 'react-native'; +import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; +import { LinearGradient } from 'expo-linear-gradient'; +import { utils } from '@bsky.app/alf'; +import { useA11y } from '#/state/a11y'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { FocusScope } from '#/components/FocusScope'; +import { LockScroll } from '#/components/LockScroll'; +import { IS_ANDROID, IS_NATIVE } from '#/env'; +var GUTTER = 24; +export function Overlay(_a) { + var children = _a.children, label = _a.label; + var t = useTheme(); + var gtPhone = useBreakpoints().gtPhone; + var reduceMotionEnabled = useA11y().reduceMotionEnabled; + var insets = useSafeAreaInsets(); + var frame = useSafeAreaFrame(); + return (_jsxs(_Fragment, { children: [_jsx(LockScroll, {}), _jsx(View, { style: [a.fixed, a.inset_0, !reduceMotionEnabled && a.fade_in], children: gtPhone ? (_jsx(View, { style: [a.absolute, a.inset_0, { opacity: 0.8 }], children: _jsx(View, { style: [ + a.fixed, + a.inset_0, + { backgroundColor: t.palette.black }, + !reduceMotionEnabled && a.fade_in, + ] }) })) : (_jsx(LinearGradient, { colors: [ + utils.alpha(t.atoms.bg.backgroundColor, 0), + t.atoms.bg.backgroundColor, + t.atoms.bg.backgroundColor, + ], start: [0.5, 0], end: [0.5, 1], style: [a.absolute, a.inset_0] })) }), _jsx(ScrollView, { showsVerticalScrollIndicator: false, style: [ + a.z_10, + gtPhone && + web({ + paddingHorizontal: GUTTER, + paddingVertical: '10vh', + }), + ], contentContainerStyle: [a.align_center], children: _jsxs(View, { style: [ + a.w_full, + a.z_20, + a.align_center, + !gtPhone && [a.justify_end, { minHeight: frame.height }], + IS_NATIVE && [ + { + paddingBottom: Math.max(insets.bottom, a.p_2xl.padding), + }, + ], + ], children: [!gtPhone && (_jsx(View, { style: [ + a.flex_1, + a.w_full, + { + minHeight: Math.max(insets.top, a.p_2xl.padding), + }, + ], children: _jsx(LinearGradient, { colors: [ + utils.alpha(t.atoms.bg.backgroundColor, 0), + t.atoms.bg.backgroundColor, + ], start: [0.5, 0], end: [0.5, 1], style: [a.absolute, a.inset_0] }) })), _jsx(FocusScope, { children: _jsx(View, { accessible: IS_ANDROID, role: "dialog", "aria-role": "dialog", "aria-label": label, style: [ + a.relative, + a.w_full, + a.p_2xl, + t.atoms.bg, + !reduceMotionEnabled && a.zoom_fade_in, + gtPhone && [ + a.rounded_md, + a.border, + t.atoms.shadow_lg, + t.atoms.border_contrast_low, + web({ + maxWidth: 420, + }), + ], + ], children: children }) })] }) })] })); +} diff --git a/src/components/PolicyUpdateOverlay/Portal.js b/src/components/PolicyUpdateOverlay/Portal.js new file mode 100644 index 0000000000..cce8b8db09 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/Portal.js @@ -0,0 +1,5 @@ +import { createPortalGroup } from '#/components/Portal'; +var portalGroup = createPortalGroup(); +export var Provider = portalGroup.Provider; +export var Portal = portalGroup.Portal; +export var Outlet = portalGroup.Outlet; diff --git a/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.js b/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.js new file mode 100644 index 0000000000..e3e1527210 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.js @@ -0,0 +1,166 @@ +import { describe, test } from '@jest/globals'; +import { computeCompletedState, syncCompletedState, } from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'; +jest.mock('../../../state/queries/nuxs'); +describe('computeCompletedState', function () { + test("initial state", function () { + var completed = computeCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: undefined, + }); + expect(completed).toBe(true); + }); + test("nux loaded state", function () { + var completed = computeCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: undefined, + }); + expect(completed).toBe(false); + }); + test("nux saving state", function () { + var completed = computeCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: true, + completedForDevice: undefined, + }); + expect(completed).toBe(true); + }); + test("nux is completed", function () { + var completed = computeCompletedState({ + nuxIsReady: true, + nuxIsCompleted: true, + nuxIsOptimisticallyCompleted: false, + completedForDevice: undefined, + }); + expect(completed).toBe(true); + }); + test("initial state, but already completed for device", function () { + var completed = computeCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + }); + expect(completed).toBe(true); + }); +}); +describe('syncCompletedState', function () { + describe('!nuxIsReady', function () { + test("!completedForDevice, no-op", function () { + var save = jest.fn(); + var setCompletedForDevice = jest.fn(); + syncCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: false, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + expect(save).not.toHaveBeenCalled(); + expect(setCompletedForDevice).not.toHaveBeenCalled(); + }); + test("completedForDevice, no-op", function () { + var save = jest.fn(); + var setCompletedForDevice = jest.fn(); + syncCompletedState({ + nuxIsReady: false, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + expect(save).not.toHaveBeenCalled(); + expect(setCompletedForDevice).not.toHaveBeenCalled(); + }); + }); + describe('nuxIsReady', function () { + describe("!nuxIsCompleted", function () { + describe("!nuxIsOptimisticallyCompleted", function () { + test("!completedForDevice, no-op", function () { + var save = jest.fn(); + var setCompletedForDevice = jest.fn(); + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: false, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + expect(save).not.toHaveBeenCalled(); + expect(setCompletedForDevice).not.toHaveBeenCalled(); + }); + test("completedForDevice, syncs to server", function () { + var save = jest.fn(); + var setCompletedForDevice = jest.fn(); + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + expect(save).toHaveBeenCalled(); + expect(setCompletedForDevice).not.toHaveBeenCalled(); + }); + }); + /** + * Catches the case where we already called `save` to sync device state + * to server, thus `nuxIsOptimisticallyCompleted` is true. + */ + describe("nuxIsOptimisticallyCompleted", function () { + test("completedForDevice, no-op", function () { + var save = jest.fn(); + var setCompletedForDevice = jest.fn(); + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: false, + nuxIsOptimisticallyCompleted: true, + completedForDevice: true, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + expect(save).not.toHaveBeenCalled(); + expect(setCompletedForDevice).not.toHaveBeenCalled(); + }); + }); + }); + describe("nuxIsCompleted", function () { + test("!completedForDevice, syncs to device", function () { + var save = jest.fn(); + var setCompletedForDevice = jest.fn(); + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: true, + nuxIsOptimisticallyCompleted: false, + completedForDevice: false, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + expect(save).not.toHaveBeenCalled(); + expect(setCompletedForDevice).toHaveBeenCalled(); + }); + test("completedForDevice, no-op", function () { + var save = jest.fn(); + var setCompletedForDevice = jest.fn(); + syncCompletedState({ + nuxIsReady: true, + nuxIsCompleted: true, + nuxIsOptimisticallyCompleted: false, + completedForDevice: true, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + expect(save).not.toHaveBeenCalled(); + expect(setCompletedForDevice).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/components/PolicyUpdateOverlay/config.js b/src/components/PolicyUpdateOverlay/config.js new file mode 100644 index 0000000000..7bdd92babb --- /dev/null +++ b/src/components/PolicyUpdateOverlay/config.js @@ -0,0 +1,12 @@ +import { ID } from '#/components/PolicyUpdateOverlay/updates/202508/config'; +/** + * The singulary active update ID. This is configured here to ensure that + * the relationship is clear. + */ +export var ACTIVE_UPDATE_ID = ID; +/** + * Toggle to enable or disable the policy update overlay feature e.g. once an + * update has run its course, set this to false. For new updates, set this to + * true and change `ACTIVE_UPDATE_ID` to the new update ID. + */ +export var POLICY_UPDATE_IS_ENABLED = false; diff --git a/src/components/PolicyUpdateOverlay/context.js b/src/components/PolicyUpdateOverlay/context.js new file mode 100644 index 0000000000..3f7a2171fe --- /dev/null +++ b/src/components/PolicyUpdateOverlay/context.js @@ -0,0 +1,52 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo, useState, } from 'react'; +import { useSession } from '#/state/session'; +import { POLICY_UPDATE_IS_ENABLED } from '#/components/PolicyUpdateOverlay/config'; +import { Provider as PortalProvider } from '#/components/PolicyUpdateOverlay/Portal'; +import { usePolicyUpdateState, } from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'; +import { ENV } from '#/env'; +var Context = createContext({ + state: { + completed: true, + complete: function () { }, + }, + /** + * Although our data will be ready to go when the app shell mounts, we don't + * want to show the overlay until we actually render it, which happens after + * sigin/signup/onboarding in `createNativeStackNavigatorWithAuth`. + */ + setIsReadyToShowOverlay: function () { }, +}); +Context.displayName = 'PolicyUpdateOverlayContext'; +export function usePolicyUpdateContext() { + var context = useContext(Context); + if (!context) { + throw new Error('usePolicyUpdateContext must be used within a PolicyUpdateProvider'); + } + return context; +} +export function Provider(_a) { + var children = _a.children; + var hasSession = useSession().hasSession; + var _b = useState(false), isReadyToShowOverlay = _b[0], setIsReadyToShowOverlay = _b[1]; + var state = usePolicyUpdateState({ + enabled: + // if the feature is enabled + POLICY_UPDATE_IS_ENABLED && + // once shell has rendered + isReadyToShowOverlay && + // only once logged in + hasSession && + // only enabled in non-test environments + ENV !== 'e2e', + }); + var ctx = useMemo(function () { return ({ + state: state, + setIsReadyToShowOverlay: function () { + if (isReadyToShowOverlay) + return; + setIsReadyToShowOverlay(true); + }, + }); }, [state, isReadyToShowOverlay, setIsReadyToShowOverlay]); + return (_jsx(PortalProvider, { children: _jsx(Context.Provider, { value: ctx, children: children }) })); +} diff --git a/src/components/PolicyUpdateOverlay/index.js b/src/components/PolicyUpdateOverlay/index.js new file mode 100644 index 0000000000..af4d00bb0f --- /dev/null +++ b/src/components/PolicyUpdateOverlay/index.js @@ -0,0 +1,35 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useEffect } from 'react'; +import { View } from 'react-native'; +import { atoms as a } from '#/alf'; +import { FullWindowOverlay } from '#/components/FullWindowOverlay'; +import { usePolicyUpdateContext } from '#/components/PolicyUpdateOverlay/context'; +import { Portal } from '#/components/PolicyUpdateOverlay/Portal'; +import { Content } from '#/components/PolicyUpdateOverlay/updates/202508'; +import { IS_IOS } from '#/env'; +export { Provider } from '#/components/PolicyUpdateOverlay/context'; +export { usePolicyUpdateContext } from '#/components/PolicyUpdateOverlay/context'; +export { Outlet } from '#/components/PolicyUpdateOverlay/Portal'; +export function PolicyUpdateOverlay() { + var _a = usePolicyUpdateContext(), state = _a.state, setIsReadyToShowOverlay = _a.setIsReadyToShowOverlay; + useEffect(function () { + /** + * Tell the context that we are ready to show the overlay. + */ + setIsReadyToShowOverlay(); + }, [setIsReadyToShowOverlay]); + /* + * See `window.clearNux` example in `/state/queries/nuxs` for a way to clear + * NUX state for local testing and debugging. + */ + if (state.completed) + return null; + return (_jsx(Portal, { children: _jsx(FullWindowOverlay, { children: _jsx(View, { style: [ + a.fixed, + a.inset_0, + // setting a zIndex when using FullWindowOverlay on iOS + // means the taps pass straight through to the underlying content (???) + // so don't set it on iOS. FullWindowOverlay already does the job. + !IS_IOS && { zIndex: 9999 }, + ], children: _jsx(Content, { state: state }) }) }) })); +} diff --git a/src/components/PolicyUpdateOverlay/logger.js b/src/components/PolicyUpdateOverlay/logger.js new file mode 100644 index 0000000000..dd2964de47 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/logger.js @@ -0,0 +1,2 @@ +import { Logger } from '#/logger'; +export var logger = Logger.create(Logger.Context.PolicyUpdate); diff --git a/src/components/PolicyUpdateOverlay/updates/202508/config.js b/src/components/PolicyUpdateOverlay/updates/202508/config.js new file mode 100644 index 0000000000..dc15b2939d --- /dev/null +++ b/src/components/PolicyUpdateOverlay/updates/202508/config.js @@ -0,0 +1,5 @@ +/* + * Keep this file separate to avoid import issues. + */ +import { Nux } from '#/state/queries/nuxs'; +export var ID = Nux.PolicyUpdate202508; diff --git a/src/components/PolicyUpdateOverlay/updates/202508/index.js b/src/components/PolicyUpdateOverlay/updates/202508/index.js new file mode 100644 index 0000000000..e52951efd2 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/updates/202508/index.js @@ -0,0 +1,81 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useA11y } from '#/state/a11y'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { InlineLinkText, Link } from '#/components/Link'; +import { Badge } from '#/components/PolicyUpdateOverlay/Badge'; +import { Overlay } from '#/components/PolicyUpdateOverlay/Overlay'; +import { Text } from '#/components/Typography'; +import { IS_ANDROID } from '#/env'; +export function Content(_a) { + var state = _a.state; + var t = useTheme(); + var _ = useLingui()._; + var screenReaderEnabled = useA11y().screenReaderEnabled; + var handleClose = useCallback(function () { + state.complete(); + }, [state]); + var linkStyle = [a.text_md]; + var links = { + terms: { + overridePresentation: false, + to: "https://bsky.social/about/support/tos", + label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Terms of Service"], ["Terms of Service"])))), + }, + privacy: { + overridePresentation: false, + to: "https://bsky.social/about/support/privacy-policy", + label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Privacy Policy"], ["Privacy Policy"])))), + }, + copyright: { + overridePresentation: false, + to: "https://bsky.social/about/support/copyright", + label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Copyright Policy"], ["Copyright Policy"])))), + }, + guidelines: { + overridePresentation: false, + to: "https://bsky.social/about/support/community-guidelines", + label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Community Guidelines"], ["Community Guidelines"])))), + }, + blog: { + overridePresentation: false, + to: "https://bsky.social/about/blog/08-14-2025-updated-terms-and-policies", + label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Our blog post"], ["Our blog post"])))), + }, + }; + var linkButtonStyles = { + overridePresentation: false, + color: 'secondary', + size: 'small', + }; + var label = IS_ANDROID + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["We\u2019re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post."], ["We\u2019re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post."])))) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["We're updating our policies"], ["We're updating our policies"])))); + return (_jsx(Overlay, { label: label, children: _jsxs(View, { style: [a.align_start, a.gap_xl], children: [_jsx(Badge, {}), screenReaderEnabled ? (_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { emoji: true, style: [a.text_2xl, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { children: "Hey there \uD83D\uDC4B" }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsx(Trans, { children: "We\u2019re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025." }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsx(Trans, { children: "We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025." }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsx(Trans, { children: "Learn more about these changes and how to share your thoughts with us by reading our blog post." }) }), _jsx(Link, __assign({}, links.terms, linkButtonStyles, { children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Terms of Service" }) }) })), _jsx(Link, __assign({}, links.privacy, linkButtonStyles, { children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Privacy Policy" }) }) })), _jsx(Link, __assign({}, links.copyright, linkButtonStyles, { children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Copyright Policy" }) }) })), _jsx(Link, __assign({}, links.blog, linkButtonStyles, { children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Read our blog post" }) }) }))] })) : (_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { emoji: true, style: [a.text_2xl, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { children: "Hey there \uD83D\uDC4B" }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsxs(Trans, { children: ["We\u2019re updating our", ' ', _jsx(InlineLinkText, __assign({}, links.terms, { style: linkStyle, children: "Terms of Service" })), ",", ' ', _jsx(InlineLinkText, __assign({}, links.privacy, { style: linkStyle, children: "Privacy Policy" })), ", and", ' ', _jsx(InlineLinkText, __assign({}, links.copyright, { style: linkStyle, children: "Copyright Policy" })), ", effective September 15th, 2025."] }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsxs(Trans, { children: ["We're also updating our", ' ', _jsx(InlineLinkText, __assign({}, links.guidelines, { style: linkStyle, children: "Community Guidelines" })), ", and we want your input! These new guidelines will take effect on October 15th, 2025."] }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsxs(Trans, { children: ["Learn more about these changes and how to share your thoughts with us by", ' ', _jsx(InlineLinkText, __assign({}, links.blog, { style: linkStyle, children: "reading our blog post." }))] }) })] })), _jsxs(View, { style: [a.w_full, a.gap_md], children: [_jsx(Button, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Continue"], ["Continue"])))), accessibilityHint: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Tap to acknowledge that you understand and agree to these updates and continue using Bluesky"], ["Tap to acknowledge that you understand and agree to these updates and continue using Bluesky"])))), color: "primary", size: "large", onPress: handleClose, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Continue" }) }) }), _jsx(Text, { style: [ + a.leading_snug, + a.text_sm, + a.italic, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "By clicking \"Continue\" you acknowledge that you understand and agree to these updates." }) })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/components/PolicyUpdateOverlay/usePolicyUpdateState.js b/src/components/PolicyUpdateOverlay/usePolicyUpdateState.js new file mode 100644 index 0000000000..fbc387b04c --- /dev/null +++ b/src/components/PolicyUpdateOverlay/usePolicyUpdateState.js @@ -0,0 +1,107 @@ +import { useMemo } from 'react'; +import { useNux, useSaveNux } from '#/state/queries/nuxs'; +import { ACTIVE_UPDATE_ID } from '#/components/PolicyUpdateOverlay/config'; +import { logger } from '#/components/PolicyUpdateOverlay/logger'; +import { IS_DEV } from '#/env'; +import { device, useStorage } from '#/storage'; +export function usePolicyUpdateState(_a) { + var enabled = _a.enabled; + var nux = useNux(ACTIVE_UPDATE_ID); + var _b = useSaveNux(), save = _b.mutate, variables = _b.variables; + var deviceStorage = useStorage(device, [ACTIVE_UPDATE_ID]); + var debugOverride = !!useStorage(device, ['policyUpdateDebugOverride'])[0] && IS_DEV; + return useMemo(function () { + var _a; + /** + * If not enabled, then just return a completed state so the app functions + * as normal. + */ + if (!enabled) { + return { + completed: true, + complete: function () { }, + }; + } + var nuxIsReady = nux.status === 'ready'; + var nuxIsCompleted = ((_a = nux.nux) === null || _a === void 0 ? void 0 : _a.completed) === true; + var nuxIsOptimisticallyCompleted = !!(variables === null || variables === void 0 ? void 0 : variables.completed); + var completedForDevice = deviceStorage[0], setCompletedForDevice = deviceStorage[1]; + var completed = computeCompletedState({ + nuxIsReady: nuxIsReady, + nuxIsCompleted: nuxIsCompleted, + nuxIsOptimisticallyCompleted: nuxIsOptimisticallyCompleted, + completedForDevice: completedForDevice, + }); + logger.debug("state", { + completed: completed, + nux: nux, + completedForDevice: completedForDevice, + }); + if (!debugOverride) { + syncCompletedState({ + nuxIsReady: nuxIsReady, + nuxIsCompleted: nuxIsCompleted, + nuxIsOptimisticallyCompleted: nuxIsOptimisticallyCompleted, + completedForDevice: completedForDevice, + save: save, + setCompletedForDevice: setCompletedForDevice, + }); + } + return { + completed: completed, + complete: function () { + logger.debug("user completed"); + save({ + id: ACTIVE_UPDATE_ID, + completed: true, + data: undefined, + }); + setCompletedForDevice(true); + }, + }; + }, [enabled, nux, save, variables, deviceStorage, debugOverride]); +} +export function computeCompletedState(_a) { + var nuxIsReady = _a.nuxIsReady, nuxIsCompleted = _a.nuxIsCompleted, nuxIsOptimisticallyCompleted = _a.nuxIsOptimisticallyCompleted, completedForDevice = _a.completedForDevice; + /** + * Assume completed to prevent flash + */ + var completed = true; + /** + * Prefer server state, if available + */ + if (nuxIsReady) { + completed = nuxIsCompleted; + } + /** + * Override with optimistic state or device state + */ + if (nuxIsOptimisticallyCompleted || !!completedForDevice) { + completed = true; + } + return completed; +} +export function syncCompletedState(_a) { + var nuxIsReady = _a.nuxIsReady, nuxIsCompleted = _a.nuxIsCompleted, nuxIsOptimisticallyCompleted = _a.nuxIsOptimisticallyCompleted, completedForDevice = _a.completedForDevice, save = _a.save, setCompletedForDevice = _a.setCompletedForDevice; + /* + * Sync device state to server state for this account + */ + if (nuxIsReady && + !nuxIsCompleted && + !nuxIsOptimisticallyCompleted && + !!completedForDevice) { + logger.debug("syncing device state to server state"); + save({ + id: ACTIVE_UPDATE_ID, + completed: true, + data: undefined, + }); + } + else if (nuxIsReady && nuxIsCompleted && !completedForDevice) { + logger.debug("syncing server state to device state"); + /* + * Sync server state to device state + */ + setCompletedForDevice(true); + } +} diff --git a/src/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate.js b/src/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate.js new file mode 100644 index 0000000000..d0cb526a56 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate.js @@ -0,0 +1,18 @@ +import { useCallback } from 'react'; +import { ACTIVE_UPDATE_ID } from '#/components/PolicyUpdateOverlay/config'; +import { logger } from '#/components/PolicyUpdateOverlay/logger'; +import { device, useStorage } from '#/storage'; +/* + * Marks the active policy update as completed in device storage. + * `usePolicyUpdateState` will react to this and replicate this status in the + * server NUX state for this account. + */ +export function usePreemptivelyCompleteActivePolicyUpdate() { + var _a = useStorage(device, [ + ACTIVE_UPDATE_ID, + ]), _completedForDevice = _a[0], setCompletedForDevice = _a[1]; + return useCallback(function () { + logger.debug("preemptively completing active policy update"); + setCompletedForDevice(true); + }, [setCompletedForDevice]); +} diff --git a/src/components/Portal.js b/src/components/Portal.js new file mode 100644 index 0000000000..98a3d1905d --- /dev/null +++ b/src/components/Portal.js @@ -0,0 +1,49 @@ +import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; +import { createContext, Fragment, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react'; +export function createPortalGroup() { + var Context = createContext({ + outlet: null, + append: function () { }, + remove: function () { }, + }); + Context.displayName = 'PortalContext'; + function Provider(props) { + var map = useRef({}); + var _a = useState(null), outlet = _a[0], setOutlet = _a[1]; + var append = useCallback(function (id, component) { + if (map.current[id]) + return; + map.current[id] = _jsx(Fragment, { children: component }, id); + setOutlet(_jsx(_Fragment, { children: Object.values(map.current) })); + }, []); + var remove = useCallback(function (id) { + map.current[id] = null; + setOutlet(_jsx(_Fragment, { children: Object.values(map.current) })); + }, []); + var contextValue = useMemo(function () { return ({ + outlet: outlet, + append: append, + remove: remove, + }); }, [outlet, append, remove]); + return (_jsx(Context.Provider, { value: contextValue, children: props.children })); + } + function Outlet() { + var ctx = useContext(Context); + return ctx.outlet; + } + function Portal(_a) { + var children = _a.children; + var _b = useContext(Context), append = _b.append, remove = _b.remove; + var id = useId(); + useEffect(function () { + append(id, children); + return function () { return remove(id); }; + }, [id, children, append, remove]); + return null; + } + return { Provider: Provider, Outlet: Outlet, Portal: Portal }; +} +var DefaultPortal = createPortalGroup(); +export var Provider = DefaultPortal.Provider; +export var Outlet = DefaultPortal.Outlet; +export var Portal = DefaultPortal.Portal; diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalGif.js b/src/components/Post/Embed/ExternalEmbed/ExternalGif.js new file mode 100644 index 0000000000..a4d4474771 --- /dev/null +++ b/src/components/Post/Embed/ExternalEmbed/ExternalGif.js @@ -0,0 +1,97 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { ActivityIndicator, Pressable, } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useExternalEmbedsPrefs } from '#/state/preferences'; +import { atoms as a, useTheme } from '#/alf'; +import { useDialogControl } from '#/components/Dialog'; +import { EmbedConsentDialog } from '#/components/dialogs/EmbedConsent'; +import { Fill } from '#/components/Fill'; +import { PlayButtonIcon } from '#/components/video/PlayButtonIcon'; +import { IS_IOS, IS_NATIVE, IS_WEB } from '#/env'; +export function ExternalGif(_a) { + var link = _a.link, params = _a.params; + var t = useTheme(); + var externalEmbedsPrefs = useExternalEmbedsPrefs(); + var _ = useLingui()._; + var consentDialogControl = useDialogControl(); + // Tracking if the placer has been activated + var _b = React.useState(false), isPlayerActive = _b[0], setIsPlayerActive = _b[1]; + // Tracking whether the gif has been loaded yet + var _c = React.useState(false), isPrefetched = _c[0], setIsPrefetched = _c[1]; + // Tracking whether the image is animating + var _d = React.useState(true), isAnimating = _d[0], setIsAnimating = _d[1]; + // Used for controlling animation + var imageRef = React.useRef(null); + var load = React.useCallback(function () { + setIsPlayerActive(true); + Image.prefetch(params.playerUri).then(function () { + // Replace the image once it's fetched + setIsPrefetched(true); + }); + }, [params.playerUri]); + var onPlayPress = React.useCallback(function (event) { + // Don't propagate on web + event.preventDefault(); + // Show consent if this is the first load + if ((externalEmbedsPrefs === null || externalEmbedsPrefs === void 0 ? void 0 : externalEmbedsPrefs[params.source]) === undefined) { + consentDialogControl.open(); + return; + } + // If the player isn't active, we want to activate it and prefetch the gif + if (!isPlayerActive) { + load(); + return; + } + // Control animation on native + setIsAnimating(function (prev) { + var _a, _b; + if (prev) { + if (IS_NATIVE) { + (_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.stopAnimating(); + } + return false; + } + else { + if (IS_NATIVE) { + (_b = imageRef.current) === null || _b === void 0 ? void 0 : _b.startAnimating(); + } + return true; + } + }); + }, [ + consentDialogControl, + externalEmbedsPrefs, + isPlayerActive, + load, + params.source, + ]); + return (_jsxs(_Fragment, { children: [_jsx(EmbedConsentDialog, { control: consentDialogControl, source: params.source, onAccept: load }), _jsxs(Pressable, { style: [ + { height: 300 }, + a.w_full, + a.overflow_hidden, + { + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + }, + ], onPress: onPlayPress, accessibilityRole: "button", accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Plays the GIF"], ["Plays the GIF"])))), accessibilityLabel: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Play ", ""], ["Play ", ""])), link.title)), children: [_jsx(Image, { source: { + uri: !isPrefetched || (IS_WEB && !isAnimating) + ? link.thumb + : params.playerUri, + }, style: { flex: 1 }, ref: imageRef, autoplay: isAnimating, contentFit: "contain", accessibilityIgnoresInvertColors: true, accessibilityLabel: link.title, accessibilityHint: link.title, cachePolicy: IS_IOS ? 'disk' : 'memory-disk' }), (!isPrefetched || !isAnimating) && (_jsxs(Fill, { style: [a.align_center, a.justify_center], children: [_jsx(Fill, { style: [ + t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, + { + opacity: 0.3, + }, + ] }), !isAnimating || !isPlayerActive ? ( // Play button when not animating or not active + _jsx(PlayButtonIcon, {})) : ( + // Activity indicator while gif loads + _jsx(ActivityIndicator, { size: "large", color: "white" }))] }))] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.js b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.js new file mode 100644 index 0000000000..9265719106 --- /dev/null +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.js @@ -0,0 +1,150 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { ActivityIndicator, Pressable, StyleSheet, useWindowDimensions, View, } from 'react-native'; +import Animated, { measure, runOnJS, useAnimatedRef, useFrameCallback, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { WebView } from 'react-native-webview'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { getPlayerAspect, } from '#/lib/strings/embed-player'; +import { useExternalEmbedsPrefs } from '#/state/preferences'; +import { EventStopper } from '#/view/com/util/EventStopper'; +import { atoms as a, useTheme } from '#/alf'; +import { useDialogControl } from '#/components/Dialog'; +import { EmbedConsentDialog } from '#/components/dialogs/EmbedConsent'; +import { Fill } from '#/components/Fill'; +import { PlayButtonIcon } from '#/components/video/PlayButtonIcon'; +import { IS_NATIVE } from '#/env'; +// This renders the overlay when the player is either inactive or loading as a separate layer +function PlaceholderOverlay(_a) { + var isLoading = _a.isLoading, isPlayerActive = _a.isPlayerActive, onPress = _a.onPress; + var _ = useLingui()._; + // If the player is active and not loading, we don't want to show the overlay. + if (isPlayerActive && !isLoading) + return null; + return (_jsx(View, { style: [a.absolute, a.inset_0, styles.overlayLayer], children: _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Play Video"], ["Play Video"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Plays the video"], ["Plays the video"])))), onPress: onPress, style: [styles.overlayContainer], children: !isPlayerActive ? (_jsx(PlayButtonIcon, {})) : (_jsx(ActivityIndicator, { size: "large", color: "white" })) }) })); +} +// This renders the webview/youtube player as a separate layer +function Player(_a) { + var params = _a.params, onLoad = _a.onLoad, isPlayerActive = _a.isPlayerActive; + // ensures we only load what's requested + // when it's a youtube video, we need to allow both bsky.app and youtube.com + var onShouldStartLoadWithRequest = React.useCallback(function (event) { + return event.url === params.playerUri || + (params.source.startsWith('youtube') && + event.url.includes('www.youtube.com')); + }, [params.playerUri, params.source]); + // Don't show the player until it is active + if (!isPlayerActive) + return null; + return (_jsx(EventStopper, { style: [a.absolute, a.inset_0, styles.playerLayer], children: _jsx(WebView, { javaScriptEnabled: true, onShouldStartLoadWithRequest: onShouldStartLoadWithRequest, mediaPlaybackRequiresUserAction: false, allowsInlineMediaPlayback: true, bounces: false, allowsFullscreenVideo: true, nestedScrollEnabled: true, source: { uri: params.playerUri }, onLoad: onLoad, style: styles.webview, setSupportMultipleWindows: false }) })); +} +// This renders the player area and handles the logic for when to show the player and when to show the overlay +export function ExternalPlayer(_a) { + var link = _a.link, params = _a.params; + var t = useTheme(); + var navigation = useNavigation(); + var insets = useSafeAreaInsets(); + var windowDims = useWindowDimensions(); + var externalEmbedsPrefs = useExternalEmbedsPrefs(); + var consentDialogControl = useDialogControl(); + var _b = React.useState(false), isPlayerActive = _b[0], setPlayerActive = _b[1]; + var _c = React.useState(true), isLoading = _c[0], setIsLoading = _c[1]; + var aspect = React.useMemo(function () { + return getPlayerAspect({ + type: params.type, + width: windowDims.width, + hasThumb: !!link.thumb, + }); + }, [params.type, windowDims.width, link.thumb]); + var viewRef = useAnimatedRef(); + var frameCallback = useFrameCallback(function () { + var measurement = measure(viewRef); + if (!measurement) + return; + var winHeight = windowDims.height, winWidth = windowDims.width; + // Get the proper screen height depending on what is going on + var realWinHeight = IS_NATIVE // If it is native, we always want the larger number + ? winHeight > winWidth + ? winHeight + : winWidth + : winHeight; // On web, we always want the actual screen height + var top = measurement.pageY; + var bot = measurement.pageY + measurement.height; + // We can use the same logic on all platforms against the screenHeight that we get above + var isVisible = top <= realWinHeight - insets.bottom && bot >= insets.top; + if (!isVisible) { + runOnJS(setPlayerActive)(false); + } + }, false); // False here disables autostarting the callback + // watch for leaving the viewport due to scrolling + React.useEffect(function () { + // We don't want to do anything if the player isn't active + if (!isPlayerActive) + return; + // Interval for scrolling works in most cases, However, for twitch embeds, if we navigate away from the screen the webview will + // continue playing. We need to watch for the blur event + var unsubscribe = navigation.addListener('blur', function () { + setPlayerActive(false); + }); + // Start watching for changes + frameCallback.setActive(true); + return function () { + unsubscribe(); + frameCallback.setActive(false); + }; + }, [navigation, isPlayerActive, frameCallback]); + var onLoad = React.useCallback(function () { + setIsLoading(false); + }, []); + var onPlayPress = React.useCallback(function (event) { + // Prevent this from propagating upward on web + event.preventDefault(); + if ((externalEmbedsPrefs === null || externalEmbedsPrefs === void 0 ? void 0 : externalEmbedsPrefs[params.source]) === undefined) { + consentDialogControl.open(); + return; + } + setPlayerActive(true); + }, [externalEmbedsPrefs, consentDialogControl, params.source]); + var onAcceptConsent = React.useCallback(function () { + setPlayerActive(true); + }, []); + return (_jsxs(_Fragment, { children: [_jsx(EmbedConsentDialog, { control: consentDialogControl, source: params.source, onAccept: onAcceptConsent }), _jsxs(Animated.View, { ref: viewRef, collapsable: false, style: [aspect, a.overflow_hidden], children: [link.thumb && (!isPlayerActive || isLoading) ? (_jsxs(_Fragment, { children: [_jsx(Image, { style: [a.flex_1], source: { uri: link.thumb }, accessibilityIgnoresInvertColors: true, loading: "lazy" }), _jsx(Fill, { style: [ + t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, + { + opacity: 0.3, + }, + ] })] })) : (_jsx(Fill, { style: [ + { + backgroundColor: t.name === 'light' ? t.palette.contrast_975 : 'black', + opacity: 0.3, + }, + ] })), _jsx(PlaceholderOverlay, { isLoading: isLoading, isPlayerActive: isPlayerActive, onPress: onPlayPress }), _jsx(Player, { isPlayerActive: isPlayerActive, params: params, onLoad: onLoad })] })] })); +} +var styles = StyleSheet.create({ + overlayContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + overlayLayer: { + zIndex: 2, + }, + playerLayer: { + zIndex: 3, + }, + webview: { + backgroundColor: 'transparent', + }, + gifContainer: { + width: '100%', + overflow: 'hidden', + }, +}); +var templateObject_1, templateObject_2; diff --git a/src/components/Post/Embed/ExternalEmbed/Gif.js b/src/components/Post/Embed/ExternalEmbed/Gif.js new file mode 100644 index 0000000000..13e266c6ea --- /dev/null +++ b/src/components/Post/Embed/ExternalEmbed/Gif.js @@ -0,0 +1,63 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useRef, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { clamp } from '#/lib/numbers'; +import { useAutoplayDisabled } from '#/state/preferences'; +import { atoms as a, useTheme } from '#/alf'; +import { Fill } from '#/components/Fill'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { GifView } from '../../../../../modules/expo-bluesky-gif-view'; +import { GifPresentationControls } from '../VideoEmbed/GifPresentationControls'; +export function GifEmbed(_a) { + var params = _a.params, thumb = _a.thumb, altText = _a.altText, isPreferredAltText = _a.isPreferredAltText, hideAlt = _a.hideAlt, _b = _a.style, style = _b === void 0 ? { width: '100%' } : _b; + var t = useTheme(); + var _ = useLingui()._; + var autoplayDisabled = useAutoplayDisabled(); + var playerRef = useRef(null); + var _c = useState({ + isPlaying: !autoplayDisabled, + isLoaded: false, + }), playerState = _c[0], setPlayerState = _c[1]; + var onPlayerStateChange = function (e) { + setPlayerState(e.nativeEvent); + }; + var onPress = function () { + var _a; + void ((_a = playerRef.current) === null || _a === void 0 ? void 0 : _a.toggleAsync()); + }; + var aspectRatio = 1; + if (params.dimensions) { + var ratio = params.dimensions.width / params.dimensions.height; + aspectRatio = clamp(ratio, 0.75, 4); + } + return (_jsx(View, { style: [ + a.rounded_md, + a.overflow_hidden, + { backgroundColor: t.palette.black }, + { aspectRatio: aspectRatio }, + style, + ], children: _jsxs(View, { style: [ + a.absolute, + /* + * Aspect ratio was being clipped weirdly on web -esb + */ + { + top: -2, + bottom: -2, + left: -2, + right: -2, + }, + ], children: [_jsx(MediaInsetBorder, {}), _jsx(GifPresentationControls, { onPress: onPress, isPlaying: playerState.isPlaying, isLoading: !playerState.isLoaded, altText: !hideAlt && isPreferredAltText ? altText : undefined }), _jsx(GifView, { source: params.playerUri, placeholderSource: thumb, style: [a.flex_1], autoplay: !autoplayDisabled, onPlayerStateChange: onPlayerStateChange, ref: playerRef, accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Animated GIF"], ["Animated GIF"])))), accessibilityLabel: altText }), !playerState.isPlaying && (_jsx(Fill, { style: [ + t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, + { + opacity: 0.3, + }, + ] }))] }) })); +} +var templateObject_1; diff --git a/src/components/Post/Embed/ExternalEmbed/index.js b/src/components/Post/Embed/ExternalEmbed/index.js new file mode 100644 index 0000000000..53d158c18e --- /dev/null +++ b/src/components/Post/Embed/ExternalEmbed/index.js @@ -0,0 +1,99 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React, { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { parseAltFromGIFDescription } from '#/lib/gif-alt-text'; +import { useHaptics } from '#/lib/haptics'; +import { shareUrl } from '#/lib/sharing'; +import { parseEmbedPlayerFromUrl } from '#/lib/strings/embed-player'; +import { toNiceDomain } from '#/lib/strings/url-helpers'; +import { useExternalEmbedsPrefs } from '#/state/preferences'; +import { atoms as a, useTheme } from '#/alf'; +import { Divider } from '#/components/Divider'; +import { Earth_Stroke2_Corner0_Rounded as Globe } from '#/components/icons/Globe'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +import { ExternalGif } from './ExternalGif'; +import { ExternalPlayer } from './ExternalPlayer'; +import { GifEmbed } from './Gif'; +export var ExternalEmbed = function (_a) { + var link = _a.link, onOpen = _a.onOpen, style = _a.style, hideAlt = _a.hideAlt; + var _ = useLingui()._; + var t = useTheme(); + var playHaptic = useHaptics(); + var externalEmbedPrefs = useExternalEmbedsPrefs(); + var niceUrl = toNiceDomain(link.uri); + var imageUri = link.thumb; + var embedPlayerParams = React.useMemo(function () { + var params = parseEmbedPlayerFromUrl(link.uri); + if (params && (externalEmbedPrefs === null || externalEmbedPrefs === void 0 ? void 0 : externalEmbedPrefs[params.source]) !== 'hide') { + return params; + } + }, [link.uri, externalEmbedPrefs]); + var hasMedia = Boolean(imageUri || embedPlayerParams); + var onPress = useCallback(function () { + playHaptic('Light'); + onOpen === null || onOpen === void 0 ? void 0 : onOpen(); + }, [playHaptic, onOpen]); + var onShareExternal = useCallback(function () { + if (link.uri && IS_NATIVE) { + playHaptic('Heavy'); + shareUrl(link.uri); + } + }, [link.uri, playHaptic]); + if ((embedPlayerParams === null || embedPlayerParams === void 0 ? void 0 : embedPlayerParams.source) === 'tenor') { + var parsedAlt = parseAltFromGIFDescription(link.description); + return (_jsx(View, { style: style, children: _jsx(GifEmbed, { params: embedPlayerParams, thumb: link.thumb, altText: parsedAlt.alt, isPreferredAltText: parsedAlt.isPreferred, hideAlt: hideAlt }) })); + } + return (_jsx(Link, { label: link.title || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Open link to ", ""], ["Open link to ", ""])), niceUrl)), to: link.uri, shouldProxy: true, onPress: onPress, onLongPress: onShareExternal, children: function (_a) { + var hovered = _a.hovered; + return (_jsxs(View, { style: [ + a.transition_color, + a.flex_col, + a.rounded_md, + a.overflow_hidden, + a.w_full, + a.border, + style, + hovered + ? t.atoms.border_contrast_high + : t.atoms.border_contrast_low, + ], children: [imageUri && !embedPlayerParams ? (_jsx(Image, { style: [a.aspect_card], source: { uri: imageUri }, accessibilityIgnoresInvertColors: true, loading: "lazy" })) : undefined, (embedPlayerParams === null || embedPlayerParams === void 0 ? void 0 : embedPlayerParams.isGif) ? (_jsx(ExternalGif, { link: link, params: embedPlayerParams })) : embedPlayerParams ? (_jsx(ExternalPlayer, { link: link, params: embedPlayerParams })) : undefined, _jsxs(View, { style: [ + a.flex_1, + a.pt_sm, + { gap: 3 }, + hasMedia && a.border_t, + hovered + ? t.atoms.border_contrast_high + : t.atoms.border_contrast_low, + ], children: [_jsxs(View, { style: [{ gap: 3 }, a.pb_xs, a.px_md], children: [!(embedPlayerParams === null || embedPlayerParams === void 0 ? void 0 : embedPlayerParams.isGif) && !(embedPlayerParams === null || embedPlayerParams === void 0 ? void 0 : embedPlayerParams.dimensions) && (_jsx(Text, { emoji: true, numberOfLines: 3, style: [a.text_md, a.font_semi_bold, a.leading_snug], children: link.title || link.uri })), link.description ? (_jsx(Text, { emoji: true, numberOfLines: link.thumb ? 2 : 4, style: [a.text_sm, a.leading_snug], children: link.description })) : undefined] }), _jsxs(View, { style: [a.px_md], children: [_jsx(Divider, {}), _jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.gap_2xs, + a.pb_sm, + { + paddingTop: 6, // off menu + }, + ], children: [_jsx(Globe, { size: "xs", style: [ + a.transition_color, + hovered + ? t.atoms.text_contrast_medium + : t.atoms.text_contrast_low, + ] }), _jsx(Text, { numberOfLines: 1, style: [ + a.transition_color, + a.text_xs, + a.leading_snug, + hovered + ? t.atoms.text_contrast_high + : t.atoms.text_contrast_medium, + ], children: toNiceDomain(link.uri) })] })] })] })] })); + } })); +}; +var templateObject_1; diff --git a/src/components/Post/Embed/FeedEmbed.js b/src/components/Post/Embed/FeedEmbed.js new file mode 100644 index 0000000000..0143400c0c --- /dev/null +++ b/src/components/Post/Embed/FeedEmbed.js @@ -0,0 +1,22 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { moderateFeedGenerator } from '@atproto/api'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { atoms as a, useTheme } from '#/alf'; +import * as FeedCard from '#/components/FeedCard'; +import { ContentHider } from '#/components/moderation/ContentHider'; +export function FeedEmbed(_a) { + var embed = _a.embed; + var t = useTheme(); + return (_jsx(FeedCard.Link, { view: embed.view, style: [a.border, t.atoms.border_contrast_low, a.p_sm, a.rounded_md], children: _jsx(FeedCard.Outer, { children: _jsxs(FeedCard.Header, { children: [_jsx(FeedCard.Avatar, { src: embed.view.avatar, size: 48 }), _jsx(FeedCard.TitleAndByline, { title: embed.view.displayName, creator: embed.view.creator, uri: embed.view.uri })] }) }) })); +} +export function ModeratedFeedEmbed(_a) { + var embed = _a.embed; + var moderationOpts = useModerationOpts(); + var moderation = useMemo(function () { + return moderationOpts + ? moderateFeedGenerator(embed.view, moderationOpts) + : undefined; + }, [embed.view, moderationOpts]); + return (_jsx(ContentHider, { modui: moderation === null || moderation === void 0 ? void 0 : moderation.ui('contentList'), childContainerStyle: [a.pt_xs], children: _jsx(FeedEmbed, { embed: embed }) })); +} diff --git a/src/components/Post/Embed/ImageEmbed.js b/src/components/Post/Embed/ImageEmbed.js new file mode 100644 index 0000000000..1927d6e1fe --- /dev/null +++ b/src/components/Post/Embed/ImageEmbed.js @@ -0,0 +1,82 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { InteractionManager, View } from 'react-native'; +import { measure, runOnJS, runOnUI, } from 'react-native-reanimated'; +import { Image } from 'expo-image'; +import { useLightboxControls } from '#/state/lightbox'; +import { atoms as a } from '#/alf'; +import { AutoSizedImage } from '#/components/images/AutoSizedImage'; +import { ImageLayoutGrid } from '#/components/images/ImageLayoutGrid'; +import { PostEmbedViewContext } from '#/components/Post/Embed/types'; +export function ImageEmbed(_a) { + var embed = _a.embed, rest = __rest(_a, ["embed"]); + var openLightbox = useLightboxControls().openLightbox; + var images = embed.view.images; + if (images.length > 0) { + var items_1 = images.map(function (img) { + var _a; + return ({ + uri: img.fullsize, + thumbUri: img.thumb, + alt: img.alt, + dimensions: (_a = img.aspectRatio) !== null && _a !== void 0 ? _a : null, + }); + }); + var _openLightbox_1 = function (index, thumbRects, fetchedDims) { + openLightbox({ + images: items_1.map(function (item, i) { + var _a, _b; + return (__assign(__assign({}, item), { thumbRect: (_a = thumbRects[i]) !== null && _a !== void 0 ? _a : null, thumbDimensions: (_b = fetchedDims[i]) !== null && _b !== void 0 ? _b : null, type: 'image' })); + }), + index: index, + }); + }; + var onPress_1 = function (index, refs, fetchedDims) { + runOnUI(function () { + 'worklet'; + var rects = []; + for (var _i = 0, refs_1 = refs; _i < refs_1.length; _i++) { + var r = refs_1[_i]; + rects.push(measure(r)); + } + runOnJS(_openLightbox_1)(index, rects, fetchedDims); + })(); + }; + var onPressIn_1 = function (_) { + InteractionManager.runAfterInteractions(function () { + Image.prefetch(items_1.map(function (i) { return i.uri; }), 'memory'); + }); + }; + if (images.length === 1) { + var image = images[0]; + return (_jsx(View, { style: [a.mt_sm, rest.style], children: _jsx(AutoSizedImage, { crop: rest.viewContext === PostEmbedViewContext.ThreadHighlighted + ? 'none' + : rest.viewContext === + PostEmbedViewContext.FeedEmbedRecordWithMedia + ? 'square' + : 'constrained', image: image, onPress: function (containerRef, dims) { return onPress_1(0, [containerRef], [dims]); }, onPressIn: function () { return onPressIn_1(0); }, hideBadge: rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia }) })); + } + return (_jsx(View, { style: [a.mt_sm, rest.style], children: _jsx(ImageLayoutGrid, { images: images, onPress: onPress_1, onPressIn: onPressIn_1, viewContext: rest.viewContext }) })); + } +} diff --git a/src/components/Post/Embed/LazyQuoteEmbed.js b/src/components/Post/Embed/LazyQuoteEmbed.js new file mode 100644 index 0000000000..1d05e1daf6 --- /dev/null +++ b/src/components/Post/Embed/LazyQuoteEmbed.js @@ -0,0 +1,28 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { createEmbedViewRecordFromPost } from '#/state/queries/postgate/util'; +import { useResolveLinkQuery } from '#/state/queries/resolve-link'; +import { atoms as a, useTheme } from '#/alf'; +import { QuoteEmbed } from '#/components/Post/Embed'; +export function LazyQuoteEmbed(_a) { + var uri = _a.uri, linkDisabled = _a.linkDisabled; + var t = useTheme(); + var data = useResolveLinkQuery(uri).data; + var view = useMemo(function () { + if (!data || data.type !== 'record' || data.kind !== 'post') + return; + return createEmbedViewRecordFromPost(data.view); + }, [data]); + return view ? (_jsx(QuoteEmbed, { embed: { + type: 'post', + view: view, + }, linkDisabled: linkDisabled })) : (_jsx(View, { style: [ + a.w_full, + a.rounded_md, + t.atoms.bg_contrast_25, + { + height: 68, + }, + ] })); +} diff --git a/src/components/Post/Embed/ListEmbed.js b/src/components/Post/Embed/ListEmbed.js new file mode 100644 index 0000000000..3e2fc5d9d1 --- /dev/null +++ b/src/components/Post/Embed/ListEmbed.js @@ -0,0 +1,22 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { moderateUserList } from '@atproto/api'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { atoms as a, useTheme } from '#/alf'; +import * as ListCard from '#/components/ListCard'; +import { ContentHider } from '#/components/moderation/ContentHider'; +export function ListEmbed(_a) { + var embed = _a.embed; + var t = useTheme(); + return (_jsx(ListCard.Default, { view: embed.view, style: [a.border, t.atoms.border_contrast_low, a.p_md, a.rounded_sm] })); +} +export function ModeratedListEmbed(_a) { + var embed = _a.embed; + var moderationOpts = useModerationOpts(); + var moderation = useMemo(function () { + return moderationOpts + ? moderateUserList(embed.view, moderationOpts) + : undefined; + }, [embed.view, moderationOpts]); + return (_jsx(ContentHider, { modui: moderation === null || moderation === void 0 ? void 0 : moderation.ui('contentList'), childContainerStyle: [a.pt_xs], children: _jsx(ListEmbed, { embed: embed }) })); +} diff --git a/src/components/Post/Embed/PostPlaceholder.js b/src/components/Post/Embed/PostPlaceholder.js new file mode 100644 index 0000000000..1e035fb8b0 --- /dev/null +++ b/src/components/Post/Embed/PostPlaceholder.js @@ -0,0 +1,24 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { StyleSheet, View } from 'react-native'; +import { usePalette } from '#/lib/hooks/usePalette'; +import { InfoCircleIcon } from '#/lib/icons'; +import { Text } from '#/view/com/util/text/Text'; +import { atoms as a, useTheme } from '#/alf'; +export function PostPlaceholder(_a) { + var children = _a.children; + var t = useTheme(); + var pal = usePalette('default'); + return (_jsxs(View, { style: [styles.errorContainer, a.border, t.atoms.border_contrast_low], children: [_jsx(InfoCircleIcon, { size: 18, style: pal.text }), _jsx(Text, { type: "lg", style: pal.text, children: children })] })); +} +var styles = StyleSheet.create({ + errorContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + borderRadius: 8, + marginTop: 8, + paddingVertical: 14, + paddingHorizontal: 14, + borderWidth: StyleSheet.hairlineWidth, + }, +}); diff --git a/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.js b/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.js new file mode 100644 index 0000000000..d68b85f4a8 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.js @@ -0,0 +1,77 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React, { useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react'; +import { useWindowDimensions } from 'react-native'; +import { IS_NATIVE, IS_WEB } from '#/env'; +var Context = React.createContext(null); +Context.displayName = 'ActiveVideoWebContext'; +export function Provider(_a) { + var children = _a.children; + if (!IS_WEB) { + throw new Error('ActiveVideoWebContext may only be used on web.'); + } + var _b = useState(null), activeViewId = _b[0], setActiveViewId = _b[1]; + var activeViewLocationRef = useRef(Infinity); + var windowHeight = useWindowDimensions().height; + // minimising re-renders by using refs + var manuallySetRef = useRef(false); + var activeViewIdRef = useRef(activeViewId); + useEffect(function () { + activeViewIdRef.current = activeViewId; + }, [activeViewId]); + var setActiveView = useCallback(function (viewId) { + setActiveViewId(viewId); + manuallySetRef.current = true; + // we don't know the exact position, but it's definitely on screen + // so just guess that it's in the middle. Any value is fine + // so long as it's not offscreen + activeViewLocationRef.current = windowHeight / 2; + }, [windowHeight]); + var sendViewPosition = useCallback(function (viewId, y) { + if (IS_NATIVE) + return; + if (viewId === activeViewIdRef.current) { + activeViewLocationRef.current = y; + } + else { + if (distanceToIdealPosition(y) < + distanceToIdealPosition(activeViewLocationRef.current)) { + // if the old view was manually set, only usurp if the old view is offscreen + if (manuallySetRef.current && + withinViewport(activeViewLocationRef.current)) { + return; + } + setActiveViewId(viewId); + activeViewLocationRef.current = y; + manuallySetRef.current = false; + } + } + function distanceToIdealPosition(yPos) { + return Math.abs(yPos - windowHeight / 2.5); + } + function withinViewport(yPos) { + return yPos > 0 && yPos < windowHeight; + } + }, [windowHeight]); + var value = useMemo(function () { return ({ + activeViewId: activeViewId, + setActiveView: setActiveView, + sendViewPosition: sendViewPosition, + }); }, [activeViewId, setActiveView, sendViewPosition]); + return _jsx(Context.Provider, { value: value, children: children }); +} +export function useActiveVideoWeb() { + var context = React.useContext(Context); + if (!context) { + throw new Error('useActiveVideoWeb must be used within a ActiveVideoWebProvider'); + } + var activeViewId = context.activeViewId, setActiveView = context.setActiveView, sendViewPosition = context.sendViewPosition; + var id = useId(); + return { + active: activeViewId === id, + setActive: function () { + setActiveView(id); + }, + currentActiveView: activeViewId, + sendPosition: function (y) { return sendViewPosition(id, y); }, + }; +} diff --git a/src/components/Post/Embed/VideoEmbed/GifPresentationControls.js b/src/components/Post/Embed/VideoEmbed/GifPresentationControls.js new file mode 100644 index 0000000000..109754f82a --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/GifPresentationControls.js @@ -0,0 +1,63 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { ActivityIndicator, StyleSheet, TouchableOpacity, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { HITSLOP_20 } from '#/lib/constants'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { Fill } from '#/components/Fill'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { PlayButtonIcon } from '#/components/video/PlayButtonIcon'; +export function GifPresentationControls(_a) { + var onPress = _a.onPress, isPlaying = _a.isPlaying, isLoading = _a.isLoading, altText = _a.altText; + var _ = useLingui()._; + var t = useTheme(); + return (_jsxs(_Fragment, { children: [_jsx(Button, { label: isPlaying ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Pause GIF"], ["Pause GIF"])))) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Play GIF"], ["Play GIF"])))), accessibilityHint: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Plays or pauses the GIF"], ["Plays or pauses the GIF"])))), style: [ + a.absolute, + a.align_center, + a.justify_center, + a.inset_0, + { zIndex: 2 }, + ], onPress: onPress, children: isLoading ? (_jsx(View, { style: [a.align_center, a.justify_center], children: _jsx(ActivityIndicator, { size: "large", color: "white" }) })) : !isPlaying ? (_jsx(PlayButtonIcon, {})) : (_jsx(_Fragment, {})) }), !isPlaying && (_jsx(Fill, { style: [ + t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, + { + opacity: 0.2, + zIndex: 1, + }, + ] })), _jsx(View, { style: styles.gifBadgeContainer, children: _jsx(Text, { style: [{ color: 'white' }, a.font_bold, a.text_xs], children: _jsx(Trans, { children: "GIF" }) }) }), altText && _jsx(AltBadge, { text: altText })] })); +} +function AltBadge(_a) { + var text = _a.text; + var control = Prompt.usePromptControl(); + var _ = useLingui()._; + return (_jsxs(_Fragment, { children: [_jsx(TouchableOpacity, { testID: "altTextButton", accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Show alt text"], ["Show alt text"])))), accessibilityHint: "", hitSlop: HITSLOP_20, onPress: control.open, style: styles.altBadgeContainer, children: _jsx(Text, { style: [{ color: 'white' }, a.font_bold, a.text_xs], accessible: false, children: _jsx(Trans, { children: "ALT" }) }) }), _jsxs(Prompt.Outer, { control: control, children: [_jsxs(Prompt.Content, { children: [_jsx(Prompt.TitleText, { children: _jsx(Trans, { children: "Alt Text" }) }), _jsx(Prompt.DescriptionText, { selectable: true, children: text })] }), _jsx(Prompt.Actions, { children: _jsx(Prompt.Action, { onPress: function () { return control.close(); }, cta: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Close"], ["Close"])))), color: "secondary" }) })] })] })); +} +var styles = StyleSheet.create({ + gifBadgeContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 4, + paddingVertical: 3, + position: 'absolute', + left: 6, + bottom: 6, + zIndex: 2, + }, + altBadgeContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 4, + paddingVertical: 3, + position: 'absolute', + right: 6, + bottom: 6, + zIndex: 2, + }, +}); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.js new file mode 100644 index 0000000000..b68e400a2b --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.js @@ -0,0 +1,46 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg, plural } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +/** + * Absolutely positioned time indicator showing how many seconds are remaining + * Time is in seconds + */ +export function TimeIndicator(_a) { + var time = _a.time, style = _a.style; + var t = useTheme(); + var _ = useLingui()._; + if (isNaN(time)) { + return null; + } + var minutes = Math.floor(time / 60); + var seconds = String(time % 60).padStart(2, '0'); + return (_jsx(View, { pointerEvents: "none", accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Time remaining: ", ""], ["Time remaining: ", ""])), plural(Number(time) || 0, { + one: '# second', + other: '# seconds', + }))), accessibilityHint: "", style: [ + { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + borderRadius: 6, + paddingHorizontal: 6, + paddingVertical: 3, + left: 6, + bottom: 6, + minHeight: 21, + }, + a.absolute, + a.justify_center, + style, + ], children: _jsx(Text, { style: [ + { color: t.palette.white, fontSize: 12, fontVariant: ['tabular-nums'] }, + a.font_semi_bold, + { lineHeight: 1.25 }, + ], children: "".concat(minutes, ":").concat(seconds) }) })); +} +var templateObject_1; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.js new file mode 100644 index 0000000000..32688ce9eb --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.js @@ -0,0 +1,103 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useImperativeHandle, useRef, useState } from 'react'; +import { Pressable, View } from 'react-native'; +import { BlueskyVideoView } from '@haileyok/bluesky-video'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_30 } from '#/lib/constants'; +import { useAutoplayDisabled } from '#/state/preferences'; +import { atoms as a, useTheme } from '#/alf'; +import { useIsWithinMessage } from '#/components/dms/MessageContext'; +import { Mute_Stroke2_Corner0_Rounded as MuteIcon } from '#/components/icons/Mute'; +import { Pause_Filled_Corner0_Rounded as PauseIcon } from '#/components/icons/Pause'; +import { Play_Filled_Corner0_Rounded as PlayIcon } from '#/components/icons/Play'; +import { SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon } from '#/components/icons/Speaker'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { useVideoMuteState } from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'; +import { GifPresentationControls } from '../GifPresentationControls'; +import { TimeIndicator } from './TimeIndicator'; +export function VideoEmbedInnerNative(_a) { + var ref = _a.ref, embed = _a.embed, setStatus = _a.setStatus, setIsLoading = _a.setIsLoading, setIsActive = _a.setIsActive; + var _ = useLingui()._; + var videoRef = useRef(null); + var autoplayDisabled = useAutoplayDisabled(); + var isWithinMessage = useIsWithinMessage(); + var _b = useVideoMuteState(), muted = _b[0], setMuted = _b[1]; + var _c = useState(false), isPlaying = _c[0], setIsPlaying = _c[1]; + var _d = useState(0), timeRemaining = _d[0], setTimeRemaining = _d[1]; + var _e = useState(), error = _e[0], setError = _e[1]; + useImperativeHandle(ref, function () { return ({ + togglePlayback: function () { + var _a; + (_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.togglePlayback(); + }, + }); }); + if (error) { + throw new Error(error); + } + var isGif = embed.presentation === 'gif'; + return (_jsxs(View, { style: [a.flex_1, a.relative], children: [_jsx(BlueskyVideoView, { url: embed.playlist, autoplay: !autoplayDisabled && !isWithinMessage, beginMuted: isGif || (autoplayDisabled ? false : muted), style: [a.rounded_sm], onActiveChange: function (e) { + setIsActive(e.nativeEvent.isActive); + }, onLoadingChange: function (e) { + setIsLoading(e.nativeEvent.isLoading); + }, onMutedChange: function (e) { + if (!isGif) { + setMuted(e.nativeEvent.isMuted); + } + }, onStatusChange: function (e) { + setStatus(e.nativeEvent.status); + setIsPlaying(e.nativeEvent.status === 'playing'); + }, onTimeRemainingChange: function (e) { + setTimeRemaining(e.nativeEvent.timeRemaining); + }, onError: function (e) { + setError(e.nativeEvent.error); + }, ref: videoRef, accessibilityLabel: embed.alt ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Video: ", ""], ["Video: ", ""])), embed.alt)) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Video"], ["Video"])))), accessibilityHint: "" }), isGif ? (_jsx(GifPresentationControls, { onPress: function () { + var _a; + (_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.togglePlayback(); + }, isPlaying: isPlaying, isLoading: false, altText: embed.alt })) : (_jsx(VideoPresentationControls, { enterFullscreen: function () { + var _a; + (_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.enterFullscreen(true); + }, toggleMuted: function () { + var _a; + (_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.toggleMuted(); + }, togglePlayback: function () { + var _a; + (_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.togglePlayback(); + }, isPlaying: isPlaying, timeRemaining: timeRemaining })), _jsx(MediaInsetBorder, {})] })); +} +function VideoPresentationControls(_a) { + var enterFullscreen = _a.enterFullscreen, toggleMuted = _a.toggleMuted, togglePlayback = _a.togglePlayback, timeRemaining = _a.timeRemaining, isPlaying = _a.isPlaying; + var _ = useLingui()._; + var t = useTheme(); + var muted = useVideoMuteState()[0]; + // show countdown when: + // 1. timeRemaining is a number - was seeing NaNs + // 2. duration is greater than 0 - means metadata has loaded + // 3. we're less than 5 second into the video + var showTime = !isNaN(timeRemaining); + return (_jsxs(View, { style: [a.absolute, a.inset_0], children: [_jsx(Pressable, { onPress: enterFullscreen, style: a.flex_1, accessibilityLabel: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Video"], ["Video"])))), accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Enters full screen"], ["Enters full screen"])))), accessibilityRole: "button" }), _jsx(ControlButton, { onPress: togglePlayback, label: isPlaying ? _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Pause"], ["Pause"])))) : _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Play"], ["Play"])))), accessibilityHint: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Plays or pauses the video"], ["Plays or pauses the video"])))), style: { left: 6 }, children: isPlaying ? (_jsx(PauseIcon, { width: 13, fill: t.palette.white })) : (_jsx(PlayIcon, { width: 13, fill: t.palette.white })) }), showTime && _jsx(TimeIndicator, { time: timeRemaining, style: { left: 33 } }), _jsx(ControlButton, { onPress: toggleMuted, label: muted + ? _(msg({ message: "Unmute", context: 'video' })) + : _(msg({ message: "Mute", context: 'video' })), accessibilityHint: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Toggles the sound"], ["Toggles the sound"])))), style: { right: 6 }, children: muted ? (_jsx(MuteIcon, { width: 13, fill: t.palette.white })) : (_jsx(UnmuteIcon, { width: 13, fill: t.palette.white })) })] })); +} +function ControlButton(_a) { + var onPress = _a.onPress, children = _a.children, label = _a.label, accessibilityHint = _a.accessibilityHint, style = _a.style; + return (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + a.justify_center, + { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + paddingHorizontal: 4, + paddingVertical: 4, + bottom: 6, + minHeight: 21, + minWidth: 21, + }, + style, + ], children: _jsx(Pressable, { onPress: onPress, style: a.flex_1, accessibilityLabel: label, accessibilityHint: accessibilityHint, accessibilityRole: "button", hitSlop: HITSLOP_30, children: children }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.js new file mode 100644 index 0000000000..e25fe7b06a --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.js @@ -0,0 +1,3 @@ +export function VideoEmbedInnerNative() { + throw new Error('VideoEmbedInnerNative may not be used on web.'); +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.js new file mode 100644 index 0000000000..bebad5c6fb --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.js @@ -0,0 +1,242 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect, useId, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { atoms as a } from '#/alf'; +import * as BandwidthEstimate from './bandwidth-estimate'; +import { Controls } from './web-controls/VideoControls'; +export function VideoEmbedInnerWeb(_a) { + var embed = _a.embed, active = _a.active, setActive = _a.setActive, onScreen = _a.onScreen, lastKnownTime = _a.lastKnownTime; + var containerRef = useRef(null); + var videoRef = useRef(null); + var _b = useState(false), focused = _b[0], setFocused = _b[1]; + var _c = useState(false), hasSubtitleTrack = _c[0], setHasSubtitleTrack = _c[1]; + var _d = useState(false), hlsLoading = _d[0], setHlsLoading = _d[1]; + var figId = useId(); + var _ = useLingui()._; + // send error up to error boundary + var _e = useState(null), error = _e[0], setError = _e[1]; + if (error) { + throw error; + } + var _f = useHLS({ + playlist: embed.playlist, + setHasSubtitleTrack: setHasSubtitleTrack, + setError: setError, + videoRef: videoRef, + setHlsLoading: setHlsLoading, + }), hlsRef = _f.hlsRef, loop = _f.loop; + useEffect(function () { + if (lastKnownTime.current && videoRef.current) { + videoRef.current.currentTime = lastKnownTime.current; + } + }, [lastKnownTime]); + return (_jsx(View, { style: [a.flex_1, a.rounded_md, a.overflow_hidden], accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Embedded video player"], ["Embedded video player"])))), accessibilityHint: "", children: _jsxs("div", { ref: containerRef, style: { height: '100%', width: '100%' }, children: [_jsxs("figure", { style: { margin: 0, position: 'absolute', inset: 0 }, children: [_jsx("video", { ref: videoRef, poster: embed.thumbnail, style: { width: '100%', height: '100%', objectFit: 'contain' }, playsInline: true, preload: "none", muted: embed.presentation === 'gif' || !focused, "aria-labelledby": embed.alt ? figId : undefined, onTimeUpdate: function (e) { + lastKnownTime.current = e.currentTarget.currentTime; + }, loop: loop }), embed.alt && (_jsx("figcaption", { id: figId, style: a.sr_only, children: embed.alt }))] }), _jsx(Controls, { videoRef: videoRef, hlsRef: hlsRef, active: active, setActive: setActive, focused: focused, setFocused: setFocused, hlsLoading: hlsLoading, onScreen: onScreen, fullscreenRef: containerRef, hasSubtitleTrack: hasSubtitleTrack, isGif: embed.presentation === 'gif', altText: embed.alt })] }) })); +} +var HLSUnsupportedError = /** @class */ (function (_super) { + __extends(HLSUnsupportedError, _super); + function HLSUnsupportedError() { + return _super.call(this, 'HLS is not supported') || this; + } + return HLSUnsupportedError; +}(Error)); +export { HLSUnsupportedError }; +var VideoNotFoundError = /** @class */ (function (_super) { + __extends(VideoNotFoundError, _super); + function VideoNotFoundError() { + return _super.call(this, 'Video not found') || this; + } + return VideoNotFoundError; +}(Error)); +export { VideoNotFoundError }; +var promiseForHls = import( +// @ts-ignore +'hls.js/dist/hls.min').then(function (mod) { return mod.default; }); +promiseForHls.value = undefined; +promiseForHls.then(function (Hls) { + promiseForHls.value = Hls; +}); +function useHLS(_a) { + var playlist = _a.playlist, setHasSubtitleTrack = _a.setHasSubtitleTrack, setError = _a.setError, videoRef = _a.videoRef, setHlsLoading = _a.setHlsLoading; + var _b = useState(function () { return promiseForHls.value; }), Hls = _b[0], setHls = _b[1]; + useEffect(function () { + if (!Hls) { + setHlsLoading(true); + promiseForHls.then(function (loadedHls) { + setHls(function () { return loadedHls; }); + setHlsLoading(false); + }); + } + }, [Hls, setHlsLoading]); + var hlsRef = useRef(undefined); + var _c = useState([]), lowQualityFragments = _c[0], setLowQualityFragments = _c[1]; + // purge low quality segments from buffer on next frag change + var handleFragChange = useNonReactiveCallback(function (_event, _a) { + var frag = _a.frag; + if (!Hls) + return; + if (!hlsRef.current) + return; + var hls = hlsRef.current; + // if the current quality level goes above 0, flush the low quality segments + if (hls.nextAutoLevel > 0) { + var flushed_1 = []; + for (var _i = 0, lowQualityFragments_1 = lowQualityFragments; _i < lowQualityFragments_1.length; _i++) { + var lowQualFrag = lowQualityFragments_1[_i]; + // avoid if close to the current fragment + if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { + continue; + } + hls.trigger(Hls.Events.BUFFER_FLUSHING, { + startOffset: lowQualFrag.start, + endOffset: lowQualFrag.end, + type: 'video', + }); + flushed_1.push(lowQualFrag); + } + setLowQualityFragments(function (prev) { return prev.filter(function (f) { return !flushed_1.includes(f); }); }); + } + }); + useEffect(function () { + if (!videoRef.current) + return; + if (!Hls) + return; + if (!Hls.isSupported()) { + throw new HLSUnsupportedError(); + } + var latestEstimate = BandwidthEstimate.get(); + var hls = new Hls({ + maxMaxBufferLength: 10, // only load 10s ahead + // note: the amount buffered is affected by both maxBufferLength and maxBufferSize + // it will buffer until it is greater than *both* of those values + // so we use maxMaxBufferLength to set the actual maximum amount of buffering instead + startLevel: latestEstimate === undefined ? -1 : Hls.DefaultConfig.startLevel, + // the '-1' value makes a test request to estimate bandwidth and quality level + // before showing the first fragment + }); + hlsRef.current = hls; + if (latestEstimate !== undefined) { + hls.bandwidthEstimate = latestEstimate; + } + hls.attachMedia(videoRef.current); + hls.loadSource(playlist); + hls.on(Hls.Events.FRAG_LOADED, function () { + BandwidthEstimate.set(hls.bandwidthEstimate); + }); + hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, function (_event, data) { + if (data.subtitleTracks.length > 0) { + setHasSubtitleTrack(true); + } + }); + hls.on(Hls.Events.FRAG_BUFFERED, function (_event, _a) { + var frag = _a.frag; + if (frag.level === 0) { + setLowQualityFragments(function (prev) { return __spreadArray(__spreadArray([], prev, true), [frag], false); }); + } + }); + hls.on(Hls.Events.ERROR, function (_event, data) { + var _a; + if (data.fatal) { + if (data.details === 'manifestLoadError' && + ((_a = data.response) === null || _a === void 0 ? void 0 : _a.code) === 404) { + setError(new VideoNotFoundError()); + } + else { + setError(data.error); + } + } + else { + console.error(data.error); + } + }); + hls.on(Hls.Events.FRAG_CHANGED, handleFragChange); + return function () { + hlsRef.current = undefined; + hls.detachMedia(); + hls.destroy(); + }; + }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange, Hls]); + var flushOnLoop = useNonReactiveCallback(function () { + if (!Hls) + return; + if (!hlsRef.current) + return; + var hls = hlsRef.current; + // `handleFragChange` will catch most stale frags, but there's a corner case - + // if there's only one segment in the video, it won't get flushed because it avoids + // flushing the currently active segment. Therefore, we have to catch it when we loop + if (hls.nextAutoLevel > 0 && + lowQualityFragments.length === 1 && + lowQualityFragments[0].start === 0) { + var lowQualFrag = lowQualityFragments[0]; + hls.trigger(Hls.Events.BUFFER_FLUSHING, { + startOffset: lowQualFrag.start, + endOffset: lowQualFrag.end, + type: 'video', + }); + setLowQualityFragments([]); + } + }); + // manually loop, so if we've flushed the first buffer it doesn't get confused + var hasLowQualityFragmentAtStart = lowQualityFragments.some(function (frag) { return frag.start === 0; }); + useEffect(function () { + if (!videoRef.current) + return; + // use `loop` prop on `

").concat(escapeHtml(record.text)).concat(record.embed + ? "

[image or embed]") + : '', "

— ").concat(escapeHtml(postAuthor.displayName || postAuthor.handle), " (@").concat(escapeHtml(postAuthor.handle), ") ").concat(escapeHtml(niceDate(i18n, timestamp)), "
"); + }, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode]); + return (_jsxs(Dialog.Inner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Embed post"], ["Embed post"])))), style: [{ maxWidth: 500 }], children: [_jsxs(View, { style: [a.gap_lg], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "Embed post" }) }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium, a.leading_normal], children: _jsx(Trans, { children: "Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website." }) })] }), _jsxs(View, { style: [ + a.border, + t.atoms.border_contrast_low, + a.rounded_sm, + a.overflow_hidden, + ], children: [_jsxs(Button, { label: showCustomisation + ? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Hide customization options"], ["Hide customization options"])))) + : _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Show customization options"], ["Show customization options"])))), color: "secondary", variant: "ghost", size: "small", shape: "default", onPress: function () { return setShowCustomisation(function (c) { return !c; }); }, style: [ + a.justify_start, + showCustomisation && t.atoms.bg_contrast_25, + ], children: [_jsx(ButtonIcon, { icon: showCustomisation ? ChevronBottomIcon : ChevronRightIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Customization options" }) })] }), showCustomisation && (_jsxs(View, { style: [a.gap_sm, a.p_md], children: [_jsx(Text, { style: [t.atoms.text_contrast_medium, a.font_semi_bold], children: _jsx(Trans, { children: "Color theme" }) }), _jsxs(SegmentedControl.Root, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Color mode"], ["Color mode"])))), type: "radio", value: colorMode, onChange: setColorMode, children: [_jsx(SegmentedControl.Item, { value: "system", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["System"], ["System"])))), children: _jsx(SegmentedControl.ItemText, { children: _jsx(Trans, { children: "System" }) }) }), _jsx(SegmentedControl.Item, { value: "light", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Light"], ["Light"])))), children: _jsx(SegmentedControl.ItemText, { children: _jsx(Trans, { children: "Light" }) }) }), _jsx(SegmentedControl.Item, { value: "dark", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Dark"], ["Dark"])))), children: _jsx(SegmentedControl.ItemText, { children: _jsx(Trans, { children: "Dark" }) }) })] })] }))] }), _jsxs(View, { style: [a.flex_row, a.gap_sm], children: [_jsx(View, { style: [a.flex_1], children: _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: CodeBracketsIcon }), _jsx(TextField.Input, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Embed HTML code"], ["Embed HTML code"])))), editable: false, selection: { start: 0, end: snippet.length }, value: snippet })] }) }), _jsx(Button, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Copy code"], ["Copy code"])))), color: "primary", variant: "solid", size: "large", onPress: function () { + navigator.clipboard.writeText(snippet); + setCopied(true); + }, children: copied ? (_jsxs(_Fragment, { children: [_jsx(ButtonIcon, { icon: CheckIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Copied!" }) })] })) : (_jsx(ButtonText, { children: _jsx(Trans, { children: "Copy code" }) })) })] })] }), _jsx(Dialog.Close, {})] })); +} +/** + * Based on a snippet of code from React, which itself was based on the escape-html library. + * Copyright (c) Meta Platforms, Inc. and affiliates + * Copyright (c) 2012-2013 TJ Holowaychuk + * Copyright (c) 2015 Andreas Lubbe + * Copyright (c) 2015 Tiancheng "Timothy" Gu + * Licensed as MIT. + */ +var matchHtmlRegExp = /["'&<>]/; +function escapeHtml(string) { + var str = String(string); + var match = matchHtmlRegExp.exec(str); + if (!match) { + return str; + } + var escape; + var html = ''; + var index; + var lastIndex = 0; + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escape = '"'; + break; + case 38: // & + escape = '&'; + break; + case 39: // ' + escape = '''; + break; + case 60: // < + escape = '<'; + break; + case 62: // > + escape = '>'; + break; + default: + continue; + } + if (lastIndex !== index) { + html += str.slice(lastIndex, index); + } + lastIndex = index + 1; + html += escape; + } + return lastIndex !== index ? html + str.slice(lastIndex, index) : html; +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/components/dialogs/EmbedConsent.js b/src/components/dialogs/EmbedConsent.js new file mode 100644 index 0000000000..b5df512f26 --- /dev/null +++ b/src/components/dialogs/EmbedConsent.js @@ -0,0 +1,41 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { embedPlayerSources, externalEmbedLabels, } from '#/lib/strings/embed-player'; +import { useSetExternalEmbedPref } from '#/state/preferences'; +import { atoms as a, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Text } from '#/components/Typography'; +export function EmbedConsentDialog(_a) { + var control = _a.control, source = _a.source, onAccept = _a.onAccept; + var _ = useLingui()._; + var setExternalEmbedPref = useSetExternalEmbedPref(); + var onShowAllPress = useCallback(function () { + for (var _i = 0, embedPlayerSources_1 = embedPlayerSources; _i < embedPlayerSources_1.length; _i++) { + var key = embedPlayerSources_1[_i]; + setExternalEmbedPref(key, 'show'); + } + onAccept(); + control.close(); + }, [control, onAccept, setExternalEmbedPref]); + var onShowPress = useCallback(function () { + setExternalEmbedPref(source, 'show'); + onAccept(); + control.close(); + }, [control, onAccept, setExternalEmbedPref, source]); + var onHidePress = useCallback(function () { + setExternalEmbedPref(source, 'hide'); + control.close(); + }, [control, setExternalEmbedPref, source]); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["External Media"], ["External Media"])))), style: web({ maxWidth: 400 }), children: [_jsxs(View, { style: a.gap_sm, children: [_jsx(Text, { style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "External Media" }) }), _jsxs(View, { style: [a.mt_sm, a.mb_2xl, a.gap_lg], children: [_jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsxs(Trans, { children: ["This content is hosted by ", externalEmbedLabels[source], ". Do you want to enable external media?"] }) }), _jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." }) })] })] }), _jsxs(View, { style: a.gap_md, children: [_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Enable external media"], ["Enable external media"])))), onPress: onShowAllPress, onAccessibilityEscape: control.close, color: "primary", size: "large", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Enable external media" }) }) }), _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Enable this source only"], ["Enable this source only"])))), onPress: onShowPress, onAccessibilityEscape: control.close, color: "secondary", size: "large", children: _jsx(ButtonText, { children: _jsxs(Trans, { children: ["Enable ", externalEmbedLabels[source], " only"] }) }) }), _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["No thanks"], ["No thanks"])))), onAccessibilityEscape: control.close, onPress: onHidePress, color: "secondary", size: "large", variant: "ghost", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "No thanks" }) }) })] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/dialogs/GifSelect.js b/src/components/dialogs/GifSelect.js new file mode 100644 index 0000000000..2ed49d0746 --- /dev/null +++ b/src/components/dialogs/GifSelect.js @@ -0,0 +1,143 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useImperativeHandle, useMemo, useRef, useState, } from 'react'; +import { View } from 'react-native'; +import { useWindowDimensions } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { cleanError } from '#/lib/strings/errors'; +import { tenorUrlToBskyGifUrl, useFeaturedGifsQuery, useGifSearchQuery, } from '#/state/queries/tenor'; +import { ErrorScreen } from '#/view/com/util/error/ErrorScreen'; +import { ErrorBoundary } from '#/view/com/util/ErrorBoundary'; +import { atoms as a, ios, native, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextField from '#/components/forms/TextField'; +import { useThrottledValue } from '#/components/hooks/useThrottledValue'; +import { ArrowLeft_Stroke2_Corner0_Rounded as Arrow } from '#/components/icons/Arrow'; +import { MagnifyingGlass_Stroke2_Corner0_Rounded as Search } from '#/components/icons/MagnifyingGlass'; +import { ListFooter, ListMaybePlaceholder } from '#/components/Lists'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +export function GifSelectDialog(_a) { + var controlRef = _a.controlRef, onClose = _a.onClose, onSelectGifProp = _a.onSelectGif; + var control = Dialog.useDialogControl(); + useImperativeHandle(controlRef, function () { return ({ + open: function () { return control.open(); }, + }); }); + var onSelectGif = useCallback(function (gif) { + control.close(function () { return onSelectGifProp(gif); }); + }, [control, onSelectGifProp]); + var renderErrorBoundary = useCallback(function (error) { return _jsx(DialogError, { details: String(error) }); }, []); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, nativeOptions: __assign({ bottomInset: 0 }, ios({ cornerRadius: undefined })), children: [_jsx(Dialog.Handle, {}), _jsx(ErrorBoundary, { renderError: renderErrorBoundary, children: _jsx(GifList, { control: control, onSelectGif: onSelectGif }) })] })); +} +function GifList(_a) { + var control = _a.control, onSelectGif = _a.onSelectGif; + var _ = useLingui()._; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var textInputRef = useRef(null); + var listRef = useRef(null); + var _b = useState(''), undeferredSearch = _b[0], setSearch = _b[1]; + var search = useThrottledValue(undeferredSearch, 500); + var height = useWindowDimensions().height; + var isSearching = search.length > 0; + var trendingQuery = useFeaturedGifsQuery(); + var searchQuery = useGifSearchQuery(search); + var _c = isSearching ? searchQuery : trendingQuery, data = _c.data, fetchNextPage = _c.fetchNextPage, isFetchingNextPage = _c.isFetchingNextPage, hasNextPage = _c.hasNextPage, error = _c.error, isPending = _c.isPending, isError = _c.isError, refetch = _c.refetch; + var flattenedData = useMemo(function () { + return (data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { return page.results; })) || []; + }, [data]); + var renderItem = useCallback(function (_a) { + var item = _a.item; + return _jsx(GifPreview, { gif: item, onSelectGif: onSelectGif }); + }, [onSelectGif]); + var onEndReached = useCallback(function () { + if (isFetchingNextPage || !hasNextPage || error) + return; + fetchNextPage(); + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]); + var hasData = flattenedData.length > 0; + var onGoBack = useCallback(function () { + var _a; + if (isSearching) { + // clear the input and reset the state + (_a = textInputRef.current) === null || _a === void 0 ? void 0 : _a.clear(); + setSearch(''); + } + else { + control.close(); + } + }, [control, isSearching]); + var listHeader = useMemo(function () { + return (_jsxs(View, { style: [ + native(a.pt_4xl), + a.relative, + a.mb_lg, + a.flex_row, + a.align_center, + !gtMobile && web(a.gap_md), + a.pb_sm, + t.atoms.bg, + ], children: [!gtMobile && IS_WEB && (_jsx(Button, { size: "small", variant: "ghost", color: "secondary", shape: "round", onPress: function () { return control.close(); }, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close GIF dialog"], ["Close GIF dialog"])))), children: _jsx(ButtonIcon, { icon: Arrow, size: "md" }) })), _jsxs(TextField.Root, { style: [!gtMobile && IS_WEB && a.flex_1], children: [_jsx(TextField.Icon, { icon: Search }), _jsx(TextField.Input, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Search GIFs"], ["Search GIFs"])))), placeholder: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Search Tenor"], ["Search Tenor"])))), onChangeText: function (text) { + var _a; + setSearch(text); + (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ offset: 0, animated: false }); + }, returnKeyType: "search", clearButtonMode: "while-editing", inputRef: textInputRef, maxLength: 50, onKeyPress: function (_a) { + var nativeEvent = _a.nativeEvent; + if (nativeEvent.key === 'Escape') { + control.close(); + } + } })] })] })); + }, [gtMobile, t.atoms.bg, _, control]); + return (_jsxs(_Fragment, { children: [gtMobile && _jsx(Dialog.Close, {}), _jsx(Dialog.InnerFlatList, { ref: listRef, data: flattenedData, renderItem: renderItem, numColumns: gtMobile ? 3 : 2, columnWrapperStyle: [a.gap_sm], contentContainerStyle: [native([a.px_xl, { minHeight: height }])], webInnerStyle: [web({ minHeight: '80vh' })], webInnerContentContainerStyle: [web(a.pb_0)], ListHeaderComponent: _jsxs(_Fragment, { children: [listHeader, !hasData && (_jsx(ListMaybePlaceholder, { isLoading: isPending, isError: isError, onRetry: refetch, onGoBack: onGoBack, emptyType: "results", sideBorders: false, topBorder: false, errorTitle: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Failed to load GIFs"], ["Failed to load GIFs"])))), errorMessage: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["There was an issue connecting to Tenor."], ["There was an issue connecting to Tenor."])))), emptyMessage: isSearching + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["No search results found for \"", "\"."], ["No search results found for \"", "\"."])), search)) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["No featured GIFs found. There may be an issue with Tenor."], ["No featured GIFs found. There may be an issue with Tenor."])))) }))] }), stickyHeaderIndices: [0], onEndReached: onEndReached, onEndReachedThreshold: 4, keyExtractor: function (item) { return item.id; }, keyboardDismissMode: "on-drag", ListFooterComponent: hasData ? (_jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage, style: { borderTopWidth: 0 } })) : null }, gtMobile ? '3 cols' : '2 cols')] })); +} +function DialogError(_a) { + var details = _a.details; + var _ = useLingui()._; + var control = Dialog.useDialogContext(); + return (_jsxs(Dialog.ScrollableInner, { style: a.gap_md, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["An error has occurred"], ["An error has occurred"])))), children: [_jsx(Dialog.Close, {}), _jsx(ErrorScreen, { title: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Oh no!"], ["Oh no!"])))), message: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["There was an unexpected issue in the application. Please let us know if this happened to you!"], ["There was an unexpected issue in the application. Please let us know if this happened to you!"])))), details: details }), _jsx(Button, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Close dialog"], ["Close dialog"])))), onPress: function () { return control.close(); }, color: "primary", size: "large", variant: "solid", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })] })); +} +export function GifPreview(_a) { + var gif = _a.gif, onSelectGif = _a.onSelectGif; + var ax = useAnalytics(); + var gtTablet = useBreakpoints().gtTablet; + var _ = useLingui()._; + var t = useTheme(); + var onPress = useCallback(function () { + ax.metric('composer:gif:select', {}); + onSelectGif(gif); + }, [ax, onSelectGif, gif]); + return (_jsx(Button, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Select GIF \"", "\""], ["Select GIF \"", "\""])), gif.title)), style: [a.flex_1, gtTablet ? { maxWidth: '33%' } : { maxWidth: '50%' }], onPress: onPress, children: function (_a) { + var pressed = _a.pressed; + return (_jsx(Image, { style: [ + a.flex_1, + a.mb_sm, + a.rounded_sm, + a.aspect_square, + { opacity: pressed ? 0.8 : 1 }, + t.atoms.bg_contrast_25, + ], source: { + uri: tenorUrlToBskyGifUrl(gif.media_formats.tinygif.url), + }, contentFit: "cover", accessibilityLabel: gif.title, accessibilityHint: "", cachePolicy: "none", accessibilityIgnoresInvertColors: true })); + } })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12; diff --git a/src/components/dialogs/InAppBrowserConsent.js b/src/components/dialogs/InAppBrowserConsent.js new file mode 100644 index 0000000000..8cb84d5266 --- /dev/null +++ b/src/components/dialogs/InAppBrowserConsent.js @@ -0,0 +1,54 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useOpenLink } from '#/lib/hooks/useOpenLink'; +import { useSetInAppBrowser } from '#/state/preferences/in-app-browser'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { SquareArrowTopRight_Stroke2_Corner0_Rounded as External } from '#/components/icons/SquareArrowTopRight'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +import { useGlobalDialogsControlContext } from './Context'; +export function InAppBrowserConsentDialog() { + var inAppBrowserConsentControl = useGlobalDialogsControlContext().inAppBrowserConsentControl; + if (IS_WEB) + return null; + return (_jsxs(Dialog.Outer, { control: inAppBrowserConsentControl.control, nativeOptions: { preventExpansion: true }, onClose: inAppBrowserConsentControl.clear, children: [_jsx(Dialog.Handle, {}), _jsx(InAppBrowserConsentInner, { href: inAppBrowserConsentControl.value })] })); +} +function InAppBrowserConsentInner(_a) { + var href = _a.href; + var control = Dialog.useDialogContext(); + var _ = useLingui()._; + var t = useTheme(); + var setInAppBrowser = useSetInAppBrowser(); + var openLink = useOpenLink(); + var onUseIAB = useCallback(function () { + control.close(function () { + setInAppBrowser(true); + if (href) { + openLink(href, true); + } + }); + }, [control, setInAppBrowser, href, openLink]); + var onUseLinking = useCallback(function () { + control.close(function () { + setInAppBrowser(false); + if (href) { + openLink(href, false); + } + }); + }, [control, setInAppBrowser, href, openLink]); + var onCancel = useCallback(function () { + control.close(); + }, [control]); + return (_jsx(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["How should we open this link?"], ["How should we open this link?"])))), children: _jsxs(View, { style: [a.gap_2xl], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_bold, a.text_2xl], children: _jsx(Trans, { children: "How should we open this link?" }) }), _jsx(Text, { style: [t.atoms.text_contrast_high, a.leading_snug, a.text_md], children: _jsx(Trans, { children: "Your choice will be remembered for future links. You can change it at any time in settings." }) })] }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Use in-app browser"], ["Use in-app browser"])))), onPress: onUseIAB, size: "large", variant: "solid", color: "primary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Use in-app browser" }) }) }), _jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Use my default browser"], ["Use my default browser"])))), onPress: onUseLinking, size: "large", variant: "solid", color: "secondary", children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Use my default browser" }) }), _jsx(ButtonIcon, { position: "right", icon: External })] }), _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: onCancel, size: "large", variant: "ghost", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/dialogs/LanguageSelectDialog.js b/src/components/dialogs/LanguageSelectDialog.js new file mode 100644 index 0000000000..a41ec5d5db --- /dev/null +++ b/src/components/dialogs/LanguageSelectDialog.js @@ -0,0 +1,178 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useMemo, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { LANGUAGES, LANGUAGES_MAP_CODE2 } from '#/locale/languages'; +import { useLanguagePrefs } from '#/state/preferences/languages'; +import { ErrorScreen } from '#/view/com/util/error/ErrorScreen'; +import { ErrorBoundary } from '#/view/com/util/ErrorBoundary'; +import { atoms as a, tokens, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { SearchInput } from '#/components/forms/SearchInput'; +import * as Toggle from '#/components/forms/Toggle'; +import { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import { Text } from '#/components/Typography'; +import { IS_LIQUID_GLASS, IS_NATIVE, IS_WEB } from '#/env'; +export function LanguageSelectDialog(_a) { + var titleText = _a.titleText, subtitleText = _a.subtitleText, control = _a.control, + /** + * Optionally can be passed to show different values than what is saved in + * langPrefs. + */ + currentLanguages = _a.currentLanguages, onSelectLanguages = _a.onSelectLanguages, maxLanguages = _a.maxLanguages; + var height = useWindowDimensions().height; + var insets = useSafeAreaInsets(); + var renderErrorBoundary = useCallback(function (error) { return _jsx(DialogError, { details: String(error) }); }, []); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { + minHeight: IS_LIQUID_GLASS ? height : height - insets.top, + }, children: [_jsx(Dialog.Handle, {}), _jsx(ErrorBoundary, { renderError: renderErrorBoundary, children: _jsx(DialogInner, { titleText: titleText, subtitleText: subtitleText, currentLanguages: currentLanguages, onSelectLanguages: onSelectLanguages, maxLanguages: maxLanguages }) })] })); +} +export function DialogInner(_a) { + var titleText = _a.titleText, subtitleText = _a.subtitleText, currentLanguages = _a.currentLanguages, onSelectLanguages = _a.onSelectLanguages, maxLanguages = _a.maxLanguages; + var control = Dialog.useDialogContext(); + var _b = useState(0), headerHeight = _b[0], setHeaderHeight = _b[1]; + var _c = useState(0), footerHeight = _c[0], setFooterHeight = _c[1]; + var allowedLanguages = useMemo(function () { + var uniqueLanguagesMap = LANGUAGES.filter(function (lang) { return !!lang.code2; }).reduce(function (acc, lang) { + acc[lang.code2] = lang; + return acc; + }, {}); + return Object.values(uniqueLanguagesMap); + }, []); + var langPrefs = useLanguagePrefs(); + var _d = useState(currentLanguages || [langPrefs.primaryLanguage]), checkedLanguagesCode2 = _d[0], setCheckedLanguagesCode2 = _d[1]; + var _e = useState(''), search = _e[0], setSearch = _e[1]; + var t = useTheme(); + var _ = useLingui()._; + var handleClose = function () { + control.close(function () { + onSelectLanguages === null || onSelectLanguages === void 0 ? void 0 : onSelectLanguages(checkedLanguagesCode2); + }); + }; + // NOTE(@elijaharita): Displayed languages are split into 3 lists for + // ordering. + var displayedLanguages = useMemo(function () { + function mapCode2List(code2List) { + return code2List.map(function (code2) { return LANGUAGES_MAP_CODE2[code2]; }).filter(Boolean); + } + // NOTE(@elijaharita): Get recent language codes and map them to language + // objects. Both the user account's saved language history and the current + // checked languages are displayed here. + var recentLanguagesCode2 = Array.from(new Set(__spreadArray(__spreadArray([], checkedLanguagesCode2, true), langPrefs.postLanguageHistory, true))).slice(0, 5) || []; + var recentLanguages = mapCode2List(recentLanguagesCode2); + // NOTE(@elijaharita): helper functions + var matchesSearch = function (lang) { + return lang.name.toLowerCase().includes(search.toLowerCase()); + }; + var isChecked = function (lang) { + return checkedLanguagesCode2.includes(lang.code2); + }; + var isInRecents = function (lang) { + return recentLanguagesCode2.includes(lang.code2); + }; + var checkedRecent = recentLanguages.filter(isChecked); + if (search) { + // NOTE(@elijaharita): if a search is active, we ALWAYS show checked + // items, as well as any items that match the search. + var uncheckedRecent = recentLanguages + .filter(function (lang) { return !isChecked(lang); }) + .filter(matchesSearch); + var unchecked = allowedLanguages.filter(function (lang) { return !isChecked(lang); }); + var all = unchecked + .filter(matchesSearch) + .filter(function (lang) { return !isInRecents(lang); }); + return { + all: all, + checkedRecent: checkedRecent, + uncheckedRecent: uncheckedRecent, + }; + } + else { + // NOTE(@elijaharita): if no search is active, we show everything. + var uncheckedRecent = recentLanguages.filter(function (lang) { return !isChecked(lang); }); + var all = allowedLanguages + .filter(function (lang) { return !recentLanguagesCode2.includes(lang.code2); }) + .filter(function (lang) { return !isInRecents(lang); }); + return { + all: all, + checkedRecent: checkedRecent, + uncheckedRecent: uncheckedRecent, + }; + } + }, [ + allowedLanguages, + search, + langPrefs.postLanguageHistory, + checkedLanguagesCode2, + ]); + var listHeader = (_jsxs(View, { style: [a.pb_xs, t.atoms.bg, IS_NATIVE && a.pt_2xl], onLayout: function (evt) { return setHeaderHeight(evt.nativeEvent.layout.height); }, children: [_jsxs(View, { style: [a.flex_row, a.w_full, a.justify_between], children: [_jsxs(View, { children: [_jsx(Text, { nativeID: "dialog-title", style: [ + t.atoms.text, + a.text_left, + a.font_semi_bold, + a.text_xl, + a.mb_sm, + ], children: titleText !== null && titleText !== void 0 ? titleText : _jsx(Trans, { children: "Choose languages" }) }), subtitleText && (_jsx(Text, { nativeID: "dialog-description", style: [ + t.atoms.text_contrast_medium, + a.text_left, + a.text_md, + a.mb_lg, + ], children: subtitleText }))] }), IS_WEB && (_jsx(Button, { variant: "ghost", size: "small", color: "secondary", shape: "round", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close dialog"], ["Close dialog"])))), onPress: handleClose, children: _jsx(ButtonIcon, { icon: XIcon }) }))] }), _jsx(View, { style: [a.w_full, a.flex_row, a.align_stretch, a.gap_xs, a.pb_0], children: _jsx(SearchInput, { value: search, onChangeText: setSearch, placeholder: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Search languages"], ["Search languages"])))), label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Search languages"], ["Search languages"])))), maxLength: 50, onClearText: function () { return setSearch(''); } }) })] })); + var isCheckedRecentEmpty = displayedLanguages.checkedRecent.length > 0 || + displayedLanguages.uncheckedRecent.length > 0; + var isDisplayedLanguagesEmpty = displayedLanguages.all.length === 0; + var flatListData = __spreadArray(__spreadArray(__spreadArray(__spreadArray(__spreadArray([], (isCheckedRecentEmpty + ? [{ type: 'header', label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Recently used"], ["Recently used"])))) }] + : []), true), displayedLanguages.checkedRecent.map(function (lang) { return ({ type: 'item', lang: lang }); }), true), displayedLanguages.uncheckedRecent.map(function (lang) { return ({ type: 'item', lang: lang }); }), true), (isDisplayedLanguagesEmpty + ? [] + : [{ type: 'header', label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["All languages"], ["All languages"])))) }]), true), displayedLanguages.all.map(function (lang) { return ({ type: 'item', lang: lang }); }), true); + var numItems = flatListData.length; + return (_jsx(Toggle.Group, { values: checkedLanguagesCode2, onChange: setCheckedLanguagesCode2, type: "checkbox", maxSelections: maxLanguages, label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Select languages"], ["Select languages"])))), style: web([a.contents]), children: _jsx(Dialog.InnerFlatList, { data: flatListData, ListHeaderComponent: listHeader, stickyHeaderIndices: [0], contentContainerStyle: [ + a.gap_0, + IS_NATIVE && { paddingBottom: footerHeight + tokens.space.xl }, + ], style: [IS_NATIVE && a.px_lg, IS_WEB && { paddingBottom: 120 }], scrollIndicatorInsets: { top: headerHeight, bottom: footerHeight }, renderItem: function (_a) { + var item = _a.item, index = _a.index; + if (item.type === 'header') { + return (_jsx(Text, { style: [ + a.px_0, + a.py_md, + a.font_semi_bold, + a.text_xs, + t.atoms.text_contrast_low, + a.pt_3xl, + ], children: item.label }, index)); + } + var lang = item.lang; + var isLastItem = index === numItems - 1; + return (_jsxs(Toggle.Item, { name: lang.code2, label: lang.name, style: [ + t.atoms.border_contrast_low, + !isLastItem && a.border_b, + a.rounded_0, + a.px_0, + a.py_md, + ], children: [_jsx(Toggle.LabelText, { style: [a.flex_1], children: lang.name }), _jsx(Toggle.Checkbox, {})] }, lang.code2)); + }, footer: _jsx(Dialog.FlatListFooter, { onLayout: function (evt) { return setFooterHeight(evt.nativeEvent.layout.height); }, children: _jsx(Button, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Close dialog"], ["Close dialog"])))), onPress: handleClose, color: "primary", size: "large", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Done" }) }) }) }) }) })); +} +function DialogError(_a) { + var details = _a.details; + var _ = useLingui()._; + var control = Dialog.useDialogContext(); + return (_jsxs(Dialog.ScrollableInner, { style: a.gap_md, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["An error has occurred"], ["An error has occurred"])))), children: [_jsx(Dialog.Close, {}), _jsx(ErrorScreen, { title: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Oh no!"], ["Oh no!"])))), message: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["There was an unexpected issue in the application. Please let us know if this happened to you!"], ["There was an unexpected issue in the application. Please let us know if this happened to you!"])))), details: details }), _jsx(Button, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Close dialog"], ["Close dialog"])))), onPress: function () { return control.close(); }, color: "primary", size: "large", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11; diff --git a/src/components/dialogs/LinkWarning.js b/src/components/dialogs/LinkWarning.js new file mode 100644 index 0000000000..74831e367c --- /dev/null +++ b/src/components/dialogs/LinkWarning.js @@ -0,0 +1,85 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useMemo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useOpenLink } from '#/lib/hooks/useOpenLink'; +import { shareUrl } from '#/lib/sharing'; +import { isPossiblyAUrl, splitApexDomain } from '#/lib/strings/url-helpers'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Text } from '#/components/Typography'; +import { useGlobalDialogsControlContext } from './Context'; +export function LinkWarningDialog() { + var linkWarningDialogControl = useGlobalDialogsControlContext().linkWarningDialogControl; + return (_jsxs(Dialog.Outer, { control: linkWarningDialogControl.control, nativeOptions: { preventExpansion: true }, webOptions: { alignCenter: true }, onClose: linkWarningDialogControl.clear, children: [_jsx(Dialog.Handle, {}), _jsx(LinkWarningDialogInner, { link: linkWarningDialogControl.value })] })); +} +export function CustomLinkWarningDialog(_a) { + var control = _a.control, link = _a.link; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(LinkWarningDialogInner, { link: link })] })); +} +function LinkWarningDialogInner(_a) { + var _b; + var link = _a.link; + var control = Dialog.useDialogContext(); + var _ = useLingui()._; + var t = useTheme(); + var openLink = useOpenLink(); + var gtMobile = useBreakpoints().gtMobile; + var potentiallyMisleading = useMemo(function () { return link && isPossiblyAUrl(link.displayText); }, [link]); + var onPressVisit = useCallback(function () { + control.close(function () { + if (!link) + return; + if (link.share) { + shareUrl(link.href); + } + else { + openLink(link.href, undefined, true); + } + }); + }, [control, link, openLink]); + var onCancel = useCallback(function () { + control.close(); + }, [control]); + return (_jsxs(Dialog.ScrollableInner, { style: web({ maxWidth: 450 }), label: potentiallyMisleading + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Potentially misleading link warning"], ["Potentially misleading link warning"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Leaving Bluesky"], ["Leaving Bluesky"])))), children: [_jsxs(View, { style: [a.gap_2xl], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_bold, a.text_2xl], children: potentiallyMisleading ? (_jsx(Trans, { children: "Potentially misleading link" })) : (_jsx(Trans, { children: "Leaving Bluesky" })) }), _jsx(Text, { style: [t.atoms.text_contrast_high, a.text_md, a.leading_snug], children: _jsx(Trans, { children: "This link is taking you to the following website:" }) }), link && _jsx(LinkBox, { href: link.href }), potentiallyMisleading && (_jsx(Text, { style: [t.atoms.text_contrast_high, a.text_md, a.leading_snug], children: _jsx(Trans, { children: "Make sure this is where you intend to go!" }) }))] }), _jsxs(View, { style: [ + a.flex_1, + a.gap_sm, + gtMobile && [a.flex_row_reverse, a.justify_start], + ], children: [_jsx(Button, { label: (link === null || link === void 0 ? void 0 : link.share) ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Share link"], ["Share link"])))) : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Visit site"], ["Visit site"])))), accessibilityHint: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Opens link ", ""], ["Opens link ", ""])), (_b = link === null || link === void 0 ? void 0 : link.href) !== null && _b !== void 0 ? _b : '')), onPress: onPressVisit, size: "large", variant: "solid", color: potentiallyMisleading ? 'secondary_inverted' : 'primary', children: _jsx(ButtonText, { children: (link === null || link === void 0 ? void 0 : link.share) ? (_jsx(Trans, { children: "Share link" })) : (_jsx(Trans, { children: "Visit site" })) }) }), _jsx(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Go back"], ["Go back"])))), onPress: onCancel, size: "large", variant: "ghost", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go back" }) }) })] })] }), _jsx(Dialog.Close, {})] })); +} +function LinkBox(_a) { + var href = _a.href; + var t = useTheme(); + var _b = useMemo(function () { + try { + var urlp = new URL(href); + var _a = splitApexDomain(urlp.hostname), subdomain = _a[0], apexdomain = _a[1]; + return [ + urlp.protocol + '//' + subdomain, + apexdomain, + urlp.pathname.replace(/\/$/, '') + urlp.search + urlp.hash, + ]; + } + catch (_b) { + return ['', href, '']; + } + }, [href]), scheme = _b[0], hostname = _b[1], rest = _b[2]; + return (_jsx(View, { style: [ + t.atoms.bg, + t.atoms.border_contrast_medium, + a.px_md, + { paddingVertical: 10 }, + a.rounded_sm, + a.border, + ], children: _jsxs(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium], children: [scheme, _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text, a.font_semi_bold], children: hostname }), rest] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/dialogs/MutedWords.js b/src/components/dialogs/MutedWords.js new file mode 100644 index 0000000000..a56e8e4b6f --- /dev/null +++ b/src/components/dialogs/MutedWords.js @@ -0,0 +1,310 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { sanitizeMutedWordValue } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { logger } from '#/logger'; +import { usePreferencesQuery, useRemoveMutedWordMutation, useUpdateMutedWordMutation, useUpsertMutedWordsMutation, } from '#/state/queries/preferences'; +import { atoms as a, native, useBreakpoints, useTheme, web, } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { Divider } from '#/components/Divider'; +import * as Toggle from '#/components/forms/Toggle'; +import { useFormatDistance } from '#/components/hooks/dates'; +import { Hashtag_Stroke2_Corner0_Rounded as Hashtag } from '#/components/icons/Hashtag'; +import { PageText_Stroke2_Corner0_Rounded as PageText } from '#/components/icons/PageText'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Loader } from '#/components/Loader'; +import * as Menu from '#/components/Menu'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +var ONE_DAY = 24 * 60 * 60 * 1000; +export function MutedWordsDialog() { + var control = useGlobalDialogsControlContext().mutedWordsDialogControl; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsx(MutedWordsInner, {})] })); +} +function MutedWordsInner() { + var _this = this; + var t = useTheme(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var _a = usePreferencesQuery(), isPreferencesLoading = _a.isLoading, preferences = _a.data, preferencesError = _a.error; + var _b = useUpsertMutedWordsMutation(), isPending = _b.isPending, addMutedWord = _b.mutateAsync; + var _c = React.useState(''), field = _c[0], setField = _c[1]; + var _d = React.useState(['content']), targets = _d[0], setTargets = _d[1]; + var _e = React.useState(''), error = _e[0], setError = _e[1]; + var _f = React.useState(['forever']), durations = _f[0], setDurations = _f[1]; + var _g = React.useState(false), excludeFollowing = _g[0], setExcludeFollowing = _g[1]; + var submit = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var sanitizedValue, surfaces, actorTarget, now, rawDuration, duration, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + sanitizedValue = sanitizeMutedWordValue(field); + surfaces = ['tag', targets.includes('content') && 'content'].filter(Boolean); + actorTarget = excludeFollowing ? 'exclude-following' : 'all'; + now = Date.now(); + rawDuration = durations.at(0); + if (rawDuration === '24_hours') { + duration = new Date(now + ONE_DAY).toISOString(); + } + else if (rawDuration === '7_days') { + duration = new Date(now + 7 * ONE_DAY).toISOString(); + } + else if (rawDuration === '30_days') { + duration = new Date(now + 30 * ONE_DAY).toISOString(); + } + if (!sanitizedValue || !surfaces.length) { + setField(''); + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Please enter a valid word, tag, or phrase to mute"], ["Please enter a valid word, tag, or phrase to mute"]))))); + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + // send raw value and rely on SDK as sanitization source of truth + return [4 /*yield*/, addMutedWord([ + { + value: field, + targets: surfaces, + actorTarget: actorTarget, + expiresAt: duration, + }, + ])]; + case 2: + // send raw value and rely on SDK as sanitization source of truth + _a.sent(); + setField(''); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + logger.error("Failed to save muted word", { message: e_1.message }); + setError(e_1.message); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Manage your muted words and tags"], ["Manage your muted words and tags"])))), children: [_jsxs(View, { children: [_jsx(Text, { style: [ + a.text_md, + a.font_semi_bold, + a.pb_sm, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Add muted words and tags" }) }), _jsx(Text, { style: [a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Posts can be muted based on their text, their tags, or both. We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." }) }), _jsx(View, { style: [a.pb_sm], children: _jsx(Dialog.Input, { autoCorrect: false, autoCapitalize: "none", autoComplete: "off", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Enter a word or tag"], ["Enter a word or tag"])))), placeholder: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Enter a word or tag"], ["Enter a word or tag"])))), value: field, onChangeText: function (value) { + if (error) { + setError(''); + } + setField(value); + }, onSubmitEditing: submit }) }), _jsxs(View, { style: [a.pb_xl, a.gap_sm], children: [_jsxs(Toggle.Group, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Select how long to mute this word for."], ["Select how long to mute this word for."])))), type: "radio", values: durations, onChange: setDurations, children: [_jsx(Text, { style: [ + a.pb_xs, + a.text_sm, + a.font_semi_bold, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Duration:" }) }), _jsxs(View, { style: [ + gtMobile && [a.flex_row, a.align_center, a.justify_start], + a.gap_sm, + ], children: [_jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.justify_start, + a.align_center, + a.gap_sm, + ], children: [_jsx(Toggle.Item, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Mute this word until you unmute it"], ["Mute this word until you unmute it"])))), name: "forever", style: [a.flex_1], children: _jsx(TargetToggle, { children: _jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [a.flex_1, a.leading_tight], children: _jsx(Trans, { children: "Forever" }) })] }) }) }), _jsx(Toggle.Item, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Mute this word for 24 hours"], ["Mute this word for 24 hours"])))), name: "24_hours", style: [a.flex_1], children: _jsx(TargetToggle, { children: _jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [a.flex_1, a.leading_tight], children: _jsx(Trans, { children: "24 hours" }) })] }) }) })] }), _jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.justify_start, + a.align_center, + a.gap_sm, + ], children: [_jsx(Toggle.Item, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Mute this word for 7 days"], ["Mute this word for 7 days"])))), name: "7_days", style: [a.flex_1], children: _jsx(TargetToggle, { children: _jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [a.flex_1, a.leading_tight], children: _jsx(Trans, { children: "7 days" }) })] }) }) }), _jsx(Toggle.Item, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Mute this word for 30 days"], ["Mute this word for 30 days"])))), name: "30_days", style: [a.flex_1], children: _jsx(TargetToggle, { children: _jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [a.flex_1, a.leading_tight], children: _jsx(Trans, { children: "30 days" }) })] }) }) })] })] })] }), _jsxs(Toggle.Group, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Select what content this mute word should apply to."], ["Select what content this mute word should apply to."])))), type: "radio", values: targets, onChange: setTargets, children: [_jsx(Text, { style: [ + a.pb_xs, + a.text_sm, + a.font_semi_bold, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Mute in:" }) }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm, a.flex_wrap], children: [_jsx(Toggle.Item, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Mute this word in post text and tags"], ["Mute this word in post text and tags"])))), name: "content", style: [a.flex_1], children: _jsxs(TargetToggle, { children: [_jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [a.flex_1, a.leading_tight], children: _jsx(Trans, { children: "Text & tags" }) })] }), _jsx(PageText, { size: "sm" })] }) }), _jsx(Toggle.Item, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Mute this word in tags only"], ["Mute this word in tags only"])))), name: "tag", style: [a.flex_1], children: _jsxs(TargetToggle, { children: [_jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [a.flex_1, a.leading_tight], children: _jsx(Trans, { children: "Tags only" }) })] }), _jsx(Hashtag, { size: "sm" })] }) })] })] }), _jsxs(View, { children: [_jsx(Text, { style: [ + a.pb_xs, + a.text_sm, + a.font_semi_bold, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Options:" }) }), _jsx(Toggle.Item, { label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Do not apply this mute word to users you follow"], ["Do not apply this mute word to users you follow"])))), name: "exclude_following", style: [a.flex_row, a.justify_between], value: excludeFollowing, onChange: setExcludeFollowing, children: _jsx(TargetToggle, { children: _jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.LabelText, { style: [a.flex_1, a.leading_tight], children: _jsx(Trans, { children: "Exclude users you follow" }) })] }) }) })] }), _jsx(View, { style: [a.pt_xs], children: _jsxs(Button, { disabled: isPending || !field, label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Add mute word with chosen settings"], ["Add mute word with chosen settings"])))), size: "large", color: "primary", variant: "solid", style: [], onPress: submit, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Add" }) }), _jsx(ButtonIcon, { icon: isPending ? Loader : Plus, position: "right" })] }) }), error && (_jsx(View, { style: [ + a.mb_lg, + a.flex_row, + a.rounded_sm, + a.p_md, + a.mb_xs, + t.atoms.bg_contrast_25, + { + backgroundColor: t.palette.negative_400, + }, + ], children: _jsx(Text, { style: [ + a.italic, + { color: t.palette.white }, + native({ marginTop: 2 }), + ], children: error }) }))] }), _jsx(Divider, {}), _jsxs(View, { style: [a.pt_2xl], children: [_jsx(Text, { style: [ + a.text_md, + a.font_semi_bold, + a.pb_md, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Your muted words" }) }), isPreferencesLoading ? (_jsx(Loader, {})) : preferencesError || !preferences ? (_jsx(View, { style: [a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25], children: _jsx(Text, { style: [a.italic, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "We're sorry, but we weren't able to load your muted words at this time. Please try again." }) }) })) : preferences.moderationPrefs.mutedWords.length ? (__spreadArray([], preferences.moderationPrefs.mutedWords, true).reverse() + .map(function (word, i) { return (_jsx(MutedWordRow, { word: word, style: [i % 2 === 0 && t.atoms.bg_contrast_25] }, word.value + i)); })) : (_jsx(View, { style: [a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25], children: _jsx(Text, { style: [a.italic, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "You haven't muted any words or tags yet" }) }) }))] }), IS_NATIVE && _jsx(View, { style: { height: 20 } })] }), _jsx(Dialog.Close, {})] })); +} +function MutedWordRow(_a) { + var _this = this; + var style = _a.style, word = _a.word; + var t = useTheme(); + var _ = useLingui()._; + var _b = useRemoveMutedWordMutation(), isPending = _b.isPending, removeMutedWord = _b.mutateAsync; + var updateMutedWord = useUpdateMutedWordMutation().mutateAsync; + var control = Prompt.usePromptControl(); + var expiryDate = word.expiresAt ? new Date(word.expiresAt) : undefined; + var isExpired = expiryDate && expiryDate < new Date(); + var formatDistance = useFormatDistance(); + var remove = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + control.close(); + removeMutedWord(word); + return [2 /*return*/]; + }); + }); }, [removeMutedWord, word, control]); + var renew = function (days) { + updateMutedWord(__assign(__assign({}, word), { expiresAt: days + ? new Date(Date.now() + days * ONE_DAY).toISOString() + : undefined })); + }; + return (_jsxs(_Fragment, { children: [_jsx(Prompt.Basic, { control: control, title: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Are you sure?"], ["Are you sure?"])))), description: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["This will delete \"", "\" from your muted words. You can always add it back later."], ["This will delete \"", "\" from your muted words. You can always add it back later."])), word.value)), onConfirm: remove, confirmButtonCta: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Remove"], ["Remove"])))), confirmButtonColor: "negative" }), _jsxs(View, { style: [ + a.flex_row, + a.justify_between, + a.py_md, + a.px_lg, + a.rounded_md, + a.gap_md, + style, + ], children: [_jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsx(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: _jsx(Text, { style: [ + a.flex_1, + a.leading_snug, + a.font_semi_bold, + web({ + overflowWrap: 'break-word', + wordBreak: 'break-word', + }), + ], children: word.targets.find(function (t) { return t === 'content'; }) ? (_jsxs(Trans, { comment: "Pattern: {wordValue} in text, tags", children: [word.value, ' ', _jsxs(Text, { style: [a.font_normal, t.atoms.text_contrast_medium], children: ["in", ' ', _jsx(Text, { style: [a.font_semi_bold, t.atoms.text_contrast_medium], children: "text & tags" })] })] })) : (_jsxs(Trans, { comment: "Pattern: {wordValue} in tags", children: [word.value, ' ', _jsxs(Text, { style: [a.font_normal, t.atoms.text_contrast_medium], children: ["in", ' ', _jsx(Text, { style: [a.font_semi_bold, t.atoms.text_contrast_medium], children: "tags" })] })] })) }) }), (expiryDate || word.actorTarget === 'exclude-following') && (_jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.flex_wrap], children: [expiryDate && + (isExpired ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: [ + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Expired" }) }), _jsx(Text, { style: [ + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: ' · ' }), _jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Renew mute word"], ["Renew mute word"])))), children: function (_a) { + var props = _a.props; + return (_jsx(Text, __assign({}, props, { style: [ + a.text_xs, + a.leading_snug, + a.font_semi_bold, + { color: t.palette.primary_500 }, + ], children: _jsx(Trans, { children: "Renew" }) }))); + } }), _jsxs(Menu.Outer, { children: [_jsx(Menu.LabelText, { children: _jsx(Trans, { children: "Renew duration" }) }), _jsxs(Menu.Group, { children: [_jsx(Menu.Item, { label: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["24 hours"], ["24 hours"])))), onPress: function () { return renew(1); }, children: _jsx(Menu.ItemText, { children: _jsx(Trans, { children: "24 hours" }) }) }), _jsx(Menu.Item, { label: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["7 days"], ["7 days"])))), onPress: function () { return renew(7); }, children: _jsx(Menu.ItemText, { children: _jsx(Trans, { children: "7 days" }) }) }), _jsx(Menu.Item, { label: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["30 days"], ["30 days"])))), onPress: function () { return renew(30); }, children: _jsx(Menu.ItemText, { children: _jsx(Trans, { children: "30 days" }) }) }), _jsx(Menu.Item, { label: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Forever"], ["Forever"])))), onPress: function () { return renew(); }, children: _jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Forever" }) }) })] })] })] })] })) : (_jsx(Text, { style: [ + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: _jsxs(Trans, { children: ["Expires", ' ', formatDistance(expiryDate, new Date(), { + addSuffix: true, + })] }) }))), word.actorTarget === 'exclude-following' && (_jsxs(Text, { style: [ + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: [expiryDate ? ' · ' : '', _jsx(Trans, { children: "Excludes users you follow" })] }))] }))] }), _jsx(Button, { label: _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Remove mute word from your list"], ["Remove mute word from your list"])))), size: "tiny", shape: "round", variant: "outline", color: "secondary", onPress: function () { return control.open(); }, style: [a.ml_sm], children: _jsx(ButtonIcon, { icon: isPending ? Loader : X }) })] })] })); +} +function TargetToggle(_a) { + var children = _a.children; + var t = useTheme(); + var ctx = Toggle.useItemContext(); + var gtMobile = useBreakpoints().gtMobile; + return (_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.justify_between, + a.gap_xs, + a.flex_1, + a.py_sm, + a.px_sm, + gtMobile && a.px_md, + a.rounded_sm, + t.atoms.bg_contrast_25, + (ctx.hovered || ctx.focused) && t.atoms.bg_contrast_50, + ctx.selected && [ + { + backgroundColor: t.palette.primary_50, + }, + ], + ctx.disabled && { + opacity: 0.8, + }, + ], children: children })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23; diff --git a/src/components/dialogs/PostInteractionSettingsDialog.js b/src/components/dialogs/PostInteractionSettingsDialog.js new file mode 100644 index 0000000000..146898709e --- /dev/null +++ b/src/components/dialogs/PostInteractionSettingsDialog.js @@ -0,0 +1,386 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useMemo, useState } from 'react'; +import { LayoutAnimation, Text as NestedText, View } from 'react-native'; +import { AtUri, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { useHaptics } from '#/lib/haptics'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { STALE } from '#/state/queries'; +import { useMyListsQuery } from '#/state/queries/my-lists'; +import { useGetPost } from '#/state/queries/post'; +import { createPostgateQueryKey, getPostgateRecord, usePostgateQuery, useWritePostgateMutation, } from '#/state/queries/postgate'; +import { createPostgateRecord, embeddingRules, } from '#/state/queries/postgate/util'; +import { createThreadgateViewQueryKey, threadgateViewToAllowUISetting, useSetThreadgateAllowMutation, useThreadgateViewQuery, } from '#/state/queries/threadgate'; +import { PostThreadContextProvider, usePostThreadContext, } from '#/state/queries/usePostThread'; +import { useAgent, useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as Toggle from '#/components/forms/Toggle'; +import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon, } from '#/components/icons/Chevron'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon } from '#/components/icons/Quote'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS } from '#/env'; +/** + * Threadgate settings dialog. Used in the composer. + */ +export function PostInteractionSettingsControlledDialog(_a) { + var control = _a.control, rest = __rest(_a, ["control"]); + var ax = useAnalytics(); + var onClose = useNonReactiveCallback(function () { + var _a, _b, _c, _d, _e; + ax.metric('composer:threadgate:save', { + hasChanged: !!rest.isDirty, + persist: !!rest.persist, + replyOptions: (_c = (_b = (_a = rest.threadgateAllowUISettings) === null || _a === void 0 ? void 0 : _a.map(function (gate) { return gate.type; })) === null || _b === void 0 ? void 0 : _b.join(',')) !== null && _c !== void 0 ? _c : '', + quotesEnabled: !((_e = (_d = rest.postgate) === null || _d === void 0 ? void 0 : _d.embeddingRules) === null || _e === void 0 ? void 0 : _e.find(function (v) { return v.$type === embeddingRules.disableRule.$type; })), + }); + }); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { + preventExpansion: true, + preventDismiss: rest.isDirty && rest.persist, + }, onClose: onClose, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, __assign({}, rest))] })); +} +function DialogInner(props) { + var _ = useLingui()._; + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Edit post interaction settings"], ["Edit post interaction settings"])))), style: [web({ maxWidth: 400 }), a.w_full], children: [_jsx(Header, {}), _jsx(PostInteractionSettingsForm, __assign({}, props)), _jsx(Dialog.Close, {})] })); +} +/** + * Threadgate settings dialog. Used in the thread. + */ +export function PostInteractionSettingsDialog(props) { + var postThreadContext = usePostThreadContext(); + return (_jsxs(Dialog.Outer, { control: props.control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(PostThreadContextProvider, { context: postThreadContext, children: _jsx(PostInteractionSettingsDialogControlledInner, __assign({}, props)) })] })); +} +export function PostInteractionSettingsDialogControlledInner(props) { + var _this = this; + var ax = useAnalytics(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var _a = useState(false), isSaving = _a[0], setIsSaving = _a[1]; + var _b = useThreadgateViewQuery({ postUri: props.rootPostUri }), threadgateViewLoaded = _b.data, isLoadingThreadgate = _b.isLoading; + var _c = usePostgateQuery({ + postUri: props.postUri, + }), postgate = _c.data, isLoadingPostgate = _c.isLoading; + var writePostgateRecord = useWritePostgateMutation().mutateAsync; + var setThreadgateAllow = useSetThreadgateAllowMutation().mutateAsync; + var _d = useState(), editedPostgate = _d[0], setEditedPostgate = _d[1]; + var _e = useState(), editedAllowUISettings = _e[0], setEditedAllowUISettings = _e[1]; + var isLoading = isLoadingThreadgate || isLoadingPostgate; + var threadgateView = threadgateViewLoaded || props.initialThreadgateView; + var isThreadgateOwnedByViewer = useMemo(function () { + return (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === new AtUri(props.rootPostUri).host; + }, [props.rootPostUri, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did]); + var postgateValue = useMemo(function () { + return (editedPostgate || postgate || createPostgateRecord({ post: props.postUri })); + }, [postgate, editedPostgate, props.postUri]); + var allowUIValue = useMemo(function () { + return (editedAllowUISettings || threadgateViewToAllowUISetting(threadgateView)); + }, [threadgateView, editedAllowUISettings]); + var onSave = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var requests, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!editedPostgate && !editedAllowUISettings) { + props.control.close(); + return [2 /*return*/]; + } + setIsSaving(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + requests = []; + if (editedPostgate) { + requests.push(writePostgateRecord({ + postUri: props.postUri, + postgate: editedPostgate, + })); + } + if (editedAllowUISettings && isThreadgateOwnedByViewer) { + requests.push(setThreadgateAllow({ + postUri: props.rootPostUri, + allow: editedAllowUISettings, + })); + } + return [4 /*yield*/, Promise.all(requests)]; + case 2: + _a.sent(); + props.control.close(); + return [3 /*break*/, 5]; + case 3: + e_1 = _a.sent(); + ax.logger.error("Failed to save post interaction settings", { + source: 'PostInteractionSettingsDialogControlledInner', + safeMessage: e_1.message, + }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."])))), 'xmark'); + return [3 /*break*/, 5]; + case 4: + setIsSaving(false); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }, [ + _, + ax, + props.postUri, + props.rootPostUri, + props.control, + editedPostgate, + editedAllowUISettings, + setIsSaving, + writePostgateRecord, + setThreadgateAllow, + isThreadgateOwnedByViewer, + ]); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Edit post interaction settings"], ["Edit post interaction settings"])))), style: [web({ maxWidth: 400 }), a.w_full], children: [isLoading ? (_jsxs(View, { style: [ + a.flex_1, + a.py_5xl, + a.gap_md, + a.align_center, + a.justify_center, + ], children: [_jsx(Loader, { size: "xl" }), _jsx(Text, { style: [a.italic, a.text_center], children: _jsx(Trans, { children: "Loading post interaction settings..." }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Header, {}), _jsx(PostInteractionSettingsForm, { replySettingsDisabled: !isThreadgateOwnedByViewer, isSaving: isSaving, onSave: onSave, postgate: postgateValue, onChangePostgate: setEditedPostgate, threadgateAllowUISettings: allowUIValue, onChangeThreadgateAllowUISettings: setEditedAllowUISettings })] })), _jsx(Dialog.Close, {})] })); +} +export function PostInteractionSettingsForm(_a) { + var _b = _a.canSave, canSave = _b === void 0 ? true : _b, onSave = _a.onSave, isSaving = _a.isSaving, postgate = _a.postgate, onChangePostgate = _a.onChangePostgate, threadgateAllowUISettings = _a.threadgateAllowUISettings, onChangeThreadgateAllowUISettings = _a.onChangeThreadgateAllowUISettings, replySettingsDisabled = _a.replySettingsDisabled, isDirty = _a.isDirty, persist = _a.persist, onChangePersist = _a.onChangePersist; + var t = useTheme(); + var _ = useLingui()._; + var playHaptic = useHaptics(); + var _c = useState(false), showLists = _c[0], setShowLists = _c[1]; + var _d = useMyListsQuery('curate'), lists = _d.data, isListsPending = _d.isPending, isListsError = _d.isError; + var _e = useState(!(postgate.embeddingRules && + postgate.embeddingRules.find(function (v) { return v.$type === embeddingRules.disableRule.$type; }))), quotesEnabled = _e[0], setQuotesEnabled = _e[1]; + var onChangeQuotesEnabled = useCallback(function (enabled) { + setQuotesEnabled(enabled); + onChangePostgate(createPostgateRecord(__assign(__assign({}, postgate), { embeddingRules: enabled ? [] : [embeddingRules.disableRule] }))); + }, [setQuotesEnabled, postgate, onChangePostgate]); + var noOneCanReply = !!threadgateAllowUISettings.find(function (v) { return v.type === 'nobody'; }); + var everyoneCanReply = !!threadgateAllowUISettings.find(function (v) { return v.type === 'everybody'; }); + var numberOfListsSelected = threadgateAllowUISettings.filter(function (v) { return v.type === 'list'; }).length; + var toggleGroupValues = useMemo(function () { + var values = []; + for (var _i = 0, threadgateAllowUISettings_1 = threadgateAllowUISettings; _i < threadgateAllowUISettings_1.length; _i++) { + var setting = threadgateAllowUISettings_1[_i]; + switch (setting.type) { + case 'everybody': + case 'nobody': + // no granularity, early return with nothing + return []; + case 'followers': + values.push('followers'); + break; + case 'following': + values.push('following'); + break; + case 'mention': + values.push('mention'); + break; + case 'list': + values.push("list:".concat(setting.list)); + break; + default: + break; + } + } + return values; + }, [threadgateAllowUISettings]); + var toggleGroupOnChange = function (values) { + var settings = []; + if (values.length === 0) { + settings.push({ type: 'everybody' }); + } + else { + for (var _i = 0, values_1 = values; _i < values_1.length; _i++) { + var value = values_1[_i]; + if (value.startsWith('list:')) { + var listId = value.slice('list:'.length); + settings.push({ type: 'list', list: listId }); + } + else { + settings.push({ type: value }); + } + } + } + onChangeThreadgateAllowUISettings(settings); + }; + return (_jsxs(View, { style: [a.flex_1, a.gap_lg], children: [_jsxs(View, { style: [a.gap_lg], children: [replySettingsDisabled && (_jsxs(View, { style: [ + a.px_md, + a.py_sm, + a.rounded_sm, + a.flex_row, + a.align_center, + a.gap_sm, + t.atoms.bg_contrast_25, + ], children: [_jsx(CircleInfo, { fill: t.atoms.text_contrast_low.color }), _jsx(Text, { style: [a.flex_1, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Reply settings are chosen by the author of the thread" }) })] })), _jsxs(View, { style: [a.gap_sm, { opacity: replySettingsDisabled ? 0.3 : 1 }], children: [_jsx(Text, { style: [a.text_md, a.font_medium], children: _jsx(Trans, { children: "Who can reply" }) }), _jsx(Toggle.Group, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Set who can reply to your post"], ["Set who can reply to your post"])))), type: "radio", maxSelections: 1, disabled: replySettingsDisabled, values: everyoneCanReply ? ['everyone'] : noOneCanReply ? ['nobody'] : [], onChange: function (val) { + if (val.includes('everyone')) { + onChangeThreadgateAllowUISettings([{ type: 'everybody' }]); + } + else if (val.includes('nobody')) { + onChangeThreadgateAllowUISettings([{ type: 'nobody' }]); + } + else { + onChangeThreadgateAllowUISettings([{ type: 'mention' }]); + } + }, children: _jsxs(View, { style: [a.flex_row, a.gap_sm], children: [_jsx(Toggle.Item, { name: "everyone", type: "checkbox", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Allow anyone to reply"], ["Allow anyone to reply"])))), style: [a.flex_1], children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "Anyone" }) })] })); + } }), _jsx(Toggle.Item, { name: "nobody", type: "checkbox", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Disable replies entirely"], ["Disable replies entirely"])))), style: [a.flex_1], children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "Nobody" }) })] })); + } })] }) }), _jsx(Toggle.Group, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Set precisely which groups of people can reply to your post"], ["Set precisely which groups of people can reply to your post"])))), values: toggleGroupValues, onChange: toggleGroupOnChange, disabled: replySettingsDisabled, children: _jsxs(Toggle.PanelGroup, { children: [_jsx(Toggle.Item, { name: "followers", type: "checkbox", label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Allow your followers to reply"], ["Allow your followers to reply"])))), hitSlop: 0, children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, adjacent: "trailing", children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "Your followers" }) })] })); + } }), _jsx(Toggle.Item, { name: "following", type: "checkbox", label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Allow people you follow to reply"], ["Allow people you follow to reply"])))), hitSlop: 0, children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, adjacent: "both", children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "People you follow" }) })] })); + } }), _jsx(Toggle.Item, { name: "mention", type: "checkbox", label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Allow people you mention to reply"], ["Allow people you mention to reply"])))), hitSlop: 0, children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, adjacent: "both", children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "People you mention" }) })] })); + } }), _jsx(Button, { label: showLists + ? _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Hide lists"], ["Hide lists"])))) + : _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Show lists of users to select from"], ["Show lists of users to select from"])))), accessibilityRole: "togglebutton", hitSlop: 0, onPress: function () { + playHaptic('Light'); + if (IS_IOS && !showLists) { + LayoutAnimation.configureNext(__assign(__assign({}, LayoutAnimation.Presets.linear), { duration: 175 })); + } + setShowLists(function (s) { return !s; }); + }, children: _jsxs(Toggle.Panel, { active: numberOfListsSelected > 0, adjacent: showLists ? 'both' : 'leading', children: [_jsx(Toggle.PanelText, { children: numberOfListsSelected === 0 ? (_jsx(Trans, { children: "Select from your lists" })) : (_jsxs(Trans, { children: ["Select from your lists", ' ', _jsx(NestedText, { style: [a.font_normal, a.italic], children: _jsx(Plural, { value: numberOfListsSelected, other: "(# selected)" }) })] })) }), _jsx(Toggle.PanelIcon, { icon: showLists ? ChevronUpIcon : ChevronDownIcon })] }) }), showLists && + (isListsPending ? (_jsx(Toggle.Panel, { children: _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "Loading lists..." }) }) })) : isListsError ? (_jsx(Toggle.Panel, { children: _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "An error occurred while loading your lists :/" }) }) })) : lists.length === 0 ? (_jsx(Toggle.Panel, { children: _jsx(Toggle.PanelText, { children: _jsx(Trans, { children: "You don't have any lists yet." }) }) })) : (lists.map(function (list, i) { return (_jsx(Toggle.Item, { name: "list:".concat(list.uri), type: "checkbox", label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Allow users in ", " to reply"], ["Allow users in ", " to reply"])), list.name)), hitSlop: 0, children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, adjacent: i === lists.length - 1 ? 'leading' : 'both', children: [_jsx(Toggle.Checkbox, {}), _jsx(UserAvatar, { size: 24, type: "list", avatar: list.avatar }), _jsx(Toggle.PanelText, { children: list.name })] })); + } }, list.uri)); })))] }) })] })] }), _jsx(Toggle.Item, { name: "quoteposts", type: "checkbox", label: quotesEnabled + ? _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Disable quote posts of this post"], ["Disable quote posts of this post"])))) + : _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Enable quote posts of this post"], ["Enable quote posts of this post"])))), value: quotesEnabled, onChange: onChangeQuotesEnabled, children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, children: [_jsx(Toggle.PanelText, { icon: QuoteIcon, children: _jsx(Trans, { children: "Allow quote posts" }) }), _jsx(Toggle.Switch, {})] })); + } }), typeof persist !== 'undefined' && (_jsx(View, { style: [{ minHeight: 24 }, a.justify_center], children: isDirty ? (_jsxs(Toggle.Item, { name: "persist", type: "checkbox", label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Save these options for next time"], ["Save these options for next time"])))), value: persist, onChange: function () { return onChangePersist === null || onChangePersist === void 0 ? void 0 : onChangePersist(!persist); }, children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.LabelText, { style: [a.text_md, a.font_normal, t.atoms.text], children: _jsx(Trans, { children: "Save these options for next time" }) })] })) : (_jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "These are your default settings" }) })) })), _jsxs(Button, { disabled: !canSave || isSaving, label: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Save"], ["Save"])))), onPress: onSave, color: "primary", size: "large", children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Save" }) }), isSaving && _jsx(ButtonIcon, { icon: Loader })] })] })); +} +function Header() { + return (_jsx(View, { style: [a.pb_lg], children: _jsx(Text, { style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "Post interaction settings" }) }) })); +} +export function usePrefetchPostInteractionSettings(_a) { + var _this = this; + var postUri = _a.postUri, rootPostUri = _a.rootPostUri; + var ax = useAnalytics(); + var queryClient = useQueryClient(); + var agent = useAgent(); + var getPost = useGetPost(); + return useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_2; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, Promise.all([ + queryClient.prefetchQuery({ + queryKey: createPostgateQueryKey(postUri), + queryFn: function () { + return getPostgateRecord({ agent: agent, postUri: postUri }).then(function (res) { return res !== null && res !== void 0 ? res : null; }); + }, + staleTime: STALE.SECONDS.THIRTY, + }), + queryClient.prefetchQuery({ + queryKey: createThreadgateViewQueryKey(rootPostUri), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var post; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, getPost({ uri: rootPostUri })]; + case 1: + post = _b.sent(); + return [2 /*return*/, (_a = post.threadgate) !== null && _a !== void 0 ? _a : null]; + } + }); + }); }, + staleTime: STALE.SECONDS.THIRTY, + }), + ])]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + e_2 = _a.sent(); + ax.logger.error("Failed to prefetch post interaction settings", { + safeMessage: e_2.message, + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }, [ax, queryClient, agent, postUri, rootPostUri, getPost]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17; diff --git a/src/components/dialogs/SearchablePeopleList.js b/src/components/dialogs/SearchablePeopleList.js new file mode 100644 index 0000000000..200fec171d --- /dev/null +++ b/src/components/dialogs/SearchablePeopleList.js @@ -0,0 +1,329 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Fragment, useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react'; +import { TextInput, View } from 'react-native'; +import { moderateProfile } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useActorAutocompleteQuery } from '#/state/queries/actor-autocomplete'; +import { useListConvosQuery } from '#/state/queries/messages/list-conversations'; +import { useProfileFollowsQuery } from '#/state/queries/profile-follows'; +import { useSession } from '#/state/session'; +import { android, atoms as a, native, useTheme, web } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { canBeMessaged } from '#/components/dms/util'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { MagnifyingGlass_Stroke2_Corner0_Rounded as Search } from '#/components/icons/MagnifyingGlass'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import * as ProfileCard from '#/components/ProfileCard'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +export function SearchablePeopleList(_a) { + var title = _a.title, showRecentConvos = _a.showRecentConvos, sortByMessageDeclaration = _a.sortByMessageDeclaration, onSelectChat = _a.onSelectChat, renderProfileCard = _a.renderProfileCard; + var t = useTheme(); + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + var control = Dialog.useDialogContext(); + var _b = useState(0), headerHeight = _b[0], setHeaderHeight = _b[1]; + var listRef = useRef(null); + var currentAccount = useSession().currentAccount; + var inputRef = useRef(null); + var _c = useState(''), searchText = _c[0], setSearchText = _c[1]; + var _d = useActorAutocompleteQuery(searchText, true, 12), results = _d.data, isError = _d.isError, isFetching = _d.isFetching; + var follows = useProfileFollowsQuery(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did).data; + var convos = useListConvosQuery({ + enabled: showRecentConvos, + status: 'accepted', + }).data; + var items = useMemo(function () { + var _items = []; + if (isError) { + _items.push({ + type: 'empty', + key: 'empty', + message: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["We're having network issues, try again"], ["We're having network issues, try again"])))), + }); + } + else if (searchText.length) { + if (results === null || results === void 0 ? void 0 : results.length) { + for (var _i = 0, results_1 = results; _i < results_1.length; _i++) { + var profile = results_1[_i]; + if (profile.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) + continue; + _items.push({ + type: 'profile', + key: profile.did, + profile: profile, + }); + } + if (sortByMessageDeclaration) { + _items = _items.sort(function (item) { + return item.type === 'profile' && canBeMessaged(item.profile) + ? -1 + : 1; + }); + } + } + } + else { + var placeholders = Array(10) + .fill(0) + .map(function (__, i) { return ({ + type: 'placeholder', + key: i + '', + }); }); + if (showRecentConvos) { + if (convos && follows) { + var usedDids = new Set(); + for (var _a = 0, _b = convos.pages; _a < _b.length; _a++) { + var page = _b[_a]; + for (var _c = 0, _d = page.convos; _c < _d.length; _c++) { + var convo = _d[_c]; + var profiles = convo.members.filter(function (m) { return m.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); + for (var _e = 0, profiles_1 = profiles; _e < profiles_1.length; _e++) { + var profile = profiles_1[_e]; + if (usedDids.has(profile.did)) + continue; + usedDids.add(profile.did); + _items.push({ + type: 'profile', + key: profile.did, + profile: profile, + }); + } + } + } + var followsItems = []; + for (var _f = 0, _g = follows.pages; _f < _g.length; _f++) { + var page = _g[_f]; + for (var _h = 0, _j = page.follows; _h < _j.length; _h++) { + var profile = _j[_h]; + if (usedDids.has(profile.did)) + continue; + followsItems.push({ + type: 'profile', + key: profile.did, + profile: profile, + }); + } + } + if (sortByMessageDeclaration) { + // only sort follows + followsItems = followsItems.sort(function (item) { + return canBeMessaged(item.profile) ? -1 : 1; + }); + } + // then append + _items.push.apply(_items, followsItems); + } + else { + _items.push.apply(_items, placeholders); + } + } + else if (follows) { + for (var _k = 0, _l = follows.pages; _k < _l.length; _k++) { + var page = _l[_k]; + for (var _m = 0, _o = page.follows; _m < _o.length; _m++) { + var profile = _o[_m]; + _items.push({ + type: 'profile', + key: profile.did, + profile: profile, + }); + } + } + if (sortByMessageDeclaration) { + _items = _items.sort(function (item) { + return item.type === 'profile' && canBeMessaged(item.profile) + ? -1 + : 1; + }); + } + } + else { + _items.push.apply(_items, placeholders); + } + } + return _items; + }, [ + _, + searchText, + results, + isError, + currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + follows, + convos, + showRecentConvos, + sortByMessageDeclaration, + ]); + if (searchText && !isFetching && !items.length && !isError) { + items.push({ type: 'empty', key: 'empty', message: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["No results"], ["No results"])))) }); + } + var renderItems = useCallback(function (_a) { + var item = _a.item; + switch (item.type) { + case 'profile': { + if (renderProfileCard) { + return _jsx(Fragment, { children: renderProfileCard(item) }, item.key); + } + else { + return (_jsx(DefaultProfileCard, { profile: item.profile, moderationOpts: moderationOpts, onPress: onSelectChat }, item.key)); + } + } + case 'placeholder': { + return _jsx(ProfileCardSkeleton, {}, item.key); + } + case 'empty': { + return _jsx(Empty, { message: item.message }, item.key); + } + default: + return null; + } + }, [moderationOpts, onSelectChat, renderProfileCard]); + useLayoutEffect(function () { + if (IS_WEB) { + setImmediate(function () { + var _a; + (_a = inputRef === null || inputRef === void 0 ? void 0 : inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); + }); + } + }, []); + var listHeader = useMemo(function () { + return (_jsxs(View, { onLayout: function (evt) { return setHeaderHeight(evt.nativeEvent.layout.height); }, style: [ + a.relative, + web(a.pt_lg), + native(a.pt_4xl), + android({ + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }), + a.pb_xs, + a.px_lg, + a.border_b, + t.atoms.border_contrast_low, + t.atoms.bg, + ], children: [_jsxs(View, { style: [a.relative, native(a.align_center), a.justify_center], children: [_jsx(Text, { style: [ + a.z_10, + a.text_lg, + a.font_bold, + a.leading_tight, + t.atoms.text_contrast_high, + ], children: title }), IS_WEB ? (_jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Close"], ["Close"])))), size: "small", shape: "round", variant: IS_WEB ? 'ghost' : 'solid', color: "secondary", style: [ + a.absolute, + a.z_20, + web({ right: -4 }), + native({ right: 0 }), + native({ height: 32, width: 32, borderRadius: 16 }), + ], onPress: function () { return control.close(); }, children: _jsx(ButtonIcon, { icon: X, size: "md" }) })) : null] }), _jsx(View, { style: web([a.pt_xs]), children: _jsx(SearchInput, { inputRef: inputRef, value: searchText, onChangeText: function (text) { + var _a; + setSearchText(text); + (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ offset: 0, animated: false }); + }, onEscape: control.close }) })] })); + }, [ + t.atoms.border_contrast_low, + t.atoms.bg, + t.atoms.text_contrast_high, + _, + title, + searchText, + control, + ]); + return (_jsx(Dialog.InnerFlatList, { ref: listRef, data: items, renderItem: renderItems, ListHeaderComponent: listHeader, stickyHeaderIndices: [0], keyExtractor: function (item) { return item.key; }, style: [ + web([a.py_0, { height: '100vh', maxHeight: 600 }, a.px_0]), + native({ height: '100%' }), + ], webInnerContentContainerStyle: a.py_0, webInnerStyle: [a.py_0, { maxWidth: 500, minWidth: 200 }], scrollIndicatorInsets: { top: headerHeight }, keyboardDismissMode: "on-drag" })); +} +function DefaultProfileCard(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, onPress = _a.onPress; + var t = useTheme(); + var _ = useLingui()._; + var enabled = canBeMessaged(profile); + var moderation = moderateProfile(profile, moderationOpts); + var handle = sanitizeHandle(profile.handle, '@'); + var displayName = sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')); + var handleOnPress = useCallback(function () { + onPress(profile.did); + }, [onPress, profile.did]); + return (_jsx(Button, { disabled: !enabled, label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Start chat with ", ""], ["Start chat with ", ""])), displayName)), onPress: handleOnPress, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed, focused = _a.focused; + return (_jsx(View, { style: [ + a.flex_1, + a.py_sm, + a.px_lg, + !enabled + ? { opacity: 0.5 } + : pressed || focused || hovered + ? t.atoms.bg_contrast_25 + : t.atoms.bg, + ], children: _jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts, disabledPreview: true }), _jsxs(View, { style: [a.flex_1], children: [_jsx(ProfileCard.Name, { profile: profile, moderationOpts: moderationOpts }), enabled ? (_jsx(ProfileCard.Handle, { profile: profile })) : (_jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_high], numberOfLines: 2, children: _jsxs(Trans, { children: [handle, " can't be messaged"] }) }))] })] }) })); + } })); +} +function ProfileCardSkeleton() { + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_1, + a.py_md, + a.px_lg, + a.gap_md, + a.align_center, + a.flex_row, + ], children: [_jsx(View, { style: [ + a.rounded_full, + { width: 42, height: 42 }, + t.atoms.bg_contrast_25, + ] }), _jsxs(View, { style: [a.flex_1, a.gap_sm], children: [_jsx(View, { style: [ + a.rounded_xs, + { width: 80, height: 14 }, + t.atoms.bg_contrast_25, + ] }), _jsx(View, { style: [ + a.rounded_xs, + { width: 120, height: 10 }, + t.atoms.bg_contrast_25, + ] })] })] })); +} +function Empty(_a) { + var message = _a.message; + var t = useTheme(); + return (_jsxs(View, { style: [a.p_lg, a.py_xl, a.align_center, a.gap_md], children: [_jsx(Text, { style: [a.text_sm, a.italic, t.atoms.text_contrast_high], children: message }), _jsx(Text, { style: [a.text_xs, t.atoms.text_contrast_low], children: "(\u256F\u00B0\u25A1\u00B0)\u256F\uFE35 \u253B\u2501\u253B" })] })); +} +function SearchInput(_a) { + var value = _a.value, onChangeText = _a.onChangeText, onEscape = _a.onEscape, inputRef = _a.inputRef; + var t = useTheme(); + var _ = useLingui()._; + var _b = useInteractionState(), hovered = _b.state, onMouseEnter = _b.onIn, onMouseLeave = _b.onOut; + var _c = useInteractionState(), focused = _c.state, onFocus = _c.onIn, onBlur = _c.onOut; + var interacted = hovered || focused; + return (_jsxs(View, __assign({}, web({ + onMouseEnter: onMouseEnter, + onMouseLeave: onMouseLeave, + }), { style: [a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Search, { size: "md", fill: interacted ? t.palette.primary_500 : t.palette.contrast_300 }), _jsx(TextInput + // @ts-ignore bottom sheet input types issue — esb + , { + // @ts-ignore bottom sheet input types issue — esb + ref: inputRef, placeholder: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Search"], ["Search"])))), value: value, onChangeText: onChangeText, onFocus: onFocus, onBlur: onBlur, style: [a.flex_1, a.py_md, a.text_md, t.atoms.text], placeholderTextColor: t.palette.contrast_500, keyboardAppearance: t.name === 'light' ? 'light' : 'dark', returnKeyType: "search", clearButtonMode: "while-editing", maxLength: 50, onKeyPress: function (_a) { + var nativeEvent = _a.nativeEvent; + if (nativeEvent.key === 'Escape') { + onEscape(); + } + }, autoCorrect: false, autoComplete: "off", autoCapitalize: "none", autoFocus: true, accessibilityLabel: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Search profiles"], ["Search profiles"])))), accessibilityHint: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Searches for profiles"], ["Searches for profiles"])))) })] }))); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/dialogs/ServerInput.js b/src/components/dialogs/ServerInput.js new file mode 100644 index 0000000000..9e5c87a129 --- /dev/null +++ b/src/components/dialogs/ServerInput.js @@ -0,0 +1,104 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useImperativeHandle, useRef, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { BSKY_SERVICE } from '#/lib/constants'; +import * as persisted from '#/state/persisted'; +import { useSession } from '#/state/session'; +import { atoms as a, platform, useBreakpoints, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as SegmentedControl from '#/components/forms/SegmentedControl'; +import * as TextField from '#/components/forms/TextField'; +import { Globe_Stroke2_Corner0_Rounded as Globe } from '#/components/icons/Globe'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +export function ServerInputDialog(_a) { + var control = _a.control, onSelect = _a.onSelect; + var ax = useAnalytics(); + var height = useWindowDimensions().height; + var formRef = useRef(null); + // persist these options between dialog open/close + var _b = useState(BSKY_SERVICE), fixedOption = _b[0], setFixedOption = _b[1]; + var _c = useState(''), previousCustomAddress = _c[0], setPreviousCustomAddress = _c[1]; + var onClose = useCallback(function () { + var _a; + var result = (_a = formRef.current) === null || _a === void 0 ? void 0 : _a.getFormState(); + if (result) { + onSelect(result); + if (result !== BSKY_SERVICE) { + setPreviousCustomAddress(result); + } + } + ax.metric('signin:hostingProviderPressed', { + hostingProviderDidChange: fixedOption !== BSKY_SERVICE, + }); + }, [ax, onSelect, fixedOption]); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, nativeOptions: platform({ + android: { minHeight: height / 2 }, + ios: { preventExpansion: true }, + }), children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { formRef: formRef, fixedOption: fixedOption, setFixedOption: setFixedOption, initialCustomAddress: previousCustomAddress })] })); +} +function DialogInner(_a) { + var formRef = _a.formRef, fixedOption = _a.fixedOption, setFixedOption = _a.setFixedOption, initialCustomAddress = _a.initialCustomAddress; + var control = Dialog.useDialogContext(); + var _ = useLingui()._; + var t = useTheme(); + var accounts = useSession().accounts; + var gtMobile = useBreakpoints().gtMobile; + var _b = useState(initialCustomAddress), customAddress = _b[0], setCustomAddress = _b[1]; + var _c = useState(persisted.get('pdsAddressHistory') || []), pdsAddressHistory = _c[0], setPdsAddressHistory = _c[1]; + useImperativeHandle(formRef, function () { return ({ + getFormState: function () { + var url; + if (fixedOption === 'custom') { + url = customAddress.trim().toLowerCase(); + if (!url) { + return null; + } + } + else { + url = fixedOption; + } + if (!url.startsWith('http://') && !url.startsWith('https://')) { + if (url === 'localhost' || url.startsWith('localhost:')) { + url = "http://".concat(url); + } + else { + url = "https://".concat(url); + } + } + if (fixedOption === 'custom') { + if (!pdsAddressHistory.includes(url)) { + var newHistory = __spreadArray([url], pdsAddressHistory.slice(0, 4), true); + setPdsAddressHistory(newHistory); + persisted.write('pdsAddressHistory', newHistory); + } + } + return url; + }, + }); }, [customAddress, fixedOption, pdsAddressHistory]); + var isFirstTimeUser = accounts.length === 0; + return (_jsx(Dialog.ScrollableInner, { accessibilityDescribedBy: "dialog-description", accessibilityLabelledBy: "dialog-title", style: web({ maxWidth: 500 }), children: _jsxs(View, { style: [a.relative, a.gap_md, a.w_full], children: [_jsx(Text, { nativeID: "dialog-title", style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "Choose your account provider" }) }), _jsxs(SegmentedControl.Root, { type: "tabs", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Account provider"], ["Account provider"])))), value: fixedOption, onChange: setFixedOption, children: [_jsx(SegmentedControl.Item, { testID: "bskyServiceSelectBtn", value: BSKY_SERVICE, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Bluesky"], ["Bluesky"])))), children: _jsx(SegmentedControl.ItemText, { children: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Bluesky"], ["Bluesky"])))) }) }), _jsx(SegmentedControl.Item, { testID: "customSelectBtn", value: "custom", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Custom"], ["Custom"])))), children: _jsx(SegmentedControl.ItemText, { children: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Custom"], ["Custom"])))) }) })] }), fixedOption === BSKY_SERVICE && isFirstTimeUser && (_jsx(View, { role: "tabpanel", children: _jsx(Admonition, { type: "tip", children: _jsx(Trans, { children: "Bluesky is an open network where you can choose your own provider. If you're new here, we recommend sticking with the default Bluesky Social option." }) }) })), fixedOption === 'custom' && (_jsxs(View, { role: "tabpanel", children: [_jsx(TextField.LabelText, { nativeID: "address-input-label", children: _jsx(Trans, { children: "Server address" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: Globe }), _jsx(Dialog.Input, { testID: "customServerTextInput", value: customAddress, onChangeText: setCustomAddress, label: "my-server.com", accessibilityLabelledBy: "address-input-label", autoCapitalize: "none", keyboardType: "url" })] }), pdsAddressHistory.length > 0 && (_jsx(View, { style: [a.flex_row, a.flex_wrap, a.mt_xs], children: pdsAddressHistory.map(function (uri) { return (_jsx(Button, { variant: "ghost", color: "primary", label: uri, style: [a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm], onPress: function () { return setCustomAddress(uri); }, children: _jsx(ButtonText, { children: uri }) }, uri)); }) }))] })), _jsx(View, { style: [a.py_xs], children: _jsxs(Text, { style: [t.atoms.text_contrast_medium, a.text_sm, a.leading_snug], children: [isFirstTimeUser ? (_jsx(Trans, { children: "If you're a developer, you can host your own server." })) : (_jsx(Trans, { children: "Bluesky is an open network where you can choose your hosting provider. If you're a developer, you can host your own server." })), ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Learn more about self hosting your PDS."], ["Learn more about self hosting your PDS."])))), to: "https://atproto.com/guides/self-hosting", children: _jsx(Trans, { children: "Learn more." }) })] }) }), _jsx(View, { style: gtMobile && [a.flex_row, a.justify_end], children: _jsx(Button, { testID: "doneBtn", variant: "solid", color: "primary", size: platform({ + native: 'large', + web: 'small', + }), onPress: function () { return control.close(); }, label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Done"], ["Done"])))), children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Done" }) }) }) })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/dialogs/Signin.js b/src/components/dialogs/Signin.js new file mode 100644 index 0000000000..66bc0f4462 --- /dev/null +++ b/src/components/dialogs/Signin.js @@ -0,0 +1,57 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { useCloseAllActiveElements } from '#/state/util'; +import { Logo } from '#/view/icons/Logo'; +import { Logotype } from '#/view/icons/Logotype'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +export function SigninDialog() { + var control = useGlobalDialogsControlContext().signinDialogControl; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsx(SigninDialogInner, { control: control })] })); +} +function SigninDialogInner(_a) { + var t = useTheme(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var requestSwitchToAccount = useLoggedOutViewControls().requestSwitchToAccount; + var closeAllActiveElements = useCloseAllActiveElements(); + var showSignIn = React.useCallback(function () { + closeAllActiveElements(); + requestSwitchToAccount({ requestedAccount: 'none' }); + }, [requestSwitchToAccount, closeAllActiveElements]); + var showCreateAccount = React.useCallback(function () { + closeAllActiveElements(); + requestSwitchToAccount({ requestedAccount: 'new' }); + }, [requestSwitchToAccount, closeAllActiveElements]); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sign in to Bluesky or create a new account"], ["Sign in to Bluesky or create a new account"])))), style: [gtMobile ? { width: 'auto', maxWidth: 420 } : a.w_full], children: [_jsxs(View, { style: [!IS_NATIVE && a.p_2xl], children: [_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.justify_center, + a.gap_sm, + a.pb_lg, + ], children: [_jsx(Logo, { width: 36 }), _jsx(View, { style: { paddingTop: 6 }, children: _jsx(Logotype, { width: 120, fill: t.atoms.text.color }) })] }), _jsx(Text, { style: [ + a.text_lg, + a.text_center, + t.atoms.text, + a.pb_2xl, + a.leading_snug, + a.mx_auto, + { + maxWidth: 300, + }, + ], children: _jsx(Trans, { children: "Sign in or create your account to join the conversation!" }) }), _jsxs(View, { style: [a.flex_col, a.gap_md], children: [_jsx(Button, { variant: "solid", color: "primary", size: "large", onPress: showCreateAccount, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Create an account"], ["Create an account"])))), children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Create an account" }) }) }), _jsx(Button, { variant: "solid", color: "secondary", size: "large", onPress: showSignIn, label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Sign in"], ["Sign in"])))), children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Sign in" }) }) })] }), IS_NATIVE && _jsx(View, { style: { height: 10 } })] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/dialogs/StarterPackDialog.js b/src/components/dialogs/StarterPackDialog.js new file mode 100644 index 0000000000..574dad2f07 --- /dev/null +++ b/src/components/dialogs/StarterPackDialog.js @@ -0,0 +1,222 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { AppBskyGraphStarterpack, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { useActorStarterPacksWithMembershipsQuery } from '#/state/queries/actor-starter-packs'; +import { useListMembershipAddMutation, useListMembershipRemoveMutation, } from '#/state/queries/list-memberships'; +import { useProfileQuery } from '#/state/queries/profile'; +import { atoms as a, native, platform, useTheme } from '#/alf'; +import { AvatarStack } from '#/components/AvatarStack'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Divider } from '#/components/Divider'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import { StarterPack } from '#/components/icons/StarterPack'; +import { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import * as bsky from '#/types/bsky'; +export function StarterPackDialog(_a) { + var control = _a.control, targetDid = _a.targetDid, enabled = _a.enabled; + var navigation = useNavigation(); + var requireEmailVerification = useRequireEmailVerification(); + var navToWizard = useCallback(function () { + control.close(); + navigation.navigate('StarterPackWizard', { + fromDialog: true, + targetDid: targetDid, + onSuccess: function () { + setTimeout(function () { + if (!control.isOpen) { + control.open(); + } + }, 0); + }, + }); + }, [navigation, control, targetDid]); + var wrappedNavToWizard = requireEmailVerification(navToWizard, { + instructions: [ + _jsx(Trans, { children: "Before creating a starter pack, you must first verify your email." }, "nav"), + ], + }); + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsx(StarterPackList, { onStartWizard: wrappedNavToWizard, targetDid: targetDid, enabled: enabled })] })); +} +function Empty(_a) { + var onStartWizard = _a.onStartWizard; + var _ = useLingui()._; + var t = useTheme(); + return (_jsxs(View, { style: [a.gap_2xl, { paddingTop: IS_WEB ? 100 : 64 }], children: [_jsxs(View, { style: [a.gap_xs, a.align_center], children: [_jsx(StarterPack, { width: 48, fill: t.atoms.border_contrast_medium.borderColor }), _jsx(Text, { style: [a.text_center], children: _jsx(Trans, { children: "You have no starter packs." }) })] }), _jsx(View, { style: [a.align_center], children: _jsxs(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Create starter pack"], ["Create starter pack"])))), color: "secondary_inverted", size: "small", onPress: onStartWizard, children: [_jsx(ButtonText, { children: _jsx(Trans, { comment: "Text on button to create a new starter pack", children: "Create" }) }), _jsx(ButtonIcon, { icon: PlusIcon })] }) })] })); +} +function StarterPackList(_a) { + var _this = this; + var onStartWizard = _a.onStartWizard, targetDid = _a.targetDid, enabled = _a.enabled; + var control = Dialog.useDialogContext(); + var _ = useLingui()._; + var subject = useProfileQuery({ did: targetDid }).data; + var _b = useActorStarterPacksWithMembershipsQuery({ did: targetDid, enabled: enabled }), data = _b.data, isError = _b.isError, isLoading = _b.isLoading, hasNextPage = _b.hasNextPage, isFetchingNextPage = _b.isFetchingNextPage, fetchNextPage = _b.fetchNextPage; + var membershipItems = (data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { return page.starterPacksWithMembership; })) || []; + var onEndReached = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isFetchingNextPage || !hasNextPage || isError) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPage()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]); + var renderItem = useCallback(function (_a) { + var item = _a.item; + return (_jsx(StarterPackItem, { starterPackWithMembership: item, targetDid: targetDid, subject: subject })); + }, [targetDid, subject]); + var onClose = useCallback(function () { + control.close(); + }, [control]); + var listHeader = (_jsxs(_Fragment, { children: [_jsxs(View, { style: [ + a.justify_between, + a.align_center, + a.flex_row, + a.pb_lg, + native(a.pt_lg), + ], children: [_jsx(Text, { style: [a.text_lg, a.font_semi_bold], children: _jsx(Trans, { children: "Add to starter packs" }) }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), onPress: onClose, variant: "ghost", color: "secondary", size: "small", shape: "round", style: { margin: -8 }, children: _jsx(ButtonIcon, { icon: XIcon }) })] }), membershipItems.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.flex_row, a.justify_between, a.align_center, a.py_md], children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold], children: _jsx(Trans, { children: "New starter pack" }) }), _jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Create starter pack"], ["Create starter pack"])))), color: "secondary_inverted", size: "small", onPress: onStartWizard, children: [_jsx(ButtonText, { children: _jsx(Trans, { comment: "Text on button to create a new starter pack", children: "Create" }) }), _jsx(ButtonIcon, { icon: PlusIcon })] })] }), _jsx(Divider, {})] }))] })); + return (_jsx(Dialog.InnerFlatList, { data: isLoading ? [{}] : membershipItems, renderItem: isLoading + ? function () { return (_jsx(View, { style: [a.align_center, a.py_2xl], children: _jsx(Loader, { size: "xl" }) })); } + : renderItem, keyExtractor: isLoading + ? function () { return 'starter_pack_dialog_loader'; } + : function (item) { return item.starterPack.uri; }, onEndReached: onEndReached, onEndReachedThreshold: 0.1, ListHeaderComponent: listHeader, ListEmptyComponent: _jsx(Empty, { onStartWizard: onStartWizard }), style: platform({ + web: [a.px_2xl, { minHeight: 500 }], + native: [a.px_2xl, a.pt_lg], + }) })); +} +function StarterPackItem(_a) { + var _b, _c; + var starterPackWithMembership = _a.starterPackWithMembership, targetDid = _a.targetDid, subject = _a.subject; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var starterPack = starterPackWithMembership.starterPack; + var isInPack = !!starterPackWithMembership.listItem; + var _d = useListMembershipAddMutation({ + subject: subject, + onSuccess: function () { + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Added to starter pack"], ["Added to starter pack"]))))); + }, + onError: function (err) { + if (!isNetworkError(err)) { + logger.error('Failed to add to starter pack', { safeMessage: err }); + } + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Failed to add to starter pack"], ["Failed to add to starter pack"])))), { type: 'error' }); + }, + }), addMembership = _d.mutate, isPendingAdd = _d.isPending; + var _e = useListMembershipRemoveMutation({ + onSuccess: function () { + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Removed from starter pack"], ["Removed from starter pack"]))))); + }, + onError: function (err) { + if (!isNetworkError(err)) { + logger.error('Failed to remove from starter pack', { safeMessage: err }); + } + Toast.show(_(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Failed to remove from starter pack"], ["Failed to remove from starter pack"])))), { type: 'error' }); + }, + }), removeMembership = _e.mutate, isPendingRemove = _e.isPending; + var isPending = isPendingAdd || isPendingRemove; + var handleToggleMembership = function () { + var _a, _b; + if (!((_a = starterPack.list) === null || _a === void 0 ? void 0 : _a.uri) || isPending) + return; + var listUri = starterPack.list.uri; + var starterPackUri = starterPack.uri; + if (!isInPack) { + addMembership({ + listUri: listUri, + actorDid: targetDid, + }); + ax.metric('starterPack:addUser', { starterPack: starterPackUri }); + } + else { + if (!((_b = starterPackWithMembership.listItem) === null || _b === void 0 ? void 0 : _b.uri)) { + console.error('Cannot remove: missing membership URI'); + return; + } + removeMembership({ + listUri: listUri, + actorDid: targetDid, + membershipUri: starterPackWithMembership.listItem.uri, + }); + ax.metric('starterPack:removeUser', { starterPack: starterPackUri }); + } + }; + var record = starterPack.record; + if (!bsky.dangerousIsType(record, AppBskyGraphStarterpack.isRecord)) { + return null; + } + return (_jsxs(View, { style: [a.flex_row, a.justify_between, a.align_center, a.py_md], children: [_jsxs(View, { children: [_jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold], numberOfLines: 1, children: record.name }), _jsx(View, { style: [a.flex_row, a.align_center, a.mt_xs], children: starterPack.listItemsSample && + starterPack.listItemsSample.length > 0 && (_jsxs(_Fragment, { children: [_jsx(AvatarStack, { size: 24, profiles: (_b = starterPack.listItemsSample) === null || _b === void 0 ? void 0 : _b.slice(0, 4).map(function (p) { return p.subject; }) }), ((_c = starterPack.list) === null || _c === void 0 ? void 0 : _c.listItemCount) && + starterPack.list.listItemCount > 4 && (_jsx(Text, { style: [ + a.text_sm, + t.atoms.text_contrast_medium, + a.ml_xs, + ], children: _jsx(Trans, { children: _jsx(Plural, { value: starterPack.list.listItemCount - 4, other: "+# more" }) }) }))] })) })] }), _jsxs(Button, { label: isInPack ? _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Remove"], ["Remove"])))) : _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Add"], ["Add"])))), color: isInPack ? 'secondary' : 'primary_subtle', size: "tiny", disabled: isPending, onPress: handleToggleMembership, children: [isPending && _jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: isInPack ? _jsx(Trans, { children: "Remove" }) : _jsx(Trans, { children: "Add" }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/components/dialogs/SwitchAccount.js b/src/components/dialogs/SwitchAccount.js new file mode 100644 index 0000000000..6fefa88054 --- /dev/null +++ b/src/components/dialogs/SwitchAccount.js @@ -0,0 +1,41 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useAccountSwitcher } from '#/lib/hooks/useAccountSwitcher'; +import { useSession } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { atoms as a } from '#/alf'; +import * as Dialog from '#/components/Dialog'; +import { AccountList } from '../AccountList'; +import { Text } from '../Typography'; +export function SwitchAccountDialog(_a) { + var control = _a.control; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var _b = useAccountSwitcher(), onPressSwitchAccount = _b.onPressSwitchAccount, pendingDid = _b.pendingDid; + var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut; + var onSelectAccount = useCallback(function (account) { + if (account.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + control.close(function () { + onPressSwitchAccount(account, 'SwitchAccount'); + }); + } + else { + control.close(); + } + }, [currentAccount, control, onPressSwitchAccount]); + var onPressAddAccount = useCallback(function () { + control.close(function () { + setShowLoggedOut(true); + }); + }, [setShowLoggedOut, control]); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Switch account"], ["Switch account"])))), children: [_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Switch account" }) }), _jsx(AccountList, { onSelectAccount: onSelectAccount, onSelectOther: onPressAddAccount, otherLabel: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Add account"], ["Add account"])))), pendingDid: pendingDid })] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dialogs/lists/CreateListFromStarterPackDialog.js b/src/components/dialogs/lists/CreateListFromStarterPackDialog.js new file mode 100644 index 0000000000..4e724b95c9 --- /dev/null +++ b/src/components/dialogs/lists/CreateListFromStarterPackDialog.js @@ -0,0 +1,197 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { AtUri, } from '@atproto/api'; +import { TID } from '@atproto/common-web'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import chunk from 'lodash.chunk'; +import { until } from '#/lib/async/until'; +import { wait } from '#/lib/async/wait'; +import { logger } from '#/logger'; +import { getAllListMembers } from '#/state/queries/list-members'; +import { useAgent, useSession } from '#/state/session'; +import { atoms as a, platform, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { CreateOrEditListDialog } from './CreateOrEditListDialog'; +export function CreateListFromStarterPackDialog(_a) { + var _this = this; + var _b; + var control = _a.control, starterPack = _a.starterPack; + var _ = useLingui()._; + var t = useTheme(); + var agent = useAgent(); + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var navigation = useNavigation(); + var queryClient = useQueryClient(); + var createDialogControl = Dialog.useDialogControl(); + var loadingDialogControl = Dialog.useDialogControl(); + var record = starterPack.record; + var onPressCreate = function () { + control.close(function () { return createDialogControl.open(); }); + }; + var addMembersAndNavigate = function (listUri) { return __awaiter(_this, void 0, void 0, function () { + var navigateToList, listItems, e_1; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + navigateToList = function () { + var urip = new AtUri(listUri); + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }); + }; + if (!starterPack.list || !currentAccount) { + loadingDialogControl.close(navigateToList); + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, wait(3000, (function () { return __awaiter(_this, void 0, void 0, function () { + var items, listitemWrites, chunks, _i, chunks_1, c; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getAllListMembers(agent, starterPack.list.uri)]; + case 1: + items = _a.sent(); + if (!(items.length > 0)) return [3 /*break*/, 7]; + listitemWrites = items.map(function (item) { + var listitemRecord = { + $type: 'app.bsky.graph.listitem', + subject: item.subject.did, + list: listUri, + createdAt: new Date().toISOString(), + }; + return { + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + rkey: TID.nextStr(), + value: listitemRecord, + }; + }); + chunks = chunk(listitemWrites, 50); + _i = 0, chunks_1 = chunks; + _a.label = 2; + case 2: + if (!(_i < chunks_1.length)) return [3 /*break*/, 5]; + c = chunks_1[_i]; + return [4 /*yield*/, agent.com.atproto.repo.applyWrites({ + repo: currentAccount.did, + writes: c, + })]; + case 3: + _a.sent(); + _a.label = 4; + case 4: + _i++; + return [3 /*break*/, 2]; + case 5: return [4 /*yield*/, until(5, 1e3, function (res) { return res.data.items.length > 0; }, function () { + return agent.app.bsky.graph.getList({ + list: listUri, + limit: 1, + }); + })]; + case 6: + _a.sent(); + _a.label = 7; + case 7: return [2 /*return*/, items]; + } + }); + }); })())]; + case 2: + listItems = _a.sent(); + queryClient.invalidateQueries({ queryKey: ['list-members', listUri] }); + ax.metric('starterPack:convertToList', { + starterPack: starterPack.uri, + memberCount: listItems.length, + }); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + logger.error('Failed to add members to list', { safeMessage: e_1 }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["List created, but failed to add some members"], ["List created, but failed to add some members"])))), { + type: 'error', + }); + return [3 /*break*/, 4]; + case 4: + loadingDialogControl.close(navigateToList); + return [2 /*return*/]; + } + }); + }); }; + var onListCreated = function (listUri) { + loadingDialogControl.open(); + addMembersAndNavigate(listUri); + }; + return (_jsxs(_Fragment, { children: [_jsxs(Dialog.Outer, { control: control, testID: "createListFromStarterPackDialog", nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Create list from starter pack"], ["Create list from starter pack"])))), style: web({ maxWidth: 400 }), children: [_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [a.text_xl, a.font_bold], children: _jsx(Trans, { children: "Create list from starter pack" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "This will create a new list with the same name, description, and members as this starter pack." }) }), _jsx(Admonition, { type: "tip", children: _jsx(Trans, { children: "Changes to the starter pack will not be reflected in the list after creation. The list will be an independent copy." }) }), _jsxs(View, { style: [ + platform({ + web: [a.flex_row_reverse], + native: [a.flex_col], + }), + a.gap_md, + a.pt_sm, + ], children: [_jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Create list"], ["Create list"])))), onPress: onPressCreate, size: platform({ + web: 'small', + native: 'large', + }), color: "primary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Create list" }) }) }), _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: function () { return control.close(); }, size: platform({ + web: 'small', + native: 'large', + }), color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] })] }), _jsx(Dialog.Close, {})] })] }), _jsx(CreateOrEditListDialog, { control: createDialogControl, purpose: "app.bsky.graph.defs#curatelist", onSave: onListCreated, initialValues: { + name: record.name, + description: record.description, + avatar: (_b = starterPack.list) === null || _b === void 0 ? void 0 : _b.avatar, + } }), _jsxs(Dialog.Outer, { control: loadingDialogControl, nativeOptions: { preventDismiss: true }, children: [_jsx(Dialog.Handle, {}), _jsx(Dialog.ScrollableInner, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Adding members to list..."], ["Adding members to list..."])))), style: web({ maxWidth: 400 }), children: _jsxs(View, { style: [a.align_center, a.gap_lg, a.py_5xl], children: [_jsx(Loader, { size: "xl" }), _jsx(Text, { style: [a.text_lg, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "Adding members to list..." }) })] }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/dialogs/lists/CreateOrEditListDialog.js b/src/components/dialogs/lists/CreateOrEditListDialog.js new file mode 100644 index 0000000000..f6b5496c73 --- /dev/null +++ b/src/components/dialogs/lists/CreateOrEditListDialog.js @@ -0,0 +1,298 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import { RichText as RichTextAPI } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { cleanError } from '#/lib/strings/errors'; +import { isOverMaxGraphemeCount } from '#/lib/strings/helpers'; +import { richTextToString } from '#/lib/strings/rich-text-helpers'; +import { shortenLinks, stripInvalidMentions } from '#/lib/strings/rich-text-manip'; +import { logger } from '#/logger'; +import { useListCreateMutation, useListMetadataMutation, } from '#/state/queries/list'; +import { useAgent } from '#/state/session'; +import { ErrorMessage } from '#/view/com/util/error/ErrorMessage'; +import * as Toast from '#/view/com/util/Toast'; +import { EditableUserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextField from '#/components/forms/TextField'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +var DISPLAY_NAME_MAX_GRAPHEMES = 64; +var DESCRIPTION_MAX_GRAPHEMES = 300; +export function CreateOrEditListDialog(_a) { + var control = _a.control, list = _a.list, purpose = _a.purpose, onSave = _a.onSave, initialValues = _a.initialValues; + var _ = useLingui()._; + var cancelControl = Dialog.useDialogControl(); + var _b = useState(false), dirty = _b[0], setDirty = _b[1]; + var height = useWindowDimensions().height; + // 'You might lose unsaved changes' warning + useEffect(function () { + if (IS_WEB && dirty) { + var abortController_1 = new AbortController(); + var signal = abortController_1.signal; + window.addEventListener('beforeunload', function (evt) { return evt.preventDefault(); }, { + signal: signal, + }); + return function () { + abortController_1.abort(); + }; + } + }, [dirty]); + var onPressCancel = useCallback(function () { + if (dirty) { + cancelControl.open(); + } + else { + control.close(); + } + }, [dirty, control, cancelControl]); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { + preventDismiss: dirty, + minHeight: height, + }, testID: "createOrEditListDialog", children: [_jsx(DialogInner, { list: list, purpose: purpose, onSave: onSave, setDirty: setDirty, onPressCancel: onPressCancel, initialValues: initialValues }), _jsx(Prompt.Basic, { control: cancelControl, title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Discard changes?"], ["Discard changes?"])))), description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Are you sure you want to discard your changes?"], ["Are you sure you want to discard your changes?"])))), onConfirm: function () { return control.close(); }, confirmButtonCta: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Discard"], ["Discard"])))), confirmButtonColor: "negative" })] })); +} +function DialogInner(_a) { + var _this = this; + var _b; + var list = _a.list, purpose = _a.purpose, onSave = _a.onSave, setDirty = _a.setDirty, onPressCancel = _a.onPressCancel, initialValues = _a.initialValues; + var activePurpose = useMemo(function () { + if (list === null || list === void 0 ? void 0 : list.purpose) { + return list.purpose; + } + if (purpose) { + return purpose; + } + return 'app.bsky.graph.defs#curatelist'; + }, [list, purpose]); + var isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist'; + var _ = useLingui()._; + var t = useTheme(); + var agent = useAgent(); + var control = Dialog.useDialogContext(); + var _c = useListCreateMutation(), createListMutation = _c.mutateAsync, createListError = _c.error, isCreateListError = _c.isError, isCreatingList = _c.isPending; + var _d = useListMetadataMutation(), updateListMutation = _d.mutateAsync, updateListError = _d.error, isUpdateListError = _d.isError, isUpdatingList = _d.isPending; + var _e = useState(''), imageError = _e[0], setImageError = _e[1]; + var _f = useState(false), displayNameTooShort = _f[0], setDisplayNameTooShort = _f[1]; + var initialDisplayName = (list === null || list === void 0 ? void 0 : list.name) || (initialValues === null || initialValues === void 0 ? void 0 : initialValues.name) || ''; + var _g = useState(initialDisplayName), displayName = _g[0], setDisplayName = _g[1]; + var initialDescription = (list === null || list === void 0 ? void 0 : list.description) || (initialValues === null || initialValues === void 0 ? void 0 : initialValues.description) || ''; + var _h = useState(function () { + var _a; + var text = (_a = list === null || list === void 0 ? void 0 : list.description) !== null && _a !== void 0 ? _a : initialValues === null || initialValues === void 0 ? void 0 : initialValues.description; + var facets = list === null || list === void 0 ? void 0 : list.descriptionFacets; + if (!text || !facets) { + return new RichTextAPI({ text: text || '' }); + } + // We want to be working with a blank state here, so let's get the + // serialized version and turn it back into a RichText + var serialized = richTextToString(new RichTextAPI({ text: text, facets: facets }), false); + var richText = new RichTextAPI({ text: serialized }); + richText.detectFacetsWithoutResolution(); + return richText; + }), descriptionRt = _h[0], setDescriptionRt = _h[1]; + var initialAvatar = (_b = list === null || list === void 0 ? void 0 : list.avatar) !== null && _b !== void 0 ? _b : initialValues === null || initialValues === void 0 ? void 0 : initialValues.avatar; + var _j = useState(initialAvatar), listAvatar = _j[0], setListAvatar = _j[1]; + var _k = useState(), newListAvatar = _k[0], setNewListAvatar = _k[1]; + // When creating with pre-filled values (from starter pack), consider dirty + // immediately so the Save button is enabled + var hasInitialValuesForCreate = !list && initialValues != null; + var dirty = hasInitialValuesForCreate || + displayName !== initialDisplayName || + descriptionRt.text !== initialDescription || + listAvatar !== initialAvatar; + useEffect(function () { + setDirty(dirty); + }, [dirty, setDirty]); + var onSelectNewAvatar = useCallback(function (img) { + setImageError(''); + if (img === null) { + setNewListAvatar(null); + setListAvatar(null); + return; + } + try { + setNewListAvatar(img); + setListAvatar(img.path); + } + catch (e) { + setImageError(cleanError(e)); + } + }, [setNewListAvatar, setListAvatar, setImageError]); + var onPressSave = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var richText, uri_1, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setImageError(''); + setDisplayNameTooShort(false); + _a.label = 1; + case 1: + _a.trys.push([1, 7, , 8]); + if (displayName.length === 0) { + setDisplayNameTooShort(true); + return [2 /*return*/]; + } + richText = new RichTextAPI({ text: descriptionRt.text.trimEnd() }, { cleanNewlines: true }); + return [4 /*yield*/, richText.detectFacets(agent)]; + case 2: + _a.sent(); + richText = shortenLinks(richText); + richText = stripInvalidMentions(richText); + if (!list) return [3 /*break*/, 4]; + return [4 /*yield*/, updateListMutation({ + uri: list.uri, + name: displayName, + description: richText.text, + descriptionFacets: richText.facets, + avatar: newListAvatar, + })]; + case 3: + _a.sent(); + Toast.show(isCurateList + ? _(msg({ message: 'User list updated', context: 'toast' })) + : _(msg({ message: 'Moderation list updated', context: 'toast' }))); + control.close(function () { return onSave === null || onSave === void 0 ? void 0 : onSave(list.uri); }); + return [3 /*break*/, 6]; + case 4: return [4 /*yield*/, createListMutation({ + purpose: activePurpose, + name: displayName, + description: richText.text, + descriptionFacets: richText.facets, + avatar: newListAvatar, + })]; + case 5: + uri_1 = (_a.sent()).uri; + Toast.show(isCurateList + ? _(msg({ message: 'User list created', context: 'toast' })) + : _(msg({ message: 'Moderation list created', context: 'toast' }))); + control.close(function () { return onSave === null || onSave === void 0 ? void 0 : onSave(uri_1); }); + _a.label = 6; + case 6: return [3 /*break*/, 8]; + case 7: + e_1 = _a.sent(); + logger.error('Failed to create/edit list', { message: String(e_1) }); + return [3 /*break*/, 8]; + case 8: return [2 /*return*/]; + } + }); + }); }, [ + list, + createListMutation, + updateListMutation, + onSave, + control, + displayName, + descriptionRt, + newListAvatar, + setImageError, + activePurpose, + isCurateList, + agent, + _, + ]); + var displayNameTooLong = isOverMaxGraphemeCount({ + text: displayName, + maxCount: DISPLAY_NAME_MAX_GRAPHEMES, + }); + var descriptionTooLong = isOverMaxGraphemeCount({ + text: descriptionRt, + maxCount: DESCRIPTION_MAX_GRAPHEMES, + }); + var cancelButton = useCallback(function () { return (_jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: onPressCancel, size: "small", color: "primary", variant: "ghost", style: [a.rounded_full], testID: "editProfileCancelBtn", children: _jsx(ButtonText, { style: [a.text_md], children: _jsx(Trans, { children: "Cancel" }) }) })); }, [onPressCancel, _]); + var saveButton = useCallback(function () { return (_jsxs(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Save"], ["Save"])))), onPress: onPressSave, disabled: !dirty || + isCreatingList || + isUpdatingList || + displayNameTooLong || + descriptionTooLong, size: "small", color: "primary", variant: "ghost", style: [a.rounded_full], testID: "editProfileSaveBtn", children: [_jsx(ButtonText, { style: [a.text_md, !dirty && t.atoms.text_contrast_low], children: _jsx(Trans, { children: "Save" }) }), (isCreatingList || isUpdatingList) && _jsx(ButtonIcon, { icon: Loader })] })); }, [ + _, + t, + dirty, + onPressSave, + isCreatingList, + isUpdatingList, + displayNameTooLong, + descriptionTooLong, + ]); + var onChangeDisplayName = useCallback(function (text) { + setDisplayName(text); + if (text.length > 0 && displayNameTooShort) { + setDisplayNameTooShort(false); + } + }, [displayNameTooShort]); + var onChangeDescription = useCallback(function (newText) { + var richText = new RichTextAPI({ text: newText }); + richText.detectFacetsWithoutResolution(); + setDescriptionRt(richText); + }, [setDescriptionRt]); + var title = list + ? isCurateList + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Edit user list"], ["Edit user list"])))) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Edit moderation list"], ["Edit moderation list"])))) + : isCurateList + ? _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Create user list"], ["Create user list"])))) + : _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Create moderation list"], ["Create moderation list"])))); + var displayNamePlaceholder = isCurateList + ? _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["e.g. Great Posters"], ["e.g. Great Posters"])))) + : _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["e.g. Spammers"], ["e.g. Spammers"])))); + var descriptionPlaceholder = isCurateList + ? _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["e.g. The posters who never miss."], ["e.g. The posters who never miss."])))) + : _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["e.g. Users that repeatedly reply with ads."], ["e.g. Users that repeatedly reply with ads."])))); + return (_jsxs(Dialog.ScrollableInner, { label: title, style: [a.overflow_hidden, web({ maxWidth: 500 })], contentContainerStyle: [a.px_0, a.pt_0], header: _jsx(Dialog.Header, { renderLeft: cancelButton, renderRight: saveButton, children: _jsx(Dialog.HeaderText, { children: title }) }), children: [isUpdateListError && (_jsx(ErrorMessage, { message: cleanError(updateListError) })), isCreateListError && (_jsx(ErrorMessage, { message: cleanError(createListError) })), imageError !== '' && _jsx(ErrorMessage, { message: imageError }), _jsxs(View, { style: [a.pt_xl, a.px_xl, a.gap_xl], children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "List avatar" }) }), _jsx(View, { style: [a.align_start], children: _jsx(EditableUserAvatar, { size: 80, avatar: listAvatar, onSelectNewAvatar: onSelectNewAvatar, type: "list" }) })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "List name" }) }), _jsx(TextField.Root, { isInvalid: displayNameTooLong || displayNameTooShort, children: _jsx(Dialog.Input, { defaultValue: displayName, onChangeText: onChangeDisplayName, label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Name"], ["Name"])))), placeholder: displayNamePlaceholder, testID: "editListNameInput" }) }), (displayNameTooLong || displayNameTooShort) && (_jsx(Text, { style: [ + a.text_sm, + a.mt_xs, + a.font_bold, + { color: t.palette.negative_400 }, + ], children: displayNameTooLong ? (_jsxs(Trans, { children: ["List name is too long.", ' ', _jsx(Plural, { value: DISPLAY_NAME_MAX_GRAPHEMES, other: "The maximum number of characters is #." })] })) : displayNameTooShort ? (_jsx(Trans, { children: "List must have a name." })) : null }))] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "List description" }) }), _jsx(TextField.Root, { isInvalid: descriptionTooLong, children: _jsx(Dialog.Input, { defaultValue: descriptionRt.text, onChangeText: onChangeDescription, multiline: true, label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Description"], ["Description"])))), placeholder: descriptionPlaceholder, testID: "editListDescriptionInput" }) }), descriptionTooLong && (_jsx(Text, { style: [ + a.text_sm, + a.mt_xs, + a.font_bold, + { color: t.palette.negative_400 }, + ], children: _jsxs(Trans, { children: ["List description is too long.", ' ', _jsx(Plural, { value: DESCRIPTION_MAX_GRAPHEMES, other: "The maximum number of characters is #." })] }) }))] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15; diff --git a/src/components/dialogs/lists/ListAddRemoveUsersDialog.js b/src/components/dialogs/lists/ListAddRemoveUsersDialog.js new file mode 100644 index 0000000000..aeb56ce220 --- /dev/null +++ b/src/components/dialogs/lists/ListAddRemoveUsersDialog.js @@ -0,0 +1,78 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useMemo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { cleanError } from '#/lib/strings/errors'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { getMembership, useDangerousListMembershipsQuery, useListMembershipAddMutation, useListMembershipRemoveMutation, } from '#/state/queries/list-memberships'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { SearchablePeopleList, } from '#/components/dialogs/SearchablePeopleList'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +export function ListAddRemoveUsersDialog(_a) { + var control = _a.control, list = _a.list, onChange = _a.onChange; + return (_jsxs(Dialog.Outer, { control: control, testID: "listAddRemoveUsersDialog", children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { list: list, onChange: onChange })] })); +} +function DialogInner(_a) { + var list = _a.list, onChange = _a.onChange; + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + var memberships = useDangerousListMembershipsQuery().data; + var renderProfileCard = useCallback(function (item) { + return (_jsx(UserResult, { profile: item.profile, onChange: onChange, memberships: memberships, list: list, moderationOpts: moderationOpts })); + }, [onChange, memberships, list, moderationOpts]); + return (_jsx(SearchablePeopleList, { title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Add people to list"], ["Add people to list"])))), renderProfileCard: renderProfileCard })); +} +function UserResult(_a) { + var profile = _a.profile, list = _a.list, memberships = _a.memberships, onChange = _a.onChange, moderationOpts = _a.moderationOpts; + var _ = useLingui()._; + var membership = useMemo(function () { return getMembership(memberships, list.uri, profile.did); }, [memberships, list.uri, profile.did]); + var _b = useListMembershipAddMutation({ + onSuccess: function () { + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Added to list"], ["Added to list"]))))); + onChange === null || onChange === void 0 ? void 0 : onChange('add', profile); + }, + onError: function (e) { return Toast.show(cleanError(e), 'xmark'); }, + }), listMembershipAdd = _b.mutate, isAddingPending = _b.isPending; + var _c = useListMembershipRemoveMutation({ + onSuccess: function () { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Removed from list"], ["Removed from list"]))))); + onChange === null || onChange === void 0 ? void 0 : onChange('remove', profile); + }, + onError: function (e) { return Toast.show(cleanError(e), 'xmark'); }, + }), listMembershipRemove = _c.mutate, isRemovingPending = _c.isPending; + var isMutating = isAddingPending || isRemovingPending; + var onToggleMembership = useCallback(function () { + if (typeof membership === 'undefined') { + return; + } + if (membership === false) { + listMembershipAdd({ + listUri: list.uri, + actorDid: profile.did, + }); + } + else { + listMembershipRemove({ + listUri: list.uri, + actorDid: profile.did, + membershipUri: membership, + }); + } + }, [list, profile, membership, listMembershipAdd, listMembershipRemove]); + if (!moderationOpts) + return null; + return (_jsx(View, { style: [a.flex_1, a.py_sm, a.px_lg], children: _jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts }), _jsxs(View, { style: [a.flex_1], children: [_jsx(ProfileCard.Name, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.Handle, { profile: profile })] }), membership !== undefined && (_jsx(Button, { label: membership === false + ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Add user to list"], ["Add user to list"])))) + : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Remove user from list"], ["Remove user from list"])))), onPress: onToggleMembership, disabled: isMutating, size: "small", variant: "solid", color: "secondary", children: isMutating ? (_jsx(ButtonIcon, { icon: Loader })) : (_jsx(ButtonText, { children: membership === false ? (_jsx(Trans, { children: "Add" })) : (_jsx(Trans, { children: "Remove" })) })) }))] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/dialogs/nuxs/ActivitySubscriptions.js b/src/components/dialogs/nuxs/ActivitySubscriptions.js new file mode 100644 index 0000000000..8c1abefde4 --- /dev/null +++ b/src/components/dialogs/nuxs/ActivitySubscriptions.js @@ -0,0 +1,108 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useNuxDialogContext } from '#/components/dialogs/nuxs'; +import { Sparkle_Stroke2_Corner0_Rounded as SparkleIcon } from '#/components/icons/Sparkle'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +export function ActivitySubscriptionsNUX() { + var t = useTheme(); + var _ = useLingui()._; + var nuxDialogs = useNuxDialogContext(); + var control = Dialog.useDialogControl(); + Dialog.useAutoOpen(control); + var onClose = useCallback(function () { + nuxDialogs.dismissActiveNux(); + }, [nuxDialogs]); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Introducing activity notifications"], ["Introducing activity notifications"])))), style: [web({ maxWidth: 400 })], contentContainerStyle: [ + { + paddingTop: 0, + paddingLeft: 0, + paddingRight: 0, + }, + ], children: [_jsxs(View, { style: [ + a.align_center, + a.overflow_hidden, + t.atoms.bg_contrast_25, + { + gap: IS_WEB ? 16 : 24, + paddingTop: IS_WEB ? 24 : 48, + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }, + ], children: [_jsxs(View, { style: [ + a.pl_sm, + a.pr_md, + a.py_sm, + a.rounded_full, + a.flex_row, + a.align_center, + a.gap_xs, + { + backgroundColor: t.palette.primary_100, + }, + ], children: [_jsx(SparkleIcon, { fill: t.palette.primary_800, size: "sm" }), _jsx(Text, { style: [ + a.font_semi_bold, + { + color: t.palette.primary_800, + }, + ], children: _jsx(Trans, { children: "New Feature" }) })] }), _jsxs(View, { style: [a.relative, a.w_full], children: [_jsx(View, { style: [ + a.absolute, + t.atoms.bg_contrast_25, + t.atoms.shadow_md, + { + shadowOpacity: 0.4, + top: 5, + bottom: 0, + left: '17%', + right: '17%', + width: '66%', + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + }, + ] }), _jsx(View, { style: [ + a.overflow_hidden, + { + aspectRatio: 398 / 228, + }, + ], children: _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../../assets/images/activity_notifications_announcement.webp'), style: [ + a.w_full, + { + aspectRatio: 398 / 268, + }, + ], alt: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature."], ["A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature."])))) }) })] })] }), _jsxs(View, { style: [ + a.align_center, + a.px_xl, + IS_WEB ? [a.pt_xl, a.gap_xl, a.pb_sm] : [a.pt_3xl, a.gap_3xl], + ], children: [_jsxs(View, { style: [a.gap_md, a.align_center], children: [_jsx(Text, { style: [ + a.text_3xl, + a.leading_tight, + a.font_bold, + a.text_center, + { + fontSize: IS_WEB ? 28 : 32, + maxWidth: 300, + }, + ], children: _jsx(Trans, { children: "Get notified when someone posts" }) }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + a.text_center, + { + maxWidth: 340, + }, + ], children: _jsx(Trans, { children: "You can now choose to be notified when specific people post. If there\u2019s someone you want timely updates from, go to their profile and find the new bell icon near the follow button." }) })] }), !IS_WEB && (_jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", variant: "solid", color: "primary", onPress: function () { + control.close(); + }, style: [a.w_full, { maxWidth: 280 }], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }))] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/dialogs/nuxs/BookmarksAnnouncement.js b/src/components/dialogs/nuxs/BookmarksAnnouncement.js new file mode 100644 index 0000000000..d7b7b4c7ab --- /dev/null +++ b/src/components/dialogs/nuxs/BookmarksAnnouncement.js @@ -0,0 +1,107 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme, web } from '#/alf'; +import { transparentifyColor } from '#/alf/util/colorGeneration'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useNuxDialogContext } from '#/components/dialogs/nuxs'; +import { Sparkle_Stroke2_Corner0_Rounded as SparkleIcon } from '#/components/icons/Sparkle'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +export function BookmarksAnnouncement() { + var t = useTheme(); + var _ = useLingui()._; + var nuxDialogs = useNuxDialogContext(); + var control = Dialog.useDialogControl(); + Dialog.useAutoOpen(control); + var onClose = useCallback(function () { + nuxDialogs.dismissActiveNux(); + }, [nuxDialogs]); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Introducing saved posts AKA bookmarks"], ["Introducing saved posts AKA bookmarks"])))), style: [web({ maxWidth: 440 })], contentContainerStyle: [ + { + paddingTop: 0, + paddingLeft: 0, + paddingRight: 0, + }, + ], children: [_jsxs(View, { style: [ + a.align_center, + a.overflow_hidden, + { + gap: 16, + paddingTop: IS_WEB ? 24 : 40, + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }, + ], children: [_jsx(LinearGradient, { colors: [t.palette.primary_25, t.palette.primary_100], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 0, y: 1 }, style: [a.absolute, a.inset_0] }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(SparkleIcon, { fill: t.palette.primary_800, size: "sm" }), _jsx(Text, { style: [ + a.font_semi_bold, + { + color: t.palette.primary_800, + }, + ], children: _jsx(Trans, { children: "New Feature" }) })] }), _jsx(View, { style: [ + a.relative, + a.w_full, + { + paddingTop: 8, + paddingHorizontal: 32, + paddingBottom: 32, + }, + ], children: _jsx(View, { style: [ + { + borderRadius: 24, + aspectRatio: 333 / 104, + }, + IS_WEB + ? [ + { + boxShadow: "0px 10px 15px -3px ".concat(transparentifyColor(t.palette.black, 0.2)), + }, + ] + : [ + t.atoms.shadow_md, + { + shadowOpacity: 0.2, + shadowOffset: { + width: 0, + height: 10, + }, + }, + ], + ], children: _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../../assets/images/bookmarks_announcement_nux.webp'), style: [ + a.w_full, + { + aspectRatio: 333 / 104, + }, + ], alt: _(msg({ + message: "A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads \"inventing a saturday that immediately follows monday\".", + comment: 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', + })) }) }) })] }), _jsxs(View, { style: [a.align_center, a.px_xl, a.pt_xl, a.gap_2xl, a.pb_sm], children: [_jsxs(View, { style: [a.gap_sm, a.align_center], children: [_jsx(Text, { style: [ + a.text_3xl, + a.leading_tight, + a.font_bold, + a.text_center, + { + fontSize: IS_WEB ? 28 : 32, + maxWidth: 300, + }, + ], children: _jsx(Trans, { children: "Saved Posts" }) }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + a.text_center, + { + maxWidth: 340, + }, + ], children: _jsx(Trans, { children: "Finally! Keep track of posts that matter to you. Save them to revisit anytime." }) })] }), !IS_WEB && (_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", color: "primary", onPress: function () { + control.close(); + }, style: [a.w_full], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }))] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dialogs/nuxs/DraftsAnnouncement.js b/src/components/dialogs/nuxs/DraftsAnnouncement.js new file mode 100644 index 0000000000..fdc7d9c0b2 --- /dev/null +++ b/src/components/dialogs/nuxs/DraftsAnnouncement.js @@ -0,0 +1,80 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useNuxDialogContext } from '#/components/dialogs/nuxs'; +import { Sparkle_Stroke2_Corner0_Rounded as SparkleIcon } from '#/components/icons/Sparkle'; +import { Text } from '#/components/Typography'; +import { IS_E2E, IS_NATIVE, IS_WEB } from '#/env'; +import { createIsEnabledCheck, isExistingUserAsOf } from './utils'; +export var enabled = createIsEnabledCheck(function (props) { + return (!IS_E2E && + IS_NATIVE && + isExistingUserAsOf('2026-02-05T00:00:00.000Z', props.currentProfile.createdAt)); +}); +export function DraftsAnnouncement() { + var t = useTheme(); + var _ = useLingui()._; + var nuxDialogs = useNuxDialogContext(); + var control = Dialog.useDialogControl(); + Dialog.useAutoOpen(control); + var onClose = useCallback(function () { + nuxDialogs.dismissActiveNux(); + }, [nuxDialogs]); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, { fill: t.palette.primary_400 }), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Introducing drafts"], ["Introducing drafts"])))), style: [web({ maxWidth: 440 })], contentContainerStyle: [ + { + paddingTop: 0, + paddingLeft: 0, + paddingRight: 0, + }, + ], children: [_jsxs(View, { style: [ + a.align_center, + a.overflow_hidden, + { + paddingTop: IS_WEB ? 24 : 40, + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }, + ], children: [_jsx(LinearGradient, { colors: [t.palette.primary_100, t.palette.primary_200], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 0, y: 1 }, style: [a.absolute, a.inset_0] }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs, { marginBottom: -12 }], children: [_jsx(SparkleIcon, { fill: t.palette.primary_800, size: "sm" }), _jsx(Text, { style: [ + a.font_semi_bold, + { + color: t.palette.primary_800, + }, + ], children: _jsx(Trans, { children: "New Feature" }) })] }), _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../../assets/images/drafts_announcement_nux.webp'), style: [ + a.w_full, + { + aspectRatio: 393 / 226, + }, + ], alt: _(msg({ + message: "A screenshot of the post composer with a new button next to the post button that says \"Drafts\", with a rainbow firework effect. Below, the text in the composer reads \"Hey, did you hear the news? Bluesky has drafts now!!!\".", + comment: 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', + })) })] }), _jsxs(View, { style: [a.align_center, a.px_xl, a.pt_xl, a.gap_2xl, a.pb_sm], children: [_jsxs(View, { style: [a.gap_sm, a.align_center], children: [_jsx(Text, { style: [ + a.text_3xl, + a.leading_tight, + a.font_bold, + a.text_center, + { + fontSize: IS_WEB ? 28 : 32, + maxWidth: 300, + }, + ], children: _jsx(Trans, { children: "Drafts" }) }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + a.text_center, + { + maxWidth: 340, + }, + ], children: _jsx(Trans, { children: "Not ready to hit post? Keep your best ideas in Drafts until the timing is just right." }) })] }), !IS_WEB && (_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", color: "primary", onPress: function () { return control.close(); }, style: [a.w_full], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Finally!" }) }) }))] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dialogs/nuxs/FindContactsAnnouncement.js b/src/components/dialogs/nuxs/FindContactsAnnouncement.js new file mode 100644 index 0000000000..cd8b48bab3 --- /dev/null +++ b/src/components/dialogs/nuxs/FindContactsAnnouncement.js @@ -0,0 +1,67 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { isFindContactsFeatureEnabled } from '#/components/contacts/country-allowlist'; +import * as Dialog from '#/components/Dialog'; +import { useNuxDialogContext } from '#/components/dialogs/nuxs'; +import { createIsEnabledCheck, isExistingUserAsOf, } from '#/components/dialogs/nuxs/utils'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_E2E, IS_NATIVE, IS_WEB } from '#/env'; +import { navigate } from '#/Navigation'; +export var enabled = createIsEnabledCheck(function (props) { + return (!IS_E2E && + IS_NATIVE && + isExistingUserAsOf('2025-12-16T00:00:00.000Z', props.currentProfile.createdAt) && + isFindContactsFeatureEnabled(props.geolocation.countryCode)); +}); +export function FindContactsAnnouncement() { + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var nuxDialogs = useNuxDialogContext(); + var control = Dialog.useDialogControl(); + Dialog.useAutoOpen(control); + var onClose = useCallback(function () { + nuxDialogs.dismissActiveNux(); + }, [nuxDialogs]); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Introducing finding friends via contacts"], ["Introducing finding friends via contacts"])))), style: [web({ maxWidth: 440 })], contentContainerStyle: [ + { + paddingTop: 0, + paddingLeft: 0, + paddingRight: 0, + }, + ], children: [_jsxs(View, { style: [a.align_center, a.pt_3xl], children: [_jsx(LinearGradient, { colors: [t.palette.primary_200, t.atoms.bg.backgroundColor], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 0, y: 1 }, style: [a.absolute, a.inset_0] }), _jsx(View, { style: [a.w_full, a.pt_sm, a.px_5xl, a.pb_4xl], children: _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../../assets/images/find_friends_illustration.webp'), style: [a.w_full, { aspectRatio: 1278 / 661 }], alt: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["An illustration depicting user avatars flowing from a contact book into the Bluesky app"], ["An illustration depicting user avatars flowing from a contact book into the Bluesky app"])))) }) })] }), _jsxs(View, { style: [a.align_center, a.px_xl, a.gap_5xl], children: [_jsxs(View, { style: [a.gap_sm, a.align_center], children: [_jsx(Text, { style: [ + a.text_4xl, + a.leading_tight, + a.font_bold, + a.text_center, + { + fontSize: IS_WEB ? 28 : 32, + maxWidth: 300, + }, + ], children: _jsx(Trans, { children: "Find your friends" }) }), _jsx(Text, { style: [ + a.text_md, + t.atoms.text_contrast_medium, + a.leading_snug, + a.text_center, + { maxWidth: 340 }, + ], children: _jsx(Trans, { children: "Bluesky is more fun with friends! Import your contacts to see who\u2019s already here." }) })] }), _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Import Contacts"], ["Import Contacts"])))), size: "large", color: "primary", onPress: function () { + ax.metric('contacts:nux:ctaPressed', {}); + control.close(function () { + navigate('FindContactsFlow'); + }); + }, style: [a.w_full], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Import Contacts" }) }) })] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.js b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.js new file mode 100644 index 0000000000..a71642ae0c --- /dev/null +++ b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.js @@ -0,0 +1,80 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { urls } from '#/lib/constants'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useNuxDialogContext } from '#/components/dialogs/nuxs'; +import { Sparkle_Stroke2_Corner0_Rounded as SparkleIcon } from '#/components/icons/Sparkle'; +import { VerifierCheck } from '#/components/icons/VerifierCheck'; +import { Link } from '#/components/Link'; +import { Span, Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +export function InitialVerificationAnnouncement() { + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var gtMobile = useBreakpoints().gtMobile; + var nuxDialogs = useNuxDialogContext(); + var control = Dialog.useDialogControl(); + Dialog.useAutoOpen(control); + var onClose = useCallback(function () { + nuxDialogs.dismissActiveNux(); + }, [nuxDialogs]); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Announcing verification on Bluesky"], ["Announcing verification on Bluesky"])))), style: [ + gtMobile ? { width: 'auto', maxWidth: 400, minWidth: 200 } : a.w_full, + ], children: [_jsxs(View, { style: [a.align_start, a.gap_xl], children: [_jsxs(View, { style: [ + a.pl_sm, + a.pr_md, + a.py_sm, + a.rounded_full, + a.flex_row, + a.align_center, + a.gap_xs, + { + backgroundColor: t.palette.primary_25, + }, + ], children: [_jsx(SparkleIcon, { fill: t.palette.primary_700, size: "sm" }), _jsx(Text, { style: [ + a.font_semi_bold, + { + color: t.palette.primary_700, + }, + ], children: _jsx(Trans, { children: "New Feature" }) })] }), _jsx(View, { style: [ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + { minHeight: 100 }, + ], children: _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../../assets/images/initial_verification_announcement_1.png'), style: [ + { + aspectRatio: 353 / 160, + }, + ], alt: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts."], ["An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts."])))) }) }), _jsxs(View, { style: [a.gap_xs], children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { children: "A new form of verification" }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsx(Trans, { children: "We\u2019re introducing a new layer of verification on Bluesky \u2014 an easy-to-see checkmark." }) })] }), _jsx(View, { style: [ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + { minHeight: 100 }, + ], children: _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../../assets/images/initial_verification_announcement_2.png'), style: [ + { + aspectRatio: 353 / 196, + }, + ], alt: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name."], ["An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name."])))) }) }), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(VerifierCheck, { width: 14 }), _jsx(Text, { style: [a.text_lg, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { children: "Who can verify?" }) })] }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsx(Trans, { children: "Bluesky will proactively verify notable and authentic accounts." }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsxs(Trans, { children: ["Trust emerges from relationships, communities, and shared context, so we\u2019re also enabling", ' ', _jsx(Span, { style: [a.font_semi_bold], children: "trusted verifiers" }), ": organizations that can directly issue verification."] }) }), _jsx(Text, { style: [a.leading_snug, a.text_md], children: _jsx(Trans, { children: "When you tap on a check, you\u2019ll see which organizations have granted verification." }) })] })] }), _jsxs(View, { style: [a.w_full, a.gap_md], children: [_jsx(Link, { overridePresentation: true, to: urls.website.blog.initialVerificationAnnouncement, label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Read blog post"], ["Read blog post"])))), size: "small", variant: "solid", color: "primary", style: [a.justify_center, a.w_full], onPress: function () { + ax.metric('verification:learn-more', { + location: 'initialAnnouncementeNux', + }); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Read blog post" }) }) }), IS_NATIVE && (_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Close"], ["Close"])))), size: "small", variant: "solid", color: "secondary", style: [a.justify_center, a.w_full], onPress: function () { + control.close(); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }))] })] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/dialogs/nuxs/LiveNowBetaDialog.js b/src/components/dialogs/nuxs/LiveNowBetaDialog.js new file mode 100644 index 0000000000..1708eb6827 --- /dev/null +++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.js @@ -0,0 +1,123 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useMemo } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, select, useTheme, utils, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useNuxDialogContext } from '#/components/dialogs/nuxs'; +import { createIsEnabledCheck, isExistingUserAsOf, } from '#/components/dialogs/nuxs/utils'; +import { Beaker_Stroke2_Corner2_Rounded as BeakerIcon } from '#/components/icons/Beaker'; +import { Text } from '#/components/Typography'; +import { IS_E2E, IS_WEB } from '#/env'; +export var enabled = createIsEnabledCheck(function (props) { + return (!IS_E2E && + isExistingUserAsOf('2026-01-16T00:00:00.000Z', props.currentProfile.createdAt) && + !props.features.enabled(props.features.LiveNowBetaDisable)); +}); +export function LiveNowBetaDialog() { + var t = useTheme(); + var _ = useLingui()._; + var nuxDialogs = useNuxDialogContext(); + var control = Dialog.useDialogControl(); + Dialog.useAutoOpen(control); + var onClose = useCallback(function () { + nuxDialogs.dismissActiveNux(); + }, [nuxDialogs]); + var shadowColor = useMemo(function () { + return select(t.name, { + light: utils.alpha(t.palette.primary_900, 0.4), + dark: utils.alpha(t.palette.primary_25, 0.4), + dim: utils.alpha(t.palette.primary_25, 0.4), + }); + }, [t]); + return (_jsxs(Dialog.Outer, { control: control, onClose: onClose, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, { fill: t.palette.primary_700 }), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Show when you\u2019re live"], ["Show when you\u2019re live"])))), style: [web({ maxWidth: 440 })], contentContainerStyle: [ + { + paddingTop: 0, + paddingLeft: 0, + paddingRight: 0, + }, + ], children: [_jsxs(View, { style: [ + a.align_center, + a.overflow_hidden, + { + gap: 16, + paddingTop: IS_WEB ? 24 : 40, + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }, + ], children: [_jsx(LinearGradient, { colors: [ + t.palette.primary_100, + utils.alpha(t.palette.primary_100, 0), + ], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 0, y: 1 }, style: [a.absolute, a.inset_0] }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(BeakerIcon, { fill: t.palette.primary_700, size: "sm" }), _jsx(Text, { style: [ + a.font_semi_bold, + { + color: t.palette.primary_700, + }, + ], children: _jsx(Trans, { children: "Beta Feature" }) })] }), _jsx(View, { style: [ + a.relative, + a.w_full, + { + paddingTop: 8, + paddingHorizontal: 32, + paddingBottom: 32, + }, + ], children: _jsx(View, { style: [ + { + borderRadius: 24, + aspectRatio: 652 / 211, + }, + IS_WEB + ? [ + { + boxShadow: "0px 10px 15px -3px ".concat(shadowColor), + }, + ] + : [ + t.atoms.shadow_md, + { + shadowColor: shadowColor, + shadowOpacity: 0.2, + shadowOffset: { + width: 0, + height: 10, + }, + }, + ], + ], children: _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../../assets/images/live_now_beta.webp'), style: [ + a.w_full, + { + aspectRatio: 652 / 211, + }, + ], alt: _(msg({ + message: "A screenshot of a post from @esb.lol, showing the user is currently livestreaming content on Twitch. The post reads: \"Hello! I'm live on Twitch, and I'm testing Bluesky's latest feature too!\"", + comment: 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', + })) }) }) })] }), _jsxs(View, { style: [a.align_center, a.px_xl, a.gap_2xl, a.pb_sm], children: [_jsxs(View, { style: [a.gap_sm, a.align_center], children: [_jsx(Text, { style: [ + a.text_3xl, + a.leading_tight, + a.font_bold, + a.text_center, + { + fontSize: IS_WEB ? 28 : 32, + maxWidth: 360, + }, + ], children: _jsx(Trans, { children: "Show when you\u2019re live" }) }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + a.text_center, + { + maxWidth: 340, + }, + ], children: _jsx(Trans, { children: "Streaming on Twitch? Set your live status on Bluesky to add a badge to your avatar. Tapping it takes people straight to your stream." }) })] }), !IS_WEB && (_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", color: "primary", onPress: function () { + control.close(); + }, style: [a.w_full], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }))] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dialogs/nuxs/index.js b/src/components/dialogs/nuxs/index.js new file mode 100644 index 0000000000..e4501d8579 --- /dev/null +++ b/src/components/dialogs/nuxs/index.js @@ -0,0 +1,137 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; +import { logger } from '#/logger'; +import { STALE } from '#/state/queries'; +import { Nux, useNuxs, useResetNuxs, useSaveNux } from '#/state/queries/nuxs'; +import { usePreferencesQuery, } from '#/state/queries/preferences'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { useOnboardingState } from '#/state/shell'; +import { DraftsAnnouncement, enabled as isDraftsAnnouncementEnabled, } from '#/components/dialogs/nuxs/DraftsAnnouncement'; +import { isSnoozed, snooze, unsnooze } from '#/components/dialogs/nuxs/snoozing'; +import { useAnalytics } from '#/analytics'; +import { useGeolocation } from '#/geolocation'; +var queuedNuxs = [ + { + id: Nux.DraftsAnnouncement, + enabled: isDraftsAnnouncementEnabled, + }, +]; +var Context = createContext({ + activeNux: undefined, + dismissActiveNux: function () { }, +}); +Context.displayName = 'NuxDialogContext'; +export function useNuxDialogContext() { + return useContext(Context); +} +export function NuxDialogs() { + var currentAccount = useSession().currentAccount; + var preferences = usePreferencesQuery().data; + var profile = useProfileQuery({ + did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + staleTime: STALE.INFINITY, // createdAt isn't gonna change + }).data; + var onboardingActive = useOnboardingState().isActive; + var isLoading = onboardingActive || + !currentAccount || + !preferences || + !profile || + // Profile isn't legit ready until createdAt is a real date. + !profile.createdAt || + profile.createdAt === '0001-01-01T00:00:00.000Z'; // TODO: Fix this in AppView. + return !isLoading ? (_jsx(Inner, { currentAccount: currentAccount, currentProfile: profile, preferences: preferences })) : null; +} +function Inner(_a) { + var currentAccount = _a.currentAccount, currentProfile = _a.currentProfile, preferences = _a.preferences; + var ax = useAnalytics(); + var geolocation = useGeolocation(); + var nuxs = useNuxs().nuxs; + var _b = useState(function () { + return isSnoozed(); + }), snoozed = _b[0], setSnoozed = _b[1]; + var _c = useState(), activeNux = _c[0], setActiveNux = _c[1]; + var saveNux = useSaveNux().mutateAsync; + var resetNuxs = useResetNuxs().mutate; + var snoozeNuxDialog = useCallback(function () { + snooze(); + setSnoozed(true); + }, [setSnoozed]); + var dismissActiveNux = useCallback(function () { + if (!activeNux) + return; + setActiveNux(undefined); + }, [activeNux, setActiveNux]); + if (__DEV__ && typeof window !== 'undefined') { + // @ts-ignore + window.clearNuxDialog = function (id) { + if (!__DEV__ || !id) + return; + resetNuxs([id]); + unsnooze(); + }; + } + useEffect(function () { + if (snoozed) + return; // comment this out to test + if (!nuxs) + return; + var _loop_1 = function (id, enabled) { + var nux = nuxs.find(function (nux) { return nux.id === id; }); + // check if completed first + if (nux && nux.completed) { + return "continue"; + } + // then check gate (track exposure) + if (enabled && + !enabled({ + features: ax.features, + currentAccount: currentAccount, + currentProfile: currentProfile, + preferences: preferences, + geolocation: geolocation, + })) { + return "continue"; + } + logger.debug("NUX dialogs: activating '".concat(id, "' NUX")); + // we have a winner + setActiveNux(id); + // immediately snooze for a day + snoozeNuxDialog(); + // immediately update remote data (affects next reload) + saveNux({ + id: id, + completed: true, + data: undefined, + }).catch(function (e) { + logger.error("NUX dialogs: failed to upsert '".concat(id, "' NUX"), { + safeMessage: e.message, + }); + }); + return "break"; + }; + for (var _i = 0, queuedNuxs_1 = queuedNuxs; _i < queuedNuxs_1.length; _i++) { + var _a = queuedNuxs_1[_i], id = _a.id, enabled = _a.enabled; + var state_1 = _loop_1(id, enabled); + if (state_1 === "break") + break; + } + }, [ + ax.features, + nuxs, + snoozed, + snoozeNuxDialog, + saveNux, + currentAccount, + currentProfile, + preferences, + geolocation, + ]); + var ctx = useMemo(function () { + return { + activeNux: activeNux, + dismissActiveNux: dismissActiveNux, + }; + }, [activeNux, dismissActiveNux]); + return (_jsx(Context.Provider, { value: ctx, children: activeNux === Nux.DraftsAnnouncement && _jsx(DraftsAnnouncement, {}) })); +} diff --git a/src/components/dialogs/nuxs/snoozing.js b/src/components/dialogs/nuxs/snoozing.js new file mode 100644 index 0000000000..e3725de06d --- /dev/null +++ b/src/components/dialogs/nuxs/snoozing.js @@ -0,0 +1,20 @@ +import { simpleAreDatesEqual } from '#/lib/strings/time'; +import { device } from '#/storage'; +export function snooze() { + device.set(['lastNuxDialog'], new Date().toISOString()); +} +export function unsnooze() { + device.set(['lastNuxDialog'], undefined); +} +export function isSnoozed() { + var lastNuxDialog = device.get(['lastNuxDialog']); + if (!lastNuxDialog) + return false; + var last = new Date(lastNuxDialog); + var now = new Date(); + // already snoozed today + if (simpleAreDatesEqual(last, now)) { + return true; + } + return false; +} diff --git a/src/components/dialogs/nuxs/utils.js b/src/components/dialogs/nuxs/utils.js new file mode 100644 index 0000000000..456887d1f1 --- /dev/null +++ b/src/components/dialogs/nuxs/utils.js @@ -0,0 +1,33 @@ +export function createIsEnabledCheck(cb) { + return cb; +} +var ONE_DAY = 1000 * 60 * 60 * 24; +export function isDaysOld(days, createdAt) { + /* + * Should never happen because we gate NUXs to only accounts with a valid + * profile and a `createdAt` (see `nuxs/index.tsx`). But if it ever did, the + * account is either old enough to be pre-onboarding, or some failure happened + * during account creation. Fail closed. - esb + */ + if (!createdAt) + return false; + var now = Date.now(); + var then = new Date(createdAt).getTime(); + var isOldEnough = then + ONE_DAY * days < now; + if (isOldEnough) + return true; + return false; +} +export function isExistingUserAsOf(date, createdAt) { + /* + * Should never happen because we gate NUXs to only accounts with a valid + * profile and a `createdAt` (see `nuxs/index.tsx`). But if it ever did, the + * account is either old enough to be pre-onboarding, or some failure happened + * during account creation. Fail closed. - esb + */ + if (!createdAt) + return false; + var threshold = Date.parse(date); + var then = new Date(createdAt).getTime(); + return then < threshold; +} diff --git a/src/components/dms/ActionsWrapper.js b/src/components/dms/ActionsWrapper.js new file mode 100644 index 0000000000..07a2b75ecf --- /dev/null +++ b/src/components/dms/ActionsWrapper.js @@ -0,0 +1,26 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a } from '#/alf'; +import { MessageContextMenu } from '#/components/dms/MessageContextMenu'; +export function ActionsWrapper(_a) { + var message = _a.message, isFromSelf = _a.isFromSelf, children = _a.children; + var _ = useLingui()._; + return (_jsx(MessageContextMenu, { message: message, children: function (trigger) { + // will always be true, since this file is platform split + return trigger.IS_NATIVE && (_jsx(View, { style: [a.flex_1, a.relative], children: _jsx(View, { style: [ + { maxWidth: '80%' }, + isFromSelf + ? [a.self_end, a.align_end] + : [a.self_start, a.align_start], + ], accessible: true, accessibilityActions: [ + { name: 'activate', label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Open message options"], ["Open message options"])))) }, + ], onAccessibilityAction: function () { return trigger.control.open('full'); }, children: children }) })); + } })); +} +var templateObject_1; diff --git a/src/components/dms/ActionsWrapper.web.js b/src/components/dms/ActionsWrapper.web.js new file mode 100644 index 0000000000..9c53a6b59f --- /dev/null +++ b/src/components/dms/ActionsWrapper.web.js @@ -0,0 +1,106 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useRef, useState } from 'react'; +import { Pressable, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useConvoActive } from '#/state/messages/convo'; +import { useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useTheme } from '#/alf'; +import { MessageContextMenu } from '#/components/dms/MessageContextMenu'; +import { DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon } from '#/components/icons/DotGrid'; +import { EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon } from '#/components/icons/Emoji'; +import { EmojiReactionPicker } from './EmojiReactionPicker'; +import { hasReachedReactionLimit } from './util'; +export function ActionsWrapper(_a) { + var message = _a.message, isFromSelf = _a.isFromSelf, children = _a.children; + var viewRef = useRef(null); + var t = useTheme(); + var _ = useLingui()._; + var convo = useConvoActive(); + var currentAccount = useSession().currentAccount; + var _b = useState(false), showActions = _b[0], setShowActions = _b[1]; + var onMouseEnter = useCallback(function () { + setShowActions(true); + }, []); + var onMouseLeave = useCallback(function () { + setShowActions(false); + }, []); + // We need to handle the `onFocus` separately because we want to know if there is a related target (the element + // that is losing focus). If there isn't that means the focus is coming from a dropdown that is now closed. + var onFocus = useCallback(function (e) { + if (e.nativeEvent.relatedTarget == null) + return; + setShowActions(true); + }, []); + var onEmojiSelect = useCallback(function (emoji) { + var _a; + if ((_a = message.reactions) === null || _a === void 0 ? void 0 : _a.find(function (reaction) { + return reaction.value === emoji && + reaction.sender.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + })) { + convo + .removeReaction(message.id, emoji) + .catch(function () { return Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to remove emoji reaction"], ["Failed to remove emoji reaction"]))))); }); + } + else { + if (hasReachedReactionLimit(message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) + return; + convo + .addReaction(message.id, emoji) + .catch(function () { + return Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to add emoji reaction"], ["Failed to add emoji reaction"])))), 'xmark'); + }); + } + }, [_, convo, message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did]); + return (_jsxs(View, { onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, + // @ts-expect-error web only + onFocus: onFocus, onBlur: onMouseLeave, style: [a.flex_1, isFromSelf ? a.flex_row : a.flex_row_reverse], ref: viewRef, children: [_jsxs(View, { style: [ + a.justify_center, + a.flex_row, + a.align_center, + isFromSelf + ? [a.mr_xs, { marginLeft: 'auto' }, a.flex_row_reverse] + : [a.ml_xs, { marginRight: 'auto' }], + ], children: [_jsx(EmojiReactionPicker, { message: message, onEmojiSelect: onEmojiSelect, children: function (_a) { + var props = _a.props, state = _a.state, IS_NATIVE = _a.IS_NATIVE, control = _a.control; + // always false, file is platform split + if (IS_NATIVE) + return null; + var showMenuTrigger = showActions || control.isOpen ? 1 : 0; + return (_jsx(Pressable, __assign({}, props, { style: [ + { opacity: showMenuTrigger }, + a.p_xs, + a.rounded_full, + (state.hovered || state.pressed) && t.atoms.bg_contrast_25, + ], children: _jsx(EmojiSmileIcon, { size: "md", style: t.atoms.text_contrast_medium }) }))); + } }), _jsx(MessageContextMenu, { message: message, children: function (_a) { + var props = _a.props, state = _a.state, IS_NATIVE = _a.IS_NATIVE, control = _a.control; + // always false, file is platform split + if (IS_NATIVE) + return null; + var showMenuTrigger = showActions || control.isOpen ? 1 : 0; + return (_jsx(Pressable, __assign({}, props, { style: [ + { opacity: showMenuTrigger }, + a.p_xs, + a.rounded_full, + (state.hovered || state.pressed) && t.atoms.bg_contrast_25, + ], children: _jsx(DotsHorizontalIcon, { size: "md", style: t.atoms.text_contrast_medium }) }))); + } })] }), _jsx(View, { style: [{ maxWidth: '80%' }, isFromSelf ? a.align_end : a.align_start], children: children })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dms/AfterReportDialog.js b/src/components/dms/AfterReportDialog.js new file mode 100644 index 0000000000..2f4184ae6b --- /dev/null +++ b/src/components/dms/AfterReportDialog.js @@ -0,0 +1,91 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { memo, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { StackActions, useNavigation } from '@react-navigation/native'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useLeaveConvo } from '#/state/queries/messages/leave-conversation'; +import { useProfileBlockMutationQueue, useProfileQuery, } from '#/state/queries/profile'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, platform, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as Toggle from '#/components/forms/Toggle'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +/** + * Dialog shown after a report is submitted, allowing the user to block the + * reporter and/or leave the conversation. + */ +export var AfterReportDialog = memo(function BlockOrDeleteDialogInner(_a) { + var control = _a.control, params = _a.params, currentScreen = _a.currentScreen; + var _ = useLingui()._; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Would you like to block this user and/or delete this conversation?"], ["Would you like to block this user and/or delete this conversation?"])))), style: [web({ maxWidth: 400 })], children: [_jsx(DialogInner, { params: params, currentScreen: currentScreen }), _jsx(Dialog.Close, {})] })] })); +}); +function DialogInner(_a) { + var params = _a.params, currentScreen = _a.currentScreen; + var t = useTheme(); + var _ = useLingui()._; + var control = Dialog.useDialogContext(); + var _b = useProfileQuery({ + did: params.message.sender.did, + }), profile = _b.data, isLoading = _b.isLoading, isError = _b.isError; + return isLoading ? (_jsx(View, { style: [a.w_full, a.py_5xl, a.align_center], children: _jsx(Loader, { size: "lg" }) })) : isError || !profile ? (_jsxs(View, { style: [a.w_full, a.gap_lg], children: [_jsxs(View, { style: [a.justify_center, a.gap_sm], children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Report submitted" }) }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Our moderation team has received your report." }) })] }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), onPress: function () { return control.close(); }, size: platform({ native: 'small', web: 'large' }), color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })] })) : (_jsx(DoneStep, { convoId: params.convoId, currentScreen: currentScreen, profile: profile })); +} +function DoneStep(_a) { + var convoId = _a.convoId, currentScreen = _a.currentScreen, profile = _a.profile; + var _ = useLingui()._; + var navigation = useNavigation(); + var control = Dialog.useDialogContext(); + var gtMobile = useBreakpoints().gtMobile; + var t = useTheme(); + var _b = useState(['block', 'leave']), actions = _b[0], setActions = _b[1]; + var shadow = useProfileShadow(profile); + var queueBlock = useProfileBlockMutationQueue(shadow)[0]; + var leaveConvo = useLeaveConvo(convoId, { + onMutate: function () { + if (currentScreen === 'conversation') { + navigation.dispatch(StackActions.replace('Messages', IS_NATIVE ? { animation: 'pop' } : {})); + } + }, + onError: function () { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Could not leave chat"], ["Could not leave chat"])))), 'xmark'); + }, + }).mutate; + var btnText = _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Done"], ["Done"])))); + var toastMsg; + if (actions.includes('leave') && actions.includes('block')) { + btnText = _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Block and Delete"], ["Block and Delete"])))); + toastMsg = _(msg({ message: 'Conversation deleted', context: 'toast' })); + } + else if (actions.includes('leave')) { + btnText = _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Delete Conversation"], ["Delete Conversation"])))); + toastMsg = _(msg({ message: 'Conversation deleted', context: 'toast' })); + } + else if (actions.includes('block')) { + btnText = _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Block User"], ["Block User"])))); + toastMsg = _(msg({ message: 'User blocked', context: 'toast' })); + } + var onPressPrimaryAction = function () { + control.close(function () { + if (actions.includes('block')) { + queueBlock(); + } + if (actions.includes('leave')) { + leaveConvo(); + } + if (toastMsg) { + Toast.show(toastMsg, 'check'); + } + }); + }; + return (_jsxs(View, { style: a.gap_2xl, children: [_jsxs(View, { style: [a.justify_center, gtMobile ? a.gap_sm : a.gap_xs], children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Report submitted" }) }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Our moderation team has received your report." }) })] }), _jsx(Toggle.Group, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Block user and/or delete this conversation"], ["Block user and/or delete this conversation"])))), values: actions, onChange: setActions, children: _jsxs(View, { style: [a.gap_md], children: [_jsxs(Toggle.Item, { name: "block", label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Block user"], ["Block user"])))), children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.LabelText, { style: [a.text_md], children: _jsx(Trans, { children: "Block user" }) })] }), _jsxs(Toggle.Item, { name: "leave", label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Delete conversation"], ["Delete conversation"])))), children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.LabelText, { style: [a.text_md], children: _jsx(Trans, { children: "Delete conversation" }) })] })] }) }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Button, { label: btnText, onPress: onPressPrimaryAction, size: "large", color: actions.length > 0 ? 'negative' : 'primary', children: _jsx(ButtonText, { children: btnText }) }), _jsx(Button, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Close"], ["Close"])))), onPress: function () { return control.close(); }, size: "large", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11; diff --git a/src/components/dms/BlockedByListDialog.js b/src/components/dms/BlockedByListDialog.js new file mode 100644 index 0000000000..03da19c401 --- /dev/null +++ b/src/components/dms/BlockedByListDialog.js @@ -0,0 +1,24 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { listUriToHref } from '#/lib/strings/url-helpers'; +import { atoms as a, useTheme } from '#/alf'; +import * as Dialog from '#/components/Dialog'; +import { InlineLinkText } from '#/components/Link'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +export function BlockedByListDialog(_a) { + var control = _a.control, listBlocks = _a.listBlocks; + var _ = useLingui()._; + var t = useTheme(); + return (_jsxs(Prompt.Outer, { control: control, testID: "blockedByListDialog", children: [_jsx(Prompt.TitleText, { children: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["User blocked by list"], ["User blocked by list"])))) }), _jsxs(View, { style: [a.gap_sm, a.pb_lg], children: [_jsxs(Text, { selectable: true, style: [a.text_md, a.leading_snug, t.atoms.text_contrast_high], children: [_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user."], ["This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user."])))), ' '] }), _jsxs(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_high], children: [_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Lists blocking this user:"], ["Lists blocking this user:"])))), ' ', listBlocks.map(function (block, i) { + return block.source.type === 'list' ? (_jsxs(React.Fragment, { children: [i === 0 ? null : ', ', _jsx(InlineLinkText, { label: block.source.list.name, to: listUriToHref(block.source.list.uri), style: [a.text_md, a.leading_snug], children: block.source.list.name })] }, block.source.list.uri)) : null; + })] })] }), _jsx(Prompt.Actions, { children: _jsx(Prompt.Action, { cta: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["I understand"], ["I understand"])))), onPress: function () { } }) }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/dms/ChatEmptyPill.js b/src/components/dms/ChatEmptyPill.js new file mode 100644 index 0000000000..ce141eea79 --- /dev/null +++ b/src/components/dms/ChatEmptyPill.js @@ -0,0 +1,75 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { Pressable, View } from 'react-native'; +import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { ScaleAndFadeIn } from '#/lib/custom-animations/ScaleAndFade'; +import { ShrinkAndPop } from '#/lib/custom-animations/ShrinkAndPop'; +import { useHaptics } from '#/lib/haptics'; +import { atoms as a, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +var AnimatedPressable = Animated.createAnimatedComponent(Pressable); +var lastIndex = 0; +export function ChatEmptyPill() { + var t = useTheme(); + var _ = useLingui()._; + var playHaptic = useHaptics(); + var _a = React.useState(lastIndex), promptIndex = _a[0], setPromptIndex = _a[1]; + var scale = useSharedValue(1); + var prompts = React.useMemo(function () { + return [ + _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Say hello!"], ["Say hello!"])))), + _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Share your favorite feed!"], ["Share your favorite feed!"])))), + _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Tell a joke!"], ["Tell a joke!"])))), + _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Share a fun fact!"], ["Share a fun fact!"])))), + _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Share a cool story!"], ["Share a cool story!"])))), + _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Send a neat website!"], ["Send a neat website!"])))), + _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Clip \uD83D\uDC34 clop \uD83D\uDC34"], ["Clip \uD83D\uDC34 clop \uD83D\uDC34"])))), + ]; + }, [_]); + var onPressIn = React.useCallback(function () { + if (IS_WEB) + return; + scale.set(function () { return withTiming(1.075, { duration: 100 }); }); + }, [scale]); + var onPressOut = React.useCallback(function () { + if (IS_WEB) + return; + scale.set(function () { return withTiming(1, { duration: 100 }); }); + }, [scale]); + var onPress = React.useCallback(function () { + runOnJS(playHaptic)(); + var randomPromptIndex = Math.floor(Math.random() * prompts.length); + while (randomPromptIndex === lastIndex) { + randomPromptIndex = Math.floor(Math.random() * prompts.length); + } + setPromptIndex(randomPromptIndex); + lastIndex = randomPromptIndex; + }, [playHaptic, prompts.length]); + var animatedStyle = useAnimatedStyle(function () { return ({ + transform: [{ scale: scale.get() }], + }); }); + return (_jsx(View, { style: [ + a.absolute, + a.w_full, + a.z_10, + a.align_center, + { + top: -50, + }, + ], children: _jsx(AnimatedPressable, { style: [ + a.px_xl, + a.py_md, + a.rounded_full, + t.atoms.bg_contrast_25, + a.align_center, + animatedStyle, + ], entering: ScaleAndFadeIn, exiting: ShrinkAndPop, onPress: onPress, onPressIn: onPressIn, onPressOut: onPressOut, children: _jsx(Text, { style: [a.font_semi_bold, a.pointer_events_none], selectable: false, children: prompts[promptIndex] }) }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/dms/ConvoMenu.js b/src/components/dms/ConvoMenu.js new file mode 100644 index 0000000000..280726ff0c --- /dev/null +++ b/src/components/dms/ConvoMenu.js @@ -0,0 +1,111 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useCallback } from 'react'; +import { Keyboard, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useConvoQuery, useMarkAsReadMutation, } from '#/state/queries/messages/conversation'; +import { useMuteConvo } from '#/state/queries/messages/mute-conversation'; +import { useProfileBlockMutationQueue } from '#/state/queries/profile'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { AfterReportDialog } from '#/components/dms/AfterReportDialog'; +import { BlockedByListDialog } from '#/components/dms/BlockedByListDialog'; +import { LeaveConvoPrompt } from '#/components/dms/LeaveConvoPrompt'; +import { ReportConversationPrompt } from '#/components/dms/ReportConversationPrompt'; +import { ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft } from '#/components/icons/ArrowBoxLeft'; +import { Bubble_Stroke2_Corner2_Rounded as Bubble } from '#/components/icons/Bubble'; +import { DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal } from '#/components/icons/DotGrid'; +import { Flag_Stroke2_Corner0_Rounded as Flag } from '#/components/icons/Flag'; +import { Mute_Stroke2_Corner0_Rounded as Mute } from '#/components/icons/Mute'; +import { Person_Stroke2_Corner0_Rounded as Person, PersonCheck_Stroke2_Corner0_Rounded as PersonCheck, PersonX_Stroke2_Corner0_Rounded as PersonX, } from '#/components/icons/Person'; +import { SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute } from '#/components/icons/Speaker'; +import * as Menu from '#/components/Menu'; +import { ReportDialog } from '#/components/moderation/ReportDialog'; +import * as Prompt from '#/components/Prompt'; +var ConvoMenu = function (_a) { + var convo = _a.convo, profile = _a.profile, control = _a.control, currentScreen = _a.currentScreen, showMarkAsRead = _a.showMarkAsRead, hideTrigger = _a.hideTrigger, blockInfo = _a.blockInfo, latestReportableMessage = _a.latestReportableMessage, style = _a.style; + var _ = useLingui()._; + var leaveConvoControl = Prompt.usePromptControl(); + var reportControl = Prompt.usePromptControl(); + var blockedByListControl = Prompt.usePromptControl(); + var blockOrDeleteControl = Prompt.usePromptControl(); + var listBlocks = blockInfo.listBlocks; + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Root, { control: control, children: [!hideTrigger && (_jsx(View, { style: [style], children: _jsx(Menu.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Chat settings"], ["Chat settings"])))), children: function (_a) { + var props = _a.props; + return (_jsx(Button, __assign({ label: props.accessibilityLabel }, props, { onPress: function () { + Keyboard.dismiss(); + props.onPress(); + }, size: "small", color: "secondary", shape: "round", variant: "ghost", style: [a.bg_transparent], children: _jsx(ButtonIcon, { icon: DotsHorizontal, size: "md" }) }))); + } }) })), _jsx(Menu.Outer, { children: _jsx(MenuContent, { profile: profile, showMarkAsRead: showMarkAsRead, blockInfo: blockInfo, convo: convo, leaveConvoControl: leaveConvoControl, reportControl: reportControl, blockedByListControl: blockedByListControl }) })] }), _jsx(LeaveConvoPrompt, { control: leaveConvoControl, convoId: convo.id, currentScreen: currentScreen }), latestReportableMessage ? (_jsxs(_Fragment, { children: [_jsx(ReportDialog, { subject: { + view: 'convo', + convoId: convo.id, + message: latestReportableMessage, + }, control: reportControl, onAfterSubmit: function () { + blockOrDeleteControl.open(); + } }), _jsx(AfterReportDialog, { control: blockOrDeleteControl, currentScreen: currentScreen, params: { + convoId: convo.id, + message: latestReportableMessage, + } })] })) : (_jsx(ReportConversationPrompt, { control: reportControl })), _jsx(BlockedByListDialog, { control: blockedByListControl, listBlocks: listBlocks })] })); +}; +ConvoMenu = React.memo(ConvoMenu); +function MenuContent(_a) { + var initialConvo = _a.convo, profile = _a.profile, showMarkAsRead = _a.showMarkAsRead, blockInfo = _a.blockInfo, leaveConvoControl = _a.leaveConvoControl, reportControl = _a.reportControl, blockedByListControl = _a.blockedByListControl; + var navigation = useNavigation(); + var _ = useLingui()._; + var markAsRead = useMarkAsReadMutation().mutate; + var listBlocks = blockInfo.listBlocks, userBlock = blockInfo.userBlock; + var isBlocking = userBlock || !!listBlocks.length; + var isDeletedAccount = profile.handle === 'missing.invalid'; + var convoId = initialConvo.id; + var convo = useConvoQuery(initialConvo).data; + var onNavigateToProfile = useCallback(function () { + navigation.navigate('Profile', { name: profile.did }); + }, [navigation, profile.did]); + var muteConvo = useMuteConvo(convoId, { + onSuccess: function (data) { + if (data.convo.muted) { + Toast.show(_(msg({ message: 'Chat muted', context: 'toast' }))); + } + else { + Toast.show(_(msg({ message: 'Chat unmuted', context: 'toast' }))); + } + }, + onError: function () { + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Could not mute chat"], ["Could not mute chat"])))), 'xmark'); + }, + }).mutate; + var _b = useProfileBlockMutationQueue(profile), queueBlock = _b[0], queueUnblock = _b[1]; + var toggleBlock = React.useCallback(function () { + if (listBlocks.length) { + blockedByListControl.open(); + return; + } + if (userBlock) { + queueUnblock(); + } + else { + queueBlock(); + } + }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock]); + return isDeletedAccount ? (_jsxs(Menu.Item, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Leave conversation"], ["Leave conversation"])))), onPress: function () { return leaveConvoControl.open(); }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Leave conversation" }) }), _jsx(Menu.ItemIcon, { icon: ArrowBoxLeft })] })) : (_jsxs(_Fragment, { children: [_jsxs(Menu.Group, { children: [showMarkAsRead && (_jsxs(Menu.Item, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Mark as read"], ["Mark as read"])))), onPress: function () { return markAsRead({ convoId: convoId }); }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Mark as read" }) }), _jsx(Menu.ItemIcon, { icon: Bubble })] })), _jsxs(Menu.Item, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Go to user's profile"], ["Go to user's profile"])))), onPress: onNavigateToProfile, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Go to profile" }) }), _jsx(Menu.ItemIcon, { icon: Person })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Mute conversation"], ["Mute conversation"])))), onPress: function () { return muteConvo({ mute: !(convo === null || convo === void 0 ? void 0 : convo.muted) }); }, children: [_jsx(Menu.ItemText, { children: (convo === null || convo === void 0 ? void 0 : convo.muted) ? (_jsx(Trans, { children: "Unmute conversation" })) : (_jsx(Trans, { children: "Mute conversation" })) }), _jsx(Menu.ItemIcon, { icon: (convo === null || convo === void 0 ? void 0 : convo.muted) ? Unmute : Mute })] })] }), _jsx(Menu.Divider, {}), _jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { label: isBlocking ? _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Unblock account"], ["Unblock account"])))) : _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Block account"], ["Block account"])))), onPress: toggleBlock, children: [_jsx(Menu.ItemText, { children: isBlocking ? _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Unblock account"], ["Unblock account"])))) : _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Block account"], ["Block account"])))) }), _jsx(Menu.ItemIcon, { icon: isBlocking ? PersonCheck : PersonX })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Report conversation"], ["Report conversation"])))), onPress: function () { return reportControl.open(); }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Report conversation" }) }), _jsx(Menu.ItemIcon, { icon: Flag })] })] }), _jsx(Menu.Divider, {}), _jsx(Menu.Group, { children: _jsxs(Menu.Item, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Leave conversation"], ["Leave conversation"])))), onPress: function () { return leaveConvoControl.open(); }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Leave conversation" }) }), _jsx(Menu.ItemIcon, { icon: ArrowBoxLeft })] }) })] })); +} +export { ConvoMenu }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12; diff --git a/src/components/dms/DateDivider.js b/src/components/dms/DateDivider.js new file mode 100644 index 0000000000..08c716703d --- /dev/null +++ b/src/components/dms/DateDivider.js @@ -0,0 +1,72 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { subDays } from 'date-fns'; +import { atoms as a, useTheme } from '#/alf'; +import { Text } from '../Typography'; +import { localDateString } from './util'; +var timeFormatter = new Intl.DateTimeFormat(undefined, { + hour: 'numeric', + minute: 'numeric', +}); +var weekdayFormatter = new Intl.DateTimeFormat(undefined, { + weekday: 'long', +}); +var longDateFormatter = new Intl.DateTimeFormat(undefined, { + weekday: 'short', + month: 'long', + day: 'numeric', +}); +var longDateFormatterWithYear = new Intl.DateTimeFormat(undefined, { + weekday: 'short', + month: 'long', + day: 'numeric', + year: 'numeric', +}); +var DateDivider = function (_a) { + var dateStr = _a.date; + var _ = useLingui()._; + var t = useTheme(); + var date; + var time = timeFormatter.format(new Date(dateStr)); + var timestamp = new Date(dateStr); + var today = new Date(); + var yesterday = subDays(today, 1); + var oneWeekAgo = subDays(today, 7); + if (localDateString(today) === localDateString(timestamp)) { + date = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Today"], ["Today"])))); + } + else if (localDateString(yesterday) === localDateString(timestamp)) { + date = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Yesterday"], ["Yesterday"])))); + } + else { + if (timestamp < oneWeekAgo) { + if (timestamp.getFullYear() === today.getFullYear()) { + date = longDateFormatter.format(timestamp); + } + else { + date = longDateFormatterWithYear.format(timestamp); + } + } + else { + date = weekdayFormatter.format(timestamp); + } + } + return (_jsx(View, { style: [a.w_full, a.my_lg], children: _jsx(Text, { style: [ + a.text_xs, + a.text_center, + t.atoms.bg, + t.atoms.text_contrast_medium, + a.px_md, + ], children: _jsxs(Trans, { children: [_jsx(Text, { style: [a.text_xs, t.atoms.text_contrast_medium, a.font_semi_bold], children: date }), ' ', "at ", time] }) }) })); +}; +DateDivider = React.memo(DateDivider); +export { DateDivider }; +var templateObject_1, templateObject_2; diff --git a/src/components/dms/EmojiPopup.android.js b/src/components/dms/EmojiPopup.android.js new file mode 100644 index 0000000000..72fe0dceac --- /dev/null +++ b/src/components/dms/EmojiPopup.android.js @@ -0,0 +1,37 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { Modal, Pressable, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '#/components/icons/Times'; +import { Text } from '#/components/Typography'; +import { EmojiPicker } from '../../../modules/expo-emoji-picker'; +export function EmojiPopup(_a) { + var children = _a.children, onEmojiSelected = _a.onEmojiSelected; + var _b = useState(false), modalVisible = _b[0], setModalVisible = _b[1]; + var _ = useLingui()._; + var t = useTheme(); + return (_jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Open full emoji list"], ["Open full emoji list"])))), accessibilityHint: "", accessibilityRole: "button", onPress: function () { return setModalVisible(true); }, children: children }), _jsx(Modal, { animationType: "slide", visible: modalVisible, onRequestClose: function () { return setModalVisible(false); }, transparent: true, statusBarTranslucent: true, navigationBarTranslucent: true, children: _jsxs(SafeAreaView, { style: [a.flex_1, t.atoms.bg], children: [_jsxs(View, { style: [ + a.pl_lg, + a.pr_md, + a.py_sm, + a.w_full, + a.align_center, + a.flex_row, + a.justify_between, + a.border_b, + t.atoms.border_contrast_low, + ], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_md], children: _jsx(Trans, { children: "Add Reaction" }) }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), onPress: function () { return setModalVisible(false); }, size: "small", variant: "ghost", color: "secondary", shape: "round", children: _jsx(ButtonIcon, { icon: CloseIcon }) })] }), _jsx(EmojiPicker, { onEmojiSelected: function (emoji) { + setModalVisible(false); + onEmojiSelected(emoji); + } })] }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dms/EmojiPopup.js b/src/components/dms/EmojiPopup.js new file mode 100644 index 0000000000..992469be75 --- /dev/null +++ b/src/components/dms/EmojiPopup.js @@ -0,0 +1 @@ +export { EmojiPicker as EmojiPopup } from '../../../modules/expo-emoji-picker'; diff --git a/src/components/dms/EmojiReactionPicker.js b/src/components/dms/EmojiReactionPicker.js new file mode 100644 index 0000000000..e9967f3bd3 --- /dev/null +++ b/src/components/dms/EmojiReactionPicker.js @@ -0,0 +1,90 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useSession } from '#/state/session'; +import { atoms as a, tokens, useTheme } from '#/alf'; +import * as ContextMenu from '#/components/ContextMenu'; +import { useContextMenuContext, useContextMenuMenuContext, } from '#/components/ContextMenu/context'; +import { EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyesIcon, EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon, } from '#/components/icons/Emoji'; +import { Text } from '#/components/Typography'; +import { EmojiPopup } from './EmojiPopup'; +import { hasAlreadyReacted, hasReachedReactionLimit } from './util'; +export function EmojiReactionPicker(_a) { + var _b; + var message = _a.message, onEmojiSelect = _a.onEmojiSelect; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var t = useTheme(); + var isFromSelf = ((_b = message.sender) === null || _b === void 0 ? void 0 : _b.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var _c = useContextMenuContext(), measurement = _c.measurement, close = _c.close; + var align = useContextMenuMenuContext().align; + var _d = useState({ width: 0, height: 0 }), layout = _d[0], setLayout = _d[1]; + var screenWidth = useWindowDimensions().width; + // 1 in 100 chance of showing heart eyes icon + var EmojiIcon = useMemo(function () { + return Math.random() < 0.01 ? EmojiHeartEyesIcon : EmojiSmileIcon; + }, []); + var position = useMemo(function () { + var _a; + return { + x: align === 'left' ? 12 : screenWidth - layout.width - 12, + y: ((_a = measurement === null || measurement === void 0 ? void 0 : measurement.y) !== null && _a !== void 0 ? _a : 0) - tokens.space.xs - layout.height, + height: layout.height, + width: layout.width, + }; + }, [measurement, align, screenWidth, layout]); + var limitReacted = hasReachedReactionLimit(message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var bgColor = t.scheme === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25; + return (_jsxs(View, { onLayout: function (evt) { return setLayout(evt.nativeEvent.layout); }, style: [ + bgColor, + a.rounded_full, + a.absolute, + { bottom: '100%' }, + isFromSelf ? a.right_0 : a.left_0, + a.flex_row, + a.p_xs, + a.gap_xs, + a.mb_xs, + a.z_20, + a.border, + t.atoms.border_contrast_low, + a.shadow_md, + ], children: [['👍', '😆', '❤️', '👀', '😢'].map(function (emoji) { + var alreadyReacted = hasAlreadyReacted(message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, emoji); + return (_jsx(ContextMenu.Item, { position: position, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["React with ", ""], ["React with ", ""])), emoji)), onPress: function () { return onEmojiSelect(emoji); }, unstyled: true, disabled: limitReacted ? !alreadyReacted : false, children: function (hovered) { return (_jsx(View, { style: [ + a.rounded_full, + hovered + ? { + backgroundColor: alreadyReacted + ? t.palette.negative_100 + : t.palette.primary_500, + } + : alreadyReacted + ? { backgroundColor: t.palette.primary_200 } + : bgColor, + { height: 40, width: 40 }, + a.justify_center, + a.align_center, + ], children: _jsx(Text, { style: [a.text_center, { fontSize: 30 }], emoji: true, children: emoji }) })); } }, emoji)); + }), _jsx(EmojiPopup, { onEmojiSelected: function (emoji) { + close(); + onEmojiSelect(emoji); + }, children: _jsx(View, { style: [ + a.rounded_full, + t.scheme === 'light' + ? t.atoms.bg_contrast_25 + : t.atoms.bg_contrast_50, + { height: 40, width: 40 }, + a.justify_center, + a.align_center, + a.border, + t.atoms.border_contrast_low, + ], children: _jsx(EmojiIcon, { size: "xl", fill: t.palette.contrast_400 }) }) })] })); +} +var templateObject_1; diff --git a/src/components/dms/EmojiReactionPicker.web.js b/src/components/dms/EmojiReactionPicker.web.js new file mode 100644 index 0000000000..14fdacf5ff --- /dev/null +++ b/src/components/dms/EmojiReactionPicker.web.js @@ -0,0 +1,78 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState } from 'react'; +import { Pressable, View } from 'react-native'; +import EmojiPicker from '@emoji-mart/react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { DropdownMenu } from 'radix-ui'; +import { useSession } from '#/state/session'; +import { useWebPreloadEmoji } from '#/view/com/composer/text-input/web/useWebPreloadEmoji'; +import { atoms as a, flatten, useTheme } from '#/alf'; +import { DotGrid3x1_Stroke2_Corner0_Rounded as DotGridIcon } from '#/components/icons/DotGrid'; +import * as Menu from '#/components/Menu'; +import { Text } from '#/components/Typography'; +import { hasAlreadyReacted, hasReachedReactionLimit } from './util'; +export function EmojiReactionPicker(_a) { + var message = _a.message, children = _a.children, onEmojiSelect = _a.onEmojiSelect; + if (!children) + throw new Error('EmojiReactionPicker requires the children prop on web'); + var _ = useLingui()._; + return (_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Add emoji reaction"], ["Add emoji reaction"])))), children: children }), _jsx(MenuInner, { message: message, onEmojiSelect: onEmojiSelect })] })); +} +function MenuInner(_a) { + var message = _a.message, onEmojiSelect = _a.onEmojiSelect; + var t = useTheme(); + var control = Menu.useMenuContext().control; + var currentAccount = useSession().currentAccount; + useWebPreloadEmoji({ immediate: true }); + var _b = useState(false), expanded = _b[0], setExpanded = _b[1]; + var _c = useState(control.isOpen), prevOpen = _c[0], setPrevOpen = _c[1]; + if (control.isOpen !== prevOpen) { + setPrevOpen(control.isOpen); + if (!control.isOpen) { + setExpanded(false); + } + } + var handleEmojiPickerResponse = function (emoji) { + handleEmojiSelect(emoji.native); + }; + var handleEmojiSelect = function (emoji) { + control.close(); + onEmojiSelect(emoji); + }; + var limitReacted = hasReachedReactionLimit(message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + return expanded ? (_jsx(DropdownMenu.Portal, { children: _jsx(DropdownMenu.Content, { sideOffset: 5, collisionPadding: { left: 5, right: 5, bottom: 5 }, children: _jsx("div", { onWheel: function (evt) { return evt.stopPropagation(); }, children: _jsx(EmojiPicker, { onEmojiSelect: handleEmojiPickerResponse, autoFocus: true }) }) }) })) : (_jsx(Menu.Outer, { style: [a.rounded_full], children: _jsxs(View, { style: [a.flex_row, a.gap_xs], children: [['👍', '😆', '❤️', '👀', '😢'].map(function (emoji) { + var alreadyReacted = hasAlreadyReacted(message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, emoji); + return (_jsx(DropdownMenu.Item, { className: [ + 'EmojiReactionPicker__Pressable', + alreadyReacted && '__selected', + limitReacted && '__disabled', + ] + .filter(Boolean) + .join(' '), onSelect: function () { return handleEmojiSelect(emoji); }, style: flatten([ + a.flex, + a.flex_col, + a.rounded_full, + a.justify_center, + a.align_center, + a.transition_transform, + { + width: 34, + height: 34, + }, + alreadyReacted && { + backgroundColor: t.atoms.bg_contrast_100.backgroundColor, + }, + ]), children: _jsx(Text, { style: [a.text_center, { fontSize: 28 }], emoji: true, children: emoji }) }, emoji)); + }), _jsx(DropdownMenu.Item, { asChild: true, className: "EmojiReactionPicker__PickerButton", children: _jsx(Pressable, { accessibilityRole: "button", role: "button", onPress: function () { return setExpanded(true); }, style: flatten([ + a.rounded_full, + { height: 34, width: 34 }, + a.justify_center, + a.align_center, + ]), children: _jsx(DotGridIcon, { size: "lg", style: t.atoms.text_contrast_medium }) }) })] }) })); +} +var templateObject_1; diff --git a/src/components/dms/LeaveConvoPrompt.js b/src/components/dms/LeaveConvoPrompt.js new file mode 100644 index 0000000000..d2ef855b43 --- /dev/null +++ b/src/components/dms/LeaveConvoPrompt.js @@ -0,0 +1,31 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { StackActions, useNavigation } from '@react-navigation/native'; +import { useLeaveConvo } from '#/state/queries/messages/leave-conversation'; +import * as Toast from '#/view/com/util/Toast'; +import * as Prompt from '#/components/Prompt'; +import { IS_NATIVE } from '#/env'; +export function LeaveConvoPrompt(_a) { + var control = _a.control, convoId = _a.convoId, currentScreen = _a.currentScreen, _b = _a.hasMessages, hasMessages = _b === void 0 ? true : _b; + var _ = useLingui()._; + var navigation = useNavigation(); + var leaveConvo = useLeaveConvo(convoId, { + onMutate: function () { + if (currentScreen === 'conversation') { + navigation.dispatch(StackActions.replace('Messages', IS_NATIVE ? { animation: 'pop' } : {})); + } + }, + onError: function () { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Could not leave chat"], ["Could not leave chat"])))), 'xmark'); + }, + }).mutate; + return (_jsx(Prompt.Basic, { control: control, title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Leave conversation"], ["Leave conversation"])))), description: hasMessages + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant."], ["Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant."])))) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Are you sure you want to leave this conversation?"], ["Are you sure you want to leave this conversation?"])))), confirmButtonCta: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Leave"], ["Leave"])))), confirmButtonColor: "negative", onConfirm: function () { return leaveConvo(); } })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/dms/MessageContext.js b/src/components/dms/MessageContext.js new file mode 100644 index 0000000000..4047604edb --- /dev/null +++ b/src/components/dms/MessageContext.js @@ -0,0 +1,11 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +var MessageContext = React.createContext(false); +MessageContext.displayName = 'MessageContext'; +export function MessageContextProvider(_a) { + var children = _a.children; + return (_jsx(MessageContext.Provider, { value: true, children: children })); +} +export function useIsWithinMessage() { + return React.useContext(MessageContext); +} diff --git a/src/components/dms/MessageContextMenu.js b/src/components/dms/MessageContextMenu.js new file mode 100644 index 0000000000..99f81b1a9f --- /dev/null +++ b/src/components/dms/MessageContextMenu.js @@ -0,0 +1,108 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useCallback } from 'react'; +import { LayoutAnimation } from 'react-native'; +import * as Clipboard from 'expo-clipboard'; +import { RichText } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useTranslate } from '#/lib/hooks/useTranslate'; +import { richTextToString } from '#/lib/strings/rich-text-helpers'; +import { useConvoActive } from '#/state/messages/convo'; +import { useLanguagePrefs } from '#/state/preferences'; +import { useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import * as ContextMenu from '#/components/ContextMenu'; +import { AfterReportDialog } from '#/components/dms/AfterReportDialog'; +import { BubbleQuestion_Stroke2_Corner0_Rounded as Translate } from '#/components/icons/Bubble'; +import { Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon } from '#/components/icons/Clipboard'; +import { Trash_Stroke2_Corner0_Rounded as Trash } from '#/components/icons/Trash'; +import { Warning_Stroke2_Corner0_Rounded as Warning } from '#/components/icons/Warning'; +import { ReportDialog } from '#/components/moderation/ReportDialog'; +import * as Prompt from '#/components/Prompt'; +import { usePromptControl } from '#/components/Prompt'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +import { EmojiReactionPicker } from './EmojiReactionPicker'; +import { hasReachedReactionLimit } from './util'; +export var MessageContextMenu = function (_a) { + var _b, _c; + var message = _a.message, children = _a.children; + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var convo = useConvoActive(); + var deleteControl = usePromptControl(); + var reportControl = usePromptControl(); + var blockOrDeleteControl = usePromptControl(); + var langPrefs = useLanguagePrefs(); + var translate = useTranslate(); + var isFromSelf = ((_b = message.sender) === null || _b === void 0 ? void 0 : _b.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var onCopyMessage = useCallback(function () { + var str = richTextToString(new RichText({ + text: message.text, + facets: message.facets, + }), true); + Clipboard.setStringAsync(str); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Copied to clipboard"], ["Copied to clipboard"])))), 'clipboard-check'); + }, [_, message.text, message.facets]); + var onPressTranslateMessage = useCallback(function () { + translate(message.text, langPrefs.primaryLanguage); + ax.metric('translate', { + sourceLanguages: [], + targetLanguage: langPrefs.primaryLanguage, + textLength: message.text.length, + }); + }, [ax, langPrefs.primaryLanguage, message.text, translate]); + var onDelete = useCallback(function () { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + convo + .deleteMessage(message.id) + .then(function () { + return Toast.show(_(msg({ message: 'Message deleted', context: 'toast' }))); + }) + .catch(function () { return Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to delete message"], ["Failed to delete message"]))))); }); + }, [_, convo, message.id]); + var onEmojiSelect = useCallback(function (emoji) { + var _a; + if ((_a = message.reactions) === null || _a === void 0 ? void 0 : _a.find(function (reaction) { + return reaction.value === emoji && + reaction.sender.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + })) { + convo + .removeReaction(message.id, emoji) + .catch(function () { return Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to remove emoji reaction"], ["Failed to remove emoji reaction"]))))); }); + } + else { + if (hasReachedReactionLimit(message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) + return; + convo + .addReaction(message.id, emoji) + .catch(function () { + return Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Failed to add emoji reaction"], ["Failed to add emoji reaction"])))), 'xmark'); + }); + } + }, [_, convo, message, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did]); + var sender = convo.convo.members.find(function (member) { return member.did === message.sender.did; }); + return (_jsxs(_Fragment, { children: [_jsxs(ContextMenu.Root, { children: [IS_NATIVE && (_jsx(ContextMenu.AuxiliaryView, { align: isFromSelf ? 'right' : 'left', children: _jsx(EmojiReactionPicker, { message: message, onEmojiSelect: onEmojiSelect }) })), _jsx(ContextMenu.Trigger, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Message options"], ["Message options"])))), contentLabel: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Message from @", ": ", ""], ["Message from @", // should always be defined + ": ", ""])), (_c = sender === null || sender === void 0 ? void 0 : sender.handle) !== null && _c !== void 0 ? _c : 'unknown' // should always be defined + , message.text)), children: children }), _jsxs(ContextMenu.Outer, { align: isFromSelf ? 'right' : 'left', children: [message.text.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(ContextMenu.Item, { testID: "messageDropdownTranslateBtn", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Translate"], ["Translate"])))), onPress: onPressTranslateMessage, children: [_jsx(ContextMenu.ItemText, { children: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Translate"], ["Translate"])))) }), _jsx(ContextMenu.ItemIcon, { icon: Translate, position: "right" })] }), _jsxs(ContextMenu.Item, { testID: "messageDropdownCopyBtn", label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Copy message text"], ["Copy message text"])))), onPress: onCopyMessage, children: [_jsx(ContextMenu.ItemText, { children: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Copy message text"], ["Copy message text"])))) }), _jsx(ContextMenu.ItemIcon, { icon: ClipboardIcon, position: "right" })] }), _jsx(ContextMenu.Divider, {})] })), _jsxs(ContextMenu.Item, { testID: "messageDropdownDeleteBtn", label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Delete message for me"], ["Delete message for me"])))), onPress: function () { return deleteControl.open(); }, children: [_jsx(ContextMenu.ItemText, { children: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Delete for me"], ["Delete for me"])))) }), _jsx(ContextMenu.ItemIcon, { icon: Trash, position: "right" })] }), !isFromSelf && (_jsxs(ContextMenu.Item, { testID: "messageDropdownReportBtn", label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Report message"], ["Report message"])))), onPress: function () { return reportControl.open(); }, children: [_jsx(ContextMenu.ItemText, { children: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Report"], ["Report"])))) }), _jsx(ContextMenu.ItemIcon, { icon: Warning, position: "right" })] }))] })] }), _jsx(ReportDialog + // currentScreen="conversation" + , { + // currentScreen="conversation" + control: reportControl, subject: { + view: 'message', + convoId: convo.convo.id, + message: message, + }, onAfterSubmit: function () { + blockOrDeleteControl.open(); + } }), _jsx(AfterReportDialog, { control: blockOrDeleteControl, currentScreen: "conversation", params: { + convoId: convo.convo.id, + message: message, + } }), _jsx(Prompt.Basic, { control: deleteControl, title: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Delete message"], ["Delete message"])))), description: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant."], ["Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant."])))), confirmButtonCta: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Delete"], ["Delete"])))), confirmButtonColor: "negative", onConfirm: onDelete })] })); +}; +MessageContextMenu = memo(MessageContextMenu); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17; diff --git a/src/components/dms/MessageItem.js b/src/components/dms/MessageItem.js new file mode 100644 index 0000000000..310c77c21f --- /dev/null +++ b/src/components/dms/MessageItem.js @@ -0,0 +1,177 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useCallback, useMemo } from 'react'; +import { View, } from 'react-native'; +import Animated, { LayoutAnimationConfig, LinearTransition, ZoomIn, ZoomOut, } from 'react-native-reanimated'; +import { AppBskyEmbedRecord, ChatBskyConvoDefs, RichText as RichTextAPI, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { useConvoActive } from '#/state/messages/convo'; +import { useSession } from '#/state/session'; +import { TimeElapsed } from '#/view/com/util/TimeElapsed'; +import { atoms as a, native, useTheme } from '#/alf'; +import { isOnlyEmoji } from '#/alf/typography'; +import { ActionsWrapper } from '#/components/dms/ActionsWrapper'; +import { InlineLinkText } from '#/components/Link'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +import { DateDivider } from './DateDivider'; +import { MessageItemEmbed } from './MessageItemEmbed'; +import { localDateString } from './util'; +var MessageItem = function (_a) { + var _b, _c; + var item = _a.item; + var t = useTheme(); + var currentAccount = useSession().currentAccount; + var _ = useLingui()._; + var convo = useConvoActive().convo; + var message = item.message, nextMessage = item.nextMessage, prevMessage = item.prevMessage; + var isPending = item.type === 'pending-message'; + var isFromSelf = ((_b = message.sender) === null || _b === void 0 ? void 0 : _b.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var nextIsMessage = ChatBskyConvoDefs.isMessageView(nextMessage); + var isNextFromSelf = nextIsMessage && ((_c = nextMessage.sender) === null || _c === void 0 ? void 0 : _c.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var isNextFromSameSender = isNextFromSelf === isFromSelf; + var isNewDay = useMemo(function () { + if (!prevMessage) + return true; + var thisDate = new Date(message.sentAt); + var prevDate = new Date(prevMessage.sentAt); + return localDateString(thisDate) !== localDateString(prevDate); + }, [message, prevMessage]); + var isLastMessageOfDay = useMemo(function () { + if (!nextMessage || !nextIsMessage) + return true; + var thisDate = new Date(message.sentAt); + var prevDate = new Date(nextMessage.sentAt); + return localDateString(thisDate) !== localDateString(prevDate); + }, [message.sentAt, nextIsMessage, nextMessage]); + var needsTail = isLastMessageOfDay || !isNextFromSameSender; + var isLastInGroup = useMemo(function () { + // if this message is pending, it means the next message is pending too + if (isPending && nextMessage) { + return false; + } + // or, if there's a 5 minute gap between this message and the next + if (ChatBskyConvoDefs.isMessageView(nextMessage)) { + var thisDate = new Date(message.sentAt); + var nextDate = new Date(nextMessage.sentAt); + var diff = nextDate.getTime() - thisDate.getTime(); + // 5 minutes + return diff > 5 * 60 * 1000; + } + return true; + }, [message, nextMessage, isPending]); + var pendingColor = t.palette.primary_200; + var rt = useMemo(function () { + return new RichTextAPI({ text: message.text, facets: message.facets }); + }, [message.text, message.facets]); + var appliedReactions = (_jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: message.reactions && message.reactions.length > 0 && (_jsx(View, { style: [isFromSelf ? a.align_end : a.align_start, a.px_sm, a.pb_2xs], children: _jsx(View, { style: [ + a.flex_row, + a.gap_2xs, + a.py_xs, + a.px_xs, + a.justify_center, + isFromSelf ? a.justify_end : a.justify_start, + a.flex_wrap, + a.pb_xs, + t.atoms.bg_contrast_25, + a.border, + t.atoms.border_contrast_low, + a.rounded_lg, + t.atoms.shadow_sm, + { + // vibe coded number + transform: [{ translateY: -11 }], + }, + ], children: message.reactions.map(function (reaction, _i, reactions) { + var label; + if (reaction.sender.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + label = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You reacted ", ""], ["You reacted ", ""])), reaction.value)); + } + else { + var senderDid_1 = reaction.sender.did; + var sender = convo.members.find(function (member) { return member.did === senderDid_1; }); + if (sender) { + label = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["", " reacted ", ""], ["", " reacted ", ""])), sanitizeDisplayName(sender.displayName || sender.handle), reaction.value)); + } + else { + label = _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Someone reacted ", ""], ["Someone reacted ", ""])), reaction.value)); + } + } + return (_jsx(Animated.View, { entering: native(ZoomIn.springify(200).delay(400)), exiting: reactions.length > 1 && native(ZoomOut.delay(200)), layout: native(LinearTransition.delay(300)), style: [a.p_2xs], accessible: true, accessibilityLabel: label, accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Double tap or long press the message to add a reaction"], ["Double tap or long press the message to add a reaction"])))), children: _jsx(Text, { emoji: true, style: [a.text_sm], children: reaction.value }) }, reaction.sender.did + reaction.value)); + }) }) })) })); + return (_jsxs(_Fragment, { children: [isNewDay && _jsx(DateDivider, { date: message.sentAt }), _jsxs(View, { style: [ + isFromSelf ? a.mr_md : a.ml_md, + nextIsMessage && !isNextFromSameSender && a.mb_md, + ], children: [_jsxs(ActionsWrapper, { isFromSelf: isFromSelf, message: message, children: [AppBskyEmbedRecord.isView(message.embed) && (_jsx(MessageItemEmbed, { embed: message.embed })), rt.text.length > 0 && (_jsx(View, { style: !isOnlyEmoji(message.text) && [ + a.py_sm, + a.my_2xs, + a.rounded_md, + { + paddingLeft: 14, + paddingRight: 14, + backgroundColor: isFromSelf + ? isPending + ? pendingColor + : t.palette.primary_500 + : t.palette.contrast_50, + borderRadius: 17, + }, + isFromSelf ? a.self_end : a.self_start, + isFromSelf + ? { borderBottomRightRadius: needsTail ? 2 : 17 } + : { borderBottomLeftRadius: needsTail ? 2 : 17 }, + ], children: _jsx(RichText, { value: rt, style: [a.text_md, isFromSelf && { color: t.palette.white }], interactiveStyle: a.underline, enableTags: true, emojiMultiplier: 3, shouldProxyLinks: true }) })), IS_NATIVE && appliedReactions] }), !IS_NATIVE && appliedReactions, isLastInGroup && (_jsx(MessageItemMetadata, { item: item, style: isFromSelf ? a.text_right : a.text_left }))] })] })); +}; +MessageItem = React.memo(MessageItem); +export { MessageItem }; +var MessageItemMetadata = function (_a) { + var item = _a.item, style = _a.style; + var t = useTheme(); + var _ = useLingui()._; + var message = item.message; + var handleRetry = useCallback(function (e) { + if (item.type === 'pending-message' && item.retry) { + e.preventDefault(); + item.retry(); + return false; + } + }, [item]); + var relativeTimestamp = useCallback(function (i18n, timestamp) { + var date = new Date(timestamp); + var now = new Date(); + var time = i18n.date(date, { + hour: 'numeric', + minute: 'numeric', + }); + var diff = now.getTime() - date.getTime(); + // if under 30 seconds + if (diff < 1000 * 30) { + return _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Now"], ["Now"])))); + } + return time; + }, [_]); + return (_jsxs(Text, { style: [ + a.text_xs, + a.mt_2xs, + a.mb_lg, + t.atoms.text_contrast_medium, + style, + ], children: [_jsx(TimeElapsed, { timestamp: message.sentAt, timeToString: relativeTimestamp, children: function (_a) { + var timeElapsed = _a.timeElapsed; + return (_jsx(Text, { style: [a.text_xs, t.atoms.text_contrast_medium], children: timeElapsed })); + } }), item.type === 'pending-message' && item.failed && (_jsxs(_Fragment, { children: [' ', "\u00B7", ' ', _jsx(Text, { style: [ + a.text_xs, + { + color: t.palette.negative_400, + }, + ], children: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Failed to send"], ["Failed to send"])))) }), item.retry && (_jsxs(_Fragment, { children: [' ', "\u00B7", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Click to retry failed message"], ["Click to retry failed message"])))), to: "#", onPress: handleRetry, style: [a.text_xs], children: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Retry"], ["Retry"])))) })] }))] }))] })); +}; +MessageItemMetadata = React.memo(MessageItemMetadata); +export { MessageItemMetadata }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/components/dms/MessageItemEmbed.js b/src/components/dms/MessageItemEmbed.js new file mode 100644 index 0000000000..00ac3a3f2c --- /dev/null +++ b/src/components/dms/MessageItemEmbed.js @@ -0,0 +1,28 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import { atoms as a, native, tokens, useTheme, web } from '#/alf'; +import { PostEmbedViewContext } from '#/components/Post/Embed'; +import { Embed } from '#/components/Post/Embed'; +import { MessageContextProvider } from './MessageContext'; +var MessageItemEmbed = function (_a) { + var embed = _a.embed; + var t = useTheme(); + var screen = useWindowDimensions(); + return (_jsx(MessageContextProvider, { children: _jsx(View, { style: [ + a.my_xs, + t.atoms.bg, + a.rounded_md, + native({ + flexBasis: 0, + width: Math.min(screen.width, 600) / 1.4, + }), + web({ + width: '100%', + minWidth: 280, + maxWidth: 360, + }), + ], children: _jsx(View, { style: { marginTop: tokens.space.sm * -1 }, children: _jsx(Embed, { embed: embed, allowNestedQuotes: true, viewContext: PostEmbedViewContext.Feed }) }) }) })); +}; +MessageItemEmbed = React.memo(MessageItemEmbed); +export { MessageItemEmbed }; diff --git a/src/components/dms/MessageProfileButton.js b/src/components/dms/MessageProfileButton.js new file mode 100644 index 0000000000..76b2b5fc81 --- /dev/null +++ b/src/components/dms/MessageProfileButton.js @@ -0,0 +1,82 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { useGetConvoAvailabilityQuery } from '#/state/queries/messages/get-convo-availability'; +import { useGetConvoForMembers } from '#/state/queries/messages/get-convo-for-members'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { canBeMessaged } from '#/components/dms/util'; +import { Message_Stroke2_Corner0_Rounded as Message } from '#/components/icons/Message'; +import { useAnalytics } from '#/analytics'; +export function MessageProfileButton(_a) { + var profile = _a.profile; + var _ = useLingui()._; + var t = useTheme(); + var ax = useAnalytics(); + var navigation = useNavigation(); + var requireEmailVerification = useRequireEmailVerification(); + var convoAvailability = useGetConvoAvailabilityQuery(profile.did).data; + var initiateConvo = useGetConvoForMembers({ + onSuccess: function (_a) { + var convo = _a.convo; + ax.metric('chat:open', { logContext: 'ProfileHeader' }); + navigation.navigate('MessagesConversation', { conversation: convo.id }); + }, + onError: function () { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to create conversation"], ["Failed to create conversation"]))))); + }, + }).mutate; + var onPress = React.useCallback(function () { + if (!(convoAvailability === null || convoAvailability === void 0 ? void 0 : convoAvailability.canChat)) { + return; + } + if (convoAvailability.convo) { + ax.metric('chat:open', { logContext: 'ProfileHeader' }); + navigation.navigate('MessagesConversation', { + conversation: convoAvailability.convo.id, + }); + } + else { + ax.metric('chat:create', { logContext: 'ProfileHeader' }); + initiateConvo([profile.did]); + } + }, [ax, navigation, profile.did, initiateConvo, convoAvailability]); + var wrappedOnPress = requireEmailVerification(onPress, { + instructions: [ + _jsx(Trans, { children: "Before you can message another user, you must first verify your email." }, "message"), + ], + }); + if (!convoAvailability) { + // show pending state based on declaration + if (canBeMessaged(profile)) { + return (_jsx(View, { testID: "dmBtnLoading", "aria-hidden": true, style: [ + a.justify_center, + a.align_center, + t.atoms.bg_contrast_25, + a.rounded_full, + // Matches size of button below to avoid layout shift + { width: 33, height: 33 }, + ], children: _jsx(Message, { style: [t.atoms.text, { opacity: 0.3 }], size: "md" }) })); + } + else { + return null; + } + } + if (convoAvailability.canChat) { + return (_jsx(_Fragment, { children: _jsx(Button, { accessibilityRole: "button", testID: "dmBtn", size: "small", color: "secondary", variant: "solid", shape: "round", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Message ", ""], ["Message ", ""])), profile.handle)), style: [a.justify_center], onPress: wrappedOnPress, children: _jsx(ButtonIcon, { icon: Message, size: "md" }) }) })); + } + else { + return null; + } +} +var templateObject_1, templateObject_2; diff --git a/src/components/dms/MessagesListBlockedFooter.js b/src/components/dms/MessagesListBlockedFooter.js new file mode 100644 index 0000000000..123beeb143 --- /dev/null +++ b/src/components/dms/MessagesListBlockedFooter.js @@ -0,0 +1,52 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useProfileBlockMutationQueue } from '#/state/queries/profile'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { Divider } from '#/components/Divider'; +import { BlockedByListDialog } from '#/components/dms/BlockedByListDialog'; +import { LeaveConvoPrompt } from '#/components/dms/LeaveConvoPrompt'; +import { ReportConversationPrompt } from '#/components/dms/ReportConversationPrompt'; +import { Text } from '#/components/Typography'; +export function MessagesListBlockedFooter(_a) { + var initialRecipient = _a.recipient, convoId = _a.convoId, hasMessages = _a.hasMessages, moderation = _a.moderation; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var _ = useLingui()._; + var recipient = useProfileShadow(initialRecipient); + var _b = useProfileBlockMutationQueue(recipient), __ = _b[0], queueUnblock = _b[1]; + var leaveConvoControl = useDialogControl(); + var reportControl = useDialogControl(); + var blockedByListControl = useDialogControl(); + var _c = React.useMemo(function () { + var modui = moderation.ui('profileView'); + var blocks = modui.alerts.filter(function (alert) { return alert.type === 'blocking'; }); + var listBlocks = blocks.filter(function (alert) { return alert.source.type === 'list'; }); + var userBlock = blocks.find(function (alert) { return alert.source.type === 'user'; }); + return { + listBlocks: listBlocks, + userBlock: userBlock, + }; + }, [moderation]), listBlocks = _c.listBlocks, userBlock = _c.userBlock; + var isBlocking = !!userBlock || !!listBlocks.length; + var onUnblockPress = React.useCallback(function () { + if (listBlocks.length) { + blockedByListControl.open(); + } + else { + queueUnblock(); + } + }, [blockedByListControl, listBlocks, queueUnblock]); + return (_jsxs(View, { style: [hasMessages && a.pt_md, a.pb_xl, a.gap_lg], children: [_jsx(Divider, {}), _jsx(Text, { style: [a.text_md, a.font_semi_bold, a.text_center], children: isBlocking ? (_jsx(Trans, { children: "You have blocked this user" })) : (_jsx(Trans, { children: "This user has blocked you" })) }), _jsxs(View, { style: [a.flex_row, a.justify_between, a.gap_lg, a.px_md], children: [_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Leave chat"], ["Leave chat"])))), color: "secondary", variant: "solid", size: "small", style: [a.flex_1], onPress: leaveConvoControl.open, children: _jsx(ButtonText, { style: { color: t.palette.negative_500 }, children: _jsx(Trans, { children: "Leave chat" }) }) }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Report"], ["Report"])))), color: "secondary", variant: "solid", size: "small", style: [a.flex_1], onPress: reportControl.open, children: _jsx(ButtonText, { style: { color: t.palette.negative_500 }, children: _jsx(Trans, { children: "Report" }) }) }), isBlocking && gtMobile && (_jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Unblock"], ["Unblock"])))), color: "secondary", variant: "solid", size: "small", style: [a.flex_1], onPress: onUnblockPress, children: _jsx(ButtonText, { style: { color: t.palette.primary_500 }, children: _jsx(Trans, { children: "Unblock" }) }) }))] }), isBlocking && !gtMobile && (_jsx(View, { style: [a.flex_row, a.justify_center, a.px_md], children: _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Unblock"], ["Unblock"])))), color: "secondary", variant: "solid", size: "small", style: [a.flex_1], onPress: onUnblockPress, children: _jsx(ButtonText, { style: { color: t.palette.primary_500 }, children: _jsx(Trans, { children: "Unblock" }) }) }) })), _jsx(LeaveConvoPrompt, { control: leaveConvoControl, currentScreen: "conversation", convoId: convoId }), _jsx(ReportConversationPrompt, { control: reportControl }), _jsx(BlockedByListDialog, { control: blockedByListControl, listBlocks: listBlocks })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/dms/MessagesListHeader.js b/src/components/dms/MessagesListHeader.js new file mode 100644 index 0000000000..657a4c7c36 --- /dev/null +++ b/src/components/dms/MessagesListHeader.js @@ -0,0 +1,90 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { makeProfileLink } from '#/lib/routes/links'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { isConvoActive, useConvo } from '#/state/messages/convo'; +import { PreviewableUserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme, web } from '#/alf'; +import { ConvoMenu } from '#/components/dms/ConvoMenu'; +import { Bell2Off_Filled_Corner0_Rounded as BellStroke } from '#/components/icons/Bell2'; +import * as Layout from '#/components/Layout'; +import { Link } from '#/components/Link'; +import { PostAlerts } from '#/components/moderation/PostAlerts'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +import { IS_WEB } from '#/env'; +var PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE; +export function MessagesListHeader(_a) { + var profile = _a.profile, moderation = _a.moderation; + var t = useTheme(); + var blockInfo = useMemo(function () { + if (!moderation) + return; + var modui = moderation.ui('profileView'); + var blocks = modui.alerts.filter(function (alert) { return alert.type === 'blocking'; }); + var listBlocks = blocks.filter(function (alert) { return alert.source.type === 'list'; }); + var userBlock = blocks.find(function (alert) { return alert.source.type === 'user'; }); + return { + listBlocks: listBlocks, + userBlock: userBlock, + }; + }, [moderation]); + return (_jsx(Layout.Header.Outer, { children: _jsxs(View, { style: [a.w_full, a.flex_row, a.gap_xs, a.align_start], children: [_jsx(View, { style: [{ minHeight: PFP_SIZE }, a.justify_center], children: _jsx(Layout.Header.BackButton, {}) }), profile && moderation && blockInfo ? (_jsx(HeaderReady, { profile: profile, moderation: moderation, blockInfo: blockInfo })) : (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_md, a.flex_1], children: [_jsx(View, { style: [ + { width: PFP_SIZE, height: PFP_SIZE }, + a.rounded_full, + t.atoms.bg_contrast_25, + ] }), _jsxs(View, { style: a.gap_xs, children: [_jsx(View, { style: [ + { width: 120, height: 16 }, + a.rounded_xs, + t.atoms.bg_contrast_25, + a.mt_xs, + ] }), _jsx(View, { style: [ + { width: 175, height: 12 }, + a.rounded_xs, + t.atoms.bg_contrast_25, + ] })] })] }), _jsx(Layout.Header.Slot, {})] }))] }) })); +} +function HeaderReady(_a) { + var _b; + var profile = _a.profile, moderation = _a.moderation, blockInfo = _a.blockInfo; + var _ = useLingui()._; + var t = useTheme(); + var convoState = useConvo(); + var verification = useSimpleVerificationState({ + profile: profile, + }); + var isDeletedAccount = (profile === null || profile === void 0 ? void 0 : profile.handle) === 'missing.invalid'; + var displayName = isDeletedAccount + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Deleted Account"], ["Deleted Account"])))) + : sanitizeDisplayName(profile.displayName || profile.handle, moderation.ui('displayName')); + // @ts-ignore findLast is polyfilled - esb + var latestMessageFromOther = convoState.items.findLast(function (item) { + return item.type === 'message' && item.message.sender.did === profile.did; + }); + var latestReportableMessage = (latestMessageFromOther === null || latestMessageFromOther === void 0 ? void 0 : latestMessageFromOther.type) === 'message' + ? latestMessageFromOther.message + : undefined; + return (_jsxs(View, { style: [a.flex_1], children: [_jsxs(View, { style: [a.w_full, a.flex_row, a.align_center, a.justify_between], children: [_jsxs(Link, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["View ", "'s profile"], ["View ", "'s profile"])), displayName)), style: [a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md], to: makeProfileLink(profile), children: [_jsx(PreviewableUserAvatar, { size: PFP_SIZE, profile: profile, moderation: moderation.ui('avatar'), disableHoverCard: moderation.blocked }), _jsxs(View, { style: [a.flex_1], children: [_jsxs(View, { style: [a.flex_row, a.align_center], children: [_jsx(Text, { emoji: true, style: [ + a.text_md, + a.font_semi_bold, + a.self_start, + web(a.leading_normal), + ], numberOfLines: 1, children: displayName }), verification.showBadge && (_jsx(View, { style: [a.pl_xs], children: _jsx(VerificationCheck, { width: 14, verifier: verification.role === 'verifier' }) }))] }), !isDeletedAccount && (_jsxs(Text, { style: [ + t.atoms.text_contrast_medium, + a.text_xs, + web([a.leading_normal, { marginTop: -2 }]), + ], numberOfLines: 1, children: ["@", profile.handle, ((_b = convoState.convo) === null || _b === void 0 ? void 0 : _b.muted) && (_jsxs(_Fragment, { children: [' ', "\u00B7", ' ', _jsx(BellStroke, { size: "xs", style: t.atoms.text_contrast_medium })] }))] }))] })] }), _jsx(View, { style: [{ minHeight: PFP_SIZE }, a.justify_center], children: _jsx(Layout.Header.Slot, { children: isConvoActive(convoState) && (_jsx(ConvoMenu, { convo: convoState.convo, profile: profile, currentScreen: "conversation", blockInfo: blockInfo, latestReportableMessage: latestReportableMessage })) }) })] }), _jsx(View, { style: [ + { + paddingLeft: PFP_SIZE + a.gap_md.gap, + }, + ], children: _jsx(PostAlerts, { modui: moderation.ui('contentList'), size: "lg", style: [a.pt_xs] }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dms/NewMessagesPill.js b/src/components/dms/NewMessagesPill.js new file mode 100644 index 0000000000..d3be1d1d84 --- /dev/null +++ b/src/components/dms/NewMessagesPill.js @@ -0,0 +1,65 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { Pressable, View } from 'react-native'; +import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Trans } from '@lingui/react/macro'; +import { ScaleAndFadeIn, ScaleAndFadeOut, } from '#/lib/custom-animations/ScaleAndFade'; +import { useHaptics } from '#/lib/haptics'; +import { atoms as a, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +import { IS_ANDROID, IS_IOS, IS_WEB } from '#/env'; +var AnimatedPressable = Animated.createAnimatedComponent(Pressable); +export function NewMessagesPill(_a) { + var onPressInner = _a.onPress; + var t = useTheme(); + var playHaptic = useHaptics(); + var bottomInset = useSafeAreaInsets().bottom; + var bottomBarHeight = IS_IOS ? 42 : IS_ANDROID ? 60 : 0; + var bottomOffset = IS_WEB ? 0 : bottomInset + bottomBarHeight; + var scale = useSharedValue(1); + var onPressIn = React.useCallback(function () { + if (IS_WEB) + return; + scale.set(function () { return withTiming(1.075, { duration: 100 }); }); + }, [scale]); + var onPressOut = React.useCallback(function () { + if (IS_WEB) + return; + scale.set(function () { return withTiming(1, { duration: 100 }); }); + }, [scale]); + var onPress = React.useCallback(function () { + runOnJS(playHaptic)(); + onPressInner === null || onPressInner === void 0 ? void 0 : onPressInner(); + }, [onPressInner, playHaptic]); + var animatedStyle = useAnimatedStyle(function () { return ({ + transform: [{ scale: scale.get() }], + }); }); + return (_jsx(View, { style: [ + a.absolute, + a.w_full, + a.z_10, + a.align_center, + { + bottom: bottomOffset + 70, + // Don't prevent scrolling in this area _except_ for in the pill itself + pointerEvents: 'box-none', + }, + ], children: _jsx(AnimatedPressable, { style: [ + a.py_sm, + a.rounded_full, + a.shadow_sm, + a.border, + t.atoms.bg_contrast_50, + t.atoms.border_contrast_medium, + { + width: 160, + alignItems: 'center', + shadowOpacity: 0.125, + shadowRadius: 12, + shadowOffset: { width: 0, height: 5 }, + pointerEvents: 'box-only', + }, + animatedStyle, + ], entering: ScaleAndFadeIn, exiting: ScaleAndFadeOut, onPress: onPress, onPressIn: onPressIn, onPressOut: onPressOut, children: _jsx(Text, { style: [a.font_semi_bold], children: _jsx(Trans, { children: "New messages" }) }) }) })); +} diff --git a/src/components/dms/ReportConversationPrompt.js b/src/components/dms/ReportConversationPrompt.js new file mode 100644 index 0000000000..6540fd0df4 --- /dev/null +++ b/src/components/dms/ReportConversationPrompt.js @@ -0,0 +1,14 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import * as Prompt from '#/components/Prompt'; +export function ReportConversationPrompt(_a) { + var control = _a.control; + var _ = useLingui()._; + return (_jsx(Prompt.Basic, { control: control, title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Report conversation"], ["Report conversation"])))), description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue."], ["To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue."])))), confirmButtonCta: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["I understand"], ["I understand"])))), onConfirm: function () { }, showCancel: false })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/dms/dialogs/NewChatDialog.js b/src/components/dms/dialogs/NewChatDialog.js new file mode 100644 index 0000000000..fc2327984f --- /dev/null +++ b/src/components/dms/dialogs/NewChatDialog.js @@ -0,0 +1,52 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { logger } from '#/logger'; +import { useGetConvoForMembers } from '#/state/queries/messages/get-convo-for-members'; +import { FAB } from '#/view/com/util/fab/FAB'; +import * as Toast from '#/view/com/util/Toast'; +import { useTheme } from '#/alf'; +import * as Dialog from '#/components/Dialog'; +import { SearchablePeopleList } from '#/components/dialogs/SearchablePeopleList'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { useAnalytics } from '#/analytics'; +export function NewChat(_a) { + var control = _a.control, onNewChat = _a.onNewChat; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var requireEmailVerification = useRequireEmailVerification(); + var createChat = useGetConvoForMembers({ + onSuccess: function (data) { + onNewChat(data.convo.id); + if (!data.convo.lastMessage) { + ax.metric('chat:create', { logContext: 'NewChatDialog' }); + } + ax.metric('chat:open', { logContext: 'NewChatDialog' }); + }, + onError: function (error) { + logger.error('Failed to create chat', { safeMessage: error }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["An issue occurred starting the chat"], ["An issue occurred starting the chat"])))), 'xmark'); + }, + }).mutate; + var onCreateChat = useCallback(function (did) { + control.close(function () { return createChat([did]); }); + }, [control, createChat]); + var onPress = useCallback(function () { + control.open(); + }, [control]); + var wrappedOnPress = requireEmailVerification(onPress, { + instructions: [ + _jsx(Trans, { children: "Before you can message another user, you must first verify your email." }, "new-chat"), + ], + }); + return (_jsxs(_Fragment, { children: [_jsx(FAB, { testID: "newChatFAB", onPress: wrappedOnPress, icon: _jsx(Plus, { size: "lg", fill: t.palette.white }), accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["New chat"], ["New chat"])))), accessibilityHint: "" }), _jsxs(Dialog.Outer, { control: control, testID: "newChatDialog", children: [_jsx(Dialog.Handle, {}), _jsx(SearchablePeopleList, { title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Start a new chat"], ["Start a new chat"])))), onSelectChat: onCreateChat, sortByMessageDeclaration: true })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/dms/dialogs/ShareViaChatDialog.js b/src/components/dms/dialogs/ShareViaChatDialog.js new file mode 100644 index 0000000000..73765bce6e --- /dev/null +++ b/src/components/dms/dialogs/ShareViaChatDialog.js @@ -0,0 +1,41 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { logger } from '#/logger'; +import { useGetConvoForMembers } from '#/state/queries/messages/get-convo-for-members'; +import * as Toast from '#/view/com/util/Toast'; +import * as Dialog from '#/components/Dialog'; +import { SearchablePeopleList } from '#/components/dialogs/SearchablePeopleList'; +import { useAnalytics } from '#/analytics'; +export function SendViaChatDialog(_a) { + var control = _a.control, onSelectChat = _a.onSelectChat; + return (_jsxs(Dialog.Outer, { control: control, testID: "sendViaChatChatDialog", children: [_jsx(Dialog.Handle, {}), _jsx(SendViaChatDialogInner, { control: control, onSelectChat: onSelectChat })] })); +} +function SendViaChatDialogInner(_a) { + var control = _a.control, onSelectChat = _a.onSelectChat; + var _ = useLingui()._; + var ax = useAnalytics(); + var createChat = useGetConvoForMembers({ + onSuccess: function (data) { + onSelectChat(data.convo.id); + if (!data.convo.lastMessage) { + ax.metric('chat:create', { logContext: 'SendViaChatDialog' }); + } + ax.metric('chat:open', { logContext: 'SendViaChatDialog' }); + }, + onError: function (error) { + logger.error('Failed to share post to chat', { message: error }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["An issue occurred while trying to open the chat"], ["An issue occurred while trying to open the chat"])))), 'xmark'); + }, + }).mutate; + var onCreateChat = useCallback(function (did) { + control.close(function () { return createChat([did]); }); + }, [control, createChat]); + return (_jsx(SearchablePeopleList, { title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Send post to..."], ["Send post to..."])))), onSelectChat: onCreateChat, showRecentConvos: true, sortByMessageDeclaration: true })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/dms/dialogs/TextInput.js b/src/components/dms/dialogs/TextInput.js new file mode 100644 index 0000000000..e11deace96 --- /dev/null +++ b/src/components/dms/dialogs/TextInput.js @@ -0,0 +1 @@ +export { BottomSheetTextInput as TextInput } from '@discord/bottom-sheet/src'; diff --git a/src/components/dms/dialogs/TextInput.web.js b/src/components/dms/dialogs/TextInput.web.js new file mode 100644 index 0000000000..7d11b555e8 --- /dev/null +++ b/src/components/dms/dialogs/TextInput.web.js @@ -0,0 +1 @@ +export { TextInput } from 'react-native'; diff --git a/src/components/dms/util.js b/src/components/dms/util.js new file mode 100644 index 0000000000..1d315c4fb2 --- /dev/null +++ b/src/components/dms/util.js @@ -0,0 +1,39 @@ +import { EMOJI_REACTION_LIMIT } from '#/lib/constants'; +export function canBeMessaged(profile) { + var _a, _b, _c; + switch ((_b = (_a = profile.associated) === null || _a === void 0 ? void 0 : _a.chat) === null || _b === void 0 ? void 0 : _b.allowIncoming) { + case 'none': + return false; + case 'all': + return true; + // if unset, treat as following + case 'following': + case undefined: + return Boolean((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.followedBy); + // any other values are invalid according to the lexicon, so + // let's treat as false to be safe + default: + return false; + } +} +export function localDateString(date) { + // can't use toISOString because it should be in local time + var mm = date.getMonth(); + var dd = date.getDate(); + var yyyy = date.getFullYear(); + // not padding with 0s because it's not necessary, it's just used for comparison + return "".concat(yyyy, "-").concat(mm, "-").concat(dd); +} +export function hasAlreadyReacted(message, myDid, emoji) { + if (!message.reactions) { + return false; + } + return !!message.reactions.find(function (reaction) { return reaction.value === emoji && reaction.sender.did === myDid; }); +} +export function hasReachedReactionLimit(message, myDid) { + if (!message.reactions) { + return false; + } + var myReactions = message.reactions.filter(function (reaction) { return reaction.sender.did === myDid; }); + return myReactions.length >= EMOJI_REACTION_LIMIT; +} diff --git a/src/components/feeds/PostFeedVideoGridRow.js b/src/components/feeds/PostFeedVideoGridRow.js new file mode 100644 index 0000000000..1c583119b9 --- /dev/null +++ b/src/components/feeds/PostFeedVideoGridRow.js @@ -0,0 +1,31 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { AppBskyEmbedVideo } from '@atproto/api'; +import { atoms as a, useGutters } from '#/alf'; +import * as Grid from '#/components/Grid'; +import { VideoPostCard, VideoPostCardPlaceholder, } from '#/components/VideoPostCard'; +import { useAnalytics } from '#/analytics'; +export function PostFeedVideoGridRow(_a) { + var slices = _a.items, sourceContext = _a.sourceContext; + var ax = useAnalytics(); + var gutters = useGutters(['base', 'base', 0, 'base']); + var posts = slices + .filter(function (slice) { return AppBskyEmbedVideo.isView(slice.post.embed); }) + .map(function (slice) { return ({ + post: slice.post, + moderation: slice.moderation, + }); }); + /** + * This should not happen because we should be filtering out posts without + * videos within the `PostFeed` component. + */ + if (posts.length !== slices.length) + return null; + return (_jsx(View, { style: [gutters], children: _jsx(View, { style: [a.flex_row, a.gap_sm], children: _jsx(Grid.Row, { gap: a.gap_sm.gap, children: posts.map(function (post) { return (_jsx(Grid.Col, { width: 1 / 2, children: _jsx(VideoPostCard, { post: post.post, sourceContext: sourceContext, moderation: post.moderation, onInteract: function () { + ax.metric('videoCard:click', { context: 'feed' }); + } }) }, post.post.uri)); }) }) }) })); +} +export function PostFeedVideoGridRowPlaceholder() { + var gutters = useGutters(['base', 'base', 0, 'base']); + return (_jsx(View, { style: [gutters], children: _jsxs(View, { style: [a.flex_row, a.gap_sm], children: [_jsx(VideoPostCardPlaceholder, {}), _jsx(VideoPostCardPlaceholder, {})] }) })); +} diff --git a/src/components/forms/DateField/index.android.js b/src/components/forms/DateField/index.android.js new file mode 100644 index 0000000000..e5eadfc5e2 --- /dev/null +++ b/src/components/forms/DateField/index.android.js @@ -0,0 +1,43 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useImperativeHandle, useState } from 'react'; +import { Keyboard } from 'react-native'; +import DatePicker from 'react-native-date-picker'; +import { useLingui } from '@lingui/react'; +import { useTheme } from '#/alf'; +import { toSimpleDateString } from '#/components/forms/DateField/utils'; +import * as TextField from '#/components/forms/TextField'; +import { DateFieldButton } from './index.shared'; +export * as utils from '#/components/forms/DateField/utils'; +export var LabelText = TextField.LabelText; +export function DateField(_a) { + var value = _a.value, inputRef = _a.inputRef, onChangeDate = _a.onChangeDate, label = _a.label, isInvalid = _a.isInvalid, testID = _a.testID, accessibilityHint = _a.accessibilityHint, maximumDate = _a.maximumDate; + var i18n = useLingui().i18n; + var t = useTheme(); + var _b = useState(false), open = _b[0], setOpen = _b[1]; + var onChangeInternal = useCallback(function (date) { + setOpen(false); + var formatted = toSimpleDateString(date); + onChangeDate(formatted); + }, [onChangeDate, setOpen]); + useImperativeHandle(inputRef, function () { return ({ + focus: function () { + Keyboard.dismiss(); + setOpen(true); + }, + blur: function () { + setOpen(false); + }, + }); }, []); + var onPress = useCallback(function () { + setOpen(true); + }, []); + var onCancel = useCallback(function () { + setOpen(false); + }, []); + return (_jsxs(_Fragment, { children: [_jsx(DateFieldButton, { label: label, value: value, onPress: onPress, isInvalid: isInvalid, accessibilityHint: accessibilityHint }), open && ( + // Android implementation of DatePicker currently does not change default button colors according to theme and only takes hex values for buttonColor + // Can remove the buttonColor setting if/when this PR is merged: https://github.com/henninghall/react-native-date-picker/pull/871 + _jsx(DatePicker, { modal: true, open: true, timeZoneOffsetInMinutes: 0, theme: t.scheme, + // @ts-ignore TODO + buttonColor: t.name === 'light' ? '#000000' : '#ffffff', date: new Date(value), onConfirm: onChangeInternal, onCancel: onCancel, mode: "date", locale: i18n.locale, is24hourSource: "locale", testID: "".concat(testID, "-datepicker"), "aria-label": label, accessibilityLabel: label, accessibilityHint: accessibilityHint, maximumDate: maximumDate ? new Date(toSimpleDateString(maximumDate)) : undefined }))] })); +} diff --git a/src/components/forms/DateField/index.js b/src/components/forms/DateField/index.js new file mode 100644 index 0000000000..3307397601 --- /dev/null +++ b/src/components/forms/DateField/index.js @@ -0,0 +1,55 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useImperativeHandle } from 'react'; +import { Keyboard, View } from 'react-native'; +import DatePicker from 'react-native-date-picker'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { toSimpleDateString } from '#/components/forms/DateField/utils'; +import * as TextField from '#/components/forms/TextField'; +import { DateFieldButton } from './index.shared'; +export * as utils from '#/components/forms/DateField/utils'; +export var LabelText = TextField.LabelText; +/** + * Date-only input. Accepts a string in the format YYYY-MM-DD, or a Date object. + * Date objects are converted to strings in the format YYYY-MM-DD. + * Returns a string in the format YYYY-MM-DD. + * + * To generate a string in the format YYYY-MM-DD from a Date object, use the + * `utils.toSimpleDateString(Date)` export of this file. + */ +export function DateField(_a) { + var value = _a.value, inputRef = _a.inputRef, onChangeDate = _a.onChangeDate, testID = _a.testID, label = _a.label, isInvalid = _a.isInvalid, accessibilityHint = _a.accessibilityHint, maximumDate = _a.maximumDate; + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var t = useTheme(); + var control = Dialog.useDialogControl(); + var onChangeInternal = useCallback(function (date) { + if (date) { + var formatted = toSimpleDateString(date); + onChangeDate(formatted); + } + }, [onChangeDate]); + useImperativeHandle(inputRef, function () { return ({ + focus: function () { + Keyboard.dismiss(); + control.open(); + }, + blur: function () { + control.close(); + }, + }); }, [control]); + return (_jsxs(_Fragment, { children: [_jsx(DateFieldButton, { label: label, value: value, onPress: function () { + Keyboard.dismiss(); + control.open(); + }, isInvalid: isInvalid, accessibilityHint: accessibilityHint }), _jsxs(Dialog.Outer, { control: control, testID: testID, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(Dialog.ScrollableInner, { label: label, children: _jsxs(View, { style: a.gap_lg, children: [_jsx(View, { style: [a.relative, a.w_full, a.align_center], children: _jsx(DatePicker, { timeZoneOffsetInMinutes: 0, theme: t.scheme, date: new Date(toSimpleDateString(value)), onDateChange: onChangeInternal, mode: "date", locale: i18n.locale, testID: "".concat(testID, "-datepicker"), "aria-label": label, accessibilityLabel: label, accessibilityHint: accessibilityHint, maximumDate: maximumDate + ? new Date(toSimpleDateString(maximumDate)) + : undefined }) }), _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Done"], ["Done"])))), onPress: function () { return control.close(); }, size: "large", color: "primary", variant: "solid", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Done" }) }) })] }) })] })] })); +} +var templateObject_1; diff --git a/src/components/forms/DateField/index.shared.js b/src/components/forms/DateField/index.shared.js new file mode 100644 index 0000000000..4fedd6cbeb --- /dev/null +++ b/src/components/forms/DateField/index.shared.js @@ -0,0 +1,64 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Pressable, View } from 'react-native'; +import { useLingui } from '@lingui/react'; +import { atoms as a, native, useTheme, web } from '#/alf'; +import * as TextField from '#/components/forms/TextField'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { CalendarDays_Stroke2_Corner0_Rounded as CalendarDays } from '#/components/icons/CalendarDays'; +import { Text } from '#/components/Typography'; +// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press +// iOS: open a dialog with an inline date picker +// Android: open the date picker modal +export function DateFieldButton(_a) { + var label = _a.label, value = _a.value, onPress = _a.onPress, isInvalid = _a.isInvalid, accessibilityHint = _a.accessibilityHint; + var i18n = useLingui().i18n; + var t = useTheme(); + var _b = useInteractionState(), pressed = _b.state, onPressIn = _b.onIn, onPressOut = _b.onOut; + var _c = useInteractionState(), hovered = _c.state, onHoverIn = _c.onIn, onHoverOut = _c.onOut; + var _d = useInteractionState(), focused = _d.state, onFocus = _d.onIn, onBlur = _d.onOut; + var _e = TextField.useSharedInputStyles(), chromeHover = _e.chromeHover, chromeFocus = _e.chromeFocus, chromeError = _e.chromeError, chromeErrorHover = _e.chromeErrorHover; + return (_jsx(View, __assign({ style: [a.relative, a.w_full] }, web({ + onMouseOver: onHoverIn, + onMouseOut: onHoverOut, + }), { children: _jsxs(Pressable, { "aria-label": label, accessibilityLabel: label, accessibilityHint: accessibilityHint, onPress: onPress, onPressIn: onPressIn, onPressOut: onPressOut, onFocus: onFocus, onBlur: onBlur, style: [ + { + paddingLeft: 14, + paddingRight: 14, + borderColor: 'transparent', + borderWidth: 2, + }, + native({ + paddingTop: 10, + paddingBottom: 10, + }), + web(a.py_md), + a.flex_row, + a.flex_1, + a.w_full, + { borderRadius: 10 }, + t.atoms.bg_contrast_50, + a.align_center, + hovered ? chromeHover : {}, + focused || pressed ? chromeFocus : {}, + isInvalid || isInvalid ? chromeError : {}, + (isInvalid || isInvalid) && (hovered || focused) + ? chromeErrorHover + : {}, + ], children: [_jsx(TextField.Icon, { icon: CalendarDays }), _jsx(Text, { style: [ + a.text_md, + a.pl_xs, + t.atoms.text, + { lineHeight: a.text_md.fontSize * 1.1875 }, + ], children: i18n.date(value, { timeZone: 'UTC' }) })] }) }))); +} diff --git a/src/components/forms/DateField/index.web.js b/src/components/forms/DateField/index.web.js new file mode 100644 index 0000000000..005df66ddb --- /dev/null +++ b/src/components/forms/DateField/index.web.js @@ -0,0 +1,57 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { StyleSheet } from 'react-native'; +// @ts-expect-error untyped +import { unstable_createElement } from 'react-native-web'; +import { toSimpleDateString } from '#/components/forms/DateField/utils'; +import * as TextField from '#/components/forms/TextField'; +import { CalendarDays_Stroke2_Corner0_Rounded as CalendarDays } from '#/components/icons/CalendarDays'; +export * as utils from '#/components/forms/DateField/utils'; +export var LabelText = TextField.LabelText; +var InputBase = React.forwardRef(function (_a, ref) { + var style = _a.style, props = __rest(_a, ["style"]); + return unstable_createElement('input', __assign(__assign({}, props), { ref: ref, type: 'date', style: [ + StyleSheet.flatten(style), + { + background: 'transparent', + border: 0, + }, + ] })); +}); +InputBase.displayName = 'InputBase'; +var Input = TextField.createInput(InputBase); +export function DateField(_a) { + var value = _a.value, inputRef = _a.inputRef, onChangeDate = _a.onChangeDate, label = _a.label, isInvalid = _a.isInvalid, testID = _a.testID, accessibilityHint = _a.accessibilityHint, maximumDate = _a.maximumDate; + var handleOnChange = React.useCallback(function (e) { + var date = e.target.valueAsDate || e.target.value; + if (date) { + var formatted = toSimpleDateString(date); + onChangeDate(formatted); + } + }, [onChangeDate]); + return (_jsxs(TextField.Root, { isInvalid: isInvalid, children: [_jsx(TextField.Icon, { icon: CalendarDays }), _jsx(Input, { value: toSimpleDateString(value), inputRef: inputRef, label: label, onChange: handleOnChange, testID: testID, accessibilityHint: accessibilityHint, + // @ts-expect-error not typed as even though it is one + max: maximumDate ? toSimpleDateString(maximumDate) : undefined })] })); +} diff --git a/src/components/forms/DateField/types.js b/src/components/forms/DateField/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/forms/DateField/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/forms/DateField/utils.js b/src/components/forms/DateField/utils.js new file mode 100644 index 0000000000..5748b4f2a0 --- /dev/null +++ b/src/components/forms/DateField/utils.js @@ -0,0 +1,5 @@ +// we need the date in the form yyyy-MM-dd to pass to the input +export function toSimpleDateString(date) { + var _date = typeof date === 'string' ? new Date(date) : date; + return _date.toISOString().split('T')[0]; +} diff --git a/src/components/forms/FormError.js b/src/components/forms/FormError.js new file mode 100644 index 0000000000..40d6606273 --- /dev/null +++ b/src/components/forms/FormError.js @@ -0,0 +1,18 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +import { Warning_Stroke2_Corner0_Rounded as Warning } from '#/components/icons/Warning'; +import { Text } from '#/components/Typography'; +export function FormError(_a) { + var error = _a.error; + var t = useTheme(); + if (!error) + return null; + return (_jsxs(View, { style: [ + { backgroundColor: t.palette.negative_400 }, + a.flex_row, + a.rounded_sm, + a.p_md, + a.gap_sm, + ], children: [_jsx(Warning, { fill: t.palette.white, size: "md" }), _jsx(View, { style: [a.flex_1], children: _jsx(Text, { style: [{ color: t.palette.white }, a.font_semi_bold, a.leading_snug], children: error }) })] })); +} diff --git a/src/components/forms/HostingProvider.js b/src/components/forms/HostingProvider.js new file mode 100644 index 0000000000..49e76331f1 --- /dev/null +++ b/src/components/forms/HostingProvider.js @@ -0,0 +1,60 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { Keyboard, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { toNiceDomain } from '#/lib/strings/url-helpers'; +import { atoms as a, tokens, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { ServerInputDialog } from '#/components/dialogs/ServerInput'; +import { Globe_Stroke2_Corner0_Rounded as GlobeIcon } from '#/components/icons/Globe'; +import { PencilLine_Stroke2_Corner0_Rounded as PencilIcon } from '#/components/icons/Pencil'; +import { Text } from '#/components/Typography'; +export function HostingProvider(_a) { + var serviceUrl = _a.serviceUrl, onSelectServiceUrl = _a.onSelectServiceUrl, onOpenDialog = _a.onOpenDialog, minimal = _a.minimal; + var serverInputControl = useDialogControl(); + var t = useTheme(); + var _ = useLingui()._; + var onPressSelectService = React.useCallback(function () { + Keyboard.dismiss(); + serverInputControl.open(); + onOpenDialog === null || onOpenDialog === void 0 ? void 0 : onOpenDialog(); + }, [onOpenDialog, serverInputControl]); + return (_jsxs(_Fragment, { children: [_jsx(ServerInputDialog, { control: serverInputControl, onSelect: onSelectServiceUrl }), minimal ? (_jsxs(View, { style: [a.flex_row, a.align_center, a.flex_wrap, a.gap_xs], children: [_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "You are creating an account on" }) }), _jsxs(Button, { label: toNiceDomain(serviceUrl), accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Changes hosting provider"], ["Changes hosting provider"])))), onPress: onPressSelectService, variant: "ghost", color: "secondary", size: "tiny", style: [ + a.px_xs, + { marginHorizontal: tokens.space.xs * -1 }, + { paddingVertical: 0 }, + ], children: [_jsx(ButtonText, { style: [a.text_sm], children: toNiceDomain(serviceUrl) }), _jsx(ButtonIcon, { icon: PencilIcon })] })] })) : (_jsx(Button, { testID: "selectServiceButton", label: toNiceDomain(serviceUrl), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Changes hosting provider"], ["Changes hosting provider"])))), variant: "solid", color: "secondary", style: [ + a.w_full, + a.flex_row, + a.align_center, + a.rounded_sm, + a.py_sm, + a.pl_md, + a.pr_sm, + a.gap_xs, + ], onPress: onPressSelectService, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + var interacted = hovered || pressed; + return (_jsxs(_Fragment, { children: [_jsx(View, { style: a.pr_xs, children: _jsx(GlobeIcon, { size: "md", fill: interacted + ? t.palette.contrast_800 + : t.palette.contrast_500 }) }), _jsx(Text, { style: [a.text_md], children: toNiceDomain(serviceUrl) }), _jsx(View, { style: [ + a.rounded_sm, + interacted + ? t.atoms.bg_contrast_300 + : t.atoms.bg_contrast_100, + { marginLeft: 'auto', padding: 6 }, + ], children: _jsx(PencilIcon, { size: "sm", style: { + color: interacted + ? t.palette.contrast_800 + : t.palette.contrast_500, + } }) })] })); + } }))] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/forms/InputGroup.js b/src/components/forms/InputGroup.js new file mode 100644 index 0000000000..c51733747d --- /dev/null +++ b/src/components/forms/InputGroup.js @@ -0,0 +1,40 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { atoms, useTheme } from '#/alf'; +/** + * NOT FINISHED, just here as a reference + */ +export function InputGroup(props) { + var t = useTheme(); + var children = React.Children.toArray(props.children); + var total = children.length; + return (_jsx(View, { style: [atoms.w_full], children: children.map(function (child, i) { + var _a; + return React.isValidElement(child) ? (_jsxs(React.Fragment, { children: [i > 0 ? (_jsx(View, { style: [atoms.border_b, { borderColor: t.palette.contrast_500 }] })) : null, React.cloneElement(child, { + // @ts-ignore + style: __spreadArray(__spreadArray([], (Array.isArray((_a = child.props) === null || _a === void 0 ? void 0 : _a.style) + ? // @ts-ignore + child.props.style + : // @ts-ignore + [child.props.style || {}]), true), [ + { + borderTopLeftRadius: i > 0 ? 0 : undefined, + borderTopRightRadius: i > 0 ? 0 : undefined, + borderBottomLeftRadius: i < total - 1 ? 0 : undefined, + borderBottomRightRadius: i < total - 1 ? 0 : undefined, + borderBottomWidth: i < total - 1 ? 0 : undefined, + }, + ], false), + })] }, i)) : null; + }) })); +} diff --git a/src/components/forms/SearchInput.js b/src/components/forms/SearchInput.js new file mode 100644 index 0000000000..b153474044 --- /dev/null +++ b/src/components/forms/SearchInput.js @@ -0,0 +1,60 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_10 } from '#/lib/constants'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import * as TextField from '#/components/forms/TextField'; +import { MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon } from '#/components/icons/MagnifyingGlass'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { IS_NATIVE } from '#/env'; +export var SearchInput = React.forwardRef(function SearchInput(_a, ref) { + var value = _a.value, label = _a.label, onClearText = _a.onClearText, rest = __rest(_a, ["value", "label", "onClearText"]); + var t = useTheme(); + var _ = useLingui()._; + var showClear = value && value.length > 0; + return (_jsxs(View, { style: [a.w_full, a.relative], children: [_jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: MagnifyingGlassIcon }), _jsx(TextField.Input, __assign({ inputRef: ref, label: label || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Search"], ["Search"])))), value: value, placeholder: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Search"], ["Search"])))), returnKeyType: "search", keyboardAppearance: t.scheme, selectTextOnFocus: IS_NATIVE, autoFocus: false, accessibilityRole: "search", autoCorrect: false, autoComplete: "off", autoCapitalize: "none", style: [ + showClear + ? { + paddingRight: 24, + } + : {}, + ] }, rest))] }), showClear && (_jsx(View, { style: [ + a.absolute, + a.z_20, + a.my_auto, + a.inset_0, + a.justify_center, + a.pr_sm, + { left: 'auto' }, + ], children: _jsx(Button, { testID: "searchTextInputClearBtn", onPress: onClearText, label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Clear search query"], ["Clear search query"])))), hitSlop: HITSLOP_10, size: "tiny", shape: "round", variant: "ghost", color: "secondary", children: _jsx(ButtonIcon, { icon: X, size: "xs" }) }) }))] })); +}); +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/forms/SegmentedControl.js b/src/components/forms/SegmentedControl.js new file mode 100644 index 0000000000..140068f7bf --- /dev/null +++ b/src/components/forms/SegmentedControl.js @@ -0,0 +1,193 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState, } from 'react'; +import { View } from 'react-native'; +import Animated, { Easing, LinearTransition } from 'react-native-reanimated'; +import { useHaptics } from '#/lib/haptics'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { atoms as a, native, platform, useTheme } from '#/alf'; +import { Button, ButtonText, } from '../Button'; +var InternalContext = createContext(null); +/** + * Segmented control component. + * + * @example + * ```tsx + * + * + * + * One + * + * + * + * + * Two + * + * + * + * ``` + */ +export function Root(_a) { + var label = _a.label, _b = _a.type, type = _b === void 0 ? 'radio' : _b, _c = _a.size, size = _c === void 0 ? 'large' : _c, value = _a.value, onChange = _a.onChange, children = _a.children, style = _a.style, accessibilityHint = _a.accessibilityHint; + var t = useTheme(); + var _d = useState(null), selectedPosition = _d[0], setSelectedPosition = _d[1]; + var contextValue = useMemo(function () { + return { + type: type, + size: size, + selectedValue: value, + selectedPosition: selectedPosition, + onSelectValue: function (val, position) { + onChange(val); + if (position) + setSelectedPosition(position); + }, + updatePosition: function (position) { + setSelectedPosition(function (currPos) { + if (currPos && + currPos.width === position.width && + currPos.x === position.x) { + return currPos; + } + return position; + }); + }, + }; + }, [value, selectedPosition, setSelectedPosition, onChange, type, size]); + return (_jsxs(View, { accessibilityLabel: label, accessibilityHint: accessibilityHint !== null && accessibilityHint !== void 0 ? accessibilityHint : '', style: [ + a.w_full, + a.flex_1, + a.relative, + a.flex_row, + t.atoms.bg_contrast_50, + { borderRadius: 14 }, + a.curve_continuous, + a.p_xs, + style, + ], role: type === 'tabs' ? 'tablist' : 'radiogroup', children: [selectedPosition !== null && (_jsx(Slider, { x: selectedPosition.x, width: selectedPosition.width })), _jsx(InternalContext.Provider, { value: contextValue, children: children })] })); +} +var InternalItemContext = createContext(null); +export function Item(_a) { + var _b, _c; + var value = _a.value, style = _a.style, children = _a.children, onPressProp = _a.onPress, props = __rest(_a, ["value", "style", "children", "onPress"]); + var playHaptic = useHaptics(); + var _d = useState(null), position = _d[0], setPosition = _d[1]; + var ctx = useContext(InternalContext); + if (!ctx) + throw new Error('SegmentedControl.Item must be used within a SegmentedControl.Root'); + var active = ctx.selectedValue === value; + // update position if change was external, and not due to onPress + var needsUpdate = active && + position && + (((_b = ctx.selectedPosition) === null || _b === void 0 ? void 0 : _b.x) !== position.x || + ((_c = ctx.selectedPosition) === null || _c === void 0 ? void 0 : _c.width) !== position.width); + // can't wait for `useEffectEvent` + var update = useNonReactiveCallback(function () { + if (position) + ctx.updatePosition(position); + }); + useLayoutEffect(function () { + if (needsUpdate) { + update(); + } + }, [needsUpdate, update]); + var onPress = useCallback(function (evt) { + playHaptic('Light'); + ctx.onSelectValue(value, position); + onPressProp === null || onPressProp === void 0 ? void 0 : onPressProp(evt); + }, [ctx, value, position, onPressProp, playHaptic]); + return (_jsx(View, { style: [a.flex_1, a.flex_row], onLayout: function (evt) { + var measuredPosition = { + x: evt.nativeEvent.layout.x, + width: evt.nativeEvent.layout.width, + }; + if (!ctx.selectedPosition && active) { + ctx.onSelectValue(value, measuredPosition); + } + setPosition(measuredPosition); + }, children: _jsx(Button, __assign({}, props, { onPress: onPress, role: ctx.type === 'tabs' ? 'tab' : 'radio', accessibilityState: { selected: active }, style: [ + a.flex_1, + a.bg_transparent, + a.px_sm, + a.py_xs, + { minHeight: ctx.size === 'large' ? 40 : 32 }, + style, + ], children: function (_a) { + var pressed = _a.pressed, hovered = _a.hovered, focused = _a.focused; + return (_jsx(InternalItemContext.Provider, { value: { active: active, pressed: pressed, hovered: hovered, focused: focused }, children: children })); + } })) })); +} +export function ItemText(_a) { + var style = _a.style, props = __rest(_a, ["style"]); + var t = useTheme(); + var ctx = useContext(InternalItemContext); + if (!ctx) + throw new Error('SegmentedControl.ItemText must be used within a SegmentedControl.Item'); + return (_jsx(ButtonText, __assign({}, props, { style: [ + a.text_center, + a.text_md, + a.font_medium, + a.px_xs, + ctx.active + ? t.atoms.text + : ctx.focused || ctx.hovered || ctx.pressed + ? t.atoms.text_contrast_medium + : t.atoms.text_contrast_low, + style, + ] }))); +} +function Slider(_a) { + var x = _a.x, width = _a.width; + var t = useTheme(); + return (_jsx(Animated.View, { layout: native(LinearTransition.easing(Easing.out(Easing.exp))), style: [ + a.absolute, + a.curve_continuous, + t.atoms.bg, + { + top: 4, + bottom: 4, + left: 0, + width: width, + borderRadius: 10, + }, + // TODO: new arch supports boxShadow on native + // in the meantime this is an attempt to get close + platform({ + web: { + boxShadow: '0px 2px 4px 0px #0000000D', + }, + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0x0d / 0xff, + shadowRadius: 4, + }, + android: { elevation: 0.25 }, + }), + platform({ + native: [{ left: x }], + web: [{ transform: [{ translateX: x }] }, a.transition_transform], + }), + ] })); +} diff --git a/src/components/forms/TextField.js b/src/components/forms/TextField.js new file mode 100644 index 0000000000..b3a48cfefb --- /dev/null +++ b/src/components/forms/TextField.js @@ -0,0 +1,290 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { createContext, useContext, useMemo, useRef } from 'react'; +import { StyleSheet, TextInput, View, } from 'react-native'; +import { HITSLOP_20 } from '#/lib/constants'; +import { mergeRefs } from '#/lib/merge-refs'; +import { android, applyFonts, atoms as a, platform, tokens, useAlf, useTheme, web, } from '#/alf'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Text } from '#/components/Typography'; +var Context = createContext({ + inputRef: null, + isInvalid: false, + hovered: false, + onHoverIn: function () { }, + onHoverOut: function () { }, + focused: false, + onFocus: function () { }, + onBlur: function () { }, +}); +Context.displayName = 'TextFieldContext'; +export function Root(_a) { + var children = _a.children, _b = _a.isInvalid, isInvalid = _b === void 0 ? false : _b, style = _a.style; + var inputRef = useRef(null); + var _c = useInteractionState(), hovered = _c.state, onHoverIn = _c.onIn, onHoverOut = _c.onOut; + var _d = useInteractionState(), focused = _d.state, onFocus = _d.onIn, onBlur = _d.onOut; + var context = useMemo(function () { return ({ + inputRef: inputRef, + hovered: hovered, + onHoverIn: onHoverIn, + onHoverOut: onHoverOut, + focused: focused, + onFocus: onFocus, + onBlur: onBlur, + isInvalid: isInvalid, + }); }, [ + inputRef, + hovered, + onHoverIn, + onHoverOut, + focused, + onFocus, + onBlur, + isInvalid, + ]); + return (_jsx(Context.Provider, { value: context, children: _jsx(View, __assign({ style: [ + a.flex_row, + a.align_center, + a.relative, + a.w_full, + a.px_md, + style, + ] }, web({ + onClick: function () { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, + onMouseOver: onHoverIn, + onMouseOut: onHoverOut, + }), { children: children })) })); +} +export function useSharedInputStyles() { + var t = useTheme(); + return useMemo(function () { + var hover = [ + { + borderColor: t.palette.contrast_100, + }, + ]; + var focus = [ + { + backgroundColor: t.palette.contrast_50, + borderColor: t.palette.primary_500, + }, + ]; + var error = [ + { + backgroundColor: t.palette.negative_25, + borderColor: t.palette.negative_300, + }, + ]; + var errorHover = [ + { + backgroundColor: t.palette.negative_25, + borderColor: t.palette.negative_500, + }, + ]; + return { + chromeHover: StyleSheet.flatten(hover), + chromeFocus: StyleSheet.flatten(focus), + chromeError: StyleSheet.flatten(error), + chromeErrorHover: StyleSheet.flatten(errorHover), + }; + }, [t]); +} +export function createInput(Component) { + return function Input(_a) { + var label = _a.label, placeholder = _a.placeholder, value = _a.value, onChangeText = _a.onChangeText, onFocus = _a.onFocus, onBlur = _a.onBlur, isInvalid = _a.isInvalid, inputRef = _a.inputRef, style = _a.style, rest = __rest(_a, ["label", "placeholder", "value", "onChangeText", "onFocus", "onBlur", "isInvalid", "inputRef", "style"]); + var t = useTheme(); + var fonts = useAlf().fonts; + var ctx = useContext(Context); + var withinRoot = Boolean(ctx.inputRef); + var _b = useSharedInputStyles(), chromeHover = _b.chromeHover, chromeFocus = _b.chromeFocus, chromeError = _b.chromeError, chromeErrorHover = _b.chromeErrorHover; + if (!withinRoot) { + return (_jsx(Root, { isInvalid: isInvalid, children: _jsx(Input, __assign({ label: label, placeholder: placeholder, value: value, onChangeText: onChangeText, isInvalid: isInvalid }, rest)) })); + } + var refs = mergeRefs([ctx.inputRef, inputRef].filter(Boolean)); + var flattened = StyleSheet.flatten([ + a.relative, + a.z_20, + a.flex_1, + a.text_md, + t.atoms.text, + a.px_xs, + { + // paddingVertical doesn't work w/multiline - esb + lineHeight: a.text_md.fontSize * 1.2, + textAlignVertical: rest.multiline ? 'top' : undefined, + minHeight: rest.multiline ? 80 : undefined, + minWidth: 0, + paddingTop: 13, + paddingBottom: 13, + }, + android({ + paddingTop: 8, + paddingBottom: 9, + }), + /* + * Margins are needed here to avoid autofill background overlapping the + * top and bottom borders - esb + */ + web({ + paddingTop: 11, + paddingBottom: 11, + marginTop: 2, + marginBottom: 2, + }), + style, + ]); + applyFonts(flattened, fonts.family); + // should always be defined on `typography` + // @ts-ignore + if (flattened.fontSize) { + // @ts-ignore + flattened.fontSize = Math.round( + // @ts-ignore + flattened.fontSize * fonts.scaleMultiplier); + } + return (_jsxs(_Fragment, { children: [_jsx(Component, __assign({ accessibilityHint: undefined, hitSlop: HITSLOP_20 }, rest, { accessibilityLabel: label, ref: refs, value: value, onChangeText: onChangeText, onFocus: function (e) { + ctx.onFocus(); + onFocus === null || onFocus === void 0 ? void 0 : onFocus(e); + }, onBlur: function (e) { + ctx.onBlur(); + onBlur === null || onBlur === void 0 ? void 0 : onBlur(e); + }, placeholder: placeholder === null ? undefined : placeholder || label, placeholderTextColor: t.palette.contrast_500, keyboardAppearance: t.name === 'light' ? 'light' : 'dark', style: flattened })), _jsx(View, { style: [ + a.z_10, + a.absolute, + a.inset_0, + { borderRadius: 10 }, + t.atoms.bg_contrast_50, + { borderColor: 'transparent', borderWidth: 2 }, + ctx.hovered ? chromeHover : {}, + ctx.focused ? chromeFocus : {}, + ctx.isInvalid || isInvalid ? chromeError : {}, + (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) + ? chromeErrorHover + : {}, + ] })] })); + }; +} +export var Input = createInput(TextInput); +export function LabelText(_a) { + var nativeID = _a.nativeID, children = _a.children; + var t = useTheme(); + return (_jsx(Text, { nativeID: nativeID, style: [a.text_sm, a.font_medium, t.atoms.text_contrast_medium, a.mb_sm], children: children })); +} +export function Icon(_a) { + var Comp = _a.icon; + var t = useTheme(); + var ctx = useContext(Context); + var _b = useMemo(function () { + var hover = [ + { + color: t.palette.contrast_800, + }, + ]; + var focus = [ + { + color: t.palette.primary_500, + }, + ]; + var errorHover = [ + { + color: t.palette.negative_500, + }, + ]; + var errorFocus = [ + { + color: t.palette.negative_500, + }, + ]; + return { + hover: hover, + focus: focus, + errorHover: errorHover, + errorFocus: errorFocus, + }; + }, [t]), hover = _b.hover, focus = _b.focus, errorHover = _b.errorHover, errorFocus = _b.errorFocus; + return (_jsx(View, { style: [a.z_20, a.pr_xs], children: _jsx(Comp, { size: "md", style: [ + { color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0 }, + ctx.hovered ? hover : {}, + ctx.focused ? focus : {}, + ctx.isInvalid && ctx.hovered ? errorHover : {}, + ctx.isInvalid && ctx.focused ? errorFocus : {}, + ] }) })); +} +export function SuffixText(_a) { + var children = _a.children, label = _a.label, accessibilityHint = _a.accessibilityHint, style = _a.style; + var t = useTheme(); + var ctx = useContext(Context); + return (_jsx(Text, { accessibilityLabel: label, accessibilityHint: accessibilityHint, numberOfLines: 1, style: [ + a.z_20, + a.pr_sm, + a.text_md, + t.atoms.text_contrast_medium, + a.pointer_events_none, + web([{ marginTop: -2 }, a.leading_snug]), + (ctx.hovered || ctx.focused) && { color: t.palette.contrast_800 }, + style, + ], children: children })); +} +export function GhostText(_a) { + var children = _a.children, value = _a.value; + var t = useTheme(); + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text + return (_jsx(View, { style: [ + a.pointer_events_none, + a.absolute, + a.z_10, + { + paddingLeft: platform({ + native: + // input padding + tokens.space.md + + // icon + tokens.space.xl + + // icon padding + tokens.space.xs + + // text input padding + tokens.space.xs, + web: + // icon + tokens.space.xl + + // icon padding + tokens.space.xs + + // text input padding + tokens.space.xs, + }), + }, + web(a.pr_md), + a.overflow_hidden, + a.max_w_full, + ], "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: _jsxs(Text, { style: [ + { color: 'transparent' }, + a.text_md, + { lineHeight: a.text_md.fontSize * 1.1875 }, + a.w_full, + ], numberOfLines: 1, children: [children, _jsx(Text, { style: [ + t.atoms.text_contrast_low, + a.text_md, + { lineHeight: a.text_md.fontSize * 1.1875 }, + ], children: value })] }) })); +} diff --git a/src/components/forms/Toggle/Panel.js b/src/components/forms/Toggle/Panel.js new file mode 100644 index 0000000000..874c650f2b --- /dev/null +++ b/src/components/forms/Toggle/Panel.js @@ -0,0 +1,75 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { createContext, useContext } from 'react'; +import { View } from 'react-native'; +import { atoms as a, tokens, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +var PanelContext = createContext({ active: false }); +/** + * A nice container for Toggles. See the Threadgate dialog for an example. + */ +export function Panel(_a) { + var children = _a.children, _b = _a.active, active = _b === void 0 ? false : _b, adjacent = _a.adjacent; + var t = useTheme(); + var leading = adjacent === 'leading' || adjacent === 'both'; + var trailing = adjacent === 'trailing' || adjacent === 'both'; + var rounding = { + borderTopLeftRadius: leading + ? tokens.borderRadius.xs + : tokens.borderRadius.md, + borderTopRightRadius: leading + ? tokens.borderRadius.xs + : tokens.borderRadius.md, + borderBottomLeftRadius: trailing + ? tokens.borderRadius.xs + : tokens.borderRadius.md, + borderBottomRightRadius: trailing + ? tokens.borderRadius.xs + : tokens.borderRadius.md, + }; + return (_jsx(View, { style: [ + a.w_full, + a.flex_row, + a.align_center, + a.gap_sm, + a.px_md, + a.py_md, + { minHeight: tokens.space._2xl + tokens.space.md * 2 }, + rounding, + active + ? { backgroundColor: t.palette.primary_50 } + : t.atoms.bg_contrast_50, + ], children: _jsx(PanelContext, { value: { active: active }, children: children }) })); +} +export function PanelText(_a) { + var children = _a.children, icon = _a.icon; + var t = useTheme(); + var ctx = useContext(PanelContext); + var text = (_jsx(Text, { style: [ + a.text_md, + a.flex_1, + ctx.active + ? [a.font_medium, t.atoms.text] + : [t.atoms.text_contrast_medium], + ], children: children })); + if (icon) { + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text + return (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs, a.flex_1], children: [_jsx(PanelIcon, { icon: icon }), text] })); + } + return text; +} +export function PanelIcon(_a) { + var Icon = _a.icon; + var t = useTheme(); + var ctx = useContext(PanelContext); + return (_jsx(Icon, { style: [ + ctx.active ? t.atoms.text : t.atoms.text_contrast_medium, + a.flex_shrink_0, + ], size: "md" })); +} +/** + * A group of panels. TODO: auto-leading/trailing + */ +export function PanelGroup(_a) { + var children = _a.children; + return _jsx(View, { style: [a.w_full, a.gap_2xs], children: children }); +} diff --git a/src/components/forms/Toggle/index.js b/src/components/forms/Toggle/index.js new file mode 100644 index 0000000000..f683b31f9d --- /dev/null +++ b/src/components/forms/Toggle/index.js @@ -0,0 +1,362 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useMemo } from 'react'; +import { Pressable, View, } from 'react-native'; +import Animated, { Easing, LinearTransition } from 'react-native-reanimated'; +import { HITSLOP_10 } from '#/lib/constants'; +import { useHaptics } from '#/lib/haptics'; +import { atoms as a, native, platform, useTheme, } from '#/alf'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { CheckThick_Stroke2_Corner0_Rounded as Checkmark } from '#/components/icons/Check'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +export * from './Panel'; +var ItemContext = createContext({ + name: '', + selected: false, + disabled: false, + isInvalid: false, + hovered: false, + pressed: false, + focused: false, +}); +ItemContext.displayName = 'ToggleItemContext'; +var GroupContext = createContext({ + type: 'checkbox', + values: [], + disabled: false, + maxSelectionsReached: false, + setFieldValue: function () { }, +}); +GroupContext.displayName = 'ToggleGroupContext'; +export function useItemContext() { + return useContext(ItemContext); +} +export function Group(_a) { + var children = _a.children, providedValues = _a.values, onChange = _a.onChange, _b = _a.disabled, disabled = _b === void 0 ? false : _b, _c = _a.type, type = _c === void 0 ? 'checkbox' : _c, maxSelections = _a.maxSelections, label = _a.label, style = _a.style; + var groupRole = type === 'radio' ? 'radiogroup' : undefined; + var values = type === 'radio' ? providedValues.slice(0, 1) : providedValues; + var setFieldValue = useCallback(function (_a) { + var name = _a.name, value = _a.value; + if (type === 'checkbox') { + var pruned = values.filter(function (v) { return v !== name; }); + var next = value ? pruned.concat(name) : pruned; + onChange(next); + } + else { + onChange([name]); + } + }, [type, onChange, values]); + var maxReached = !!(type === 'checkbox' && + maxSelections && + values.length >= maxSelections); + var context = useMemo(function () { return ({ + values: values, + type: type, + disabled: disabled, + maxSelectionsReached: maxReached, + setFieldValue: setFieldValue, + }); }, [values, disabled, type, maxReached, setFieldValue]); + return (_jsx(GroupContext.Provider, { value: context, children: _jsx(View, __assign({ style: [a.w_full, style], role: groupRole }, (groupRole === 'radiogroup' + ? { + 'aria-label': label, + accessibilityLabel: label, + accessibilityRole: groupRole, + } + : {}), { children: children })) })); +} +export function Item(_a) { + var children = _a.children, name = _a.name, _b = _a.value, value = _b === void 0 ? false : _b, _c = _a.disabled, itemDisabled = _c === void 0 ? false : _c, onChange = _a.onChange, isInvalid = _a.isInvalid, style = _a.style, _d = _a.type, type = _d === void 0 ? 'checkbox' : _d, label = _a.label, rest = __rest(_a, ["children", "name", "value", "disabled", "onChange", "isInvalid", "style", "type", "label"]); + var _e = useContext(GroupContext), selectedValues = _e.values, groupType = _e.type, groupDisabled = _e.disabled, setFieldValue = _e.setFieldValue, maxSelectionsReached = _e.maxSelectionsReached; + var _f = useInteractionState(), hovered = _f.state, onHoverIn = _f.onIn, onHoverOut = _f.onOut; + var _g = useInteractionState(), pressed = _g.state, onPressIn = _g.onIn, onPressOut = _g.onOut; + var _h = useInteractionState(), focused = _h.state, onFocus = _h.onIn, onBlur = _h.onOut; + var playHaptic = useHaptics(); + var role = groupType === 'radio' ? 'radio' : type; + var selected = selectedValues.includes(name) || !!value; + var disabled = groupDisabled || itemDisabled || (!selected && maxSelectionsReached); + var onPress = useCallback(function () { + playHaptic('Light'); + var next = !selected; + setFieldValue({ name: name, value: next }); + onChange === null || onChange === void 0 ? void 0 : onChange(next); + }, [playHaptic, name, selected, onChange, setFieldValue]); + var state = useMemo(function () { return ({ + name: name, + selected: selected, + disabled: disabled !== null && disabled !== void 0 ? disabled : false, + isInvalid: isInvalid !== null && isInvalid !== void 0 ? isInvalid : false, + hovered: hovered, + pressed: pressed, + focused: focused, + }); }, [name, selected, disabled, hovered, pressed, focused, isInvalid]); + return (_jsx(ItemContext.Provider, { value: state, children: _jsx(Pressable, __assign({ accessibilityHint: undefined, hitSlop: HITSLOP_10 }, rest, { disabled: disabled, "aria-disabled": disabled !== null && disabled !== void 0 ? disabled : false, "aria-checked": selected, "aria-invalid": isInvalid, "aria-label": label, role: role, accessibilityRole: role, accessibilityState: { + disabled: disabled !== null && disabled !== void 0 ? disabled : false, + selected: selected, + }, accessibilityLabel: label, onPress: onPress, onHoverIn: onHoverIn, onHoverOut: onHoverOut, onPressIn: onPressIn, onPressOut: onPressOut, onFocus: onFocus, onBlur: onBlur, style: [a.flex_row, a.align_center, a.gap_sm, style], children: typeof children === 'function' ? children(state) : children })) })); +} +export function LabelText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + var disabled = useItemContext().disabled; + return (_jsx(Text, { style: [ + a.font_semi_bold, + a.leading_tight, + a.user_select_none, + { + color: disabled + ? t.atoms.text_contrast_low.color + : t.atoms.text_contrast_high.color, + }, + native({ + paddingTop: 2, + }), + style, + ], children: children })); +} +// TODO(eric) refactor to memoize styles without knowledge of state +export function createSharedToggleStyles(_a) { + var t = _a.theme, hovered = _a.hovered, selected = _a.selected, disabled = _a.disabled, isInvalid = _a.isInvalid; + var base = []; + var baseHover = []; + var indicator = []; + if (selected) { + base.push({ + backgroundColor: t.palette.primary_500, + borderColor: t.palette.primary_500, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.primary_400, + borderColor: t.palette.primary_400, + }); + } + } + else { + base.push({ + backgroundColor: t.palette.contrast_25, + borderColor: t.palette.contrast_100, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.contrast_50, + borderColor: t.palette.contrast_200, + }); + } + } + if (isInvalid) { + base.push({ + backgroundColor: t.palette.negative_25, + borderColor: t.palette.negative_300, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.negative_25, + borderColor: t.palette.negative_600, + }); + } + if (selected) { + base.push({ + backgroundColor: t.palette.negative_500, + borderColor: t.palette.negative_500, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.negative_400, + borderColor: t.palette.negative_400, + }); + } + } + } + if (disabled) { + base.push({ + backgroundColor: t.palette.contrast_100, + borderColor: t.palette.contrast_400, + }); + if (selected) { + base.push({ + backgroundColor: t.palette.primary_100, + borderColor: t.palette.contrast_400, + }); + } + } + return { + baseStyles: base, + baseHoverStyles: disabled ? [] : baseHover, + indicatorStyles: indicator, + }; +} +export function Checkbox() { + var t = useTheme(); + var _a = useItemContext(), selected = _a.selected, hovered = _a.hovered, focused = _a.focused, disabled = _a.disabled, isInvalid = _a.isInvalid; + var _b = createSharedToggleStyles({ + theme: t, + hovered: hovered, + focused: focused, + selected: selected, + disabled: disabled, + isInvalid: isInvalid, + }), baseStyles = _b.baseStyles, baseHoverStyles = _b.baseHoverStyles; + return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + t.atoms.border_contrast_high, + a.transition_color, + { + borderWidth: 1, + height: 24, + width: 24, + borderRadius: 6, + }, + baseStyles, + hovered ? baseHoverStyles : {}, + ], children: selected && _jsx(Checkmark, { width: 14, fill: t.palette.white }) })); +} +export function Switch() { + var t = useTheme(); + var _a = useItemContext(), selected = _a.selected, hovered = _a.hovered, disabled = _a.disabled, isInvalid = _a.isInvalid; + var _b = useMemo(function () { + var base = []; + var baseHover = []; + var indicator = []; + if (selected) { + base.push({ + backgroundColor: t.palette.primary_500, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.primary_400, + }); + } + } + else { + base.push({ + backgroundColor: t.palette.contrast_200, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.contrast_100, + }); + } + } + if (isInvalid) { + base.push({ + backgroundColor: t.palette.negative_200, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.negative_100, + }); + } + if (selected) { + base.push({ + backgroundColor: t.palette.negative_500, + }); + if (hovered) { + baseHover.push({ + backgroundColor: t.palette.negative_400, + }); + } + } + } + if (disabled) { + base.push({ + backgroundColor: t.palette.contrast_50, + }); + if (selected) { + base.push({ + backgroundColor: t.palette.primary_100, + }); + } + } + return { + baseStyles: base, + baseHoverStyles: disabled ? [] : baseHover, + indicatorStyles: indicator, + }; + }, [t, hovered, disabled, selected, isInvalid]), baseStyles = _b.baseStyles, baseHoverStyles = _b.baseHoverStyles, indicatorStyles = _b.indicatorStyles; + return (_jsx(View, { style: [ + a.relative, + a.rounded_full, + t.atoms.bg, + { + height: 28, + width: 48, + padding: 3, + }, + a.transition_color, + baseStyles, + hovered ? baseHoverStyles : {}, + ], children: _jsx(Animated.View, { layout: LinearTransition.duration(platform({ + web: 100, + default: 200, + })).easing(Easing.inOut(Easing.cubic)), style: [ + a.rounded_full, + { + backgroundColor: t.palette.white, + height: 22, + width: 22, + }, + selected ? { alignSelf: 'flex-end' } : { alignSelf: 'flex-start' }, + indicatorStyles, + ] }) })); +} +export function Radio() { + var props = useContext(ItemContext); + return _jsx(BaseRadio, __assign({}, props)); +} +export function BaseRadio(_a) { + var hovered = _a.hovered, focused = _a.focused, selected = _a.selected, disabled = _a.disabled, isInvalid = _a.isInvalid; + var t = useTheme(); + var _b = createSharedToggleStyles({ + theme: t, + hovered: hovered, + focused: focused, + selected: selected, + disabled: disabled, + isInvalid: isInvalid, + }), baseStyles = _b.baseStyles, baseHoverStyles = _b.baseHoverStyles, indicatorStyles = _b.indicatorStyles; + return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + t.atoms.border_contrast_high, + a.transition_color, + { + borderWidth: 1, + height: 25, + width: 25, + margin: -1, + }, + baseStyles, + hovered ? baseHoverStyles : {}, + ], children: selected && (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + { height: 12, width: 12 }, + { backgroundColor: t.palette.white }, + indicatorStyles, + ] })) })); +} +export var Platform = IS_NATIVE ? Switch : Checkbox; diff --git a/src/components/forms/ToggleButton.js b/src/components/forms/ToggleButton.js new file mode 100644 index 0000000000..fdc8385f3b --- /dev/null +++ b/src/components/forms/ToggleButton.js @@ -0,0 +1,127 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View, } from 'react-native'; +import { atoms as a, native, useTheme } from '#/alf'; +import * as Toggle from '#/components/forms/Toggle'; +import { Text } from '#/components/Typography'; +/** + * @deprecated - use SegmentedControl + */ +export function Group(_a) { + var children = _a.children, multiple = _a.multiple, props = __rest(_a, ["children", "multiple"]); + var t = useTheme(); + return (_jsx(Toggle.Group, __assign({ type: multiple ? 'checkbox' : 'radio' }, props, { children: _jsx(View, { style: [ + a.w_full, + a.flex_row, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border_contrast_low, + { borderWidth: 1 }, + ], children: children }) }))); +} +/** + * @deprecated - use SegmentedControl + */ +export function Button(_a) { + var children = _a.children, props = __rest(_a, ["children"]); + return (_jsx(Toggle.Item, __assign({}, props, { style: [a.flex_grow, a.flex_1], children: _jsx(ButtonInner, { children: children }) }))); +} +function ButtonInner(_a) { + var children = _a.children; + var t = useTheme(); + var state = Toggle.useItemContext(); + var _b = useMemo(function () { + var base = []; + var hover = []; + var active = []; + hover.push(t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25); + if (state.selected) { + active.push({ + backgroundColor: t.palette.contrast_800, + }); + hover.push({ + backgroundColor: t.palette.contrast_800, + }); + if (state.disabled) { + active.push({ + backgroundColor: t.palette.contrast_500, + }); + } + } + if (state.disabled) { + base.push({ + backgroundColor: t.palette.contrast_100, + }); + } + return { + baseStyles: base, + hoverStyles: hover, + activeStyles: active, + }; + }, [t, state]), baseStyles = _b.baseStyles, hoverStyles = _b.hoverStyles, activeStyles = _b.activeStyles; + return (_jsx(View, { style: [ + { + borderLeftWidth: 1, + marginLeft: -1, + }, + a.flex_grow, + a.py_md, + native({ + paddingBottom: 10, + }), + a.px_md, + t.atoms.bg, + t.atoms.border_contrast_low, + baseStyles, + activeStyles, + (state.hovered || state.pressed) && hoverStyles, + ], children: children })); +} +/** + * @deprecated - use SegmentedControl + */ +export function ButtonText(_a) { + var children = _a.children; + var t = useTheme(); + var state = Toggle.useItemContext(); + var textStyles = useMemo(function () { + var text = []; + if (state.selected) { + text.push(t.atoms.text_inverted); + } + if (state.disabled) { + text.push({ + opacity: 0.5, + }); + } + return text; + }, [t, state]); + return (_jsx(Text, { style: [ + a.text_center, + a.font_semi_bold, + t.atoms.text_contrast_medium, + textStyles, + ], children: children })); +} diff --git a/src/components/hooks/dates.js b/src/components/hooks/dates.js new file mode 100644 index 0000000000..914d41a12c --- /dev/null +++ b/src/components/hooks/dates.js @@ -0,0 +1,32 @@ +/** + * Hooks for date-fns localized formatters. + * + * Our app supports some languages that are not included in date-fns by + * default, in which case it will fall back to English. + * + * {@link https://github.com/date-fns/date-fns/blob/main/docs/i18n.md} + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { useCallback } from 'react'; +import { formatDistance } from 'date-fns'; +import { useDateLocale } from '#/locale/i18nProvider'; +/** + * Returns a localized `formatDistance` function. + * {@link formatDistance} + */ +export function useFormatDistance() { + var locale = useDateLocale(); + return useCallback(function (date, baseDate, options) { + return formatDistance(date, baseDate, __assign(__assign({}, options), { locale: locale })); + }, [locale]); +} diff --git a/src/components/hooks/useDelayedLoading.js b/src/components/hooks/useDelayedLoading.js new file mode 100644 index 0000000000..1d535ee5e3 --- /dev/null +++ b/src/components/hooks/useDelayedLoading.js @@ -0,0 +1,13 @@ +import React from 'react'; +export function useDelayedLoading(delay, initialState) { + if (initialState === void 0) { initialState = true; } + var _a = React.useState(initialState), isLoading = _a[0], setIsLoading = _a[1]; + React.useEffect(function () { + var timeout; + // on initial load, show a loading spinner for a hot sec to prevent flash + if (isLoading) + timeout = setTimeout(function () { return setIsLoading(false); }, delay); + return function () { return timeout && clearTimeout(timeout); }; + }, [isLoading, delay]); + return isLoading; +} diff --git a/src/components/hooks/useFollowMethods.js b/src/components/hooks/useFollowMethods.js new file mode 100644 index 0000000000..369d58ba99 --- /dev/null +++ b/src/components/hooks/useFollowMethods.js @@ -0,0 +1,107 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { logger } from '#/logger'; +import { useProfileFollowMutationQueue } from '#/state/queries/profile'; +import { useRequireAuth } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +export function useFollowMethods(_a) { + var _this = this; + var profile = _a.profile, logContext = _a.logContext; + var _ = useLingui()._; + var requireAuth = useRequireAuth(); + var _b = useProfileFollowMutationQueue(profile, logContext), queueFollow = _b[0], queueUnfollow = _b[1]; + var follow = React.useCallback(function () { + requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueFollow()]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + logger.error("useFollowMethods: failed to follow", { message: String(e_1) }); + if ((e_1 === null || e_1 === void 0 ? void 0 : e_1.name) !== 'AbortError') { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["An issue occurred, please try again."], ["An issue occurred, please try again."])))), 'xmark'); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }); + }, [_, queueFollow, requireAuth]); + var unfollow = React.useCallback(function () { + requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + var e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueUnfollow()]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + e_2 = _a.sent(); + logger.error("useFollowMethods: failed to unfollow", { + message: String(e_2), + }); + if ((e_2 === null || e_2 === void 0 ? void 0 : e_2.name) !== 'AbortError') { + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["An issue occurred, please try again."], ["An issue occurred, please try again."])))), 'xmark'); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }); + }, [_, queueUnfollow, requireAuth]); + return { + follow: follow, + unfollow: unfollow, + }; +} +var templateObject_1, templateObject_2; diff --git a/src/components/hooks/useFullscreen.js b/src/components/hooks/useFullscreen.js new file mode 100644 index 0000000000..4e5aebfe6f --- /dev/null +++ b/src/components/hooks/useFullscreen.js @@ -0,0 +1,44 @@ +import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from 'react'; +import { IS_WEB, IS_WEB_FIREFOX, IS_WEB_SAFARI } from '#/env'; +function fullscreenSubscribe(onChange) { + document.addEventListener('fullscreenchange', onChange); + return function () { return document.removeEventListener('fullscreenchange', onChange); }; +} +export function useFullscreen(ref) { + if (!IS_WEB) + throw new Error("'useFullscreen' is a web-only hook"); + var isFullscreen = useSyncExternalStore(fullscreenSubscribe, function () { + return Boolean(document.fullscreenElement); + }); + var scrollYRef = useRef(null); + var _a = useState(isFullscreen), prevIsFullscreen = _a[0], setPrevIsFullscreen = _a[1]; + var toggleFullscreen = useCallback(function () { + if (isFullscreen) { + document.exitFullscreen(); + } + else { + if (!ref) + throw new Error('No ref provided'); + if (!ref.current) + return; + scrollYRef.current = window.scrollY; + ref.current.requestFullscreen(); + } + }, [isFullscreen, ref]); + useEffect(function () { + if (prevIsFullscreen === isFullscreen) + return; + setPrevIsFullscreen(isFullscreen); + // Chrome has an issue where it doesn't scroll back to the top after exiting fullscreen + // Let's play it safe and do it if not FF or Safari, since anything else will probably be chromium + if (prevIsFullscreen && !IS_WEB_FIREFOX && !IS_WEB_SAFARI) { + setTimeout(function () { + if (scrollYRef.current !== null) { + window.scrollTo(0, scrollYRef.current); + scrollYRef.current = null; + } + }, 100); + } + }, [isFullscreen, prevIsFullscreen]); + return [isFullscreen, toggleFullscreen]; +} diff --git a/src/components/hooks/useHeaderOffset.js b/src/components/hooks/useHeaderOffset.js new file mode 100644 index 0000000000..4abbcc1601 --- /dev/null +++ b/src/components/hooks/useHeaderOffset.js @@ -0,0 +1,17 @@ +import { useWindowDimensions } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useBreakpoints } from '#/alf'; +import { IS_LIQUID_GLASS } from '#/env'; +export function useHeaderOffset() { + var gtMobile = useBreakpoints().gtMobile; + var fontScale = useWindowDimensions().fontScale; + var insets = useSafeAreaInsets(); + if (gtMobile) { + return 0; + } + var navBarHeight = 52 + (IS_LIQUID_GLASS ? insets.top : 0); + var tabBarPad = 10 + 10 + 3; // padding + border + var normalLineHeight = 20; // matches tab bar + var tabBarText = normalLineHeight * fontScale; + return navBarHeight + tabBarPad + tabBarText - 4; // for some reason, this calculation is wrong by 4 pixels, which we adjust +} diff --git a/src/components/hooks/useInteractionState.js b/src/components/hooks/useInteractionState.js new file mode 100644 index 0000000000..9111db8d3f --- /dev/null +++ b/src/components/hooks/useInteractionState.js @@ -0,0 +1,15 @@ +import React from 'react'; +export function useInteractionState() { + var _a = React.useState(false), state = _a[0], setState = _a[1]; + var onIn = React.useCallback(function () { + setState(true); + }, []); + var onOut = React.useCallback(function () { + setState(false); + }, []); + return React.useMemo(function () { return ({ + state: state, + onIn: onIn, + onOut: onOut, + }); }, [state, onIn, onOut]); +} diff --git a/src/components/hooks/useOnGesture/index.js b/src/components/hooks/useOnGesture/index.js new file mode 100644 index 0000000000..e31d570db4 --- /dev/null +++ b/src/components/hooks/useOnGesture/index.js @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +import { useGlobalGestureEvents, } from '#/state/global-gesture-events'; +/** + * Listen for global gesture events. Callback should be wrapped with + * `useCallback` or otherwise memoized to avoid unnecessary re-renders. + */ +export function useOnGesture(onGestureCallback) { + var ctx = useGlobalGestureEvents(); + useEffect(function () { + ctx.register(); + ctx.events.on('begin', onGestureCallback); + return function () { + ctx.unregister(); + ctx.events.off('begin', onGestureCallback); + }; + }, [ctx, onGestureCallback]); +} diff --git a/src/components/hooks/useOnGesture/index.web.js b/src/components/hooks/useOnGesture/index.web.js new file mode 100644 index 0000000000..76e8e75ffd --- /dev/null +++ b/src/components/hooks/useOnGesture/index.web.js @@ -0,0 +1 @@ +export function useOnGesture() { } diff --git a/src/components/hooks/useOnKeyboard.js b/src/components/hooks/useOnKeyboard.js new file mode 100644 index 0000000000..4eb31db67a --- /dev/null +++ b/src/components/hooks/useOnKeyboard.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { Keyboard } from 'react-native'; +export function useOnKeyboardDidShow(cb) { + React.useEffect(function () { + var subscription = Keyboard.addListener('keyboardDidShow', cb); + return function () { + subscription.remove(); + }; + }, [cb]); +} diff --git a/src/components/hooks/useRefreshOnFocus.js b/src/components/hooks/useRefreshOnFocus.js new file mode 100644 index 0000000000..01d0ee4191 --- /dev/null +++ b/src/components/hooks/useRefreshOnFocus.js @@ -0,0 +1,12 @@ +import { useCallback, useRef } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; +export function useRefreshOnFocus(refetch) { + var firstTimeRef = useRef(true); + useFocusEffect(useCallback(function () { + if (firstTimeRef.current) { + firstTimeRef.current = false; + return; + } + refetch(); + }, [refetch])); +} diff --git a/src/components/hooks/useRichText.js b/src/components/hooks/useRichText.js new file mode 100644 index 0000000000..90aa5271fc --- /dev/null +++ b/src/components/hooks/useRichText.js @@ -0,0 +1,78 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { RichText as RichTextAPI } from '@atproto/api'; +import { useAgent } from '#/state/session'; +export function useRichText(text) { + var _a = React.useState(text), prevText = _a[0], setPrevText = _a[1]; + var _b = React.useState(function () { return new RichTextAPI({ text: text }); }), rawRT = _b[0], setRawRT = _b[1]; + var _c = React.useState(null), resolvedRT = _c[0], setResolvedRT = _c[1]; + var agent = useAgent(); + if (text !== prevText) { + setPrevText(text); + setRawRT(new RichTextAPI({ text: text })); + setResolvedRT(null); + // This will queue an immediate re-render + } + React.useEffect(function () { + var ignore = false; + function resolveRTFacets() { + return __awaiter(this, void 0, void 0, function () { + var resolvedRT; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + resolvedRT = new RichTextAPI({ text: text }); + return [4 /*yield*/, resolvedRT.detectFacets(agent)]; + case 1: + _a.sent(); + if (!ignore) { + setResolvedRT(resolvedRT); + } + return [2 /*return*/]; + } + }); + }); + } + resolveRTFacets(); + return function () { + ignore = true; + }; + }, [text, agent]); + var isResolving = resolvedRT === null; + return [resolvedRT !== null && resolvedRT !== void 0 ? resolvedRT : rawRT, isResolving]; +} diff --git a/src/components/hooks/useStarterPackEntry.js b/src/components/hooks/useStarterPackEntry.js new file mode 100644 index 0000000000..7f551fe970 --- /dev/null +++ b/src/components/hooks/useStarterPackEntry.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { httpStarterPackUriToAtUri } from '#/lib/strings/starter-pack'; +import { useSetActiveStarterPack } from '#/state/shell/starter-pack'; +export function useStarterPackEntry() { + var _a = React.useState(false), ready = _a[0], setReady = _a[1]; + var setActiveStarterPack = useSetActiveStarterPack(); + React.useEffect(function () { + var href = window.location.href; + var atUri = httpStarterPackUriToAtUri(href); + if (atUri) { + var url = new URL(href); + // Determines if an App Clip is loading this landing page + var isClip = url.searchParams.get('clip') === 'true'; + setActiveStarterPack({ + uri: atUri, + isClip: isClip, + }); + } + setReady(true); + }, [setActiveStarterPack]); + return ready; +} diff --git a/src/components/hooks/useStarterPackEntry.native.js b/src/components/hooks/useStarterPackEntry.native.js new file mode 100644 index 0000000000..ce62c96796 --- /dev/null +++ b/src/components/hooks/useStarterPackEntry.native.js @@ -0,0 +1,97 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { createStarterPackLinkFromAndroidReferrer, httpStarterPackUriToAtUri, } from '#/lib/strings/starter-pack'; +import { useHasCheckedForStarterPack } from '#/state/preferences/used-starter-packs'; +import { useSetActiveStarterPack } from '#/state/shell/starter-pack'; +import { IS_ANDROID } from '#/env'; +import { Referrer, SharedPrefs } from '../../../modules/expo-bluesky-swiss-army'; +export function useStarterPackEntry() { + var _this = this; + var _a = React.useState(false), ready = _a[0], setReady = _a[1]; + var setActiveStarterPack = useSetActiveStarterPack(); + var hasCheckedForStarterPack = useHasCheckedForStarterPack(); + React.useEffect(function () { + if (ready) + return; + // On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So, + // let's just ensure we never check again after the first time. + if (hasCheckedForStarterPack) { + setReady(true); + return; + } + // Safety for Android. Very unlike this could happen, but just in case. The response should be nearly immediate + var timeout = setTimeout(function () { + setReady(true); + }, 500); + (function () { return __awaiter(_this, void 0, void 0, function () { + var uri, res, starterPackUri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!IS_ANDROID) return [3 /*break*/, 2]; + return [4 /*yield*/, Referrer.getGooglePlayReferrerInfoAsync()]; + case 1: + res = _a.sent(); + if (res && res.installReferrer) { + uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer); + } + return [3 /*break*/, 3]; + case 2: + starterPackUri = SharedPrefs.getString('starterPackUri'); + if (starterPackUri) { + uri = httpStarterPackUriToAtUri(starterPackUri); + SharedPrefs.setValue('starterPackUri', null); + } + _a.label = 3; + case 3: + if (uri) { + setActiveStarterPack({ + uri: uri, + }); + } + setReady(true); + return [2 /*return*/]; + } + }); + }); })(); + return function () { + clearTimeout(timeout); + }; + }, [ready, setActiveStarterPack, hasCheckedForStarterPack]); + return ready; +} diff --git a/src/components/hooks/useThrottledValue.js b/src/components/hooks/useThrottledValue.js new file mode 100644 index 0000000000..f89a80712c --- /dev/null +++ b/src/components/hooks/useThrottledValue.js @@ -0,0 +1,21 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +export function useThrottledValue(value, time) { + var pendingValueRef = useRef(value); + var _a = useState(value), throttledValue = _a[0], setThrottledValue = _a[1]; + useEffect(function () { + pendingValueRef.current = value; + }, [value]); + var handleTick = useNonReactiveCallback(function () { + if (pendingValueRef.current !== throttledValue) { + setThrottledValue(pendingValueRef.current); + } + }); + useEffect(function () { + var id = setInterval(handleTick, time); + return function () { + clearInterval(id); + }; + }, [handleTick, time]); + return throttledValue; +} diff --git a/src/components/hooks/useWelcomeModal.js b/src/components/hooks/useWelcomeModal.js new file mode 100644 index 0000000000..2c919ad396 --- /dev/null +++ b/src/components/hooks/useWelcomeModal.js @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; +import { useSession } from '#/state/session'; +import { IS_WEB } from '#/env'; +export function useWelcomeModal() { + var hasSession = useSession().hasSession; + var _a = useState(false), isOpen = _a[0], setIsOpen = _a[1]; + var open = function () { return setIsOpen(true); }; + var close = function () { return setIsOpen(false); }; + useEffect(function () { + // Only show modal if: + // 1. User is not logged in + // 2. We're on the web (this is a web-only feature) + // 3. We're on the homepage (path is '/' or '/home') + // 4. Modal hasn't been shown before + if (IS_WEB && !hasSession && typeof window !== 'undefined') { + var currentPath = window.location.pathname; + var isHomePage = currentPath === '/'; + var hasModalBeenShown = localStorage.getItem('welcomeModalShown') === 'true'; + if (isHomePage && !hasModalBeenShown) { + // Mark that the modal has been shown, don't show again + localStorage.setItem('welcomeModalShown', 'true'); + // Small delay to ensure the page has loaded + var timer_1 = setTimeout(function () { + open(); + }, 1000); + return function () { return clearTimeout(timer_1); }; + } + } + }, [hasSession]); + return { isOpen: isOpen, open: open, close: close }; +} diff --git a/src/components/hooks/useWelcomeModal.native.js b/src/components/hooks/useWelcomeModal.native.js new file mode 100644 index 0000000000..863f1ee7d4 --- /dev/null +++ b/src/components/hooks/useWelcomeModal.native.js @@ -0,0 +1,3 @@ +export function useWelcomeModal() { + throw new Error('useWelcomeModal is web only'); +} diff --git a/src/components/icons/Accessibility.js b/src/components/icons/Accessibility.js new file mode 100644 index 0000000000..8380c14022 --- /dev/null +++ b/src/components/icons/Accessibility.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Accessibility_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm-2.86.26.014.002c.944.125 1.893.238 2.846.238.95 0 1.904-.113 2.846-.238l.014-.002h.003a1 1 0 0 1 .273 1.98l-.006.002-.017.002c-.67.089-1.341.162-2.014.21.195 1.32.65 2.33 1.626 3.357a1 1 0 0 1-1.45 1.378 8.3 8.3 0 0 1-1.234-1.647 8.2 8.2 0 0 1-1.342 1.673 1 1 0 0 1-1.398-1.43c.673-.658 1.088-1.274 1.342-1.922.163-.42.269-.878.32-1.404a33 33 0 0 1-2.075-.215l-.017-.002-.006-.001a1 1 0 0 1 .271-1.982l.004.001Z', +}); diff --git a/src/components/icons/Alien.js b/src/components/icons/Alien.js new file mode 100644 index 0000000000..7d413bbe38 --- /dev/null +++ b/src/components/icons/Alien.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Alien_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5 11a7 7 0 0 1 14 0c0 2.625-1.547 5.138-3.354 7.066a17.23 17.23 0 0 1-2.55 2.242 8.246 8.246 0 0 1-.924.577 2.904 2.904 0 0 1-.172.083 2.904 2.904 0 0 1-.172-.083 8.246 8.246 0 0 1-.923-.577 17.227 17.227 0 0 1-2.55-2.242C6.547 16.138 5 13.625 5 11Zm6.882 10.012Zm.232-.001h-.003a.047.047 0 0 0 .007.001l-.004-.001ZM12 2a9 9 0 0 0-9 9c0 3.375 1.953 6.362 3.895 8.434a19.216 19.216 0 0 0 2.856 2.508c.425.3.82.545 1.159.72.168.087.337.164.498.222.14.05.356.116.592.116s.451-.066.592-.116c.16-.058.33-.135.498-.222.339-.175.734-.42 1.159-.72.85-.6 1.87-1.457 2.856-2.508C19.047 17.362 21 14.375 21 11a9 9 0 0 0-9-9ZM7.38 9.927c2.774-.094 3.459 1.31 3.591 3.19a.89.89 0 0 1-.855.956c-2.774.094-3.458-1.31-3.59-3.19a.89.89 0 0 1 .854-.956Zm9.236 0c-2.774-.094-3.458 1.31-3.59 3.19a.89.89 0 0 0 .854.956c2.774.094 3.459-1.31 3.591-3.19a.89.89 0 0 0-.855-.956Z', +}); diff --git a/src/components/icons/AndroidLogo.js b/src/components/icons/AndroidLogo.js new file mode 100644 index 0000000000..b5114ecb8f --- /dev/null +++ b/src/components/icons/AndroidLogo.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var AndroidLogo = createSinglePathSVG({ + path: 'm15.604 2.47-.642 1.112a4.997 4.997 0 0 1 2.05 4.034H7.005c0-1.658.809-3.123 2.05-4.034L8.41 2.47a.313.313 0 0 1 .542-.313l.627 1.086a4.97 4.97 0 0 1 2.428-.63c.882 0 1.709.23 2.428.63l.627-1.085a.311.311 0 0 1 .426-.115c.151.087.202.278.115.428ZM9.506 5.114a.625.625 0 1 0 1.25 0 .625.625 0 0 0-1.25 0Zm3.753 0a.625.625 0 1 0 1.25 0 .625.625 0 0 0-1.25 0ZM3.878 8.866a1.251 1.251 0 0 1 2.501 0v5.004a1.252 1.252 0 0 1-2.501 0V8.866Zm13.759 0a1.251 1.251 0 0 1 2.501 0v5.004a1.251 1.251 0 0 1-2.501 0V8.866ZM7.005 17.622h1.25v3.128a1.251 1.251 0 0 0 2.502 0v-3.128h2.502v3.128a1.251 1.251 0 0 0 2.501 0v-3.128h1.25V8.241H7.006v9.381Z', +}); diff --git a/src/components/icons/Apple.js b/src/components/icons/Apple.js new file mode 100644 index 0000000000..c3401d1cfb --- /dev/null +++ b/src/components/icons/Apple.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Apple_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8.148 2.034a1 1 0 0 1 1.35-.419c1.054.556 1.873 1.266 2.46 2.174.369.57.63 1.197.807 1.873 1.143-.349 2.194-.46 3.15-.342a5.122 5.122 0 0 1 3.134 1.556c1.564 1.63 2.086 4.183 1.922 6.58-.166 2.414-1.043 4.938-2.607 6.619-.792.851-1.78 1.505-2.95 1.782-1.063.251-2.213.176-3.415-.262-1.202.438-2.351.513-3.414.262-1.17-.277-2.158-.93-2.95-1.782-1.563-1.682-2.44-4.205-2.606-6.62-.164-2.396.358-4.949 1.921-6.579A5.121 5.121 0 0 1 8.085 5.32c.777-.095 1.617-.04 2.518.172a4.011 4.011 0 0 0-.325-.618c-.372-.576-.912-1.068-1.712-1.49a1 1 0 0 1-.418-1.35Zm.897 17.877c.71.167 1.557.117 2.562-.31a1 1 0 0 1 .784 0c1.005.428 1.853.477 2.563.31.715-.17 1.37-.579 1.946-1.198 1.171-1.26 1.932-3.303 2.076-5.394.144-2.109-.353-3.998-1.37-5.059a3.124 3.124 0 0 0-1.936-.955c-.83-.102-1.912.043-3.282.621a1 1 0 0 1-.778 0c-1.37-.578-2.45-.723-3.281-.62-.815.1-1.445.443-1.935.954-1.017 1.06-1.514 2.95-1.37 5.059.144 2.09.905 4.135 2.076 5.394.575.62 1.23 1.029 1.945 1.198Z', +}); diff --git a/src/components/icons/AppleLogo.js b/src/components/icons/AppleLogo.js new file mode 100644 index 0000000000..91a436acde --- /dev/null +++ b/src/components/icons/AppleLogo.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var AppleLogo = createSinglePathSVG({ + path: 'M14.57 4.348c.65-.775 1.11-1.85 1.11-2.926 0-.149-.013-.3-.04-.422-1.057.04-2.314.708-3.072 1.592-.595.68-1.15 1.756-1.15 2.846 0 .164.027.327.04.38.066.012.175.027.283.027.948 0 2.14-.638 2.83-1.497Zm4.835 3.847.052-.035c-1.407-2.03-3.546-2.084-4.14-2.084-.911 0-1.726.325-2.411.598-.497.198-.925.368-1.271.368-.383 0-.822-.178-1.311-.377-.618-.25-1.317-.534-2.087-.534C5.64 6.13 3 8.296 3 12.379c0 2.545.975 5.227 2.182 6.954C6.224 20.803 7.13 22 8.43 22c.616 0 1.068-.193 1.543-.395.526-.225 1.082-.462 1.922-.462.849 0 1.356.223 1.845.437.455.2.895.393 1.58.393 1.42 0 2.353-1.292 3.246-2.586 1.003-1.47 1.422-2.913 1.435-2.98-.081-.027-2.802-1.13-2.802-4.246 0-2.51 1.855-3.734 2.207-3.966Z', +}); diff --git a/src/components/icons/Arrow.js b/src/components/icons/Arrow.js new file mode 100644 index 0000000000..4b1f7042d2 --- /dev/null +++ b/src/components/icons/Arrow.js @@ -0,0 +1,16 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z', +}); +export var ArrowTop_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11 20V6.164l-4.293 4.293a1 1 0 1 1-1.414-1.414l5.293-5.293.151-.138a2 2 0 0 1 2.677.138l5.293 5.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 6.164V20a1 1 0 0 1-2 0Z', +}); +export var ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z', +}); +export var ArrowRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M21 12a1 1 0 0 1-.293.707l-6 6a1 1 0 0 1-1.414-1.414L17.586 13H4a1 1 0 1 1 0-2h13.586l-4.293-4.293a1 1 0 0 1 1.414-1.414l6 6A1 1 0 0 1 21 12Z', +}); +export var ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z', +}); diff --git a/src/components/icons/ArrowBoxLeft.js b/src/components/icons/ArrowBoxLeft.js new file mode 100644 index 0000000000..a8909ad4e5 --- /dev/null +++ b/src/components/icons/ArrowBoxLeft.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowBoxLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.293 3.293A1 1 0 0 1 4 3h7.25a1 1 0 1 1 0 2H5v14h6.25a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4a1 1 0 0 1 .293-.707Zm11.5 3.5a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z', +}); +export var ArrowBoxLeft_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h5.25a1 1 0 1 1 0 2H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h5.25a1 1 0 1 1 0 2H6Zm8.793 1.793a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/ArrowCornerDownRight.js b/src/components/icons/ArrowCornerDownRight.js new file mode 100644 index 0000000000..43a3c52d18 --- /dev/null +++ b/src/components/icons/ArrowCornerDownRight.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowCornerDownRight_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z', +}); diff --git a/src/components/icons/ArrowOutOfBox.js b/src/components/icons/ArrowOutOfBox.js new file mode 100644 index 0000000000..7973289379 --- /dev/null +++ b/src/components/icons/ArrowOutOfBox.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowOutOfBox_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.707 3.293a1 1 0 0 0-1.414 0l-4.5 4.5a1 1 0 0 0 1.414 1.414L11 6.414v8.836a1 1 0 1 0 2 0V6.414l2.793 2.793a1 1 0 1 0 1.414-1.414l-4.5-4.5ZM5 12.75a1 1 0 1 0-2 0V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-7.25a1 1 0 1 0-2 0V19H5v-6.25Z', +}); +export var ArrowOutOfBoxModified_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M20 13.75a1 1 0 0 1 1 1V18a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3v-3.25a1 1 0 1 1 2 0V18a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.25a1 1 0 0 1 1-1ZM12 3a1 1 0 0 1 .707.293l4.5 4.5a1 1 0 1 1-1.414 1.414L13 6.414v8.836a1 1 0 1 1-2 0V6.414L8.207 9.207a1 1 0 1 1-1.414-1.414l4.5-4.5A1 1 0 0 1 12 3Z', +}); diff --git a/src/components/icons/ArrowRotate.js b/src/components/icons/ArrowRotate.js new file mode 100644 index 0000000000..ee0050a081 --- /dev/null +++ b/src/components/icons/ArrowRotate.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5 3a1 1 0 0 1 1 1v1.423c.498-.46 1.02-.869 1.58-1.213C8.863 3.423 10.302 3 12.028 3a9 9 0 1 1-8.487 12 1 1 0 0 1 1.885-.667A7 7 0 1 0 12.028 5c-1.37 0-2.444.327-3.402.915-.474.29-.93.652-1.383 1.085H9a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z', +}); +export var ArrowRotateClockwise_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2.972 12a9 9 0 0 1 9-9c1.726 0 3.165.423 4.448 1.21.561.344 1.082.756 1.58 1.218V4a1 1 0 1 1 2 0v4a1 1 0 0 1-1 1h-4a1 1 0 1 1 0-2h1.756a8.3 8.3 0 0 0-1.382-1.085C14.417 5.327 13.341 5 11.972 5a7 7 0 1 0 6.601 9.333A1 1 0 0 1 20.46 15a9 9 0 0 1-17.487-3Z', +}); diff --git a/src/components/icons/ArrowShareRight.js b/src/components/icons/ArrowShareRight.js new file mode 100644 index 0000000000..b39c8b5195 --- /dev/null +++ b/src/components/icons/ArrowShareRight.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowShareRight_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M11.839 4.744c0-1.488 1.724-2.277 2.846-1.364l.107.094 7.66 7.256.128.134c.558.652.558 1.62 0 2.272l-.128.135-7.66 7.255c-1.115 1.057-2.953.267-2.953-1.27v-2.748c-3.503.055-5.417.41-6.592.97-.997.474-1.525 1.122-2.084 2.14l-.243.46c-.558 1.088-2.09.583-2.08-.515l.015-.748c.111-3.68.777-6.5 2.546-8.415 1.83-1.98 4.63-2.771 8.438-2.884V4.744Zm2 3.256c0 .79-.604 1.41-1.341 1.494l-.149.01c-3.9.057-6.147.813-7.48 2.254-.963 1.043-1.562 2.566-1.842 4.79.38-.327.826-.622 1.361-.877 1.656-.788 4.08-1.14 7.938-1.169l.153.007c.754.071 1.36.704 1.36 1.491v2.675L20.884 12l-7.045-6.676V8Z', +}); diff --git a/src/components/icons/ArrowTopCircle.js b/src/components/icons/ArrowTopCircle.js new file mode 100644 index 0000000000..faa15c1c00 --- /dev/null +++ b/src/components/icons/ArrowTopCircle.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowTopCircle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.63 3.225a1 1 0 0 1 1.337.068l3 3 .068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 10.414V16a1 1 0 1 1-2 0v-5.586l-1.293 1.293a1 1 0 1 1-1.414-1.414l3-3 .076-.068Z', +}); diff --git a/src/components/icons/ArrowTriangle.js b/src/components/icons/ArrowTriangle.js new file mode 100644 index 0000000000..f6d53519f3 --- /dev/null +++ b/src/components/icons/ArrowTriangle.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowTriangleBottom_Stroke2_Corner1_Rounded = createSinglePathSVG({ + path: 'M4.213 6.886c-.673-1.35.334-2.889 1.806-2.889H17.98c1.472 0 2.479 1.539 1.806 2.89l-5.982 11.997c-.74 1.484-2.87 1.484-3.61 0L4.213 6.886Z', +}); diff --git a/src/components/icons/ArrowsDiagonal.js b/src/components/icons/ArrowsDiagonal.js new file mode 100644 index 0000000000..3d086db654 --- /dev/null +++ b/src/components/icons/ArrowsDiagonal.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ArrowsDiagonalOut_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM4 13a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z', +}); +export var ArrowsDiagonalIn_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z', +}); +export var ArrowsDiagonalOut_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z', +}); +export var ArrowsDiagonalIn_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-5a2 2 0 0 1-2-2V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/AspectRatio.js b/src/components/icons/AspectRatio.js new file mode 100644 index 0000000000..65aa9c98ff --- /dev/null +++ b/src/components/icons/AspectRatio.js @@ -0,0 +1,10 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var AspectRatio11_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Z', +}); +export var AspectRatio43_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2 20.5c-.552 0-1-.41-1-.917V4.917C1 4.41 1.448 4 2 4h20c.552 0 1 .41 1 .917v14.666c0 .507-.448.917-1 .917H2Zm1-1.833h18V5.833H3v12.834Z', +}); +export var AspectRatio34_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 2c0-.552.41-1 .917-1h14.666c.507 0 .917.448.917 1v20c0 .552-.41 1-.917 1H4.917C4.41 23 4 22.552 4 22V2Zm1.833 1v18h12.834V3H5.833Z', +}); diff --git a/src/components/icons/At.js b/src/components/icons/At.js new file mode 100644 index 0000000000..76ca617dc1 --- /dev/null +++ b/src/components/icons/At.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var At_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.2 4.2 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458s2.514-4.586 5.044-4.23c.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.22 2.22 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529s.784 3.03 1.98 3.198 2.543-.819 2.784-2.529-.784-3.03-1.98-3.198Z', +}); +export var At_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.2 4.2 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458s2.514-4.586 5.044-4.23c.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.22 2.22 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529s.784 3.03 1.98 3.198 2.544-.819 2.784-2.529-.784-3.03-1.98-3.198Z', +}); diff --git a/src/components/icons/Atom.js b/src/components/icons/Atom.js new file mode 100644 index 0000000000..c996ff4ae6 --- /dev/null +++ b/src/components/icons/Atom.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Atom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6.17 5.004c-.553-.029-.814.107-.937.23-.122.122-.258.383-.23.936.03.552.222 1.28.611 2.15.282.628.655 1.304 1.112 2.005a28.26 28.26 0 0 1 1.72-1.88 28.258 28.258 0 0 1 1.88-1.719 14.886 14.886 0 0 0-2.007-1.112c-.868-.39-1.597-.581-2.15-.61Zm5.83.44c-.985-.688-1.953-1.247-2.863-1.655-.998-.447-1.978-.736-2.863-.782-.885-.047-1.791.148-2.455.812-.664.664-.859 1.57-.812 2.455.046.885.335 1.865.782 2.863.408.91.967 1.878 1.655 2.863-.688.985-1.247 1.953-1.655 2.863-.447.998-.736 1.978-.782 2.863-.047.885.148 1.791.812 2.455.664.663 1.57.859 2.455.812.885-.046 1.865-.335 2.863-.782.91-.408 1.878-.967 2.863-1.655.985.688 1.952 1.247 2.863 1.655.998.447 1.978.736 2.863.782.885.047 1.791-.148 2.455-.812.663-.664.859-1.57.812-2.455-.046-.885-.335-1.865-.782-2.863-.408-.91-.967-1.878-1.655-2.863.688-.985 1.247-1.952 1.655-2.863.447-.998.736-1.978.782-2.863.047-.885-.148-1.791-.812-2.455-.664-.664-1.57-.859-2.455-.812-.885.046-1.865.335-2.863.782-.91.408-1.878.967-2.863 1.655Zm0 2.497A25.9 25.9 0 0 0 9.86 9.86 25.899 25.899 0 0 0 7.94 12c.569.711 1.211 1.431 1.92 2.14.709.709 1.429 1.351 2.14 1.92a25.925 25.925 0 0 0 2.14-1.92A25.925 25.925 0 0 0 16.06 12a25.921 25.921 0 0 0-1.92-2.14A25.904 25.904 0 0 0 12 7.94Zm5.274 2.384a28.232 28.232 0 0 0-1.72-1.88 28.27 28.27 0 0 0-1.88-1.719 14.89 14.89 0 0 1 2.007-1.112c.868-.39 1.597-.581 2.15-.61.552-.029.813.107.936.23.123.122.258.383.23.936-.03.552-.222 1.28-.611 2.15a14.883 14.883 0 0 1-1.112 2.005Zm0 3.35a28.24 28.24 0 0 1-1.72 1.88 28.24 28.24 0 0 1-1.88 1.719c.702.457 1.378.83 2.007 1.112.868.39 1.597.581 2.15.61.552.03.813-.106.936-.23.123-.122.258-.383.23-.935-.03-.553-.222-1.282-.611-2.15a14.888 14.888 0 0 0-1.112-2.006Zm-6.949 3.599a28.23 28.23 0 0 1-1.88-1.72 28.27 28.27 0 0 1-1.719-1.88 14.89 14.89 0 0 0-1.112 2.007c-.39.868-.581 1.597-.61 2.15-.029.552.107.813.23.936.122.123.383.258.936.23.552-.03 1.28-.222 2.15-.611a14.884 14.884 0 0 0 2.005-1.112Z', +}); diff --git a/src/components/icons/Bars.js b/src/components/icons/Bars.js new file mode 100644 index 0000000000..3f19b4eeb4 --- /dev/null +++ b/src/components/icons/Bars.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Bars3_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 5a1 1 0 0 0 0 2h18a1 1 0 1 0 0-2H3Zm-1 7a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/Beaker.js b/src/components/icons/Beaker.js new file mode 100644 index 0000000000..847a661ebb --- /dev/null +++ b/src/components/icons/Beaker.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Beaker_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M13.5 4a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 5a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM8 6a1 1 0 0 0 0 2v2.64q-.319.374-.711.8l-.129.142c-.312.342-.649.711-.974 1.092-.731.857-1.488 1.866-1.89 2.99A4.9 4.9 0 0 0 4 17.298 4.7 4.7 0 0 0 8.702 22h6.596A4.7 4.7 0 0 0 20 17.298c0-.575-.114-1.122-.297-1.634-.401-1.124-1.157-2.133-1.89-2.99-.324-.38-.66-.75-.973-1.092h0l-.129-.141c-.26-.286-.5-.55-.711-.8V8a1 1 0 1 0 0-2H8Zm2 5.35V8h4v3.35l.22.275c.306.383.661.777 1.013 1.163l.13.143h0c.315.345.628.688.93 1.042.372.435.704.861.974 1.28l-.159.025c-.845.13-1.838.242-2.581.222-.842-.022-1.475-.217-2.227-.454l-.027-.008c-.746-.235-1.61-.507-2.746-.538-.743-.02-1.617.064-2.38.165q.26-.342.56-.692c.302-.354.615-.697.93-1.042l.13-.143c.352-.386.707-.78 1.014-1.163L10 11.35Zm7.41 5.905q.316-.048.586-.095.004.07.004.138A2.7 2.7 0 0 1 15.298 20H8.702A2.7 2.7 0 0 1 6 17.298q0-.213.039-.434c.236-.043.53-.093.853-.142.845-.13 1.837-.242 2.581-.222.842.022 1.475.217 2.227.454l.027.008c.746.235 1.61.507 2.746.538.931.024 2.07-.113 2.937-.245Z', +}); diff --git a/src/components/icons/Bell.js b/src/components/icons/Bell.js new file mode 100644 index 0000000000..b90fab9ab3 --- /dev/null +++ b/src/components/icons/Bell.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Bell_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.216 8.815a7.853 7.853 0 0 1 15.568 0l1.207 9.053A1 1 0 0 1 20 19h-3.354c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM12 4a5.853 5.853 0 0 0-5.802 5.08L5.142 17h13.716l-1.056-7.92A5.853 5.853 0 0 0 12 4Z', +}); +export var Bell_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a7.853 7.853 0 0 0-7.784 6.815l-1.207 9.053A1 1 0 0 0 4 19h3.354c.904 1.748 2.607 3 4.646 3 2.039 0 3.742-1.252 4.646-3H20a1 1 0 0 0 .991-1.132l-1.207-9.053A7.853 7.853 0 0 0 12 2Zm2.222 17H9.778c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1Z', +}); diff --git a/src/components/icons/Bell2.js b/src/components/icons/Bell2.js new file mode 100644 index 0000000000..76486874c4 --- /dev/null +++ b/src/components/icons/Bell2.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Bell2_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.702 8.943a7.307 7.307 0 0 1 14.596 0l.19 3.798 1.321 2.641A1.809 1.809 0 0 1 19.191 18H16.9a5.002 5.002 0 0 1-9.8 0H4.809a1.809 1.809 0 0 1-1.618-2.618l1.32-2.641.19-3.798ZM9.17 18a3.001 3.001 0 0 0 5.658 0H9.171ZM12 4a5.307 5.307 0 0 0-5.3 5.042l-.19 3.798a2 2 0 0 1-.21.795L5.119 16h13.764l-1.183-2.365a2 2 0 0 1-.208-.795l-.19-3.798A5.308 5.308 0 0 0 12 4Z', +}); +export var Bell2_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a7.307 7.307 0 0 0-7.298 6.943l-.19 3.798-1.321 2.641A1.809 1.809 0 0 0 4.809 18H7.1a5.002 5.002 0 0 0 9.8 0h2.291a1.809 1.809 0 0 0 1.618-2.618l-1.32-2.641-.19-3.798A7.308 7.308 0 0 0 12 2Zm0 18a3.001 3.001 0 0 1-2.83-2h5.66A3.001 3.001 0 0 1 12 20Z', +}); +export var Bell2Off_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19h-.94c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053c.116-.87.372-1.69.743-2.442L2.293 3.707a1 1 0 0 1 0-1.414Zm4.19 5.604c-.134.376-.23.772-.285 1.183L5.142 17h10.444L6.483 7.897ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM8.834 2.666a7.853 7.853 0 0 1 10.95 6.15l.645 4.832a1 1 0 0 1-1.983.265l-.644-4.833A5.853 5.853 0 0 0 9.64 4.495a1 1 0 0 1-.807-1.83Z', +}); +export var Bell2Off_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'm19.785 8.815 1.034 7.761L7.595 3.352a7.853 7.853 0 0 1 12.19 5.463ZM4 19h3.354c.904 1.748 2.607 3 4.646 3 2.038 0 3.742-1.252 4.646-3h.94l2.707 2.707a1 1 0 0 0 1.414-1.414l-18-18a1 1 0 0 0-1.414 1.414l2.666 2.666a7.842 7.842 0 0 0-.743 2.442l-1.207 9.053A1 1 0 0 0 4 19Zm8 1c-.823 0-1.613-.363-2.222-1h4.443c-.608.637-1.398 1-2.221 1Z', +}); diff --git a/src/components/icons/BellPlus.js b/src/components/icons/BellPlus.js new file mode 100644 index 0000000000..c132f74734 --- /dev/null +++ b/src/components/icons/BellPlus.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var BellPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a1 1 0 0 1 0 2 5.85 5.85 0 0 0-5.802 5.08L5.143 17h13.715l-.382-2.868-.01-.102a1 1 0 0 1 1.973-.262l.02.1.532 4a1 1 0 0 1-.99 1.132h-3.357c-.905 1.747-2.606 3-4.644 3s-3.74-1.253-4.643-3H4a1 1 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.61.637 1.397 1 2.22 1s1.611-.363 2.22-1H9.78ZM17 2.5a1 1 0 0 1 1 1V6h2.5a1 1 0 0 1 0 2H18v2.5a1 1 0 0 1-2 0V8h-2.5a1 1 0 1 1 0-2H16V3.5a1 1 0 0 1 1-1Z', +}); diff --git a/src/components/icons/BellRinging.js b/src/components/icons/BellRinging.js new file mode 100644 index 0000000000..a31f08d73b --- /dev/null +++ b/src/components/icons/BellRinging.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var BellRinging_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a7.854 7.854 0 0 1 7.785 6.815l1.055 7.92.018.224a2 2 0 0 1-2 2.041h-2.215c-.904 1.747-2.605 3-4.643 3s-3.739-1.253-4.643-3H5.142a2 2 0 0 1-1.982-2.265l1.056-7.92.057-.363A7.854 7.854 0 0 1 12 2ZM9.78 19c.609.637 1.398 1 2.22 1s1.611-.363 2.22-1H9.78ZM12 4a5.854 5.854 0 0 0-5.76 4.81l-.041.27L5.142 17h13.716l-1.056-7.92A5.854 5.854 0 0 0 12 4ZM2.718 7.464a1 1 0 1 1-1.953-.427l1.953.427Zm20.518-.427a1 1 0 0 1-1.954.427l1.954-.427ZM3.193 2.105a1 1 0 0 1 1.531 1.287 9.47 9.47 0 0 0-2.006 4.072L.765 7.037a11.46 11.46 0 0 1 2.428-4.932Zm16.205-.123a1 1 0 0 1 1.34.047l.069.076.217.265a11.46 11.46 0 0 1 2.212 4.667l-.978.213-.976.214a9.46 9.46 0 0 0-1.826-3.853l-.18-.22-.062-.081a1 1 0 0 1 .184-1.328Z', +}); +export var BellRinging_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a7.854 7.854 0 0 1 7.784 6.815l1.207 9.053a1 1 0 0 1-.99 1.132h-3.354c-.904 1.748-2.608 3-4.647 3-2.038 0-3.742-1.252-4.646-3H4a1.002 1.002 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.608.637 1.398 1 2.221 1s1.613-.363 2.222-1H9.779ZM3.193 2.104a1 1 0 0 1 1.53 1.288A9.47 9.47 0 0 0 2.72 7.464a1 1 0 0 1-1.954-.427 11.46 11.46 0 0 1 2.428-4.933Zm16.205-.122a1 1 0 0 1 1.409.122 11.47 11.47 0 0 1 2.429 4.933 1 1 0 0 1-1.954.427 9.47 9.47 0 0 0-2.006-4.072 1 1 0 0 1 .122-1.41Z', +}); diff --git a/src/components/icons/BirthdayCake.js b/src/components/icons/BirthdayCake.js new file mode 100644 index 0000000000..d8c976fa5c --- /dev/null +++ b/src/components/icons/BirthdayCake.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var BirthdayCake_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'm12 .757 2.122 2.122A3 3 0 0 1 13 7.829V9h4.5a3 3 0 0 1 3 3v1.646c0 .603-.18 1.177-.5 1.658V19a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-3.696a3 3 0 0 1-.5-1.658V12a3 3 0 0 1 3-3H11V7.829a3 3 0 0 1-1.121-4.95L12 .757ZM6.5 11a1 1 0 0 0-1 1v1.646a1 1 0 0 0 .629.928l.5.2a1 1 0 0 0 .742 0l1.015-.405a3 3 0 0 1 2.228 0l1.015.405a1 1 0 0 0 .742 0l1.015-.405a3 3 0 0 1 2.228 0l1.015.405a1 1 0 0 0 .742 0l.5-.2a1 1 0 0 0 .629-.928V12a1 1 0 0 0-1-1h-11ZM6 16.674V19a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-2.326a3 3 0 0 1-2.114-.043l-1.015-.405a1 1 0 0 0-.742 0l-1.015.405a3 3 0 0 1-2.228 0l-1.015-.405a1 1 0 0 0-.742 0l-1.015.405A3 3 0 0 1 6 16.674ZM12.002 6a1 1 0 0 0 .706-1.707L12 3.586l-.707.707A1 1 0 0 0 12.002 6Z', +}); diff --git a/src/components/icons/Bookmark.js b/src/components/icons/Bookmark.js new file mode 100644 index 0000000000..9fde1694b5 --- /dev/null +++ b/src/components/icons/Bookmark.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +// custom, not part of icon library +export var Bookmark = createSinglePathSVG({ + path: 'M9.7 16.895a4 4 0 0 1 4.6 0l3.7 2.6V6.5a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v12.995l3.7-2.6Zm10.3 2.6c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2.001 2.001 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v12.995Z', +}); +// custom, not part of icon library +export var BookmarkFilled = createSinglePathSVG({ + path: 'M16 2.5a4 4 0 0 1 4 4v12.995c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2.001 2.001 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8Z', +}); +// custom, not part of icon library, for LARGE (64px) size +export var BookmarkDeleteLarge = createSinglePathSVG({ + path: 'M14.2 2.625c.834 0 1.482 0 2.001.042.523.043.949.131 1.331.326.635.324 1.151.84 1.475 1.475.195.382.283.807.326 1.33.042.52.042 1.168.042 2.002v11.09c0 .495 0 .893-.027 1.199-.028.301-.087.585-.26.809-.249.323-.63.518-1.037.533-.282.01-.547-.107-.808-.26-.265-.154-.588-.385-.991-.673l-3.54-2.528c-.36-.258-.461-.322-.559-.347a.626.626 0 0 0-.306 0c-.098.025-.199.09-.559.347l-3.54 2.528c-.403.288-.726.519-.991.674-.261.152-.526.269-.808.259a1.376 1.376 0 0 1-1.038-.534c-.172-.223-.231-.507-.259-.808a7.31 7.31 0 0 1-.024-.528l-.003-.67V7.8c0-.834 0-1.482.042-2.001.043-.523.13-.949.325-1.331a3.376 3.376 0 0 1 1.476-1.475c.382-.195.808-.283 1.33-.326.52-.042 1.168-.042 2.002-.042h4.4Zm-4.4.75c-.846 0-1.458 0-1.94.04-.477.039-.792.114-1.051.246A2.626 2.626 0 0 0 5.66 4.81c-.132.259-.208.574-.247 1.051-.04.482-.039 1.094-.039 1.94v11.09l.003.658c.003.186.01.34.021.473.025.267.07.37.106.418a.626.626 0 0 0 .472.243c.059.002.168-.022.4-.158.23-.133.52-.34.935-.636l3.54-2.529c.308-.22.543-.396.81-.464.222-.056.454-.056.676 0 .267.068.5.244.81.464l3.54 2.529c.414.296.704.503.933.636.233.137.343.16.402.158a.626.626 0 0 0 .472-.243c.036-.048.081-.15.106-.419.024-.263.024-.62.024-1.13V7.8c0-.846 0-1.458-.04-1.94-.039-.477-.114-.792-.246-1.051A2.627 2.627 0 0 0 17.19 3.66c-.259-.132-.575-.207-1.051-.246-.482-.04-1.094-.04-1.94-.04H9.8Zm4.056 4.238a.375.375 0 0 1 .53.53L12.53 10l1.857 1.856a.375.375 0 0 1-.53.53L12 10.53l-1.856 1.857a.375.375 0 0 1-.53-.53L11.47 10 9.613 8.144a.375.375 0 0 1 .53-.53L12 9.47l1.856-1.857Z', +}); diff --git a/src/components/icons/BroomSparkle.js b/src/components/icons/BroomSparkle.js new file mode 100644 index 0000000000..e8e33729cf --- /dev/null +++ b/src/components/icons/BroomSparkle.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var BroomSparkle_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M20.494 2.13a1 1 0 0 1 .375 1.364l-4.658 8.2.263.095c1.52.554 2.737 2.062 2.484 3.864-.336 2.393-1.358 4.12-3.245 6.047a1 1 0 0 1-1.102.222l-9.5-4a1 1 0 0 1-.166-1.754c1.458-.971 2.623-1.923 3.498-3.3 1.057-1.662 3.154-2.854 5.281-2.08l.58.212 4.827-8.494a1 1 0 0 1 1.363-.375ZM13.04 12.669c-.983-.358-2.19.142-2.91 1.273-.737 1.16-1.628 2.054-2.601 2.828l1.708.72a.2.2 0 0 0 .177-.011l2.13-1.218a.2.2 0 0 1 .29.237l-.551 1.653a.2.2 0 0 0 .112.247l3.353 1.413c1.359-1.494 1.994-2.76 2.23-4.435.094-.675-.359-1.405-1.188-1.707l-2.75-1ZM4.407 7.184a.5.5 0 0 1-.224.224l-1.29.645a.5.5 0 0 0 0 .894l1.29.645a.5.5 0 0 1 .224.224l.645 1.29a.5.5 0 0 0 .894 0l.645-1.29a.5.5 0 0 1 .224-.224l1.29-.645a.5.5 0 0 0 0-.894l-1.29-.645a.5.5 0 0 1-.224-.224l-.645-1.29a.5.5 0 0 0-.894 0l-.645 1.29ZM9.559 3.72a.36.36 0 0 0 .16-.16l.46-.921a.357.357 0 0 1 .64 0l.46.921q.054.106.16.16l.921.46a.357.357 0 0 1 0 .64l-.921.46a.36.36 0 0 0-.16.16l-.46.921a.357.357 0 0 1-.64 0l-.46-.921a.36.36 0 0 0-.16-.16l-.921-.46a.357.357 0 0 1 0-.64l.921-.46Z', +}); diff --git a/src/components/icons/Bubble.js b/src/components/icons/Bubble.js new file mode 100644 index 0000000000..6c6be4b713 --- /dev/null +++ b/src/components/icons/Bubble.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', +}); +export var Bubble_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M2.002 6a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2h-1a3 3 0 0 1-3-3V6Zm3-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h7a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-14Z', +}); +export var Bubble_Stroke2_Corner3_Rounded = createSinglePathSVG({ + path: 'M2.002 7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2a4 4 0 0 1-4-4V7Zm4-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h6a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-12Z', +}); +export var Bubbles_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z', +}); diff --git a/src/components/icons/BubbleInfo.js b/src/components/icons/BubbleInfo.js new file mode 100644 index 0000000000..c9d8e79767 --- /dev/null +++ b/src/components/icons/BubbleInfo.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var BubbleInfo_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6.002 5h12a1 1 0 0 1 1 1v10.036a1 1 0 0 1-1 1h-2.626a2 2 0 0 0-1.276.46l-2.098 1.738-2.065-1.731a2 2 0 0 0-1.285-.467h-2.65a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm12-2h-12a3 3 0 0 0-3 3v10.036a3 3 0 0 0 3 3h2.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h2.626a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3ZM13 11.75a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2ZM12 10a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', +}); diff --git a/src/components/icons/BulletList.js b/src/components/icons/BulletList.js new file mode 100644 index 0000000000..f356ca58d2 --- /dev/null +++ b/src/components/icons/BulletList.js @@ -0,0 +1,14 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var BulletList_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 47 38', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: 2, + path: 'M22.333 31.667H45M22.333 6.333H45m-33.333 0A5.333 5.333 0 1 1 1 6.333a5.333 5.333 0 0 1 10.667 0Zm0 25.334a5.333 5.333 0 1 1-10.667 0 5.333 5.333 0 0 1 10.667 0Z', +}); +export var BulletList_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Zm-6 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', +}); +export var BulletList_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm0 10a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm10-1a1 1 0 1 0 0 2h7a1 1 0 1 0 0-2h-7Zm-1-9a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/CC.js b/src/components/icons/CC.js new file mode 100644 index 0000000000..d2a43f012b --- /dev/null +++ b/src/components/icons/CC.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CC_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Zm10.957 6.293a1 1 0 1 0 0 1.414 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414Zm-6.331-.22a1 1 0 1 0 .331 1.634 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414.994.994 0 0 0-.331-.22Z', +}); +export var CC_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm11.543 7.293a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.242 1 1 0 0 0-1.414-1.414 1 1 0 0 1-1.414-1.414Zm-6 0a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.243 1 1 0 0 0-1.414-1.415 1 1 0 0 1-1.414-1.414Z', +}); diff --git a/src/components/icons/Calendar.js b/src/components/icons/Calendar.js new file mode 100644 index 0000000000..e35a9ec0b9 --- /dev/null +++ b/src/components/icons/Calendar.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Calendar_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8 2a1 1 0 0 1 1 1v1h6V3a1 1 0 1 1 2 0v1h2a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2V3a1 1 0 0 1 1-1ZM5 6v3h14V6H5Zm14 5H5v8h14v-8Z', +}); diff --git a/src/components/icons/CalendarClock.js b/src/components/icons/CalendarClock.js new file mode 100644 index 0000000000..0e1eb2ee2c --- /dev/null +++ b/src/components/icons/CalendarClock.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z', +}); diff --git a/src/components/icons/CalendarDays.js b/src/components/icons/CalendarDays.js new file mode 100644 index 0000000000..eb8029aaa6 --- /dev/null +++ b/src/components/icons/CalendarDays.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CalendarDays_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V9h14v10H5ZM5 7h14V5H5v2Zm3 10.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM17.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM9.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 17.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z', +}); diff --git a/src/components/icons/Camera.js b/src/components/icons/Camera.js new file mode 100644 index 0000000000..414b37ad88 --- /dev/null +++ b/src/components/icons/Camera.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Camera_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM13.965 5h-3.93L8.63 7.11A2 2 0 0 1 6.965 8H4v11h16V8h-2.965a2 2 0 0 1-1.664-.89L13.965 5ZM12 11a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z', +}); +export var Camera_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM12 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z', +}); diff --git a/src/components/icons/Car.js b/src/components/icons/Car.js new file mode 100644 index 0000000000..ec398ff104 --- /dev/null +++ b/src/components/icons/Car.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Car_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M7.018 6a1 1 0 0 0-.808.412L5.4 5.824l.809.588L3 10.825V17a1 1 0 1 0 2 0 1 1 0 0 1 1-1h12a1 1 0 0 1 1 1 1 1 0 1 0 2 0v-5.998l-3.22-4.577A1 1 0 0 0 16.962 6H7.018ZM23 11.686V17a3 3 0 0 1-5.83 1H6.83A3.001 3.001 0 0 1 1 17v-5.5a1 1 0 1 1 0-2h.49l3.102-4.265A3 3 0 0 1 7.018 4h9.944a3 3 0 0 1 2.453 1.274l3.104 4.412H23a1 1 0 1 1 0 2ZM5 13a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1Zm10 0a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/Celebrate.js b/src/components/icons/Celebrate.js new file mode 100644 index 0000000000..7caaec1c28 --- /dev/null +++ b/src/components/icons/Celebrate.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Celebrate_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M13.832 1.014a1 1 0 0 1 1.154.818L14 2l.986-.168v.003l.001.005.002.014.007.045.021.156c.017.13.035.312.048.527.025.42.028 1.01-.082 1.6-.127.69-.447 1.31-.701 1.726a7.107 7.107 0 0 1-.498.712l-.01.014-.004.005-.002.001v.002L13 6l.767.642a1 1 0 0 1-1.535-1.282l.002-.003.017-.02a5.13 5.13 0 0 0 .324-.47c.198-.325.378-.705.442-1.049.068-.37.071-.78.051-1.12a6.268 6.268 0 0 0-.05-.504l-.004-.025m.818-1.155a1 1 0 0 0-.818 1.155Zm5.257 1.545a1 1 0 0 1 .602 1.28l-.45 1.25a1 1 0 0 1-1.882-.678l.45-1.25a1 1 0 0 1 1.28-.602ZM6.524 7.136a2 2 0 0 1 3.294-.732l7.778 7.778a2 2 0 0 1-.732 3.294L4.653 21.91c-1.596.579-3.142-.967-2.563-2.563L6.524 7.136Zm9.658 8.46L8.404 7.818 3.97 20.03l12.212-4.434Zm5.712-8.543a1 1 0 0 1-.447 1.341l-1 .5a1 1 0 1 1-.894-1.788l1-.5a1 1 0 0 1 1.341.447Zm-4.687-.26a1 1 0 0 1 0 1.414l-1 1a1 1 0 1 1-1.414-1.414l1-1a1 1 0 0 1 1.414 0Zm-.206 4.165A1 1 0 0 1 18.042 10L18 11l.042-1 .003.001h.014l.035.003.117.008a7.693 7.693 0 0 1 1.594.306c.423.135.861.352 1.168.516a11.873 11.873 0 0 1 .508.288l.032.02.01.006.004.002L21 12l.527-.85a1 1 0 0 1-1.054 1.7l-.005-.003-.023-.014a7.477 7.477 0 0 0-.415-.236 5.497 5.497 0 0 0-.835-.374A5.684 5.684 0 0 0 17.973 12l-.015-.002h-.002a1 1 0 0 1-.955-1.041Z', +}); diff --git a/src/components/icons/ChainLink.js b/src/components/icons/ChainLink.js new file mode 100644 index 0000000000..ad4f695d36 --- /dev/null +++ b/src/components/icons/ChainLink.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ChainLink_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z', +}); diff --git a/src/components/icons/Check.js b/src/components/icons/Check.js new file mode 100644 index 0000000000..e45b774378 --- /dev/null +++ b/src/components/icons/Check.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Check_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M21.59 3.193a1 1 0 0 1 .217 1.397l-11.706 16a1 1 0 0 1-1.429.193l-6.294-5a1 1 0 1 1 1.244-1.566l5.48 4.353 11.09-15.16a1 1 0 0 1 1.398-.217Z', +}); +export var CheckThick_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M21.474 2.98a2.5 2.5 0 0 1 .545 3.494l-10.222 14a2.5 2.5 0 0 1-3.528.52L2.49 16.617a2.5 2.5 0 0 1 3.018-3.986l3.75 2.84L17.98 3.525a2.5 2.5 0 0 1 3.493-.545Z', +}); diff --git a/src/components/icons/Chevron.js b/src/components/icons/Chevron.js new file mode 100644 index 0000000000..4686701824 --- /dev/null +++ b/src/components/icons/Chevron.js @@ -0,0 +1,22 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z', +}); +export var ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z', +}); +export var ChevronTop_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 6a1 1 0 0 1 .707.293l8 8a1 1 0 0 1-1.414 1.414L12 8.414l-7.293 7.293a1 1 0 0 1-1.414-1.414l8-8A1 1 0 0 1 12 6Z', +}); +export var ChevronBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z', +}); +export var ChevronTopBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.293 4.293a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1-1.414 1.414L12 6.414 8.707 9.707a1 1 0 0 1-1.414-1.414l4-4Zm-4 10a1 1 0 0 1 1.414 0L12 17.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z', +}); +/** + * NOTE: Use with size `2xs` + */ +export var TinyChevronBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M10.928 18.882c.757.499 1.786.417 2.452-.25l9-9a1.953 1.953 0 0 0-2.76-2.76L12 14.493l-7.62-7.62a1.952 1.952 0 0 0-2.76 2.76l9 9 .308.25Z', +}); diff --git a/src/components/icons/Circle.js b/src/components/icons/Circle.js new file mode 100644 index 0000000000..ec26314574 --- /dev/null +++ b/src/components/icons/Circle.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Circle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z', +}); diff --git a/src/components/icons/CircleAndSquare.js b/src/components/icons/CircleAndSquare.js new file mode 100644 index 0000000000..b37ca19c2f --- /dev/null +++ b/src/components/icons/CircleAndSquare.js @@ -0,0 +1,5 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Circle_And_Square_Stroke1_Corner0_Rounded_Filled = createSinglePathSVG({ + viewBox: '0 0 62 53', + path: 'M28.173.231a5.653 5.653 0 0 1 7.018 3.83l2.66 9.046a20 20 0 0 1 3.986-.397c11.026 0 19.964 8.937 19.964 19.962l-.006.516c-.274 10.787-9.104 19.448-19.958 19.448l-.514-.007c-8.332-.21-15.394-5.528-18.178-12.938l-8.805 2.59a5.654 5.654 0 0 1-7.02-3.83L.232 14.34a5.654 5.654 0 0 1 3.83-7.018L28.172.23ZM41.838 14.71c-1.17 0-2.313.111-3.42.325l3.863 13.137a5.653 5.653 0 0 1-3.83 7.019L25.07 39.126c2.593 6.732 9.122 11.51 16.768 11.51 9.92 0 17.963-8.043 17.963-17.964S51.758 14.71 41.837 14.71ZM33.271 4.624a3.653 3.653 0 0 0-4.535-2.474L4.624 9.24a3.653 3.653 0 0 0-2.475 4.535l7.09 24.113a3.654 3.654 0 0 0 4.536 2.475l8.762-2.577a20 20 0 0 1-.662-5.114c0-8.961 5.905-16.544 14.037-19.069l-2.64-8.98Zm3.204 10.899c-7.302 2.28-12.601 9.096-12.601 17.15 0 1.571.203 3.095.582 4.548l13.431-3.948a3.654 3.654 0 0 0 2.474-4.536l-3.886-13.214Z', +}); diff --git a/src/components/icons/CircleBanSign.js b/src/components/icons/CircleBanSign.js new file mode 100644 index 0000000000..8b053a0751 --- /dev/null +++ b/src/components/icons/CircleBanSign.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CircleBanSign_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 0 0-6.32 12.906L16.906 5.68A7.962 7.962 0 0 0 12 4Zm6.32 3.094L7.094 18.32A8 8 0 0 0 18.32 7.094ZM2 12C2 6.477 6.477 2 12 2a9.972 9.972 0 0 1 7.071 2.929A9.972 9.972 0 0 1 22 12c0 5.523-4.477 10-10 10a9.972 9.972 0 0 1-7.071-2.929A9.972 9.972 0 0 1 2 12Z', +}); diff --git a/src/components/icons/CircleCheck.js b/src/components/icons/CircleCheck.js new file mode 100644 index 0000000000..8b98ecf6c5 --- /dev/null +++ b/src/components/icons/CircleCheck.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CircleCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm13.633-3.274a1 1 0 0 1 .141 1.407l-4.5 5.5a1 1 0 0 1-1.481.074l-2-2a1 1 0 1 1 1.414-1.414l1.219 1.219 3.8-4.645a1 1 0 0 1 1.407-.141Z', +}); diff --git a/src/components/icons/CircleInfo.js b/src/components/icons/CircleInfo.js new file mode 100644 index 0000000000..c58e0fc186 --- /dev/null +++ b/src/components/icons/CircleInfo.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CircleInfo_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z', +}); diff --git a/src/components/icons/CirclePlus.js b/src/components/icons/CirclePlus.js new file mode 100644 index 0000000000..b6a61e2bb7 --- /dev/null +++ b/src/components/icons/CirclePlus.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CirclePlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 3a1 1 0 0 1 1 1v3h3l.102.005a1 1 0 0 1 0 1.99L16 13h-3v3a1 1 0 1 1-2 0v-3H8a1 1 0 0 1 0-2h3V8a1 1 0 0 1 1-1Z', +}); diff --git a/src/components/icons/CircleQuestion.js b/src/components/icons/CircleQuestion.js new file mode 100644 index 0000000000..13d0cfb81b --- /dev/null +++ b/src/components/icons/CircleQuestion.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CircleQuestion_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z M12 9a1 1 0 0 0-.879.522 1 1 0 0 1-1.754-.96A3 3 0 0 1 12 7c1.515 0 2.567 1.006 2.866 2.189.302 1.189-.156 2.574-1.524 3.258A.62.62 0 0 0 13 13a1 1 0 1 1-2 0c0-.992.56-1.898 1.447-2.342.455-.227.572-.618.48-.978C12.836 9.314 12.529 9 12 9Z M13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z', +}); diff --git a/src/components/icons/CircleX.js b/src/components/icons/CircleX.js new file mode 100644 index 0000000000..810697a3a8 --- /dev/null +++ b/src/components/icons/CircleX.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CircleX_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.293-3.707a1 1 0 0 1 1.414 0L12 10.586l2.293-2.293a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 0 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/Clipboard.js b/src/components/icons/Clipboard.js new file mode 100644 index 0000000000..04c0a8aa1f --- /dev/null +++ b/src/components/icons/Clipboard.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Clipboard_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M8.17 4A3.001 3.001 0 0 1 11 2h2c1.306 0 2.418.835 2.83 2H17a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h1.17ZM8 6H7a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V6Zm6 0V5a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v1h4Z', +}); diff --git a/src/components/icons/Clock.js b/src/components/icons/Clock.js new file mode 100644 index 0000000000..1d9bdd8e90 --- /dev/null +++ b/src/components/icons/Clock.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Clock_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm1 6a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l2.5 2.5a1 1 0 0 0 1.414-1.414L13 11.586V8Z', +}); diff --git a/src/components/icons/CodeBrackets.js b/src/components/icons/CodeBrackets.js new file mode 100644 index 0000000000..d6847c6862 --- /dev/null +++ b/src/components/icons/CodeBrackets.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CodeBrackets_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M14.242 3.03a1 1 0 0 1 .728 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414L3.414 12l3.293 3.293a1 1 0 1 1-1.414 1.414l-4-4a1 1 0 0 1 0-1.414l4-4a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 1 1-1.414-1.414L20.586 12l-3.293-3.293a1 1 0 0 1 0-1.414Z', +}); +export var CodeBrackets_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M14.243 3.03a1 1 0 0 1 .727 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414l-2.586 2.586a1 1 0 0 0 0 1.414l2.586 2.586a1 1 0 1 1-1.414 1.414l-2.586-2.586a3 3 0 0 1 0-4.242l2.586-2.586a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l2.586 2.586a3 3 0 0 1 0 4.242l-2.586 2.586a1 1 0 1 1-1.414-1.414l2.586-2.586a1 1 0 0 0 0-1.414l-2.586-2.586a1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/CodeLines.js b/src/components/icons/CodeLines.js new file mode 100644 index 0000000000..99d37e512f --- /dev/null +++ b/src/components/icons/CodeLines.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var CodeLines_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M2 5a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm15 0a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2h-3a1 1 0 0 1-1-1ZM2 12a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm10 0a1 1 0 0 1 1-1h8a1 1 0 1 1 0 2h-8a1 1 0 0 1-1-1ZM2 19a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm12 0a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/ColorPalette.js b/src/components/icons/ColorPalette.js new file mode 100644 index 0000000000..4551cc39b7 --- /dev/null +++ b/src/components/icons/ColorPalette.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ColorPalette_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 12c0-4.09 3.527-7.5 8-7.5s8 3.41 8 7.5c0 1.579-.419 2.056-.708 2.236-.388.241-1.031.286-2.058.153-.33-.043-.652-.096-.991-.152a65.905 65.905 0 0 0-.531-.087c-.52-.081-1.077-.156-1.61-.164-1.065-.016-2.336.245-2.996 1.567-.418.834-.295 1.67-.078 2.314.18.534.47 1.055.683 1.437v.001l.097.175.01.018C7.432 19.407 4 16.033 4 12Zm8-9.5C6.532 2.5 2 6.7 2 12s4.532 9.5 10 9.5c.401 0 .812-.04 1.166-.193.41-.176.761-.517.866-1.028.085-.416-.03-.796-.118-1.029a5.981 5.981 0 0 0-.351-.73l-.12-.215c-.215-.392-.403-.73-.52-1.078-.13-.387-.111-.614-.029-.78.146-.291.404-.473 1.178-.461.385.005.825.06 1.329.14.15.023.308.05.47.077.36.059.742.122 1.105.17 1.021.132 2.325.213 3.373-.439C21.496 15.22 22 13.874 22 12c0-5.3-4.532-9.5-10-9.5Zm3.5 8.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM9 12.25a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm1.5-2.75a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', +}); diff --git a/src/components/icons/Contacts.js b/src/components/icons/Contacts.js new file mode 100644 index 0000000000..739d5765ce --- /dev/null +++ b/src/components/icons/Contacts.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Contacts_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M7 4a1 1 0 0 0-1 1v12.05q.243-.05.5-.05H18V4H7Zm5 7c.894 0 1.57.188 2.068.513.445.29.683.647.806.937l.046.12c.285.836-.43 1.43-1.03 1.43h-3.78c-.6 0-1.315-.594-1.03-1.43l.046-.12c.123-.29.361-.647.806-.937C10.43 11.188 11.106 11 12 11Zm0-4a1.75 1.75 0 1 1 0 3.5A1.75 1.75 0 0 1 12 7Zm8 11a1 1 0 0 1-1 1H6.5a.5.5 0 0 0 0 1H19a1 1 0 1 1 0 2H6.5A2.5 2.5 0 0 1 4 19.5V5a3 3 0 0 1 3-3h11a2 2 0 0 1 2 2v14Z', +}); +export var Contacts_Filled_Corner2_Rounded = createSinglePathSVG({ + path: 'M7 2a3 3 0 0 0-3 3v14.5A2.5 2.5 0 0 0 6.5 22H19a1 1 0 1 0 0-2H6.5a.5.5 0 0 1 0-1H19a1 1 0 0 0 1-1V4a2 2 0 0 0-2-2H7Zm5 5a1.75 1.75 0 1 0 0 3.5A1.75 1.75 0 0 0 12 7Zm0 4c-.894 0-1.57.188-2.068.512a2.07 2.07 0 0 0-.852 1.058c-.286.838.432 1.43 1.03 1.43h3.78c.598 0 1.316-.592 1.03-1.43a2.07 2.07 0 0 0-.852-1.058C13.57 11.189 12.894 11 12 11Z', +}); diff --git a/src/components/icons/Crop.js b/src/components/icons/Crop.js new file mode 100644 index 0000000000..9b2a43702f --- /dev/null +++ b/src/components/icons/Crop.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Crop_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6 2a1 1 0 0 1 1 1v2h11a1 1 0 0 1 1 1v11h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2H6a1 1 0 0 1-1-1V7H3a1 1 0 0 1 0-2h2V3a1 1 0 0 1 1-1Zm1 5v10h10V7H7Z', +}); diff --git a/src/components/icons/DotGrid.js b/src/components/icons/DotGrid.js new file mode 100644 index 0000000000..c9a8862cd5 --- /dev/null +++ b/src/components/icons/DotGrid.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var DotGrid3x1_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', +}); +export var DotGrid2x3_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M9 17a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm-6-7a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4ZM9 3a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z', +}); diff --git a/src/components/icons/Download.js b/src/components/icons/Download.js new file mode 100644 index 0000000000..0037788ece --- /dev/null +++ b/src/components/icons/Download.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Download_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z', +}); diff --git a/src/components/icons/EditBig.js b/src/components/icons/EditBig.js new file mode 100644 index 0000000000..93cba65411 --- /dev/null +++ b/src/components/icons/EditBig.js @@ -0,0 +1,8 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var EditBig_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 48 48', + path: 'M19.667 4.458a1 1 0 1 0 0-2v2Zm25 23a1 1 0 0 0-2 0h2ZM3.912 45.543l.454-.891-.454.89Zm-2.33-2.33.89-.455h0l-.89.454Zm39.173 2.33-.454-.891h0l.454.89Zm2.33-2.33-.89-.455.89.454ZM1.581 6.37l-.89-.454h0l.89.454Zm2.331-2.331-.454-.891.454.89ZM14.333 32.79h-1a1 1 0 0 0 1 1v-1Zm.781-8.781.707.707-.707-.707ZM36.562 2.562l-.707-.707v0l.707.707Zm7.543 0-.707.707v0l.707-.707Zm.457.458.707-.707v0l-.707.707Zm0 7.542.707.707-.707-.707ZM23.114 32.01l.707.707-.707-.707Zm12.02 14.114v-1h-25.6v2h25.6v-1ZM1 37.591h1v-25.6H0v25.6h1ZM9.533 3.458v1h10.134v-2H9.533v1Zm34.134 24h-1V37.59h2V27.457h-1ZM9.533 46.124v-1c-1.51 0-2.582 0-3.421-.07-.828-.067-1.34-.195-1.746-.402l-.454.89-.454.892c.735.374 1.54.537 2.491.614.94.077 2.107.076 3.584.076v-1ZM1 37.591H0c0 1.477 0 2.645.076 3.584.078.951.24 1.756.614 2.491l.891-.454.891-.454c-.207-.406-.335-.918-.403-1.746C2.001 40.173 2 39.101 2 37.591H1Zm2.912 7.952.454-.891a4.33 4.33 0 0 1-1.894-1.894l-.89.454-.892.454a6.33 6.33 0 0 0 2.768 2.768l.454-.891Zm31.221.581v1c1.477 0 2.645.001 3.585-.076.95-.078 1.756-.24 2.49-.614l-.453-.891-.454-.891c-.406.207-.919.335-1.746.403-.84.068-1.912.07-3.422.07v1Zm8.534-8.533h-1c0 1.51-.001 2.582-.07 3.421-.067.828-.196 1.34-.403 1.746l.891.454.891.454c.375-.735.537-1.54.615-2.49.076-.94.076-2.108.076-3.585h-1Zm-2.912 7.952.454.89a6.33 6.33 0 0 0 2.767-2.767l-.89-.454-.892-.454a4.33 4.33 0 0 1-1.893 1.894l.454.89ZM1 11.99h1c0-1.51 0-2.582.07-3.422.067-.827.195-1.34.402-1.745l-.89-.454-.892-.454c-.374.734-.536 1.54-.614 2.49C-.001 9.345 0 10.513 0 11.99h1Zm8.533-8.533v-1c-1.477 0-2.645-.001-3.584.076-.951.077-1.756.24-2.49.614l.453.89.454.892c.406-.207.918-.336 1.746-.403.839-.069 1.911-.07 3.421-.07v-1ZM1.581 6.37l.891.454A4.33 4.33 0 0 1 4.366 4.93l-.454-.891-.454-.891A6.33 6.33 0 0 0 .69 5.916l.891.454Zm12.752 19.525h-1v6.896h2v-6.896h-1Zm0 6.896v1h6.896v-2h-6.896v1Zm.781-8.781.707.707L37.27 3.269l-.707-.707-.707-.707-21.448 21.448.707.707Zm28.99-21.448-.706.707.457.458.707-.707.707-.707-.457-.458-.707.707Zm.458 8-.707-.707-21.448 21.448.707.707.707.707L45.27 11.269l-.707-.707Zm0-7.542-.707.707a4.333 4.333 0 0 1 0 6.128l.707.707.707.707a6.333 6.333 0 0 0 0-8.956l-.707.707Zm-8-.458.707.707a4.333 4.333 0 0 1 6.129 0l.707-.707.707-.707a6.333 6.333 0 0 0-8.957 0l.707.707ZM21.23 32.791v1c.972 0 1.905-.386 2.593-1.074l-.708-.707-.707-.707a1.67 1.67 0 0 1-1.178.488v1Zm-6.896-6.896h1c0-.442.176-.866.489-1.178l-.708-.707-.707-.707a3.67 3.67 0 0 0-1.074 2.592h1Z', +}); +export var EditBig_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M17.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-9 9A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l9-9ZM10 12.414V14h1.586l8-8L18 4.414l-8 8ZM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Z', +}); diff --git a/src/components/icons/Emoji.js b/src/components/icons/Emoji.js new file mode 100644 index 0000000000..aec36eff4f --- /dev/null +++ b/src/components/icons/Emoji.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var EmojiSad_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6.343 6.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 6.343 6.343ZM19.071 4.93c-3.905-3.905-10.237-3.905-14.142 0-3.905 3.905-3.905 10.237 0 14.142 3.905 3.905 10.237 3.905 14.142 0 3.905-3.905 3.905-10.237 0-14.142Zm-3.537 9.535a5 5 0 0 0-7.07 0 1 1 0 1 0 1.413 1.415 3 3 0 0 1 4.243 0 1 1 0 0 0 1.414-1.415ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5ZM9.25 11c.69 0 1.25-.672 1.25-1.5S9.94 8 9.25 8 8 8.672 8 9.5 8.56 11 9.25 11Z', +}); +export var EmojiSmile_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M17.657 6.343A8 8 0 1 0 6.343 17.657 8 8 0 0 0 17.657 6.343ZM4.929 4.93c3.905-3.905 10.237-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0-3.905-3.905-3.905-10.237 0-14.142Zm3.536 9.192a1 1 0 0 1 1.414 0 3 3 0 0 0 4.243 0 1 1 0 0 1 1.414 1.415 5 5 0 0 1-7.071 0 1 1 0 0 1 0-1.415ZM10.5 9.5c0 .828-.56 1.5-1.25 1.5S8 10.328 8 9.5 8.56 8 9.25 8s1.25.672 1.25 1.5ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5Z', +}); +export var EmojiArc_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-5a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm-5.894 7.803a1 1 0 0 1 1.341-.447c1.719.859 3.387.859 5.106 0a1 1 0 1 1 .894 1.788c-2.281 1.141-4.613 1.141-6.894 0a1 1 0 0 1-.447-1.341Z', +}); +export var EmojiHeartEyes_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2ZM9.351 12.13c1.898-1.507 2.176-2.95 1.613-3.83a1.524 1.524 0 0 0-1.225-.707 1.562 1.562 0 0 0-1.218.53 1.561 1.561 0 0 0-1.326-.082c-.456.186-.8.588-.91 1.083-.227 1.02.527 2.282 2.826 3.048a.256.256 0 0 0 .24-.043Zm5.538.042c2.299-.766 3.053-2.027 2.826-3.048a1.524 1.524 0 0 0-.91-1.082 1.561 1.561 0 0 0-1.326.081 1.562 1.562 0 0 0-1.217-.53 1.524 1.524 0 0 0-1.226.706c-.563.881-.285 2.325 1.613 3.83.068.054.158.07.24.043Zm1.072 2.38a4 4 0 0 1-7.924 0c-.04-.293.218-.525.514-.499 2.309.206 4.587.206 6.896 0 .296-.026.555.206.514.5Z', +}); diff --git a/src/components/icons/Envelope.js b/src/components/icons/Envelope.js new file mode 100644 index 0000000000..0551761097 --- /dev/null +++ b/src/components/icons/Envelope.js @@ -0,0 +1,10 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Envelope_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z', +}); +export var Envelope_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 11.708 2.654 4.06A.998.998 0 0 1 3 4h18c.122 0 .238.022.346.061L12 11.708ZM2 19V6.11l9.367 7.664a1 1 0 0 0 1.266 0L22 6.11V19a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z', +}); +export var Envelope_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6.161 4H17.84c.527 0 .982 0 1.356.03.395.033.789.104 1.167.297a3 3 0 0 1 1.311 1.311c.193.378.264.772.296 1.167.031.375.031.83.031 1.356v7.678c0 .527 0 .981-.03 1.356-.033.395-.104.789-.297 1.167a3 3 0 0 1-1.311 1.311c-.378.193-.772.264-1.167.296-.375.031-.83.031-1.357.031H6.162c-.527 0-.981 0-1.356-.03-.395-.033-.789-.104-1.167-.297a3 3 0 0 1-1.311-1.311c-.193-.378-.264-.772-.296-1.167A18 18 0 0 1 2 15.838V8.162c0-.527 0-.981.03-1.356.033-.395.104-.789.297-1.167a3 3 0 0 1 1.311-1.311c.378-.193.772-.264 1.167-.296C5.18 4 5.635 4 6.161 4ZM5.046 6.018l6.32 5.172a1 1 0 0 0 1.267 0l6.321-5.172A20 20 0 0 0 17.8 6H6.2c-.525 0-.88 0-1.154.018Zm14.953 1.73-6.1 4.99a3 3 0 0 1-3.799 0L4 7.748V15.8c0 .577 0 .949.024 1.232.022.272.06.372.085.422a1 1 0 0 0 .437.437c.05.025.15.063.422.085C5.25 18 5.623 18 6.2 18h11.6c.577 0 .949 0 1.232-.024.272-.022.372-.06.422-.085a1 1 0 0 0 .437-.437c.025-.05.063-.15.085-.422C20 16.75 20 16.377 20 15.8V7.747Z', +}); diff --git a/src/components/icons/EnveopeOpen.js b/src/components/icons/EnveopeOpen.js new file mode 100644 index 0000000000..8d0f152d1e --- /dev/null +++ b/src/components/icons/EnveopeOpen.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Envelope_Open_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6.386c1.064-.002 2 .86 2 2.001V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.613c0-1.142.936-2.003 2-2.001V4Zm2 6.946 6 2 6-2V4H6v6.946ZM9 8a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1Zm2.367 6.843L4 12.387V19h16v-6.613l-7.367 2.456a2 2 0 0 1-1.265 0Z', +}); diff --git a/src/components/icons/Explosion.js b/src/components/icons/Explosion.js new file mode 100644 index 0000000000..e3188c4929 --- /dev/null +++ b/src/components/icons/Explosion.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Explosion_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a1 1 0 0 1 .889.542l1.679 3.259 3.491-1.117a1 1 0 0 1 1.257 1.257L18.2 9.432l3.259 1.679a1 1 0 0 1 0 1.778l-3.124 1.61 2.609 5.098a1 1 0 0 1-1.346 1.346l-5.098-2.61-1.61 3.125a1 1 0 0 1-1.778 0l-1.679-3.259-3.491 1.117a1 1 0 0 1-1.257-1.257L5.8 14.568l-3.259-1.679a1 1 0 0 1 0-1.778l3.124-1.61-2.609-5.098a1 1 0 0 1 1.346-1.346l5.098 2.61-.455.89.455-.89 1.61-3.125A1 1 0 0 1 12 2Zm0 3.183-.72 1.4a2 2 0 0 1-2.69.864L6.248 6.248 7.447 8.59a2 2 0 0 1-.865 2.69L5.183 12l1.534.79a2 2 0 0 1 .989 2.387L7.18 16.82l1.643-.526a2 2 0 0 1 2.387.99l.79 1.533.72-1.4a2 2 0 0 1 2.69-.864l2.342 1.199-1.199-2.342a2 2 0 0 1 .864-2.69l1.4-.72-1.534-.79a2 2 0 0 1-.989-2.387l.526-1.643-1.643.526a2 2 0 0 1-2.387-.99L12 5.184Z', +}); diff --git a/src/components/icons/Eye.js b/src/components/icons/Eye.js new file mode 100644 index 0000000000..cb1c694ee5 --- /dev/null +++ b/src/components/icons/Eye.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Eye_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.135 12C5.413 16.088 8.77 18 12 18s6.587-1.912 8.865-6C18.587 7.912 15.23 6 12 6c-3.228 0-6.587 1.912-8.865 6ZM12 4c4.24 0 8.339 2.611 10.888 7.54a1 1 0 0 1 0 .92C20.338 17.388 16.24 20 12 20c-4.24 0-8.339-2.611-10.888-7.54a1 1 0 0 1 0-.92C3.662 6.612 7.76 4 12 4Zm0 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z', +}); +export var Eye_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 6c-3.127 0-6.367 1.79-8.638 5.606a.77.77 0 0 0 0 .788C5.633 16.209 8.873 18 12 18s6.367-1.79 8.638-5.606a.77.77 0 0 0 0-.788C18.367 7.791 15.127 6 12 6Zm0-2c3.952 0 7.79 2.272 10.357 6.583a2.77 2.77 0 0 1 0 2.834C19.79 17.727 15.952 20 12 20s-7.79-2.272-10.357-6.583a2.77 2.77 0 0 1 0-2.834C4.21 6.273 8.048 4 12 4Zm0 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z', +}); diff --git a/src/components/icons/EyeSlash.js b/src/components/icons/EyeSlash.js new file mode 100644 index 0000000000..1f7bf9a065 --- /dev/null +++ b/src/components/icons/EyeSlash.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var EyeSlash_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2.293 2.293a1 1 0 0 1 1.414 0L7.335 5.92l.03.03 3.22 3.222 4.243 4.242 3.22 3.22.03.03 3.63 3.629a1 1 0 0 1-1.415 1.414l-3.09-3.09c-2.65 1.478-5.625 1.778-8.421.869-3.039-.987-5.779-3.37-7.67-7.027a1 1 0 0 1 0-.918c1.086-2.1 2.452-3.78 3.996-5.019L2.293 3.707a1 1 0 0 1 0-1.414Zm4.24 5.654 2.021 2.021a4 4 0 0 0 5.478 5.478l1.688 1.688c-2.042.982-4.246 1.124-6.32.45-2.34-.76-4.594-2.586-6.265-5.584.97-1.739 2.135-3.083 3.398-4.053Zm3.535 3.535 2.45 2.45a2 2 0 0 1-2.45-2.45Zm.81-5.405c3.573-.49 7.45 1.369 9.987 5.923a14.797 14.797 0 0 1-1.347 2.02 1 1 0 1 0 1.564 1.247 17.078 17.078 0 0 0 1.806-2.808 1 1 0 0 0 0-.918c-2.833-5.479-7.584-8.088-12.281-7.446a1 1 0 0 0 .271 1.982Z', +}); diff --git a/src/components/icons/Filter.js b/src/components/icons/Filter.js new file mode 100644 index 0000000000..5311fd8703 --- /dev/null +++ b/src/components/icons/Filter.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Filter_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z', +}); diff --git a/src/components/icons/FilterTimeline.js b/src/components/icons/FilterTimeline.js new file mode 100644 index 0000000000..af262230c4 --- /dev/null +++ b/src/components/icons/FilterTimeline.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var FilterTimeline_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.002 5a1 1 0 0 0-2 0v11.587l-1.295-1.294a1 1 0 0 0-1.414 1.414l3.002 3a1 1 0 0 0 1.414 0l2.998-3a1 1 0 0 0-1.414-1.414l-1.291 1.292V5ZM16 16a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2h-4Zm-3-4a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1Zm-1-6a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2h-8Z', +}); diff --git a/src/components/icons/Flag.js b/src/components/icons/Flag.js new file mode 100644 index 0000000000..211dba3353 --- /dev/null +++ b/src/components/icons/Flag.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Flag_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 4a2 2 0 0 1 2-2h13.131c1.598 0 2.55 1.78 1.665 3.11L18.202 9l2.594 3.89c.886 1.33-.067 3.11-1.665 3.11H6v5a1 1 0 1 1-2 0V4Zm2 10h13.131l-2.593-3.89a2 2 0 0 1 0-2.22L19.13 4H6v10Z', +}); diff --git a/src/components/icons/Flame.js b/src/components/icons/Flame.js new file mode 100644 index 0000000000..789fc2f3e9 --- /dev/null +++ b/src/components/icons/Flame.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Flame_Stroke2_Corner1_Rounded = createSinglePathSVG({ + path: 'M11.158 2.879c.584-.835 1.757-1.137 2.673-.507.951.654 2.597 1.92 4.013 3.694S20.5 10.194 20.5 13c0 4.997-3.752 9-8.5 9s-8.5-4.003-8.5-9c0-2.035.874-4.636 2.578-6.712.746-.91 2.034-.855 2.786-.133l2.294-3.276Zm-3.04 15.758C6.538 17.386 5.5 15.37 5.5 13c0-1.511.666-3.616 2.042-5.342.87.797 2.254.653 2.939-.325l2.286-3.265c.871.606 2.299 1.723 3.514 3.246C17.53 8.879 18.5 10.804 18.5 13c0 2.369-1.038 4.386-2.618 5.637q.117-.518.118-1.061c0-2.601-2.038-4.382-2.911-5.04a1.8 1.8 0 0 0-2.177 0C10.038 13.195 8 14.976 8 17.577q0 .543.118 1.061ZM12 14.222c-.825.648-2 1.859-2 3.354C10 19.043 11.016 20 12 20s2-.957 2-2.424c0-1.495-1.175-2.706-2-3.354Z', +}); diff --git a/src/components/icons/FlipImage.js b/src/components/icons/FlipImage.js new file mode 100644 index 0000000000..e4994c1c70 --- /dev/null +++ b/src/components/icons/FlipImage.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var FlipVertical_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v5h-2V5H5v4H3V4Zm20 9H1v-2h22v2Zm-2.293 7.707A1 1 0 0 1 20 21h-1v-2h2v1a1 1 0 0 1-.293.707ZM17 19v2h-2v-2h2Zm-4 0v2h-2v-2h2Zm-4 0v2H7v-2h2Zm-4 0v2H4a1 1 0 0 1-1-1v-1h2Zm0-2H3v-2h2v2Zm14-2v2h2v-2h-2Z', +}); +export var FlipHorizontal_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 21a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5v2H5v14h4v2H4Zm9-20v22h-2V1h2Zm7.707 2.293A1 1 0 0 1 21 4v1h-2V3h1a1 1 0 0 1 .707.293ZM19 7h2v2h-2V7Zm0 4h2v2h-2v-2Zm0 4h2v2h-2v-2Zm0 4h2v1a1 1 0 0 1-1 1h-1v-2Zm-2 0v2h-2v-2h2ZM15 5h2V3h-2v2Z', +}); diff --git a/src/components/icons/FloppyDisk.js b/src/components/icons/FloppyDisk.js new file mode 100644 index 0000000000..01cfcf981e --- /dev/null +++ b/src/components/icons/FloppyDisk.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var FloppyDisk_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z', +}); diff --git a/src/components/icons/Freeze.js b/src/components/icons/Freeze.js new file mode 100644 index 0000000000..71dc18f7ce --- /dev/null +++ b/src/components/icons/Freeze.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Freeze_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M8.789 2.293a1 1 0 0 1 1.414 0l1.793 1.793 1.793-1.793a1 1 0 1 1 1.414 1.414l-2.207 2.207v4.355l3.772-2.178.808-3.015a1 1 0 1 1 1.931.518l-.656 2.449 2.45.656a1 1 0 1 1-.518 1.932l-3.015-.808L13.998 12l3.77 2.177 3.015-.808a1 1 0 1 1 .517 1.932l-2.449.656.657 2.45a1 1 0 1 1-1.932.517l-.808-3.015-3.772-2.178v4.355l2.207 2.207a1 1 0 0 1-1.414 1.414l-1.793-1.793-1.793 1.793a1 1 0 0 1-1.414-1.414l2.207-2.207v-4.353l-3.77 2.176-.807 3.015a1 1 0 0 1-1.932-.518l.656-2.449-2.449-.656a1 1 0 1 1 .518-1.932l3.015.808L9.997 12l-3.77-2.177-3.015.808a1 1 0 0 1-.518-1.932l2.45-.656-.657-2.45a1 1 0 0 1 1.932-.517l.808 3.015 3.77 2.176V5.914L8.788 3.707a1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/GameController.js b/src/components/icons/GameController.js new file mode 100644 index 0000000000..82392214a9 --- /dev/null +++ b/src/components/icons/GameController.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var GameController_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M1 8a3 3 0 0 1 3-3h16a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8Zm3-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4Zm4 2a1 1 0 0 1 1 1v1h1a1 1 0 1 1 0 2H9v1a1 1 0 1 1-2 0v-1H6a1 1 0 1 1 0-2h1v-1a1 1 0 0 1 1-1Zm5.5 4.5a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm3-3a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Z', +}); diff --git a/src/components/icons/Gif.js b/src/components/icons/Gif.js new file mode 100644 index 0000000000..32a685a244 --- /dev/null +++ b/src/components/icons/Gif.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Gif_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3Zm1 14V6h16v12H4Zm2-5.713c0 1.54.92 2.463 2.48 2.463 1.434 0 2.353-.807 2.353-2.06v-.166c0-.578-.267-.834-.884-.834h-.806c-.416 0-.632.182-.632.535 0 .357.22.55.632.55h.146v.063c0 .36-.299.609-.735.609-.597 0-.904-.4-.904-1.168v-.52c0-.775.307-1.155.951-1.155.325 0 .538.152.746.3.089.064.176.127.272.177a.82.82 0 0 0 .409.108c.385 0 .656-.263.656-.636 0-.353-.26-.679-.664-.915-.409-.24-.96-.388-1.548-.388C6.955 9.25 6 10.2 6 11.67v.617Zm6.358 2.385c.526 0 .813-.31.813-.872v-3.627c0-.558-.295-.873-.825-.873s-.825.31-.825.873V13.8c0 .558.302.872.837.872Zm3.367-.872c0 .566-.283.872-.802.872-.538 0-.848-.318-.848-.872v-3.635c0-.512.314-.826.82-.826h2.496c.35 0 .609.272.609.64 0 .369-.26.629-.609.629h-1.666v.973h1.47c.365 0 .608.248.608.613 0 .36-.247.613-.608.613h-1.47v.993Z', +}); +export var GifSquare_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V5h14v14H5Zm10.725-5.2c0 .566-.283.872-.802.872-.538 0-.848-.318-.848-.872v-3.635c0-.512.314-.826.82-.826h2.496c.35 0 .609.272.609.64 0 .369-.26.629-.609.629h-1.666v.973h1.47c.365 0 .608.248.608.613 0 .36-.247.613-.608.613h-1.47v.993Zm-3.367.872c.526 0 .813-.31.813-.872v-3.627c0-.558-.295-.873-.825-.873s-.825.31-.825.873V13.8c0 .558.302.872.837.872Zm-3.879.078C6.92 14.75 6 13.827 6 12.287v-.617c0-1.47.955-2.42 2.472-2.42.589 0 1.139.147 1.548.388.404.236.664.562.664.915 0 .373-.271.636-.656.636a.82.82 0 0 1-.41-.108 2.34 2.34 0 0 1-.271-.177c-.208-.148-.421-.3-.746-.3-.644 0-.95.38-.95 1.155v.52c0 .768.306 1.168.903 1.168.436 0 .735-.248.735-.61v-.061h-.146c-.412 0-.632-.194-.632-.551 0-.353.216-.535.632-.535h.806c.617 0 .884.256.884.834v.166c0 1.253-.92 2.06-2.354 2.06Z', +}); diff --git a/src/components/icons/Gift1.js b/src/components/icons/Gift1.js new file mode 100644 index 0000000000..368eb3bbdd --- /dev/null +++ b/src/components/icons/Gift1.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Gift1_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6 4.667A2.667 2.667 0 0 1 8.667 2c1.34 0 2.538.608 3.333 1.564A4.324 4.324 0 0 1 15.333 2 2.667 2.667 0 0 1 18 4.667c0 .859-.25 1.66-.681 2.333H20a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.681A4.313 4.313 0 0 1 6 4.667ZM10.333 7H11v-.667A2.333 2.333 0 0 0 8.667 4 .667.667 0 0 0 8 4.667 2.333 2.333 0 0 0 10.333 7ZM13 6.333V7h.667A2.333 2.333 0 0 0 16 4.667.667.667 0 0 0 15.333 4 2.333 2.333 0 0 0 13 6.333ZM11 9H5v2h6V9Zm2 2V9h6v2h-6Zm-2 2H6v6h5v-6Zm2 6v-6h5v6h-5Z', +}); +export var Gift1_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M6 4.667A2.667 2.667 0 0 1 8.667 2c1.34 0 2.538.608 3.333 1.564A4.324 4.324 0 0 1 15.333 2 2.667 2.667 0 0 1 18 4.667c0 .859-.25 1.66-.681 2.333H20a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-7V7h.667A2.333 2.333 0 0 0 16 4.667.667.667 0 0 0 15.333 4 2.333 2.333 0 0 0 13 6.333V7h-2v-.667A2.333 2.333 0 0 0 8.667 4 .667.667 0 0 0 8 4.667 2.333 2.333 0 0 0 10.333 7H11v4H4a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.681A4.313 4.313 0 0 1 6 4.667ZM11 13H4v7a1 1 0 0 0 1 1h6v-8Zm9 0h-7v8h6a1 1 0 0 0 1-1v-7Z', +}); diff --git a/src/components/icons/Globe.js b/src/components/icons/Globe.js new file mode 100644 index 0000000000..7d125ab974 --- /dev/null +++ b/src/components/icons/Globe.js @@ -0,0 +1,10 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Globe_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z', +}); +export var Earth_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.953 3.81a1 1 0 0 1-.726.727l-3.444.866-.772 1.533a1 1 0 0 1-1.493.35L4.4 9.493Zm.883-1.84L7.756 9.51l.44-.874a1 1 0 0 1 .649-.52l3.306-.832.807-3.227a7.993 7.993 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.43.162a1 1 0 0 1 .77-.29l1.89.121a1 1 0 0 1 .494.168l2.869 1.928a1 1 0 0 1 .336 1.277l-.973 1.946a1 1 0 0 1-.894.553h-2.92a1 1 0 0 1-.831-.445L9.225 14.5a1 1 0 0 1 .126-1.262l1.08-1.076Zm.915 1.913.177-.177 1.171.074 1.914 1.286-.303.607h-1.766l-1.194-1.79Z', +}); +export var Earth_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.67 2.679a2.95 2.95 0 0 1-2.14 2.142l-2.173.547a.32.32 0 0 0-.205.164 2.316 2.316 0 0 1-3.457.81L4.4 9.493Zm.883-1.84 2.171 1.63a.315.315 0 0 0 .471-.11c.303-.6.851-1.04 1.503-1.204l2.174-.546a.95.95 0 0 0 .687-.688l.97.242-.97-.242.67-2.678a7.993 7.993 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.048.543a2.2 2.2 0 0 1 1.69-.636l.827.053c.52.033 1.023.204 1.456.495l1.37.921a2.453 2.453 0 0 1-1.367 4.489h-.98a2.95 2.95 0 0 1-2.45-1.312L9.77 15.32a2.2 2.2 0 0 1 .278-2.776Zm1.563 1.36a.197.197 0 0 0-.177.306l.823 1.235c.176.263.471.42.787.42h.98a.453.453 0 0 0 .252-.828l-1.37-.921a.95.95 0 0 0-.468-.159l-.827-.053Z', +}); diff --git a/src/components/icons/Group.js b/src/components/icons/Group.js new file mode 100644 index 0000000000..2c01193348 --- /dev/null +++ b/src/components/icons/Group.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Group3_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm5.826 7.376c-.919-.779-2.052-1.03-3.1-.787a1 1 0 0 1-.451-1.949c1.671-.386 3.45.028 4.844 1.211 1.397 1.185 2.348 3.084 2.524 5.579a1 1 0 0 1-.997 1.07H18a1 1 0 1 1 0-2h3.007c-.29-1.47-.935-2.49-1.681-3.124ZM3.126 19h9.747c-.61-3.495-2.867-5-4.873-5-2.006 0-4.263 1.505-4.873 5ZM8 12c3.47 0 6.64 2.857 6.998 7.93A1 1 0 0 1 14 21H2a1 1 0 0 1-.998-1.07C1.36 14.857 4.53 12 8 12Z', +}); diff --git a/src/components/icons/Growth.js b/src/components/icons/Growth.js new file mode 100644 index 0000000000..ef85aab6f5 --- /dev/null +++ b/src/components/icons/Growth.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Growth_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h1a8.003 8.003 0 0 1 7.75 6.006A7.985 7.985 0 0 1 19 6h1a1 1 0 0 1 1 1v1a8 8 0 0 1-8 8v4a1 1 0 1 1-2 0v-7a8 8 0 0 1-8-8V4Zm2 1a6 6 0 0 1 6 6 6 6 0 0 1-6-6Zm8 9a6 6 0 0 1 6-6 6 6 0 0 1-6 6Z', +}); diff --git a/src/components/icons/Haptic.js b/src/components/icons/Haptic.js new file mode 100644 index 0000000000..b1af83ad99 --- /dev/null +++ b/src/components/icons/Haptic.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Haptic_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M9 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H9ZM6 6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6Zm4 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm-5.793.293a1 1 0 0 1 0 1.414L3.155 9.76a.546.546 0 0 0-.05.713c.678.906.678 2.15 0 3.056a.546.546 0 0 0 .05.713l1.052 1.052a1 1 0 1 1-1.414 1.414L1.74 15.655a2.546 2.546 0 0 1-.237-3.327.55.55 0 0 0 0-.655 2.546 2.546 0 0 1 .237-3.328l1.052-1.052a1 1 0 0 1 1.414 0Zm15.586 0a1 1 0 0 1 1.414 0l1.052 1.052c.896.896.997 2.314.237 3.327a.55.55 0 0 0 0 .656 2.546 2.546 0 0 1-.237 3.327l-1.052 1.052a1 1 0 0 1-1.414-1.414l1.052-1.052a.546.546 0 0 0 .05-.713 2.55 2.55 0 0 1 0-3.056.546.546 0 0 0-.05-.713l-1.052-1.052a1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/Hashtag.js b/src/components/icons/Hashtag.js new file mode 100644 index 0000000000..b035af2644 --- /dev/null +++ b/src/components/icons/Hashtag.js @@ -0,0 +1,14 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var HashtagWide_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 46 46', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: 2, + path: 'M14.333 1 9 45M37 1l-5.333 44M1 11.667h44m0 22.666H1', +}); +export var Hashtag_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M9.124 3.008a1 1 0 0 1 .868 1.116L9.632 7h5.985l.39-3.124a1 1 0 0 1 1.985.248L17.632 7H20a1 1 0 1 1 0 2h-2.617l-.75 6H20a1 1 0 1 1 0 2h-3.617l-.39 3.124a1 1 0 1 1-1.985-.248l.36-2.876H8.382l-.39 3.124a1 1 0 1 1-1.985-.248L6.368 17H4a1 1 0 1 1 0-2h2.617l.75-6H4a1 1 0 1 1 0-2h3.617l.39-3.124a1 1 0 0 1 1.117-.868ZM9.383 9l-.75 6h5.984l.75-6H9.383Z', +}); +export var Hashtag_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M9.186 2.512a1.5 1.5 0 0 0-1.674 1.302L7.176 6.5H4a1.5 1.5 0 1 0 0 3h2.8l-.624 5H4a1.5 1.5 0 0 0 0 3h1.8l-.288 2.314a1.5 1.5 0 1 0 2.976.372l.336-2.686h4.977l-.29 2.314a1.5 1.5 0 1 0 2.977.372l.336-2.686H20a1.5 1.5 0 0 0 0-3h-2.8l.624-5H20a1.5 1.5 0 0 0 0-3h-1.8l.288-2.314a1.5 1.5 0 1 0-2.976-.372L15.176 6.5h-4.977l.29-2.314a1.5 1.5 0 0 0-1.303-1.674ZM9.2 14.5l.625-5h4.977l-.625 5H9.199Z', +}); diff --git a/src/components/icons/Heart2.js b/src/components/icons/Heart2.js new file mode 100644 index 0000000000..01431cc375 --- /dev/null +++ b/src/components/icons/Heart2.js @@ -0,0 +1,16 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Heart2_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 51 46', + strokeLinejoin: 'round', + strokeWidth: 2, + path: 'M49 17c0 15.333-22 26.667-24 26.667S1 32.333 1 17C1 6.333 7.667 1 14.333 1S25 5 25 5s4-4 10.667-4S49 6.333 49 17Z', +}); +export var Heart2_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M16.734 5.091c-1.238-.276-2.708.047-4.022 1.38a1 1 0 0 1-1.424 0C9.974 5.137 8.504 4.814 7.266 5.09c-1.263.282-2.379 1.206-2.92 2.556C3.33 10.18 4.252 14.84 12 19.348c7.747-4.508 8.67-9.168 7.654-11.7-.541-1.351-1.657-2.275-2.92-2.557Zm4.777 1.812c1.604 4-.494 9.69-9.022 14.47a1 1 0 0 1-.978 0C2.983 16.592.885 10.902 2.49 6.902c.779-1.942 2.414-3.334 4.342-3.764 1.697-.378 3.552.003 5.169 1.286 1.617-1.283 3.472-1.664 5.17-1.286 1.927.43 3.562 1.822 4.34 3.764Z', +}); +export var Heart2_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z', +}); +export var LikeRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M3.92 19v-4.153a1 1 0 0 1 1-1H9l.103.005a1 1 0 0 1 0 1.99L9 15.847H7.285c.854.737 1.784 1.38 2.631 1.9.702.431 1.329.769 1.78.997q.162.08.291.143a25.561 25.561 0 0 0 3.67-2.326c2.144-1.642 4.073-3.756 4.315-6.023a1 1 0 0 1 1.988.212c-.336 3.154-2.89 5.717-5.086 7.398a27.6 27.6 0 0 1-4.34 2.704l-.078.038-.021.01-.007.003-.002.001-.001.001a1 1 0 0 1-.827.01v0h-.002l-.004-.002-.013-.006q-.016-.006-.045-.02l-.162-.075a27.39 27.39 0 0 1-2.503-1.361 22 22 0 0 1-2.95-2.143V19a1 1 0 0 1-2 0ZM2 10c0-2.214.696-3.971 1.833-5.184A5.7 5.7 0 0 1 8 3a7.1 7.1 0 0 1 4 1.228A7.117 7.117 0 0 1 16 3c1.231 0 2.452.402 3.469 1.185l.031-1.702a1 1 0 0 1 2 .035l-.081 4.5a1 1 0 0 1-1 .983H16.5a1 1 0 1 1 0-2h2.02A3.68 3.68 0 0 0 16 5a5.12 5.12 0 0 0-3.11 1.053 3 3 0 0 0-.155.129l-.029.025v.002l-.003.002-.072.064a1 1 0 0 1-1.338-.068l-.028-.025a3 3 0 0 0-.155-.13A5.119 5.119 0 0 0 8 5c-.982 0-1.965.392-2.708 1.185C4.554 6.97 4 8.214 4 10q0 .507.099 1.002l.075.328.02.1a1 1 0 0 1-1.925.5l-.03-.097-.102-.446A7 7 0 0 1 2 10Z', +}); diff --git a/src/components/icons/Home.js b/src/components/icons/Home.js new file mode 100644 index 0000000000..c6382075ec --- /dev/null +++ b/src/components/icons/Home.js @@ -0,0 +1,10 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Home_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.37 1.724a1 1 0 0 1 1.26 0l8 6.5A1 1 0 0 1 21 9v11a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-5h-2v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 .37-.776l8-6.5ZM5 9.476V19h4v-5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5h4V9.476l-7-5.688-7 5.688Z', +}); +export var Home_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12.659 3.905a1 1 0 0 0-1.318 0l-6 5.25A1 1 0 0 0 5 9.907V18a1 1 0 0 0 1 1h3v-3a3 3 0 0 1 6 0v3h3a1 1 0 0 0 1-1V9.907a1 1 0 0 0-.341-.752l-6-5.25ZM10.024 2.4a3 3 0 0 1 3.952 0l6 5.25A3 3 0 0 1 21 9.907V18a3 3 0 0 1-3 3h-3a2 2 0 0 1-2-2v-3a1 1 0 0 0-2 0v3a2 2 0 0 1-2 2H6a3 3 0 0 1-3-3V9.907A3 3 0 0 1 4.024 7.65l6-5.25Z', +}); +export var Home_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M13.261 1.736a2 2 0 0 0-2.522 0l-7 5.687A2 2 0 0 0 3 8.976V19a2 2 0 0 0 2 2h3v-8a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8h3a2 2 0 0 0 2-2V8.976a2 2 0 0 0-.739-1.553l-7-5.687ZM14 21h-4v-7h4v7Z', +}); diff --git a/src/components/icons/HomeOpen.js b/src/components/icons/HomeOpen.js new file mode 100644 index 0000000000..dd3c9bd550 --- /dev/null +++ b/src/components/icons/HomeOpen.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var HomeOpen_Stoke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.37 1.724a1 1 0 0 1 1.26 0l8 6.5A1 1 0 0 1 21 9v11a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-5h-2v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 .37-.776l8-6.5ZM5 9.476V19h4v-5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5h4V9.476l-7-5.688-7 5.688Z', +}); +export var HomeOpen_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.63 1.724a1 1 0 0 0-1.26 0l-8 6.5A1 1 0 0 0 3 9v11a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-6h4v6a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V9a1 1 0 0 0-.37-.776l-8-6.5Z', +}); diff --git a/src/components/icons/Image.js b/src/components/icons/Image.js new file mode 100644 index 0000000000..b087ff220e --- /dev/null +++ b/src/components/icons/Image.js @@ -0,0 +1,10 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Image_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 46 46', + strokeLinecap: 'round', + strokeWidth: 1.5, + path: 'm1.417 28.645 7.586-5.676a5.33 5.33 0 0 1 6.867.809c3.98 4.286 8.594 8.182 14.88 8.182 5.794 0 9.633-2.147 13.333-5.847m-38 18.637h33.334a5.333 5.333 0 0 0 5.333-5.333V6.083A5.333 5.333 0 0 0 39.417.75H6.083A5.333 5.333 0 0 0 .75 6.083v33.334a5.333 5.333 0 0 0 5.333 5.333ZM36.75 14.083a5.333 5.333 0 1 1-10.667 0 5.333 5.333 0 0 1 10.667 0Z', +}); +export var Image_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z', +}); diff --git a/src/components/icons/Key.js b/src/components/icons/Key.js new file mode 100644 index 0000000000..b207a3d4a5 --- /dev/null +++ b/src/components/icons/Key.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Key_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M3 12a4 4 0 0 1 7.212-2.385c.363.488.963.885 1.696.885h8.111l1.2 1.5-1.2 1.5h-1.783l-1.789-.894a1 1 0 0 0-.894 0l-1.79.894h-1.855c-.733 0-1.333.397-1.696.885A4 4 0 0 1 3 12Zm4-6a6 6 0 1 0 4.817 9.579.3.3 0 0 1 .076-.072l.017-.007H14a1 1 0 0 0 .447-.106L16 14.618l1.553.776c.139.07.292.106.447.106h2.02a2 2 0 0 0 1.561-.75l1.2-1.5a2 2 0 0 0 0-2.5l-1.2-1.5a2 2 0 0 0-1.562-.75h-8.11l-.016-.007a.3.3 0 0 1-.077-.071A6 6 0 0 0 7 6Zm0 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', +}); diff --git a/src/components/icons/Lab.js b/src/components/icons/Lab.js new file mode 100644 index 0000000000..a9df23eb69 --- /dev/null +++ b/src/components/icons/Lab.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Lab_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M13.5 4a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 5a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM8 6a1 1 0 0 0 0 2v2.64c-.212.25-.45.515-.711.8l-.129.142c-.312.342-.649.711-.974 1.092-.731.857-1.488 1.866-1.89 2.99A4.845 4.845 0 0 0 4 17.297 4.702 4.702 0 0 0 8.702 22h6.596A4.702 4.702 0 0 0 20 17.298c0-.575-.114-1.122-.297-1.634-.401-1.124-1.157-2.133-1.89-2.99-.324-.38-.66-.75-.973-1.092l-.129-.141c-.26-.286-.5-.55-.711-.8V8a1 1 0 1 0 0-2H8Zm2 5.35V8h4v3.35l.22.275c.306.383.661.777 1.013 1.163l.13.143c.315.345.628.688.93 1.042.372.435.704.861.974 1.28l-.159.025c-.845.13-1.838.242-2.581.222-.842-.022-1.475-.217-2.227-.454l-.027-.008c-.746-.235-1.61-.507-2.746-.538-.743-.02-1.617.064-2.38.165.173-.228.36-.459.56-.692.302-.354.615-.697.93-1.042l.13-.143c.352-.386.707-.78 1.014-1.163L10 11.35Zm7.41 5.905c.21-.032.407-.064.586-.095A2.702 2.702 0 0 1 15.298 20H8.702A2.702 2.702 0 0 1 6 17.298c0-.142.013-.286.039-.434.236-.043.53-.093.853-.142.845-.13 1.837-.242 2.581-.222.842.022 1.475.217 2.227.454l.027.008c.746.235 1.61.507 2.746.538.931.024 2.07-.113 2.937-.245Z', +}); diff --git a/src/components/icons/Leaf.js b/src/components/icons/Leaf.js new file mode 100644 index 0000000000..6667e463d2 --- /dev/null +++ b/src/components/icons/Leaf.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Leaf_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h1a8.003 8.003 0 0 1 7.75 6.006A7.985 7.985 0 0 1 19 6h1a1 1 0 0 1 1 1v1a8 8 0 0 1-8 8v4a1 1 0 1 1-2 0v-7a8 8 0 0 1-8-8V4Zm2 1a6 6 0 0 1 6 6 6 6 0 0 1-6-6Zm8 9a6 6 0 0 1 6-6 6 6 0 0 1-6 6Z', +}); diff --git a/src/components/icons/ListMagnifyingGlass.js b/src/components/icons/ListMagnifyingGlass.js new file mode 100644 index 0000000000..a56ac258ba --- /dev/null +++ b/src/components/icons/ListMagnifyingGlass.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ListMagnifyingGlass_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm1 4a1 1 0 0 0 0 2h5a1 1 0 0 0 0-2H4Zm-1 7a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm0 5a1 1 0 0 1 1-1h13a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm9-8a4 4 0 1 1 7.446 2.032l.99.989a1 1 0 1 1-1.415 1.414l-.99-.989A4 4 0 0 1 12 12Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', +}); diff --git a/src/components/icons/ListPlus.js b/src/components/icons/ListPlus.js new file mode 100644 index 0000000000..92e97ac99d --- /dev/null +++ b/src/components/icons/ListPlus.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +/* + * This icon is off-menu, not part of the icon set + */ +export var ListPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3c.552 0 1 .41 1 .917V13.5h3.583c.507 0 .917.448.917 1s-.41 1-.917 1H18v3.583c0 .507-.448.917-1 .917s-1-.41-1-.917V15.5h-3.583c-.507 0-.917-.448-.917-1s.41-1 .917-1H16V9.917C16 9.41 16.448 9 17 9Z', +}); diff --git a/src/components/icons/ListSparkle.js b/src/components/icons/ListSparkle.js new file mode 100644 index 0000000000..f2b6ee8670 --- /dev/null +++ b/src/components/icons/ListSparkle.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var ListSparkle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3a1 1 0 0 1 .92.606l1.342 3.132 3.132 1.343a1 1 0 0 1 0 1.838l-3.132 1.343-1.343 3.132a1 1 0 0 1-1.838 0l-1.343-3.132-3.132-1.343a1 1 0 0 1 0-1.838l3.132-1.343 1.343-3.132A1 1 0 0 1 17 9Zm0 3.539-.58 1.355a1 1 0 0 1-.526.525L14.539 15l1.355.58a1 1 0 0 1 .525.526L17 17.461l.58-1.355a1 1 0 0 1 .526-.525L19.461 15l-1.355-.58a1 1 0 0 1-.525-.526L17 12.539Z', +}); diff --git a/src/components/icons/Live.js b/src/components/icons/Live.js new file mode 100644 index 0000000000..2a0d6fb6c6 --- /dev/null +++ b/src/components/icons/Live.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Live_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2 12A9.97 9.97 0 0 1 4.929 4.93l.076-.068a1 1 0 0 1 1.407 1.406l-.07.076A7.97 7.97 0 0 0 4 12c0 2.072.786 3.958 2.078 5.38l.265.277.07.076a1 1 0 0 1-1.408 1.407l-.076-.07-.331-.346A9.97 9.97 0 0 1 2 12Zm18 0a7.97 7.97 0 0 0-2.078-5.379l-.265-.278-.07-.076a1 1 0 0 1 1.408-1.406l.076.068.331.347A9.97 9.97 0 0 1 22 12c0 2.761-1.12 5.262-2.929 7.07a1 1 0 1 1-1.414-1.413A7.97 7.97 0 0 0 20 12ZM6 12c0-1.656.673-3.158 1.758-4.243a1 1 0 0 1 1.414 1.414A3.99 3.99 0 0 0 8 12.001c0 1.035.393 1.978 1.04 2.689l.132.138.068.077a1 1 0 0 1-1.407 1.406l-.075-.069-.2-.208A5.98 5.98 0 0 1 6 12Zm10 0a3.98 3.98 0 0 0-1.04-2.69l-.132-.139-.068-.075a1 1 0 0 1 1.407-1.407l.075.068.2.208A5.98 5.98 0 0 1 18 12a5.99 5.99 0 0 1-1.758 4.243 1 1 0 0 1-1.414-1.415A3.99 3.99 0 0 0 16 12Zm-6 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Z', +}); diff --git a/src/components/icons/Loader.js b/src/components/icons/Loader.js new file mode 100644 index 0000000000..03975c9aa2 --- /dev/null +++ b/src/components/icons/Loader.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Loader_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z', +}); diff --git a/src/components/icons/Lock.js b/src/components/icons/Lock.js new file mode 100644 index 0000000000..53a9117f5a --- /dev/null +++ b/src/components/icons/Lock.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Lock_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z', +}); +export var Lock_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M7 7a5 5 0 0 1 10 0v2a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7Zm0 4a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7Zm8-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z', +}); diff --git a/src/components/icons/Logo.js b/src/components/icons/Logo.js new file mode 100644 index 0000000000..9a5fd17145 --- /dev/null +++ b/src/components/icons/Logo.js @@ -0,0 +1,35 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import Svg, { Path } from 'react-native-svg'; +import { useCommonSVGProps } from './common'; +import { createSinglePathSVG } from './TEMPLATE'; +export var Mark = createSinglePathSVG({ + path: 'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z', +}); +export function Full(props) { + var _a, _b; + var _c = useCommonSVGProps(props), fill = _c.fill, size = _c.size, style = _c.style, gradient = _c.gradient, rest = __rest(_c, ["fill", "size", "style", "gradient"]); + var ratio = 123 / 555; + return (_jsxs(Svg, __assign({ fill: "none" }, rest, { viewBox: "0 0 555 123", width: size, height: size * ratio, style: [style], children: [gradient, _jsx(Path, { fill: (_a = props.markFill) !== null && _a !== void 0 ? _a : fill, fillRule: "evenodd", clipRule: "evenodd", d: "M101.821 7.673C112.575-.367 130-6.589 130 13.21c0 3.953-2.276 33.214-3.611 37.965-4.641 16.516-21.549 20.729-36.591 18.179 26.292 4.457 32.979 19.218 18.535 33.98-27.433 28.035-39.428-7.034-42.502-16.02-.563-1.647-.827-2.418-.831-1.763-.004-.655-.268.116-.831 1.763-3.074 8.986-15.07 44.055-42.502 16.02C7.223 88.571 13.91 73.81 40.202 69.353c-15.041 2.55-31.95-1.663-36.59-18.179C2.275 46.424 0 17.162 0 13.21 0-6.59 17.426-.368 28.18 7.673 43.084 18.817 59.114 41.413 65 53.54c5.886-12.125 21.917-34.722 36.821-45.866Z" }), _jsx(Path, { fill: (_b = props.textFill) !== null && _b !== void 0 ? _b : fill, fillRule: "evenodd", clipRule: "evenodd", d: "m454.459 63.823 24.128-25.056h32.638l4.825 15.104c3.561 11.357 6.664 22.598 9.422 33.72 2.527-9.6 5.744-20.84 9.536-33.603l4.826-15.221H555l-22.864 65.335c-2.413 6.673-5.4 11.475-9.192 14.168-3.791 2.693-9.192 3.98-16.315 3.98-2.413 0-4.481-.117-6.319-.352v-11.59h5.514c6.549 0 9.767-4.099 9.767-9.719 0-2.81-.92-6.908-2.758-12.177l-17.177-49.478-22.239 22.665L497.2 99.184h-16.545l-17.234-28.101-8.962 9.133v18.968h-14.246V15.817h14.246v48.006Zm-48.373-26.46c16.889 0 25.622 6.79 26.196 20.49h-13.673c-.344-7.377-4.595-9.954-12.523-9.954-6.894 0-10.341 2.342-10.341 7.026 0 4.215 2.987 6.089 9.881 7.377l7.469 1.17c14.361 2.694 20.566 8.08 20.566 18.384 0 12.176-9.652 18.967-26.311 18.967-17.235 0-26.311-6.908-27.116-20.842h14.132c.804 7.494 4.481 10.304 13.213 10.304 7.813 0 11.72-2.459 11.72-7.26 0-4.332-2.758-6.44-11.605-7.962l-6.778-1.17c-12.983-2.224-19.418-8.313-19.418-18.265 0-11.358 8.847-18.266 24.588-18.266ZM270.534 76.351c0 7.61 3.677 11.474 11.145 11.474 7.008 0 13.212-5.268 13.213-15.22v-33.84h14.476v60.418h-14.016v-8.782c-4.481 6.791-10.686 10.187-18.614 10.187-12.523 0-20.68-7.728-20.68-21.778V38.767h14.476v37.585Zm75.432-38.99c8.961 0 16.085 3.045 21.37 9.016s7.928 13.933 7.928 23.651v3.513h-44.35c1.034 10.42 6.664 15.572 15.396 15.572 6.663 0 11.144-2.927 13.557-8.664h13.903c-3.103 12.294-13.443 20.139-27.575 20.139-8.847 0-15.971-2.927-21.371-8.664-5.4-5.737-8.157-13.348-8.157-22.95 0-9.483 2.643-17.094 8.043-22.949 5.4-5.737 12.409-8.664 21.256-8.664ZM195.628 15.817c17.809 0 26.426 9.251 26.426 21.545 0 8.196-3.677 14.168-10.915 17.914 9.306 3.396 14.247 11.24 14.247 20.022 0 14.87-9.767 23.886-28.494 23.886h-38.26V15.817h36.996Zm51.264 83.367h-14.477V15.817h14.477v83.367ZM174.143 86.07h21.944c8.732 0 13.443-4.098 13.443-11.474 0-7.728-4.481-11.592-13.443-11.592h-21.944V86.07Zm171.708-37.233c-7.928 0-13.443 4.683-14.822 14.401h29.758c-1.264-8.781-6.549-14.401-14.936-14.401Zm-171.708 1.756h20.336c7.927 0 12.178-4.215 12.178-11.24 0-6.44-4.366-10.539-12.178-10.539h-20.336v21.779Z" })] }))); +} diff --git a/src/components/icons/Macintosh.js b/src/components/icons/Macintosh.js new file mode 100644 index 0000000000..2dd52aa638 --- /dev/null +++ b/src/components/icons/Macintosh.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Macintosh_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M4 5a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v11c0 .889-.386 1.687-1 2.236V20a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-1.764c-.614-.55-1-1.348-1-2.236V5Zm3 14v1h10v-1H7ZM7 4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H7Zm0 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V6Zm2 1v4h6V7H9Zm4 8a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/MagnifyingGlass.js b/src/components/icons/MagnifyingGlass.js new file mode 100644 index 0000000000..5e4824f9d0 --- /dev/null +++ b/src/components/icons/MagnifyingGlass.js @@ -0,0 +1,14 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var MagnifyingGlass_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11 5a6 6 0 1 0 0 12 6 6 0 0 0 0-12Zm-8 6a8 8 0 1 1 14.32 4.906l3.387 3.387a1 1 0 0 1-1.414 1.414l-3.387-3.387A8 8 0 0 1 3 11Z', +}); +export var MagnifyingGlass_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5 11a6 6 0 1 1 12 0 6 6 0 0 1-12 0Zm6-8a8 8 0 1 0 4.906 14.32l3.387 3.387a1 1 0 0 0 1.414-1.414l-3.387-3.387A8 8 0 0 0 11 3Zm4 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z', +}); +export var MagnifyingGlassX_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M20 12a8 8 0 1 0-8 8c2.204 0 4.2-.89 5.648-2.333A7.97 7.97 0 0 0 20 12Zm-5.707-3.707a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 1 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 1 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 1 1 1.414-1.414L12 10.586l2.293-2.293ZM22 12a9.96 9.96 0 0 1-2.269 6.34l2.891 2.89a1 1 0 1 1-1.414 1.415l-2.893-2.893A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10Z', +}); +export var MagnifyingGlassX_Stroke2_Corner0_Rounded_Large = createSinglePathSVG({ + viewBox: '0 0 64 64', + path: 'M55 32C55 19.298 44.703 9 32 9S9 19.298 9 32s10.298 23 23 23a22.93 22.93 0 0 0 16.235-6.708A22.93 22.93 0 0 0 55 32Zm-15.707-8.707a1 1 0 1 1 1.414 1.414L33.414 32l7.293 7.293a1 1 0 1 1-1.414 1.414L32 33.414l-7.293 7.293a1 1 0 1 1-1.414-1.414L30.586 32l-7.293-7.293a1 1 0 1 1 1.414-1.414L32 30.586l7.293-7.293ZM57 32a24.9 24.9 0 0 1-6.66 16.985l8.808 8.808a1 1 0 0 1-1.414 1.414l-8.81-8.81A24.9 24.9 0 0 1 32 57C18.193 57 7 45.807 7 32S18.193 7 32 7s25 11.193 25 25Z', +}); diff --git a/src/components/icons/Menu.js b/src/components/icons/Menu.js new file mode 100644 index 0000000000..6f282b2978 --- /dev/null +++ b/src/components/icons/Menu.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Menu_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/Message.js b/src/components/icons/Message.js new file mode 100644 index 0000000000..bd55ed77d8 --- /dev/null +++ b/src/components/icons/Message.js @@ -0,0 +1,14 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Message_Stroke1_Corner0_Rounded_Filled = createSinglePathSVG({ + viewBox: '0 0 51 51', + strokeWidth: 2, + strokeLinecap: 'square', + strokeLinejoin: 'round', + path: 'M9 1h32a8 8 0 0 1 8 8v21.333a8 8 0 0 1-8 8H27.667L14.333 49V38.333H9a8 8 0 0 1-8-8V9a8 8 0 0 1 8-8Z', +}); +export var Message_Stroke2_Corner0_Rounded_Filled = createSinglePathSVG({ + path: 'M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10a9.968 9.968 0 0 1-4.136-.893l-4.68.876a1 1 0 0 1-1.164-1.184l.931-4.537A9.965 9.965 0 0 1 2 12Zm4.25 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm4.5 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm5.75 1.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', +}); +export var Message_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 12a8 8 0 1 1 4.445 7.169 1 1 0 0 0-.629-.088l-3.537.662.7-3.415a1 1 0 0 0-.09-.66A7.961 7.961 0 0 1 4 12Zm8-10C6.477 2 2 6.477 2 12c0 1.523.341 2.968.951 4.262l-.93 4.537a1 1 0 0 0 1.163 1.184l4.68-.876A9.968 9.968 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2ZM7.5 13.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', +}); diff --git a/src/components/icons/Moon.js b/src/components/icons/Moon.js new file mode 100644 index 0000000000..64f03b67a4 --- /dev/null +++ b/src/components/icons/Moon.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Moon_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.097 2.53a1 1 0 0 1-.041 1.07 6 6 0 0 0 8.345 8.344 1 1 0 0 1 1.563.908c-.434 5.122-4.728 9.144-9.962 9.144-5.522 0-9.998-4.476-9.998-9.998 0-5.234 4.021-9.528 9.144-9.962a1 1 0 0 1 .949.494ZM9.424 4.424a7.998 7.998 0 1 0 10.152 10.152A8 8 0 0 1 9.424 4.424Z', +}); diff --git a/src/components/icons/MusicNote.js b/src/components/icons/MusicNote.js new file mode 100644 index 0000000000..aaae8cec25 --- /dev/null +++ b/src/components/icons/MusicNote.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var MusicNote_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M18.423 2.428a2 2 0 0 1 2.575 1.916V15.5c0 2.096-1.97 3.5-4 3.5-2.03 0-4-1.404-4-3.5s1.97-3.5 4-3.5c.7 0 1.392.167 2 .471V4.344l-8 2.4V18.5c0 2.096-1.97 3.5-4 3.5-2.03 0-4-1.404-4-3.5s1.97-3.5 4-3.5c.7 0 1.393.167 2 .471V6.744a2 2 0 0 1 1.425-1.916l8-2.4ZM8.998 18.5c0-.666-.717-1.5-2-1.5s-2 .834-2 1.5c0 .665.717 1.5 2 1.5s2-.835 2-1.5Zm10-3c0-.665-.717-1.5-2-1.5s-2 .835-2 1.5.717 1.5 2 1.5 2-.835 2-1.5Z', +}); diff --git a/src/components/icons/Mute.js b/src/components/icons/Mute.js new file mode 100644 index 0000000000..57a8d47b31 --- /dev/null +++ b/src/components/icons/Mute.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Mute_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M20.707 3.293a1 1 0 0 1 0 1.414l-16 16a1 1 0 0 1-1.414-1.414l2.616-2.616A1.998 1.998 0 0 1 5 15V9a2 2 0 0 1 2-2h2.697l5.748-3.832A1 1 0 0 1 17 4v1.586l2.293-2.293a1 1 0 0 1 1.414 0ZM15 7.586 7.586 15H7V9h2.697a2 2 0 0 0 1.11-.336L15 5.87v1.717Zm2 3.657-2 2v4.888l-2.933-1.955-1.442 1.442 4.82 3.214A1 1 0 0 0 17 20v-8.757Z', +}); diff --git a/src/components/icons/News2.js b/src/components/icons/News2.js new file mode 100644 index 0000000000..31dfaa55dd --- /dev/null +++ b/src/components/icons/News2.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var News2_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M1 5a1 1 0 0 1 1-1h7a3.99 3.99 0 0 1 3 1.354A3.99 3.99 0 0 1 15 4h7a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-6.723c-.52 0-1 .125-1.4.372-.421.26-.761.633-.983 1.075a1 1 0 0 1-1.788 0 2.664 2.664 0 0 0-.983-1.075c-.4-.247-.88-.372-1.4-.372H2a1 1 0 0 1-1-1V5Zm10 3a2 2 0 0 0-2-2H3v12h5.723c.776 0 1.564.173 2.277.569V8Zm2 10.569V8a2 2 0 0 1 2-2h6v12h-5.723c-.776 0-1.564.173-2.277.569Z', +}); diff --git a/src/components/icons/Newskie.js b/src/components/icons/Newskie.js new file mode 100644 index 0000000000..0e44bd28b8 --- /dev/null +++ b/src/components/icons/Newskie.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Newskie = createSinglePathSVG({ + path: 'M11.183 8.561c0 .544.348.984.892.984.545 0 .893-.44.893-.985V6.985c0-.544-.348-.985-.893-.985-.543 0-.892.44-.892.985v1.576Zm5.94 7.481c0 .539-.438.942-.976.942H8.004c-.538 0-.975-.411-.975-.95 0-2.782 2.264-5.021 5.046-5.021 2.783 0 5.047 2.247 5.047 5.03Zm-.43-4.584a.983.983 0 0 1 0-1.393l1.114-1.114a.985.985 0 0 1 1.393 1.393l-1.114 1.114a.985.985 0 0 1-1.393 0Zm2.897 3.741h1.575c.544 0 .985.349.985.892 0 .544-.44.892-.985.892h-1.67a.872.872 0 0 1-.89-.887c0-.543.44-.897.985-.897Zm-14.045.893c0-.544-.44-.892-.985-.892H2.985c-.544 0-.985.349-.985.892 0 .544.44.892.985.892H4.56c.545 0 .985-.349.985-.892Zm1.913-6.027a.985.985 0 0 1-1.393 1.393L4.95 10.344A.985.985 0 0 1 6.344 8.95l1.114 1.114Z', +}); diff --git a/src/components/icons/Newspaper.js b/src/components/icons/Newspaper.js new file mode 100644 index 0000000000..73b6731072 --- /dev/null +++ b/src/components/icons/Newspaper.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Newspaper_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M1 6.5A2.5 2.5 0 0 1 3.5 4H9a4 4 0 0 1 3 1.354A4 4 0 0 1 15 4h5.5A2.5 2.5 0 0 1 23 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-5.223c-.52 0-1 .125-1.4.372-.421.26-.761.633-.983 1.075a1 1 0 0 1-1.788 0 2.66 2.66 0 0 0-.983-1.075c-.4-.247-.88-.372-1.4-.372H3.5A2.5 2.5 0 0 1 1 17.5v-11ZM11 8a2 2 0 0 0-2-2H3.5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h5.223c.776 0 1.564.173 2.277.569V8Zm2 10.569A4.7 4.7 0 0 1 15.277 18H20.5a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5H15a2 2 0 0 0-2 2v10.569Z', +}); diff --git a/src/components/icons/PageText.js b/src/components/icons/PageText.js new file mode 100644 index 0000000000..57e6ca082b --- /dev/null +++ b/src/components/icons/PageText.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var PageText_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5 2a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H5Zm1 18V4h12v16H6Zm3-6a1 1 0 1 0 0 2h2a1 1 0 1 0 0-2H9Zm-1-3a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1Zm1-5a1 1 0 0 0 0 2h6a1 1 0 1 0 0-2H9Z', +}); diff --git a/src/components/icons/PageX.js b/src/components/icons/PageX.js new file mode 100644 index 0000000000..abc549ceab --- /dev/null +++ b/src/components/icons/PageX.js @@ -0,0 +1,5 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var PageX_Stroke2_Corner0_Rounded_Large = createSinglePathSVG({ + viewBox: '0 0 64 64', + path: 'M32.457 7c1.68 0 3.29.668 4.478 1.855L49.813 21.73a6.33 6.33 0 0 1 1.854 4.479v24.458A6.333 6.333 0 0 1 45.333 57H18.666a6.334 6.334 0 0 1-6.333-6.333V13.333A6.334 6.334 0 0 1 18.666 7h13.791ZM18.666 9a4.334 4.334 0 0 0-4.333 4.333v37.334A4.334 4.334 0 0 0 18.666 55h26.667a4.333 4.333 0 0 0 4.333-4.333V26.209c0-.418-.061-.829-.177-1.223a1 1 0 0 1-.155.014H40a6.334 6.334 0 0 1-6.325-6.008l-.008-.326V9.333q0-.08.013-.156A4.3 4.3 0 0 0 32.457 9H18.666Zm18.627 22.293a1 1 0 1 1 1.414 1.414L33.414 38l5.293 5.293a1 1 0 1 1-1.414 1.414L32 39.414l-5.293 5.293a1 1 0 1 1-1.414-1.414L30.586 38l-5.293-5.293a1 1 0 1 1 1.414-1.414L32 36.586l5.293-5.293Zm-1.626-12.627.006.224A4.333 4.333 0 0 0 40 23h8.253L35.667 10.414v8.252Z', +}); diff --git a/src/components/icons/PaintRoller.js b/src/components/icons/PaintRoller.js new file mode 100644 index 0000000000..2806657364 --- /dev/null +++ b/src/components/icons/PaintRoller.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var PaintRoller_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6 6a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3H5v3a1 1 0 0 0 1 1h7a1 1 0 0 1 1 1v2.17c1.165.412 2 1.524 2 2.83v3a1 1 0 1 1-2 0v-3a1 1 0 1 0-2 0v3a1 1 0 1 1-2 0v-3c0-1.306.835-2.418 2-2.83V14H6a3 3 0 0 1-3-3V8a2 2 0 0 1 2-2h1Zm3-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H9Z', +}); diff --git a/src/components/icons/PaperPlane.js b/src/components/icons/PaperPlane.js new file mode 100644 index 0000000000..7eed3a7ea9 --- /dev/null +++ b/src/components/icons/PaperPlane.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var PaperPlane_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.374 3.22a1 1 0 0 1 1.073-.114l16 8a1 1 0 0 1 0 1.788l-16 8a1 1 0 0 1-1.417-1.136L4.97 12 3.03 4.243a1 1 0 0 1 .344-1.023ZM6.781 13l-1.284 5.133L17.764 12 5.497 5.867 6.781 11H9a1 1 0 1 1 0 2H6.78Z', +}); diff --git a/src/components/icons/Pause.js b/src/components/icons/Pause.js new file mode 100644 index 0000000000..3d929eeb5b --- /dev/null +++ b/src/components/icons/Pause.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Pause_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4Zm2 1v14h2V5H6Zm8-1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Zm2 1v14h2V5h-2Z', +}); +export var Pause_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4ZM14 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Z', +}); +export var Pause_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Zm7 1a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Z', +}); +export var Pause_Filled_Corner2_Rounded = createSinglePathSVG({ + path: 'M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6ZM14 6a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Z', +}); diff --git a/src/components/icons/Pencil.js b/src/components/icons/Pencil.js new file mode 100644 index 0000000000..584da5f24a --- /dev/null +++ b/src/components/icons/Pencil.js @@ -0,0 +1,10 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Pencil_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M13.586 1.5a2 2 0 0 1 2.828 0L19.5 4.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 5.086 21H1a1 1 0 0 1-1-1v-4.086A2 2 0 0 1 .586 14.5l13-13ZM15 2.914l-13 13V19h3.086l13-13L15 2.914ZM11 20a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', +}); +export var PencilLine_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', +}); +export var PencilLine_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M15.379 2.707a3 3 0 0 1 4.242 0l1.672 1.672a3 3 0 0 1 0 4.242l-12.5 12.5A3 3 0 0 1 6.672 22H3a1 1 0 0 1-1-1v-3.672a3 3 0 0 1 .879-2.121l12.5-12.5Zm2.828 1.414a1 1 0 0 0-1.414 0l-12.5 12.5a1 1 0 0 0-.293.707V20h2.672a1 1 0 0 0 .707-.293l12.5-12.5.707.707-.707-.707a1 1 0 0 0 0-1.414L18.207 4.12ZM13 21a1 1 0 0 1 1-1h7a1 1 0 0 1 0 2h-7a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/PeopleRemove2.js b/src/components/icons/PeopleRemove2.js new file mode 100644 index 0000000000..ecc5473ff2 --- /dev/null +++ b/src/components/icons/PeopleRemove2.js @@ -0,0 +1,11 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var PeopleRemove2_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '-15 0 65 64', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + path: 'M20.603 46.333H3.532c-1.572 0-2.816-1.358-2.472-2.891 2.033-9.046 9.421-15.775 19.543-15.775q1.367 0 2.666.16m18.667 7.84L36.603 41m0 0-5.334 5.333M36.603 41l-5.334-5.333M36.603 41l5.333 5.333m-12-36A9.333 9.333 0 1 1 20.603 1a9.333 9.333 0 0 1 9.333 9.333Z', +}); +export var PeopleRemove2_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z', +}); diff --git a/src/components/icons/Person.js b/src/components/icons/Person.js new file mode 100644 index 0000000000..5d73b2a9c6 --- /dev/null +++ b/src/components/icons/Person.js @@ -0,0 +1,32 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Person_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z', +}); +export var Person_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 14c-2.95 0-5.163 1.733-6.08 4.21a.47.47 0 0 0 .09.493.9.9 0 0 0 .687.297h10.606a.9.9 0 0 0 .687-.297.47.47 0 0 0 .09-.493C17.163 15.732 14.95 14 12 14Zm-7.955 3.516C5.235 14.296 8.168 12 12 12s6.765 2.296 7.956 5.516c.34.92.107 1.828-.434 2.473A2.9 2.9 0 0 1 17.303 21H6.697a2.9 2.9 0 0 1-2.219-1.011 2.46 2.46 0 0 1-.433-2.473Z', +}); +export var PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z', +}); +export var PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z', +}); +export var PersonX_Stroke2_Corner0_Rounded_Large = createSinglePathSVG({ + viewBox: '0 0 64 64', + path: 'M24.952 33.435c10.897 0 19.195 6.496 22.58 15.65.784 2.117.258 4.183-1 5.683-1.242 1.48-3.194 2.414-5.33 2.415h-32.5c-2.136 0-4.089-.935-5.331-2.415-1.258-1.5-1.783-3.566-1-5.682 3.386-9.155 11.683-15.651 22.58-15.651Zm0 2.298c-9.885 0-17.355 5.849-20.425 14.15-.47 1.271-.174 2.48.604 3.407.795.947 2.096 1.594 3.57 1.594h32.5c1.476 0 2.777-.647 3.571-1.594.778-.928 1.074-2.136.605-3.406-3.07-8.302-10.54-14.15-20.425-14.15Zm33.262-14.59a1.15 1.15 0 1 1 1.625 1.626l-5.688 5.687 5.686 5.688a1.15 1.15 0 1 1-1.625 1.625l-5.686-5.688-5.687 5.688a1.15 1.15 0 1 1-1.625-1.625l5.687-5.688-5.689-5.687a1.15 1.15 0 1 1 1.625-1.625l5.689 5.687 5.688-5.687ZM24.949 5.552c6.557 0 11.874 5.316 11.874 11.874 0 6.557-5.317 11.874-11.874 11.874s-11.874-5.317-11.874-11.874S18.39 5.55 24.949 5.55Zm0 2.299a9.575 9.575 0 0 0-9.575 9.575A9.575 9.575 0 0 0 24.949 27a9.575 9.575 0 0 0 9.575-9.575A9.575 9.575 0 0 0 24.95 7.85Z', +}); +export var PersonPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z', +}); +export var PersonPlus_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z', +}); +export var PersonPlus_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 14c-2.95 0-5.163 1.733-6.08 4.21a.47.47 0 0 0 .09.493.9.9 0 0 0 .687.297H11a1 1 0 1 1 0 2H6.697a2.9 2.9 0 0 1-2.219-1.011 2.46 2.46 0 0 1-.433-2.473C5.235 14.296 8.168 12 12 12c.787 0 1.54.097 2.252.282a1 1 0 1 1-.504 1.936A7 7 0 0 0 12 14Zm6 0a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z', +}); +export var PersonGroup_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm7.301 9.7c-.836-2.6-2.88-3.503-4.575-3.111a1 1 0 0 1-.451-1.949c2.815-.651 5.81.966 6.93 4.448a2.49 2.49 0 0 1-.506 2.43A2.92 2.92 0 0 1 20 20h-2a1 1 0 1 1 0-2h2a.92.92 0 0 0 .69-.295.49.49 0 0 0 .112-.505ZM8 14c-1.865 0-3.878 1.274-4.681 4.151a.57.57 0 0 0 .132.55c.15.171.4.299.695.299h7.708a.93.93 0 0 0 .695-.299.57.57 0 0 0 .132-.55C11.878 15.274 9.865 14 8 14Zm0-2c2.87 0 5.594 1.98 6.607 5.613.53 1.9-1.09 3.387-2.753 3.387H4.146c-1.663 0-3.283-1.487-2.753-3.387C2.406 13.981 5.129 12 8 12Z', +}); +export var Person_Filled_Corner2_Rounded = createSinglePathSVG({ + path: 'M12.233 2a4.433 4.433 0 1 0 0 8.867 4.433 4.433 0 0 0 0-8.867ZM12.233 12.133c-3.888 0-6.863 2.263-8.071 5.435-.346.906-.11 1.8.44 2.436.535.619 1.36.996 2.25.996h10.762c.89 0 1.716-.377 2.25-.996.55-.636.786-1.53.441-2.436-1.208-3.173-4.184-5.435-8.072-5.435Z', +}); diff --git a/src/components/icons/Phone.js b/src/components/icons/Phone.js new file mode 100644 index 0000000000..e9fd6380df --- /dev/null +++ b/src/components/icons/Phone.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Phone_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v16a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H8Zm2 2a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z', +}); +export var PhoneHaptic_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M16 6a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6ZM2.87 7.225a1 1 0 0 1 1.337 1.482L3.155 9.759a.546.546 0 0 0-.05.714l.119.173c.52.827.52 1.88 0 2.707l-.12.174a.546.546 0 0 0 .051.714l1.052 1.052.069.076a1 1 0 0 1-1.407 1.406l-.076-.068-1.052-1.052a2.55 2.55 0 0 1-.237-3.328l.048-.075a.55.55 0 0 0 0-.504l-.048-.075a2.55 2.55 0 0 1 .237-3.328l1.052-1.052.076-.068Zm16.923.068a1 1 0 0 1 1.338-.068l.076.068 1.052 1.052.16.174c.696.837.78 2.03.209 2.958l-.133.196a.55.55 0 0 0 0 .654l.133.196a2.55 2.55 0 0 1-.21 2.958l-.16.174-1.05 1.052a1 1 0 1 1-1.415-1.414l1.052-1.052.064-.077a.55.55 0 0 0 .04-.552l-.053-.085a2.545 2.545 0 0 1 0-3.054l.052-.085a.55.55 0 0 0-.039-.552l-.064-.077-1.052-1.052-.068-.076a1 1 0 0 1 .068-1.338ZM13 6l.103.005a1 1 0 0 1 0 1.99L13 8h-2a1 1 0 1 1 0-2h2Zm5 12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12Z', +}); diff --git a/src/components/icons/Pin.js b/src/components/icons/Pin.js new file mode 100644 index 0000000000..d15c4bcd94 --- /dev/null +++ b/src/components/icons/Pin.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z', +}); +export var Pin_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z', +}); diff --git a/src/components/icons/PinLocation.js b/src/components/icons/PinLocation.js new file mode 100644 index 0000000000..3176cb5c57 --- /dev/null +++ b/src/components/icons/PinLocation.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var PinLocation_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a8 8 0 0 1 8 8c0 3.305-1.953 6.29-3.745 8.355a25.964 25.964 0 0 1-3.333 3.197c-.101.08-.181.142-.237.184l-.067.05-.018.014-.005.004-.002.002h-.001c-.003-.004-.042-.055-.592-.806l.592.807a1.001 1.001 0 0 1-1.184 0v-.001l-.003-.002-.005-.004-.018-.014-.067-.05a23.449 23.449 0 0 1-1.066-.877 25.973 25.973 0 0 1-2.504-2.503C5.953 16.29 4 13.305 4 10a8 8 0 0 1 8-8Zm0 2a6 6 0 0 0-6 6c0 2.56 1.547 5.076 3.255 7.044A23.978 23.978 0 0 0 12 19.723a23.976 23.976 0 0 0 2.745-2.679C16.453 15.076 18 12.56 18 10a6 6 0 0 0-6-6Zm-.002 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z', +}); +export var PinLocationFilled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.591 21.806h.002l.001-.002.006-.004.018-.014a10.028 10.028 0 0 0 .304-.235 25.952 25.952 0 0 0 3.333-3.196C18.048 16.29 20 13.305 20 10a8 8 0 1 0-16 0c0 3.305 1.952 6.29 3.745 8.355a25.955 25.955 0 0 0 3.333 3.196 15.733 15.733 0 0 0 .304.235l.018.014.006.004.002.002a1 1 0 0 0 1.183 0Zm-.593-9.306a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z', +}); diff --git a/src/components/icons/Pizza.js b/src/components/icons/Pizza.js new file mode 100644 index 0000000000..2817002444 --- /dev/null +++ b/src/components/icons/Pizza.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Pizza_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M18.438 3.253a2 2 0 0 1 2.309 2.31L18.243 20.17c-.186 1.083-1.244 1.868-2.387 1.588a18.525 18.525 0 0 1-8.702-4.912 18.525 18.525 0 0 1-4.912-8.702C1.962 7 2.747 5.943 3.83 5.757l14.608-2.504Zm-1.474 2.282a3 3 0 0 1-5.914 1.014l-3.368.577a13.022 13.022 0 0 0 3.376 5.816c.35.35.713.675 1.09.976a4.002 4.002 0 0 1 5.571-2.53l1.056-6.164-1.81.311Zm.388 7.991a2 2 0 0 0-3.346 1.634c.916.504 1.879.89 2.868 1.158l.478-2.792Zm-.817 4.77a15 15 0 0 1-6.891-3.94 15.02 15.02 0 0 1-3.94-6.89l-1.505.257a16.525 16.525 0 0 0 4.369 7.709 16.525 16.525 0 0 0 7.709 4.37l.258-1.505ZM13.022 6.212a1 1 0 0 0 1.97-.338l-1.97.338Z', +}); diff --git a/src/components/icons/Play.js b/src/components/icons/Play.js new file mode 100644 index 0000000000..ec623d035d --- /dev/null +++ b/src/components/icons/Play.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Play_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5.507 2.13a1 1 0 0 1 1.008.013l15 9a1 1 0 0 1 0 1.714l-15 9A1 1 0 0 1 5 21V3a1 1 0 0 1 .507-.87ZM7 4.766v14.468L19.056 12 7 4.766Z', +}); +export var Play_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z', +}); +export var Play_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z', +}); +export var Play_Filled_Corner2_Rounded = createSinglePathSVG({ + path: 'M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z', +}); diff --git a/src/components/icons/Plus.js b/src/components/icons/Plus.js new file mode 100644 index 0000000000..de45fb9b63 --- /dev/null +++ b/src/components/icons/Plus.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var PlusLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z', +}); +export var PlusSmall_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 6a1 1 0 0 1 1 1v4h4a1 1 0 1 1 0 2h-4v4a1 1 0 1 1-2 0v-4H7a1 1 0 1 1 0-2h4V7a1 1 0 0 1 1-1Z', +}); diff --git a/src/components/icons/QrCode.js b/src/components/icons/QrCode.js new file mode 100644 index 0000000000..15fbfa8520 --- /dev/null +++ b/src/components/icons/QrCode.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var QrCode_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm6 0H5v4h4V5ZM3 15a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4Zm6 0H5v4h4v-4ZM13 5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V5Zm6 0h-4v4h4V5ZM14 13a1 1 0 0 1 1 1v1h1a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1Zm3 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm0 4a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-1v1a1 1 0 1 1-2 0v-2Z', +}); diff --git a/src/components/icons/Quote.js b/src/components/icons/Quote.js new file mode 100644 index 0000000000..e964d0a398 --- /dev/null +++ b/src/components/icons/Quote.js @@ -0,0 +1,16 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var OpenQuote_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.574 4.178a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.163 8.163 0 0 1 1.569-1.15 7.029 7.029 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588a5.99 5.99 0 0 0-.316.276C4.842 7.835 4.004 9.085 4.004 11v7h5v-6h-2a1 1 0 0 1-1-1V6.766Zm12.57-2.588a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.166 8.166 0 0 1 1.569-1.15 7.038 7.038 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588c-.105.085-.21.177-.316.276-.846.793-1.684 2.043-1.684 3.958v7h5v-6h-2a1 1 0 0 1-1-1V6.766Z', +}); +export var OpenQuote_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15C3.166 6.665 2.004 8.415 2.004 11v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5ZM19.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15-1.154 1.082-2.316 2.832-2.316 5.417v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5Z', +}); +export var CloseQuote_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2.004 5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936A1 1 0 0 1 5.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234c.104-.085.21-.177.316-.276.846-.793 1.684-2.043 1.684-3.958V6h-5v6h2a1 1 0 0 1 1 1v4.234Zm6-12.234a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234V13a1 1 0 0 0-1-1h-2V6h5v7c0 1.915-.838 3.165-1.684 3.958-.106.1-.212.191-.316.276Z', +}); +export var CloseQuote_Stroke2_Corner1_Rounded = createSinglePathSVG({ + path: 'M2.003 5.999a2 2 0 0 1 2-1.999h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936a1 1 0 0 1-1.351-.935L5 14H4a2 2 0 0 1-2-2.001l.002-6Zm5 11.236L7 12.999A1 1 0 0 0 6 12H4l.003-6h5v7c0 1.915-.837 3.165-1.683 3.958-.106.1-.213.192-.317.277Zm6-11.235a2 2 0 0 1 2-2h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-1a2 2 0 0 1-2-2V6Zm7 0h-5v6h2a1 1 0 0 1 1 1v4.234c.105-.085.211-.177.317-.276.846-.793 1.684-2.043 1.684-3.958V6Z', +}); +export var CloseQuote_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7ZM14.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7Z', +}); diff --git a/src/components/icons/RaisingHand.js b/src/components/icons/RaisingHand.js new file mode 100644 index 0000000000..05b31981a7 --- /dev/null +++ b/src/components/icons/RaisingHand.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var RaisingHand4Finger_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M10.25 4a.75.75 0 0 0-.75.75V11a1 1 0 1 1-2 0V6.75a.75.75 0 0 0-1.5 0V14a6 6 0 0 0 12 0V9a2 2 0 0 0-2 2v1.5a1 1 0 0 1-.684.949l-.628.21A2.469 2.469 0 0 0 13 16a1 1 0 1 1-2 0 4.469 4.469 0 0 1 3-4.22V11c0-.703.181-1.364.5-1.938V5.75a.75.75 0 0 0-1.5 0V9a1 1 0 1 1-2 0V4.75a.75.75 0 0 0-.75-.75Zm2.316-.733A2.75 2.75 0 0 1 16.5 5.75v1.54c.463-.187.97-.29 1.5-.29h1a1 1 0 0 1 1 1v6a8 8 0 1 1-16 0V6.75a2.75 2.75 0 0 1 3.571-2.625 2.751 2.751 0 0 1 4.995-.858Z', +}); +export var RaisingHand4Finger_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12.5 4a.5.5 0 0 0-.5.5V10a1 1 0 1 1-2 0V5.5a.5.5 0 0 0-1 0V11a1 1 0 1 1-2 0V7.5a.5.5 0 0 0-1 0v6a6.5 6.5 0 1 0 13 0V9c-.513 0-.979.192-1.333.509-.41.368-.667.899-.667 1.491v.838c0 .826-.529 1.559-1.312 1.82A2.47 2.47 0 0 0 14 16a1 1 0 1 1-2 0 4.47 4.47 0 0 1 3-4.22V11c0-1.014.379-1.941 1-2.646V5.5a.5.5 0 0 0-1 0V10a1 1 0 1 1-2 0V4.5a.5.5 0 0 0-.5-.5Zm2.112-.838A2.5 2.5 0 0 1 18 5.5v1.626q.481-.124 1-.126a2 2 0 0 1 2 2v4.5a8.5 8.5 0 0 1-17 0v-6a2.5 2.5 0 0 1 3.039-2.442 2.5 2.5 0 0 1 3.349-1.896 2.498 2.498 0 0 1 4.224 0Z', +}); diff --git a/src/components/icons/Reply.js b/src/components/icons/Reply.js new file mode 100644 index 0000000000..3347231456 --- /dev/null +++ b/src/components/icons/Reply.js @@ -0,0 +1,9 @@ +import { createSinglePathSVG } from './TEMPLATE'; +// custom, off spec +export var Reply = createSinglePathSVG({ + path: 'M20.002 7a2 2 0 0 0-2-2h-12a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v1.918l3.375-2.7a1 1 0 0 1 .625-.218h5a2 2 0 0 0 2-2V7Zm2 8a4 4 0 0 1-4 4h-4.648l-4.727 3.781A1.001 1.001 0 0 1 7.002 22v-3h-1a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8Z', +}); +// custom, off spec +export var ReplyFilled = createSinglePathSVG({ + path: 'M22.002 15a4 4 0 0 1-4 4h-4.648l-4.727 3.781A1.001 1.001 0 0 1 7.002 22v-3h-1a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8Z', +}); diff --git a/src/components/icons/Repost.js b/src/components/icons/Repost.js new file mode 100644 index 0000000000..65a3fbd608 --- /dev/null +++ b/src/components/icons/Repost.js @@ -0,0 +1,13 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Repost_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M16.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H5v4a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1h13.586l-1.293-1.293a1 1 0 0 1 0-1.414ZM21 13v5a1 1 0 0 1-1 1H6.414l1.293 1.293a1 1 0 1 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 0 1 1.414 1.414L6.414 17H19v-4a1 1 0 1 1 2 0Z', +}); +export var Repost_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z', +}); +export var Repost_Stroke2_Corner3_Rounded = createSinglePathSVG({ + path: 'M16.793 2.293a1 1 0 0 1 1.414 0L20.5 4.586a2 2 0 0 1 0 2.828l-2.293 2.293a1 1 0 0 1-1.414-1.414L18.086 7H7a2 2 0 0 0-2 2v2a1 1 0 1 1-2 0V9a4 4 0 0 1 4-4h11.086l-1.293-1.293a1 1 0 0 1 0-1.414ZM20 12a1 1 0 0 1 1 1v2a4 4 0 0 1-4 4H5.914l1.293 1.293a1 1 0 1 1-1.414 1.414L3.5 19.414a2 2 0 0 1 0-2.828l2.293-2.293a1 1 0 0 1 1.414 1.414L5.914 17H17a2 2 0 0 0 2-2v-2a1 1 0 0 1 1-1Z', +}); +export var RepostRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6.043 14.293a1 1 0 1 1 1.414 1.414L5.164 18l2.293 2.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47Zm6.22 0a1 1 0 0 1 1.414 1.414L12.384 17H18a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3h-5.616l1.293 1.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47ZM3 11V8a3 3 0 0 1 3-3h5.586l-1.293-1.293-.068-.076a1 1 0 0 1 1.406-1.406l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L11.586 7H6a1 1 0 0 0-1 1v3a1 1 0 1 1-2 0Zm13.543-8.707a1 1 0 0 1 1.338-.068l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L18.836 6l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z', +}); diff --git a/src/components/icons/Rose.js b/src/components/icons/Rose.js new file mode 100644 index 0000000000..357f86db67 --- /dev/null +++ b/src/components/icons/Rose.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Rose_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M10.75 2.469a2 2 0 0 1 2.5 0l1.42 1.136 1.44-.576A1.378 1.378 0 0 1 18 4.309V8a6.002 6.002 0 0 1-5 5.917v1.69a6.12 6.12 0 0 1 4.224-1.606A1.796 1.796 0 0 1 19 15.857a6.143 6.143 0 0 1-6 6.141V22h-2v-.002a6.143 6.143 0 0 1-6-6.222A1.796 1.796 0 0 1 6.858 14 6.12 6.12 0 0 1 11 15.607v-1.69A6.002 6.002 0 0 1 6 8V4.308c0-.975.985-1.641 1.89-1.28l1.44.577 1.42-1.136ZM7.004 16.003a4.143 4.143 0 0 1 3.995 3.994 4.143 4.143 0 0 1-3.995-3.994Zm9.994 0a4.143 4.143 0 0 1-3.995 3.994 4.143 4.143 0 0 1 3.995-3.994ZM13.42 5.167 12 4.03l-1.42 1.136a2 2 0 0 1-1.992.295L8 5.227V8a4 4 0 0 0 8 0V5.227l-.588.235a2 2 0 0 1-1.992-.295Z', +}); diff --git a/src/components/icons/SettingsGear2.js b/src/components/icons/SettingsGear2.js new file mode 100644 index 0000000000..697552f412 --- /dev/null +++ b/src/components/icons/SettingsGear2.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', +}); +export var SettingsGear2_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M9.996 2.869A1.951 1.951 0 0 1 11.62 2h.76c.653 0 1.262.326 1.624.869l1.141 1.712 1.749-.404a1.951 1.951 0 0 1 1.819.522l.588.589c.476.475.673 1.162.522 1.818l-.404 1.749 1.712 1.141c.543.362.869.971.869 1.624v.76c0 .653-.326 1.262-.869 1.624l-1.712 1.141.404 1.749a1.951 1.951 0 0 1-.522 1.819l-.588.588a1.951 1.951 0 0 1-1.819.522l-1.749-.404-1.141 1.712A1.951 1.951 0 0 1 12.38 22h-.76a1.951 1.951 0 0 1-1.624-.869L8.855 19.42l-1.749.404a1.951 1.951 0 0 1-1.818-.522l-.59-.588a1.951 1.951 0 0 1-.52-1.819l.403-1.749-1.712-1.141A1.951 1.951 0 0 1 2 12.38v-.76c0-.653.326-1.262.869-1.624L4.58 8.855l-.404-1.749A1.951 1.951 0 0 1 4.7 5.288l.589-.59a1.951 1.951 0 0 1 1.818-.52l1.749.403 1.141-1.712ZM8.5 12a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z', +}); diff --git a/src/components/icons/SettingsSlider.js b/src/components/icons/SettingsSlider.js new file mode 100644 index 0000000000..64f08f9a68 --- /dev/null +++ b/src/components/icons/SettingsSlider.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var SettingsSliderVertical_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7 3a1 1 0 0 1 1 1v1.126a4 4 0 0 1 0 7.748V20a1 1 0 1 1-2 0v-7.126a4 4 0 0 1 0-7.748V4a1 1 0 0 1 1-1Zm10 0a1 1 0 0 1 1 1v9.126a4 4 0 1 1-2 0V4a1 1 0 0 1 1-1ZM7 7a2 2 0 1 0 0 4 2 2 0 1 0 0-4Zm10 8a2 2 0 1 0 0 4 2 2 0 1 0 0-4Z', +}); diff --git a/src/components/icons/Shaka.js b/src/components/icons/Shaka.js new file mode 100644 index 0000000000..c44886c6f8 --- /dev/null +++ b/src/components/icons/Shaka.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Shaka_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M2.275 4.312A1 1 0 0 1 3 4h.55a4 4 0 0 1 3.656 2.375l.616 1.388.47-.47 1.001-1a2.414 2.414 0 0 1 3.948.807 2.41 2.41 0 0 1 2.5 1.5 2.41 2.41 0 0 1 2.234 1.01l.805-.804c.95-.95 2.49-.95 3.44 0 .93.93.958 2.439.035 3.395-1.228 1.271-3.406 3.497-5.078 5.035a94.045 94.045 0 0 1-2.82 2.467c-2.481 2.1-6.156 1.748-8.264-.684L3.87 16.452a6 6 0 0 1-1.458-3.614l-.41-7.785a1 1 0 0 1 .274-.741Zm14.022 6.977-1.004 1.004-.007.006-.493.494a.414.414 0 1 1-.586-.586l1.5-1.5a.414.414 0 0 1 .59.582Zm-4.18 1.595a2.414 2.414 0 0 0 4.09 1.323l1.5-1.5.01-.01 2.477-2.477c.169-.169.443-.169.612 0a.42.42 0 0 1 .01.591c-1.228 1.272-3.37 3.46-4.993 4.953a92.183 92.183 0 0 1-2.757 2.412c-1.62 1.371-4.05 1.162-5.461-.467L5.38 15.143a4 4 0 0 1-.972-2.41l-.35-6.668a2 2 0 0 1 1.32 1.123l1.208 2.718a1 1 0 0 0 1.42.456A2.421 2.421 0 0 0 10.26 11.4c.118.294.296.57.534.807.373.373.839.599 1.323.677Zm.676-2.091 1-1a.414.414 0 1 0-.586-.586l-1 1a.414.414 0 1 0 .586.586Zm-2-2 .5-.5a.414.414 0 1 0-.586-.586l-1 1a.414.414 0 0 0 .586.586l.5-.5Z', +}); diff --git a/src/components/icons/Shapes.js b/src/components/icons/Shapes.js new file mode 100644 index 0000000000..8e40d9a7f3 --- /dev/null +++ b/src/components/icons/Shapes.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Shapes_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7 3a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2H8v2a1 1 0 1 1-2 0V8H4a1 1 0 0 1 0-2h2V4a1 1 0 0 1 1-1Zm6 4a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 1v4h4v-4H5Zm9.171-.829a1 1 0 0 1 1.415 0L17 15.585l1.414-1.414a1 1 0 1 1 1.414 1.414L18.414 17l1.414 1.414a1 1 0 0 1-1.414 1.414L17 18.414l-1.415 1.414a1 1 0 0 1-1.414-1.414l1.415-1.415-1.415-1.414a1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/Shield.js b/src/components/icons/Shield.js new file mode 100644 index 0000000000..f9cd426ccb --- /dev/null +++ b/src/components/icons/Shield.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Shield_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.675 2.054a1 1 0 0 1 .65 0l8 2.75A1 1 0 0 1 21 5.75v6.162c0 2.807-1.149 4.83-2.813 6.405-1.572 1.488-3.632 2.6-5.555 3.636l-.157.085a1 1 0 0 1-.95 0l-.157-.085c-1.923-1.037-3.983-2.148-5.556-3.636C4.15 16.742 3 14.719 3 11.912V5.75a1 1 0 0 1 .675-.946l8-2.75ZM5 6.464v5.448c0 2.166.851 3.687 2.188 4.952 1.276 1.209 2.964 2.158 4.812 3.157 1.848-1 3.536-1.948 4.813-3.157C18.148 15.6 19 14.078 19 11.912V6.464l-7-2.407-7 2.407Z', +}); +export var ShieldCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.325 2.054a1 1 0 0 0-.65 0l-8 2.75A1 1 0 0 0 3 5.75v6.162c0 2.807 1.149 4.83 2.813 6.405 1.572 1.488 3.632 2.6 5.555 3.636l.157.085a1 1 0 0 0 .95 0l.157-.085c1.923-1.037 3.983-2.148 5.556-3.636C19.85 16.742 21 14.719 21 11.912V5.75a1 1 0 0 0-.675-.946l-8-2.75ZM5 11.912V6.464l7-2.407 7 2.407v5.448c0 2.166-.851 3.687-2.188 4.952-1.276 1.209-2.964 2.158-4.812 3.157-1.848-1-3.536-1.948-4.813-3.157C5.851 15.6 5 14.078 5 11.912Zm10.207-1.205a1 1 0 0 0-1.414-1.414L11 12.086l-.793-.793a1 1 0 0 0-1.414 1.414l1.5 1.5a1 1 0 0 0 1.414 0l3.5-3.5Z', +}); diff --git a/src/components/icons/Sparkle.js b/src/components/icons/Sparkle.js new file mode 100644 index 0000000000..6a5eaec622 --- /dev/null +++ b/src/components/icons/Sparkle.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Sparkle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a1 1 0 0 1 1 1c0 3.188.669 5.256 1.882 6.536C16.084 10.805 18.01 11.5 21 11.5a1 1 0 1 1 0 2c-2.99 0-4.916.695-6.118 1.964C13.67 16.744 13 18.812 13 22a1 1 0 1 1-2 0c0-3.188-.669-5.256-1.882-6.536C7.916 14.195 5.99 13.5 3 13.5a1 1 0 1 1 0-2c2.99 0 4.916-.695 6.118-1.964C10.33 8.256 11 6.188 11 3a1 1 0 0 1 1-1Zm0 6.734a7.608 7.608 0 0 1-1.43 2.178A7.285 7.285 0 0 1 8.349 12.5c.846.397 1.589.921 2.22 1.588A7.607 7.607 0 0 1 12 16.267a7.607 7.607 0 0 1 1.43-2.179 7.284 7.284 0 0 1 2.221-1.588 7.284 7.284 0 0 1-2.22-1.588A7.608 7.608 0 0 1 12 8.734Z', +}); diff --git a/src/components/icons/Speaker.js b/src/components/icons/Speaker.js new file mode 100644 index 0000000000..1bb242e196 --- /dev/null +++ b/src/components/icons/Speaker.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var SpeakerVolumeFull_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/SquareArrowTopRight.js b/src/components/icons/SquareArrowTopRight.js new file mode 100644 index 0000000000..2d77af4b69 --- /dev/null +++ b/src/components/icons/SquareArrowTopRight.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var SquareArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-7.293 7.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM3 6a1 1 0 0 1 1-1h5a1 1 0 0 1 0 2H5v12h12v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6Z', +}); diff --git a/src/components/icons/SquareBehindSquare4.js b/src/components/icons/SquareBehindSquare4.js new file mode 100644 index 0000000000..453ee014a2 --- /dev/null +++ b/src/components/icons/SquareBehindSquare4.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var SquareBehindSquare4_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z', +}); diff --git a/src/components/icons/Star.js b/src/components/icons/Star.js new file mode 100644 index 0000000000..f112a46f2e --- /dev/null +++ b/src/components/icons/Star.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Star_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 1a1 1 0 0 1 .902.568l2.643 5.517 6.085.799a1 1 0 0 1 .557 1.718l-4.45 4.207 1.118 6.008a1 1 0 0 1-1.46 1.063L12 17.962 6.604 20.88a1 1 0 0 1-1.459-1.063l1.117-6.008-4.45-4.207a1 1 0 0 1 .558-1.718l6.085-.8 2.643-5.516A1 1 0 0 1 12 1Zm0 3.315-1.975 4.123a1 1 0 0 1-.772.56l-4.538.595 3.317 3.137a1 1 0 0 1 .296.91l-.834 4.485 4.03-2.179a1 1 0 0 1 .952 0l4.03 2.179-.834-4.485a1 1 0 0 1 .296-.91l3.317-3.137-4.538-.596a1 1 0 0 1-.772-.56L12 4.316Z', +}); +export var Star_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.902 1.568a1 1 0 0 0-1.804 0L8.455 7.085l-6.085.799a1 1 0 0 0-.557 1.718l4.45 4.207-1.117 6.008a1 1 0 0 0 1.458 1.063L12 17.962l5.396 2.918a1 1 0 0 0 1.459-1.063l-1.117-6.008 4.45-4.207a1 1 0 0 0-.558-1.718l-6.085-.8-2.643-5.516Z', +}); diff --git a/src/components/icons/StarterPack.js b/src/components/icons/StarterPack.js new file mode 100644 index 0000000000..ada59618cf --- /dev/null +++ b/src/components/icons/StarterPack.js @@ -0,0 +1,7 @@ +import { createMultiPathSVG } from './TEMPLATE'; +export var StarterPack = createMultiPathSVG({ + paths: [ + 'M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672c.734-.197 1.17-.951.973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z', + 'M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z', + ], +}); diff --git a/src/components/icons/StreamingLive.js b/src/components/icons/StreamingLive.js new file mode 100644 index 0000000000..09f866d6b3 --- /dev/null +++ b/src/components/icons/StreamingLive.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var StreamingLive_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4Zm8 12.5c1.253 0 2.197.609 2.674 1.5H9.326c.477-.891 1.42-1.5 2.674-1.5Zm0-2c2.404 0 4.235 1.475 4.822 3.5H20V6H4v12h3.178c.587-2.025 2.418-3.5 4.822-3.5Zm-1.25-3.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0ZM12 7.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Zm5.75 2a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z', +}); diff --git a/src/components/icons/TEMPLATE.js b/src/components/icons/TEMPLATE.js new file mode 100644 index 0000000000..960d19b080 --- /dev/null +++ b/src/components/icons/TEMPLATE.js @@ -0,0 +1,54 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; +import { useCommonSVGProps } from '#/components/icons/common'; +export var IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef(function LogoImpl(props, ref) { + var _a = useCommonSVGProps(props), fill = _a.fill, size = _a.size, style = _a.style, rest = __rest(_a, ["fill", "size", "style"]); + return (_jsx(Svg, __assign({ fill: "none" }, rest, { + // @ts-ignore it's fiiiiine + ref: ref, viewBox: "0 0 24 24", width: size, height: size, style: [style], children: _jsx(Path, { fill: fill, fillRule: "evenodd", clipRule: "evenodd", d: "M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z" }) }))); +}); +export function createSinglePathSVG(_a) { + var path = _a.path, viewBox = _a.viewBox, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 0 : _b, _c = _a.strokeLinecap, strokeLinecap = _c === void 0 ? 'butt' : _c, _d = _a.strokeLinejoin, strokeLinejoin = _d === void 0 ? 'miter' : _d; + return React.forwardRef(function LogoImpl(props, ref) { + var _a = useCommonSVGProps(props), fill = _a.fill, size = _a.size, style = _a.style, gradient = _a.gradient, rest = __rest(_a, ["fill", "size", "style", "gradient"]); + var hasStroke = strokeWidth > 0; + return (_jsxs(Svg, __assign({ fill: "none" }, rest, { ref: ref, viewBox: viewBox || '0 0 24 24', width: size, height: size, style: [style], children: [gradient, _jsx(Path, { fill: hasStroke ? 'none' : fill, stroke: hasStroke ? fill : 'none', strokeWidth: strokeWidth, strokeLinecap: strokeLinecap, strokeLinejoin: strokeLinejoin, fillRule: "evenodd", clipRule: "evenodd", d: path })] }))); + }); +} +export function createSinglePathSVG2(_a) { + var path = _a.path; + return React.forwardRef(function LogoImpl(props, ref) { + var _a = useCommonSVGProps(props), fill = _a.fill, size = _a.size, style = _a.style, gradient = _a.gradient, rest = __rest(_a, ["fill", "size", "style", "gradient"]); + return (_jsxs(Svg, __assign({ fill: "none" }, rest, { ref: ref, viewBox: "0 0 24 24", width: size, height: size, style: style, children: [gradient, _jsx(Path, { fill: fill, fillRule: "evenodd", clipRule: "evenodd", d: path })] }))); + }); +} +export function createMultiPathSVG(_a) { + var paths = _a.paths; + return React.forwardRef(function LogoImpl(props, ref) { + var _a = useCommonSVGProps(props), fill = _a.fill, size = _a.size, style = _a.style, gradient = _a.gradient, rest = __rest(_a, ["fill", "size", "style", "gradient"]); + return (_jsxs(Svg, __assign({ fill: "none" }, rest, { ref: ref, viewBox: "0 0 24 24", width: size, height: size, style: [style], children: [gradient, paths.map(function (path, i) { return (_jsx(Path, { fill: fill, fillRule: "evenodd", clipRule: "evenodd", d: path }, i)); })] }))); + }); +} diff --git a/src/components/icons/TextSize.js b/src/components/icons/TextSize.js new file mode 100644 index 0000000000..834accf7c3 --- /dev/null +++ b/src/components/icons/TextSize.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var TextSize_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z', +}); diff --git a/src/components/icons/Ticket.js b/src/components/icons/Ticket.js new file mode 100644 index 0000000000..f09b367706 --- /dev/null +++ b/src/components/icons/Ticket.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 0-1 1v4.17a1 1 0 0 0 .667.944 2.001 2.001 0 0 1 0 3.772A1 1 0 0 0 2 14.83V19a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-4.17a1 1 0 0 0-.667-.944 2.001 2.001 0 0 1 0-3.772A1 1 0 0 0 22 9.17V5a1 1 0 0 0-1-1H3Zm1 4.535V6h16v2.535A4 4 0 0 0 18 12c0 1.482.805 2.773 2 3.465V18H4v-2.535A4 4 0 0 0 6 12a4 4 0 0 0-2-3.465ZM15 15a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-1-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm1-5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z', +}); diff --git a/src/components/icons/Times.js b/src/components/icons/Times.js new file mode 100644 index 0000000000..7bacfd652c --- /dev/null +++ b/src/components/icons/Times.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var TimesLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z', +}); diff --git a/src/components/icons/TitleCase.js b/src/components/icons/TitleCase.js new file mode 100644 index 0000000000..3650ae35b6 --- /dev/null +++ b/src/components/icons/TitleCase.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var TitleCase_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z', +}); diff --git a/src/components/icons/Trash.js b/src/components/icons/Trash.js new file mode 100644 index 0000000000..c0fe7d6519 --- /dev/null +++ b/src/components/icons/Trash.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Trash_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z', +}); +export var Trash_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M7.416 5H3a1 1 0 0 0 0 2h1.064l.814 12.2A3 3 0 0 0 7.87 22h8.258a3 3 0 0 0 2.993-2.8L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1s-1.687.386-2.236 1Zm6.087 2H6.07l.804 12.067a1 1 0 0 0 .998.933h8.258a1 1 0 0 0 .998-.933L17.93 7h-2.08ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z', +}); diff --git a/src/components/icons/Tree.js b/src/components/icons/Tree.js new file mode 100644 index 0000000000..46bc6da36b --- /dev/null +++ b/src/components/icons/Tree.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Tree_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M6 2a2.998 2.998 0 0 1 1 5.825V8a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a2.998 2.998 0 0 1-2.826-2H9a3.98 3.98 0 0 1-2-.537V16a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a2.998 2.998 0 0 1-2.826-2H9a4 4 0 0 1-4-4V7.825A2.998 2.998 0 0 1 6 2Zm7 16a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm0-8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5ZM6 4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z', +}); diff --git a/src/components/icons/Trending.js b/src/components/icons/Trending.js new file mode 100644 index 0000000000..96ea11d838 --- /dev/null +++ b/src/components/icons/Trending.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Trending2_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'm18.192 5.004 1.864 5.31a1 1 0 0 0 1.887-.662L20.08 4.34c-.665-1.893-3.378-1.741-3.834.207l-3.381 14.449-2.985-9.605C9.3 7.531 6.684 7.506 6.07 9.355l-1.18 3.56-.969-2.312a1 1 0 0 0-1.844.772l.97 2.315c.715 1.71 3.159 1.613 3.741-.144l1.18-3.56 2.985 9.605c.607 1.952 3.392 1.848 3.857-.138l3.381-14.449Z', +}); +export var Trending3_Stroke2_Corner1_Rounded = createSinglePathSVG({ + path: 'M15 7a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V9.414L14.414 15a2 2 0 0 1-2.828 0L9 12.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L7.586 11a2 2 0 0 1 2.828 0L13 13.586 18.586 8H16a1 1 0 0 1-1-1Z', +}); diff --git a/src/components/icons/UFO.js b/src/components/icons/UFO.js new file mode 100644 index 0000000000..82686ed63b --- /dev/null +++ b/src/components/icons/UFO.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var UFO_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.03 6.443c-.889.17-1.707.385-2.431.638-.966.338-1.82.764-2.45 1.286C1.523 8.884 1 9.6 1 10.5c0 1.321 1.098 2.24 2.203 2.821.854.45 1.928.817 3.142 1.093l-3.19 5.052a1 1 0 1 0 1.69 1.068l5.767-9.13a8.502 8.502 0 0 1 2.776 0l5.766 9.13a1 1 0 0 0 1.692-1.068l-3.191-5.052c1.214-.276 2.288-.644 3.142-1.093 1.105-.58 2.203-1.5 2.203-2.821 0-.9-.524-1.616-1.148-2.133-.631-.522-1.485-.948-2.45-1.286a17.147 17.147 0 0 0-2.433-.638 5 5 0 0 0-9.938 0Zm2.095-.301A28.736 28.736 0 0 1 12 6c.992 0 1.957.049 2.875.142a3.001 3.001 0 0 0-5.75 0Zm7.389 6.466c1.403-.262 2.55-.635 3.352-1.057C20.87 11.023 21 10.611 21 10.5c0-.066-.036-.271-.423-.592-.381-.315-.993-.644-1.836-.939C17.063 8.382 14.68 8 12 8c-2.68 0-5.063.382-6.74.969-.844.295-1.456.624-1.837.94-.387.32-.423.525-.423.591 0 .11.13.523 1.134 1.051.802.422 1.95.795 3.352 1.057l1.441-2.282a1.952 1.952 0 0 1 1.328-.89 10.51 10.51 0 0 1 3.49 0c.57.094 1.042.437 1.328.89l1.44 2.282Z', +}); diff --git a/src/components/icons/UserCircle.js b/src/components/icons/UserCircle.js new file mode 100644 index 0000000000..abf0f91f4d --- /dev/null +++ b/src/components/icons/UserCircle.js @@ -0,0 +1,7 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var UserCircle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 0 0-5.935 13.365C7.56 15.895 9.612 15 12 15c2.388 0 4.44.894 5.935 2.365A8 8 0 0 0 12 4Zm4.412 14.675C15.298 17.636 13.792 17 12 17c-1.791 0-3.298.636-4.412 1.675A7.96 7.96 0 0 0 12 20a7.96 7.96 0 0 0 4.412-1.325ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10a9.98 9.98 0 0 1-3.462 7.567A9.965 9.965 0 0 1 12 22a9.965 9.965 0 0 1-6.538-2.433A9.98 9.98 0 0 1 2 12Zm10-4a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z', +}); +export var UserCircle_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3-12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm-3 10a7.976 7.976 0 0 1-5.714-2.4C7.618 16.004 9.605 15 12 15c2.396 0 4.383 1.005 5.714 2.6A7.976 7.976 0 0 1 12 20Z', +}); diff --git a/src/components/icons/Verified.js b/src/components/icons/Verified.js new file mode 100644 index 0000000000..1c4193d323 --- /dev/null +++ b/src/components/icons/Verified.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Verified_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M10.467 2.698a2.03 2.03 0 0 1 3.066 0l1.358 1.564a.03.03 0 0 0 .027.01l2.046-.325a2.03 2.03 0 0 1 2.348 1.97l.036 2.071q0 .016.014.026l1.776 1.066a2.03 2.03 0 0 1 .532 3.018l-1.304 1.61a.03.03 0 0 0-.005.029l.675 1.957a2.03 2.03 0 0 1-1.533 2.655l-2.033.395a.03.03 0 0 0-.022.019l-.742 1.933a2.03 2.03 0 0 1-2.88 1.049l-1.811-1.005a.03.03 0 0 0-.03 0l-1.81 1.005a2.03 2.03 0 0 1-2.881-1.049l-.742-1.933a.03.03 0 0 0-.022-.02l-2.033-.394a2.03 2.03 0 0 1-1.533-2.655l.675-1.957a.03.03 0 0 0-.005-.03L2.33 12.099a2.03 2.03 0 0 1 .532-3.018l1.776-1.066a.03.03 0 0 0 .014-.026l.036-2.07a2.03 2.03 0 0 1 2.348-1.97l2.045.324a.03.03 0 0 0 .028-.01l1.358-1.564Zm1.52 1.304-.01.008-1.358 1.563a2.03 2.03 0 0 1-1.85.674l-2.046-.324H6.71l-.011.006-.009.01-.002.013-.036 2.07a2.03 2.03 0 0 1-.985 1.706l-1.775 1.066-.01.009-.004.012v.014q0 .003.006.01l1.304 1.61c.44.544.57 1.277.342 1.94l-.675 1.957-.002.012q0 .006.004.013l.01.01.01.005 2.034.394a2.03 2.03 0 0 1 1.509 1.266l.741 1.934q.004.01.007.01l.011.008.013.002q.003.001.012-.004l1.811-1.005a2.03 2.03 0 0 1 1.97 0l1.81 1.005.013.004.013-.003.011-.006.007-.011.742-1.934a2.03 2.03 0 0 1 1.508-1.266l2.033-.394.012-.005.009-.01.004-.012-.002-.013-.675-1.958a2.03 2.03 0 0 1 .342-1.94l1.304-1.609.006-.01v-.014l-.005-.012-.009-.009-1.775-1.066a2.03 2.03 0 0 1-.985-1.705l-.036-2.071-.002-.012-.009-.01-.011-.007h-.013l-2.045.324a2.03 2.03 0 0 1-1.85-.674l-1.36-1.563-.009-.008L12 4l-.013.002Zm3.22 5.79a1 1 0 0 1 0 1.415l-3.146 3.146a1.5 1.5 0 0 1-2.122 0l-1.146-1.146a1 1 0 1 1 1.414-1.414l.793.793 2.793-2.793a1 1 0 0 1 1.414 0Z', +}); diff --git a/src/components/icons/VerifiedCheck.js b/src/components/icons/VerifiedCheck.js new file mode 100644 index 0000000000..513665f633 --- /dev/null +++ b/src/components/icons/VerifiedCheck.js @@ -0,0 +1,30 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import Svg, { Circle, Path } from 'react-native-svg'; +import { useCommonSVGProps } from '#/components/icons/common'; +export var VerifiedCheck = React.forwardRef(function LogoImpl(props, ref) { + var _a = useCommonSVGProps(props), fill = _a.fill, size = _a.size, style = _a.style, rest = __rest(_a, ["fill", "size", "style"]); + return (_jsxs(Svg, __assign({ fill: "none" }, rest, { ref: ref, viewBox: "0 0 24 24", width: size, height: size, style: [style], children: [_jsx(Circle, { cx: "12", cy: "12", r: "11.5", fill: fill }), _jsx(Path, { fill: "#fff", fillRule: "evenodd", clipRule: "evenodd", d: "M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z" })] }))); +}); diff --git a/src/components/icons/VerifierCheck.js b/src/components/icons/VerifierCheck.js new file mode 100644 index 0000000000..1bc04de708 --- /dev/null +++ b/src/components/icons/VerifierCheck.js @@ -0,0 +1,30 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; +import { useCommonSVGProps } from '#/components/icons/common'; +export var VerifierCheck = React.forwardRef(function LogoImpl(props, ref) { + var _a = useCommonSVGProps(props), fill = _a.fill, size = _a.size, style = _a.style, rest = __rest(_a, ["fill", "size", "style"]); + return (_jsxs(Svg, __assign({ fill: "none" }, rest, { ref: ref, viewBox: "0 0 24 24", width: size, height: size, style: [style], children: [_jsx(Path, { fill: fill, fillRule: "evenodd", clipRule: "evenodd", d: "M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z" }), _jsx(Path, { fill: "#fff", fillRule: "evenodd", clipRule: "evenodd", d: "M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z" })] }))); +}); diff --git a/src/components/icons/VideoClip.js b/src/components/icons/VideoClip.js new file mode 100644 index 0000000000..5a0584b7d2 --- /dev/null +++ b/src/components/icons/VideoClip.js @@ -0,0 +1,11 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var VideoClip_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 46 46', + strokeLinecap: 'square', + strokeLinejoin: 'round', + strokeWidth: 2, + path: 'M1 23h10.667M1 23V12m0 11v11m10.667-11h22.666m-22.666 0v11m0-11V12m22.666 11H45m-10.667 0v12.222m0-12.222V12M45 23V12m0 11v12.222M34.333 45h5.334A5.333 5.333 0 0 0 45 39.667v-4.445M34.333 45v-9.778m0 9.778H11.667M34.333 1h5.334A5.333 5.333 0 0 1 45 6.333V12M34.333 1v11m0-11H11.667m22.666 11H45M34.333 35.222H45M11.667 45H6.333A5.333 5.333 0 0 1 1 39.667V34m10.667 11V34m0-33H6.333A5.333 5.333 0 0 0 1 6.333V12M11.667 1v11M1 12h10.667M1 34h10.667', +}); +export var VideoClip_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 011-1h16a1 1 0 011 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2h2V13Zm0 4h-2V19h2ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z', +}); diff --git a/src/components/icons/Warning.js b/src/components/icons/Warning.js new file mode 100644 index 0000000000..02f7c016f9 --- /dev/null +++ b/src/components/icons/Warning.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Warning_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z', +}); diff --git a/src/components/icons/Window.js b/src/components/icons/Window.js new file mode 100644 index 0000000000..2c84c00362 --- /dev/null +++ b/src/components/icons/Window.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Window_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3H6ZM5 18v-6h14v6a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1Zm0-8h14V6a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v4Zm6-3.5a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2h-6ZM7.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', +}); diff --git a/src/components/icons/Wrench.js b/src/components/icons/Wrench.js new file mode 100644 index 0000000000..04596b10da --- /dev/null +++ b/src/components/icons/Wrench.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Wrench_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M14.5 4a5.5 5.5 0 0 0-5.078 7.616 1 1 0 0 1-.216 1.092L4.37 17.543a1 1 0 0 0 0 1.414l.672.672a1 1 0 0 0 1.414 0l4.835-4.835a1 1 0 0 1 1.092-.216A5.5 5.5 0 0 0 20 9.414l-1.293 1.293a3.828 3.828 0 1 1-5.414-5.414L14.585 4 14.5 4ZM7 9.5a7.5 7.5 0 0 1 9.969-7.084 1 1 0 0 1 .378 1.651l-2.64 2.64a1.829 1.829 0 0 0 2.586 2.586l2.64-2.64a1 1 0 0 1 1.65.378 7.5 7.5 0 0 1-9.328 9.627l-4.384 4.385a3 3 0 0 1-4.242 0l-.672-.672a3 3 0 0 1 0-4.242l4.385-4.385A7.5 7.5 0 0 1 7 9.5Z', +}); diff --git a/src/components/icons/Zap.js b/src/components/icons/Zap.js new file mode 100644 index 0000000000..b30da07faa --- /dev/null +++ b/src/components/icons/Zap.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var Zap_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'm9.368 4-4 8h2.944a1.5 1.5 0 0 1 1.427 1.963l-1.65 5.087L19.374 9h-3.49a1.5 1.5 0 0 1-1.287-2.272L16.234 4H9.368Zm-1.65-1.17A1.5 1.5 0 0 1 9.058 2h8.058a1.5 1.5 0 0 1 1.286 2.272L16.766 7h3.92c1.38 0 2.028 1.703.998 2.62L8.042 21.77c-1.142 1.018-2.896-.127-2.424-1.583L7.624 14H4.56a1.5 1.5 0 0 1-1.342-2.17l4.5-9Z', +}); diff --git a/src/components/icons/common.js b/src/components/icons/common.js new file mode 100644 index 0000000000..3dfa205304 --- /dev/null +++ b/src/components/icons/common.js @@ -0,0 +1,55 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { StyleSheet } from 'react-native'; +import { Defs, LinearGradient, Stop } from 'react-native-svg'; +import { nanoid } from 'nanoid/non-secure'; +import { tokens, useTheme } from '#/alf'; +export var sizes = { + '2xs': 8, + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, + '2xl': 32, + '3xl': 48, +}; +export function useCommonSVGProps(props) { + var t = useTheme(); + var fill = props.fill, size = props.size, gradient = props.gradient, rest = __rest(props, ["fill", "size", "gradient"]); + var style = StyleSheet.flatten(rest.style); + var _size = Number(size ? sizes[size] : rest.width || sizes.md); + var _fill = fill || (style === null || style === void 0 ? void 0 : style.color) || t.palette.primary_500; + var gradientDef = null; + if (gradient && tokens.gradients[gradient]) { + var id = gradient + '_' + nanoid(); + var config = tokens.gradients[gradient]; + _fill = "url(#".concat(id, ")"); + gradientDef = (_jsx(Defs, { children: _jsx(LinearGradient, { id: id, x1: "0", y1: "0", x2: "100%", y2: "0", gradientTransform: "rotate(45)", children: config.values.map(function (_a) { + var stop = _a[0], fill = _a[1]; + return (_jsx(Stop, { offset: stop, stopColor: fill }, stop)); + }) }) })); + } + return __assign({ fill: _fill, size: _size, style: style, gradient: gradientDef }, rest); +} diff --git a/src/components/images/AutoSizedImage.js b/src/components/images/AutoSizedImage.js new file mode 100644 index 0000000000..bb61eed908 --- /dev/null +++ b/src/components/images/AutoSizedImage.js @@ -0,0 +1,137 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo, useRef } from 'react'; +import { Pressable, View } from 'react-native'; +import Animated, { useAnimatedRef, } from 'react-native-reanimated'; +import { Image } from 'expo-image'; +import { utils } from '@bsky.app/alf'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useLargeAltBadgeEnabled } from '#/state/preferences/large-alt-badge'; +import { atoms as a, useTheme } from '#/alf'; +import { ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen } from '#/components/icons/ArrowsDiagonal'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +export function ConstrainedImage(_a) { + var aspectRatio = _a.aspectRatio, fullBleed = _a.fullBleed, children = _a.children, minMobileAspectRatio = _a.minMobileAspectRatio; + var t = useTheme(); + /** + * Computed as a % value to apply as `paddingTop`, this basically controls + * the height of the image. + */ + var outerAspectRatio = useMemo(function () { + var ratio = IS_NATIVE + ? Math.min(1 / aspectRatio, minMobileAspectRatio !== null && minMobileAspectRatio !== void 0 ? minMobileAspectRatio : 16 / 9) // 9:16 bounding box + : Math.min(1 / aspectRatio, 1); // 1:1 bounding box + return "".concat(ratio * 100, "%"); + }, [aspectRatio, minMobileAspectRatio]); + return (_jsx(View, { style: [a.w_full], children: _jsx(View, { style: [a.overflow_hidden, { paddingTop: outerAspectRatio }], children: _jsx(View, { style: [a.absolute, a.inset_0, a.flex_row], children: _jsx(View, { style: [ + a.h_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + fullBleed ? a.w_full : { aspectRatio: aspectRatio }, + ], children: children }) }) }) })); +} +export function AutoSizedImage(_a) { + var image = _a.image, _b = _a.crop, crop = _b === void 0 ? 'constrained' : _b, hideBadge = _a.hideBadge, onPress = _a.onPress, onLongPress = _a.onLongPress, onPressIn = _a.onPressIn; + var t = useTheme(); + var _ = useLingui()._; + var largeAlt = useLargeAltBadgeEnabled(); + var containerRef = useAnimatedRef(); + var fetchedDimsRef = useRef(null); + var aspectRatio; + var dims = image.aspectRatio; + if (dims) { + aspectRatio = dims.width / dims.height; + if (Number.isNaN(aspectRatio)) { + aspectRatio = undefined; + } + } + var constrained; + var max; + var rawIsCropped; + if (aspectRatio !== undefined) { + var ratio = 1 / 2; // max of 1:2 ratio in feeds + constrained = Math.max(aspectRatio, ratio); + max = Math.max(aspectRatio, 0.25); // max of 1:4 in thread + rawIsCropped = aspectRatio < constrained; + } + var cropDisabled = crop === 'none'; + var isCropped = rawIsCropped && !cropDisabled; + var isContain = aspectRatio === undefined; + var hasAlt = !!image.alt; + var contents = (_jsxs(Animated.View, { ref: containerRef, collapsable: false, style: { flex: 1 }, children: [_jsx(Image, { contentFit: isContain ? 'contain' : 'cover', style: [a.w_full, a.h_full], source: image.thumb, accessible: true, accessibilityIgnoresInvertColors: true, accessibilityLabel: image.alt, accessibilityHint: "", onLoad: function (e) { + if (!isContain) { + fetchedDimsRef.current = { + width: e.source.width, + height: e.source.height, + }; + } + }, loading: "lazy" }), _jsx(MediaInsetBorder, {}), (hasAlt || isCropped) && !hideBadge ? (_jsxs(View, { accessible: false, style: [ + a.absolute, + a.flex_row, + { + bottom: a.p_xs.padding, + right: a.p_xs.padding, + gap: 3, + }, + largeAlt && [ + { + gap: 4, + }, + ], + ], children: [isCropped && (_jsx(View, { style: [ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + padding: 3, + opacity: 0.8, + }, + largeAlt && [ + { + padding: 5, + }, + ], + ], children: _jsx(Fullscreen, { fill: t.atoms.text_contrast_high.color, width: largeAlt ? 18 : 12 }) })), hasAlt && (_jsx(View, { style: [ + a.justify_center, + a.rounded_xs, + t.atoms.bg_contrast_25, + { + padding: 3, + opacity: 0.8, + }, + largeAlt && [ + { + padding: 5, + }, + ], + ], children: _jsx(Text, { style: [a.font_bold, largeAlt ? a.text_xs : { fontSize: 8 }], children: "ALT" }) }))] })) : null] })); + if (cropDisabled) { + return (_jsx(Pressable, { onPress: function () { return onPress === null || onPress === void 0 ? void 0 : onPress(containerRef, fetchedDimsRef.current); }, onLongPress: onLongPress, onPressIn: onPressIn, + // alt here is what screen readers actually use + accessibilityLabel: image.alt, accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Views full image"], ["Views full image"])))), accessibilityRole: "button", android_ripple: { + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, + }, style: [ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + { aspectRatio: max !== null && max !== void 0 ? max : 1 }, + ], children: contents })); + } + else { + return (_jsx(ConstrainedImage, { fullBleed: crop === 'square', aspectRatio: constrained !== null && constrained !== void 0 ? constrained : 1, children: _jsx(Pressable, { onPress: function () { return onPress === null || onPress === void 0 ? void 0 : onPress(containerRef, fetchedDimsRef.current); }, onLongPress: onLongPress, onPressIn: onPressIn, + // alt here is what screen readers actually use + accessibilityLabel: image.alt, accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Views full image"], ["Views full image"])))), accessibilityRole: "button", android_ripple: { + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, + }, style: [a.h_full], children: contents }) })); + } +} +var templateObject_1, templateObject_2; diff --git a/src/components/images/Gallery.js b/src/components/images/Gallery.js new file mode 100644 index 0000000000..2cb87a5be0 --- /dev/null +++ b/src/components/images/Gallery.js @@ -0,0 +1,60 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Pressable, View } from 'react-native'; +import { Image } from 'expo-image'; +import { utils } from '@bsky.app/alf'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useLargeAltBadgeEnabled } from '#/state/preferences/large-alt-badge'; +import { atoms as a, useTheme } from '#/alf'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { PostEmbedViewContext } from '#/components/Post/Embed/types'; +import { Text } from '#/components/Typography'; +export function GalleryItem(_a) { + var images = _a.images, index = _a.index, imageStyle = _a.imageStyle, onPress = _a.onPress, onPressIn = _a.onPressIn, onLongPress = _a.onLongPress, viewContext = _a.viewContext, insetBorderStyle = _a.insetBorderStyle, containerRefs = _a.containerRefs, thumbDimsRef = _a.thumbDimsRef; + var t = useTheme(); + var _ = useLingui()._; + var largeAltBadge = useLargeAltBadgeEnabled(); + var image = images[index]; + var hasAlt = !!image.alt; + var hideBadges = viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia; + return (_jsxs(View, { style: a.flex_1, ref: containerRefs[index], collapsable: false, children: [_jsxs(Pressable, { onPress: onPress + ? function () { return onPress(index, containerRefs, thumbDimsRef.current.slice()); } + : undefined, onPressIn: onPressIn ? function () { return onPressIn(index); } : undefined, onLongPress: onLongPress ? function () { return onLongPress(index); } : undefined, android_ripple: { + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, + }, style: [ + a.flex_1, + a.overflow_hidden, + t.atoms.bg_contrast_25, + imageStyle, + ], accessibilityRole: "button", accessibilityLabel: image.alt || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Image"], ["Image"])))), accessibilityHint: "", children: [_jsx(Image, { source: { uri: image.thumb }, style: [a.flex_1], accessible: true, accessibilityLabel: image.alt, accessibilityHint: "", accessibilityIgnoresInvertColors: true, onLoad: function (e) { + thumbDimsRef.current[index] = { + width: e.source.width, + height: e.source.height, + }; + }, loading: "lazy" }), _jsx(MediaInsetBorder, { style: insetBorderStyle })] }), hasAlt && !hideBadges ? (_jsx(View, { accessible: false, style: [ + a.absolute, + a.flex_row, + a.align_center, + a.rounded_xs, + t.atoms.bg_contrast_25, + { + gap: 3, + padding: 3, + bottom: a.p_xs.padding, + right: a.p_xs.padding, + opacity: 0.8, + }, + largeAltBadge && [ + { + gap: 4, + padding: 5, + }, + ], + ], children: _jsx(Text, { style: [a.font_bold, largeAltBadge ? a.text_xs : { fontSize: 8 }], children: "ALT" }) })) : null] })); +} +var templateObject_1; diff --git a/src/components/images/ImageLayoutGrid.js b/src/components/images/ImageLayoutGrid.js new file mode 100644 index 0000000000..edea41a762 --- /dev/null +++ b/src/components/images/ImageLayoutGrid.js @@ -0,0 +1,109 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useRef } from 'react'; +import { View } from 'react-native'; +import { useAnimatedRef } from 'react-native-reanimated'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { PostEmbedViewContext } from '#/components/Post/Embed/types'; +import { GalleryItem } from './Gallery'; +export function ImageLayoutGrid(_a) { + var style = _a.style, props = __rest(_a, ["style"]); + var gtMobile = useBreakpoints().gtMobile; + var gap = props.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + ? gtMobile + ? a.gap_xs + : a.gap_2xs + : a.gap_xs; + return (_jsx(View, { style: style, children: _jsx(View, { style: [gap, a.rounded_md, a.overflow_hidden], children: _jsx(ImageLayoutGridInner, __assign({}, props, { gap: gap })) }) })); +} +function ImageLayoutGridInner(props) { + var gap = props.gap; + var count = props.images.length; + var containerRef1 = useAnimatedRef(); + var containerRef2 = useAnimatedRef(); + var containerRef3 = useAnimatedRef(); + var containerRef4 = useAnimatedRef(); + var thumbDimsRef = useRef([]); + switch (count) { + case 2: { + var containerRefs = [containerRef1, containerRef2]; + return (_jsxs(View, { style: [a.flex_1, a.flex_row, gap], children: [_jsx(View, { style: [a.flex_1, a.aspect_square], children: _jsx(GalleryItem, __assign({}, props, { index: 0, insetBorderStyle: noCorners(['topRight', 'bottomRight']), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) }), _jsx(View, { style: [a.flex_1, a.aspect_square], children: _jsx(GalleryItem, __assign({}, props, { index: 1, insetBorderStyle: noCorners(['topLeft', 'bottomLeft']), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) })] })); + } + case 3: { + var containerRefs = [containerRef1, containerRef2, containerRef3]; + return (_jsxs(View, { style: [a.flex_1, a.flex_row, gap], children: [_jsx(View, { style: [a.flex_1, a.aspect_square], children: _jsx(GalleryItem, __assign({}, props, { index: 0, insetBorderStyle: noCorners(['topRight', 'bottomRight']), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) }), _jsxs(View, { style: [a.flex_1, a.aspect_square, gap], children: [_jsx(View, { style: [a.flex_1], children: _jsx(GalleryItem, __assign({}, props, { index: 1, insetBorderStyle: noCorners([ + 'topLeft', + 'bottomLeft', + 'bottomRight', + ]), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) }), _jsx(View, { style: [a.flex_1], children: _jsx(GalleryItem, __assign({}, props, { index: 2, insetBorderStyle: noCorners([ + 'topLeft', + 'bottomLeft', + 'topRight', + ]), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) })] })] })); + } + case 4: { + var containerRefs = [ + containerRef1, + containerRef2, + containerRef3, + containerRef4, + ]; + return (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.flex_row, gap], children: [_jsx(View, { style: [a.flex_1, { aspectRatio: 1.5 }], children: _jsx(GalleryItem, __assign({}, props, { index: 0, insetBorderStyle: noCorners([ + 'bottomLeft', + 'topRight', + 'bottomRight', + ]), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) }), _jsx(View, { style: [a.flex_1, { aspectRatio: 1.5 }], children: _jsx(GalleryItem, __assign({}, props, { index: 1, insetBorderStyle: noCorners([ + 'topLeft', + 'bottomLeft', + 'bottomRight', + ]), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) })] }), _jsxs(View, { style: [a.flex_row, gap], children: [_jsx(View, { style: [a.flex_1, { aspectRatio: 1.5 }], children: _jsx(GalleryItem, __assign({}, props, { index: 2, insetBorderStyle: noCorners([ + 'topLeft', + 'topRight', + 'bottomRight', + ]), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) }), _jsx(View, { style: [a.flex_1, { aspectRatio: 1.5 }], children: _jsx(GalleryItem, __assign({}, props, { index: 3, insetBorderStyle: noCorners([ + 'topLeft', + 'bottomLeft', + 'topRight', + ]), containerRefs: containerRefs, thumbDimsRef: thumbDimsRef })) })] })] })); + } + default: + return null; + } +} +function noCorners(corners) { + var styles = []; + if (corners.includes('topLeft')) { + styles.push({ borderTopLeftRadius: 0 }); + } + if (corners.includes('topRight')) { + styles.push({ borderTopRightRadius: 0 }); + } + if (corners.includes('bottomLeft')) { + styles.push({ borderBottomLeftRadius: 0 }); + } + if (corners.includes('bottomRight')) { + styles.push({ borderBottomRightRadius: 0 }); + } + return styles; +} diff --git a/src/components/intents/IntentDialogs.js b/src/components/intents/IntentDialogs.js new file mode 100644 index 0000000000..2abb8c2274 --- /dev/null +++ b/src/components/intents/IntentDialogs.js @@ -0,0 +1,18 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import * as Dialog from '#/components/Dialog'; +import { VerifyEmailIntentDialog } from '#/components/intents/VerifyEmailIntentDialog'; +var Context = React.createContext({}); +Context.displayName = 'IntentDialogsContext'; +export var useIntentDialogs = function () { return React.useContext(Context); }; +export function Provider(_a) { + var children = _a.children; + var verifyEmailDialogControl = Dialog.useDialogControl(); + var _b = React.useState(), verifyEmailState = _b[0], setVerifyEmailState = _b[1]; + var value = React.useMemo(function () { return ({ + verifyEmailDialogControl: verifyEmailDialogControl, + verifyEmailState: verifyEmailState, + setVerifyEmailState: setVerifyEmailState, + }); }, [verifyEmailDialogControl, verifyEmailState, setVerifyEmailState]); + return (_jsxs(Context.Provider, { value: value, children: [children, _jsx(VerifyEmailIntentDialog, {})] })); +} diff --git a/src/components/intents/VerifyEmailIntentDialog.js b/src/components/intents/VerifyEmailIntentDialog.js new file mode 100644 index 0000000000..6adef6a614 --- /dev/null +++ b/src/components/intents/VerifyEmailIntentDialog.js @@ -0,0 +1,99 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useEffect, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useAgent, useSession } from '#/state/session'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useConfirmEmail } from '#/components/dialogs/EmailDialog/data/useConfirmEmail'; +import { Divider } from '#/components/Divider'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Resend } from '#/components/icons/ArrowRotate'; +import { useIntentDialogs } from '#/components/intents/IntentDialogs'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +export function VerifyEmailIntentDialog() { + var control = useIntentDialogs().verifyEmailDialogControl; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsx(Inner, { control: control })] })); +} +function Inner(_a) { + var _this = this; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var _ = useLingui()._; + var state = useIntentDialogs().verifyEmailState; + var _b = useState('loading'), status = _b[0], setStatus = _b[1]; + var _c = useState(false), sending = _c[0], setSending = _c[1]; + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + var confirmEmail = useConfirmEmail({ + onSuccess: function () { return setStatus('success'); }, + onError: function () { return setStatus('failure'); }, + }).mutate; + useEffect(function () { + if (state === null || state === void 0 ? void 0 : state.code) { + confirmEmail({ token: state.code }); + } + }, [state === null || state === void 0 ? void 0 : state.code, confirmEmail]); + var onPressResendEmail = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setSending(true); + return [4 /*yield*/, agent.com.atproto.server.requestEmailConfirmation()]; + case 1: + _a.sent(); + setSending(false); + setStatus('resent'); + return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Verify email dialog"], ["Verify email dialog"])))), style: [ + gtMobile ? { width: 'auto', maxWidth: 400, minWidth: 200 } : a.w_full, + ], children: [_jsxs(View, { style: [a.gap_xl], children: [status === 'loading' ? (_jsx(View, { style: [a.py_2xl, a.align_center, a.justify_center], children: _jsx(Loader, { size: "xl", fill: t.atoms.text_contrast_low.color }) })) : status === 'success' ? (_jsxs(View, { style: [a.gap_sm, IS_NATIVE && a.pb_xl], children: [_jsx(Text, { style: [a.font_bold, a.text_2xl], children: _jsx(Trans, { children: "Email Verified" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "Thanks, you have successfully verified your email address. You can close this dialog." }) })] })) : status === 'failure' ? (_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_bold, a.text_2xl], children: _jsx(Trans, { children: "Invalid Verification Code" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one." }) })] })) : (_jsxs(View, { style: [a.gap_sm, IS_NATIVE && a.pb_xl], children: [_jsx(Text, { style: [a.font_bold, a.text_2xl], children: _jsx(Trans, { children: "Email Resent" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsxs(Trans, { children: ["We have sent another verification email to", ' ', _jsx(Text, { style: [a.text_md, a.font_semi_bold], children: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email }), "."] }) })] })), status === 'failure' && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Resend Verification Email"], ["Resend Verification Email"])))), onPress: onPressResendEmail, color: "secondary_inverted", size: "large", disabled: sending, children: [_jsx(ButtonIcon, { icon: sending ? Loader : Resend }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Resend Email" }) })] })] }))] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/interstitials/Trending.js b/src/components/interstitials/Trending.js new file mode 100644 index 0000000000..f4e5a31fd8 --- /dev/null +++ b/src/components/interstitials/Trending.js @@ -0,0 +1,56 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { ScrollView, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useTrendingSettings, useTrendingSettingsApi, } from '#/state/preferences/trending'; +import { useTrendingTopics } from '#/state/queries/trending/useTrendingTopics'; +import { useTrendingConfig } from '#/state/service-config'; +import { LoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { BlockDrawerGesture } from '#/view/shell/BlockDrawerGesture'; +import { atoms as a, useGutters, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Trending2_Stroke2_Corner2_Rounded as Graph } from '#/components/icons/Trending'; +import * as Prompt from '#/components/Prompt'; +import { TrendingTopicLink } from '#/components/TrendingTopics'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +export function TrendingInterstitial() { + var enabled = useTrendingConfig().enabled; + var trendingDisabled = useTrendingSettings().trendingDisabled; + return enabled && !trendingDisabled ? _jsx(Inner, {}) : null; +} +export function Inner() { + var _a; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var gutters = useGutters([0, 'base', 0, 'base']); + var trendingPrompt = Prompt.usePromptControl(); + var setTrendingDisabled = useTrendingSettingsApi().setTrendingDisabled; + var _b = useTrendingTopics(), trending = _b.data, error = _b.error, isLoading = _b.isLoading; + var noTopics = !isLoading && !error && !((_a = trending === null || trending === void 0 ? void 0 : trending.topics) === null || _a === void 0 ? void 0 : _a.length); + var onConfirmHide = React.useCallback(function () { + ax.metric('trendingTopics:hide', { context: 'interstitial' }); + setTrendingDisabled(true); + }, [ax, setTrendingDisabled]); + return error || noTopics ? null : (_jsxs(View, { style: [t.atoms.border_contrast_low, a.border_t, a.border_b], children: [_jsx(BlockDrawerGesture, { children: _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, decelerationRate: "fast", children: _jsxs(View, { style: [gutters, a.flex_row, a.align_center, a.gap_lg], children: [_jsx(View, { style: { paddingLeft: 4, paddingRight: 2 }, children: _jsx(Graph, { size: "sm" }) }), isLoading ? (_jsxs(View, { style: [a.py_lg, a.flex_row, a.gap_lg, a.align_center], children: [_jsx(LoadingPlaceholder, { width: 80, height: undefined, style: { alignSelf: 'stretch' } }), _jsx(LoadingPlaceholder, { width: 50, height: undefined, style: { alignSelf: 'stretch' } }), _jsx(LoadingPlaceholder, { width: 120, height: undefined, style: { alignSelf: 'stretch' } }), _jsx(LoadingPlaceholder, { width: 30, height: undefined, style: { alignSelf: 'stretch' } }), _jsx(LoadingPlaceholder, { width: 180, height: undefined, style: { alignSelf: 'stretch' } }), _jsx(Text, { style: [ + t.atoms.text_contrast_medium, + a.text_sm, + a.font_semi_bold, + ], children: ' ' })] })) : !(trending === null || trending === void 0 ? void 0 : trending.topics) ? null : (_jsxs(_Fragment, { children: [trending.topics.map(function (topic) { return (_jsx(TrendingTopicLink, { topic: topic, onPress: function () { + ax.metric('trendingTopic:click', { + context: 'interstitial', + }); + }, children: _jsx(View, { style: [a.py_lg], children: _jsx(Text, { style: [ + t.atoms.text_contrast_medium, + a.text_sm, + a.font_semi_bold, + ], children: topic.topic }) }) }, topic.link)); }), _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Hide trending topics"], ["Hide trending topics"])))), size: "tiny", variant: "ghost", color: "secondary", shape: "round", onPress: function () { return trendingPrompt.open(); }, children: _jsx(ButtonIcon, { icon: X }) })] }))] }) }) }), _jsx(Prompt.Basic, { control: trendingPrompt, title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Hide trending topics?"], ["Hide trending topics?"])))), description: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You can update this later from your settings."], ["You can update this later from your settings."])))), confirmButtonCta: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Hide"], ["Hide"])))), onConfirm: onConfirmHide })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/interstitials/TrendingVideos.js b/src/components/interstitials/TrendingVideos.js new file mode 100644 index 0000000000..3d1d80f03a --- /dev/null +++ b/src/components/interstitials/TrendingVideos.js @@ -0,0 +1,132 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo } from 'react'; +import { ScrollView, View } from 'react-native'; +import { AppBskyEmbedVideo, AtUri } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { VIDEO_FEED_URI } from '#/lib/constants'; +import { makeCustomFeedLink } from '#/lib/routes/links'; +import { useTrendingSettingsApi } from '#/state/preferences/trending'; +import { RQKEY, usePostFeedQuery } from '#/state/queries/post-feed'; +import { BlockDrawerGesture } from '#/view/shell/BlockDrawerGesture'; +import { atoms as a, useGutters, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRight } from '#/components/icons/Chevron'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Link } from '#/components/Link'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { CompactVideoPostCard, CompactVideoPostCardPlaceholder, } from '#/components/VideoPostCard'; +import { useAnalytics } from '#/analytics'; +var CARD_WIDTH = 108; +var FEED_DESC = "feedgen|".concat(VIDEO_FEED_URI); +var FEED_PARAMS = { + feedCacheKey: 'discover', +}; +export function TrendingVideos() { + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var gutters = useGutters([0, 'base']); + var _a = usePostFeedQuery(FEED_DESC, FEED_PARAMS), data = _a.data, isLoading = _a.isLoading, error = _a.error; + // Refetch on unmount if nothing else is using this query. + var queryClient = useQueryClient(); + useEffect(function () { + return function () { + var query = queryClient + .getQueryCache() + .find({ queryKey: RQKEY(FEED_DESC, FEED_PARAMS) }); + if (query && query.getObserversCount() <= 1) { + query.fetch(); + } + }; + }, [queryClient]); + var setTrendingVideoDisabled = useTrendingSettingsApi().setTrendingVideoDisabled; + var trendingPrompt = Prompt.usePromptControl(); + var onConfirmHide = useCallback(function () { + setTrendingVideoDisabled(true); + ax.metric('trendingVideos:hide', { context: 'interstitial:discover' }); + }, [ax, setTrendingVideoDisabled]); + if (error) { + return null; + } + return (_jsxs(View, { style: [ + a.pt_sm, + a.pb_lg, + a.border_t, + a.overflow_hidden, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + ], children: [_jsxs(View, { style: [ + gutters, + a.pb_sm, + a.flex_row, + a.align_center, + a.justify_between, + ], children: [_jsx(Text, { style: [a.text_sm, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { children: "Trending Videos" }) }), _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Dismiss this section"], ["Dismiss this section"])))), size: "tiny", variant: "solid", color: "secondary", shape: "square", onPress: function () { return trendingPrompt.open(); }, children: _jsx(ButtonIcon, { icon: X, size: "sm" }) })] }), _jsx(BlockDrawerGesture, { children: _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, decelerationRate: "fast", snapToInterval: CARD_WIDTH + a.gap_md.gap, style: [a.overflow_visible], children: _jsx(View, { style: [ + a.flex_row, + a.gap_md, + { + paddingLeft: gutters.paddingLeft, + paddingRight: gutters.paddingRight, + }, + ], children: isLoading ? (Array(10) + .fill(0) + .map(function (_, i) { return (_jsx(View, { style: [{ width: CARD_WIDTH }], children: _jsx(CompactVideoPostCardPlaceholder, {}) }, i)); })) : error || !data ? (_jsx(Text, { children: _jsx(Trans, { children: "Whoops! Trending videos failed to load." }) })) : (_jsx(VideoCards, { data: data })) }) }) }), _jsx(Prompt.Basic, { control: trendingPrompt, title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Hide trending videos?"], ["Hide trending videos?"])))), description: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You can update this later from your settings."], ["You can update this later from your settings."])))), confirmButtonCta: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Hide"], ["Hide"])))), onConfirm: onConfirmHide })] })); +} +function VideoCards(_a) { + var data = _a.data; + var ax = useAnalytics(); + var items = useMemo(function () { + return data.pages + .flatMap(function (page) { return page.slices; }) + .map(function (slice) { return slice.items[0]; }) + .filter(Boolean) + .filter(function (item) { return AppBskyEmbedVideo.isView(item.post.embed); }) + .slice(0, 8); + }, [data]); + return (_jsxs(_Fragment, { children: [items.map(function (item) { return (_jsx(View, { style: [{ width: CARD_WIDTH }], children: _jsx(CompactVideoPostCard, { post: item.post, moderation: item.moderation, sourceContext: { + type: 'feedgen', + uri: VIDEO_FEED_URI, + sourceInterstitial: 'discover', + }, onInteract: function () { + ax.metric('videoCard:click', { + context: 'interstitial:discover', + }); + } }) }, item.post.uri)); }), _jsx(ViewMoreCard, {})] })); +} +function ViewMoreCard() { + var t = useTheme(); + var _ = useLingui()._; + var href = useMemo(function () { + var urip = new AtUri(VIDEO_FEED_URI); + return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover'); + }, []); + return (_jsx(View, { style: [{ width: CARD_WIDTH * 2 }], children: _jsx(Link, { to: href, label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["View more"], ["View more"])))), style: [ + a.justify_center, + a.align_center, + a.flex_1, + a.rounded_lg, + a.border, + t.atoms.border_contrast_low, + t.atoms.bg, + t.atoms.shadow_sm, + ], children: function (_a) { + var pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.gap_md, + { + opacity: pressed ? 0.6 : 1, + }, + ], children: [_jsx(Text, { style: [a.text_md], children: _jsx(Trans, { children: "View more" }) }), _jsx(Button, { color: "primary", size: "small", shape: "round", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["View more trending videos"], ["View more trending videos"])))), children: _jsx(ButtonIcon, { icon: ChevronRight }) })] })); + } }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/moderation/ContentHider.js b/src/components/moderation/ContentHider.js new file mode 100644 index 0000000000..8410f63a11 --- /dev/null +++ b/src/components/moderation/ContentHider.js @@ -0,0 +1,166 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo, useState } from 'react'; +import { LayoutAnimation, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { ADULT_CONTENT_LABELS, isJustAMute, } from '#/lib/moderation'; +import { useGlobalLabelStrings } from '#/lib/moderation/useGlobalLabelStrings'; +import { getDefinition, getLabelStrings } from '#/lib/moderation/useLabelInfo'; +import { useModerationCauseDescription } from '#/lib/moderation/useModerationCauseDescription'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { useLabelDefinitions } from '#/state/preferences'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { Button } from '#/components/Button'; +import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog'; +import { Text } from '#/components/Typography'; +export function ContentHider(_a) { + var testID = _a.testID, modui = _a.modui, ignoreMute = _a.ignoreMute, style = _a.style, activeStyle = _a.activeStyle, childContainerStyle = _a.childContainerStyle, children = _a.children; + var blur = modui === null || modui === void 0 ? void 0 : modui.blurs[0]; + if (!blur || (ignoreMute && isJustAMute(modui))) { + return (_jsx(View, { testID: testID, style: style, children: typeof children === 'function' ? children({ active: false }) : children })); + } + return (_jsx(ContentHiderActive, { testID: testID, modui: modui, style: [style, activeStyle], childContainerStyle: childContainerStyle, children: typeof children === 'function' ? children({ active: true }) : children })); +} +function ContentHiderActive(_a) { + var testID = _a.testID, modui = _a.modui, style = _a.style, childContainerStyle = _a.childContainerStyle, children = _a.children; + var t = useTheme(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var _b = useState(false), override = _b[0], setOverride = _b[1]; + var control = useModerationDetailsDialogControl(); + var labelDefs = useLabelDefinitions().labelDefs; + var globalLabelStrings = useGlobalLabelStrings(); + var i18n = useLingui().i18n; + var blur = modui === null || modui === void 0 ? void 0 : modui.blurs[0]; + var desc = useModerationCauseDescription(blur); + var labelName = useMemo(function () { + if (!(modui === null || modui === void 0 ? void 0 : modui.blurs) || !blur) { + return undefined; + } + if (blur.type !== 'label' || + (blur.type === 'label' && blur.source.type !== 'user')) { + if (desc.isSubjectAccount) { + return _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["", " (Account)"], ["", " (Account)"])), desc.name)); + } + else { + return desc.name; + } + } + var hasAdultContentLabel = false; + var selfBlurNames = modui.blurs + .filter(function (cause) { + if (cause.type !== 'label') { + return false; + } + if (cause.source.type !== 'user') { + return false; + } + if (ADULT_CONTENT_LABELS.includes(cause.label.val)) { + if (hasAdultContentLabel) { + return false; + } + hasAdultContentLabel = true; + } + return true; + }) + .slice(0, 2) + .map(function (cause) { + if (cause.type !== 'label') { + return; + } + var def = cause.labelDef || getDefinition(labelDefs, cause.label); + if (def.identifier === 'porn' || def.identifier === 'sexual') { + return _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Adult Content"], ["Adult Content"])))); + } + return getLabelStrings(i18n.locale, globalLabelStrings, def).name; + }); + if (selfBlurNames.length === 0) { + return desc.name; + } + return __spreadArray([], new Set(selfBlurNames), true).join(', '); + }, [ + _, + modui === null || modui === void 0 ? void 0 : modui.blurs, + blur, + desc.name, + desc.isSubjectAccount, + labelDefs, + i18n.locale, + globalLabelStrings, + ]); + return (_jsxs(View, { testID: testID, style: [a.overflow_hidden, style], children: [_jsx(ModerationDetailsDialog, { control: control, modcause: blur }), _jsx(Button, { onPress: function (e) { + e.preventDefault(); + e.stopPropagation(); + if (!modui.noOverride) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setOverride(function (v) { return !v; }); + } + else { + control.open(); + } + }, label: desc.name, accessibilityHint: modui.noOverride + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Learn more about the moderation applied to this content"], ["Learn more about the moderation applied to this content"])))) + : override + ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Hides the content"], ["Hides the content"])))) + : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Shows the content"], ["Shows the content"])))), children: function (state) { return (_jsxs(View, { style: [ + a.flex_row, + a.w_full, + a.justify_start, + a.align_center, + a.py_md, + a.px_lg, + a.gap_xs, + a.rounded_sm, + t.atoms.bg_contrast_25, + gtMobile && [a.gap_sm, a.py_lg, a.mt_xs, a.px_xl], + (state.hovered || state.pressed) && t.atoms.bg_contrast_50, + ], children: [_jsx(desc.icon, { size: "md", fill: t.atoms.text_contrast_medium.color, style: { marginLeft: -2 } }), _jsx(Text, { style: [ + a.flex_1, + a.text_left, + a.font_semi_bold, + a.leading_snug, + gtMobile && [a.font_semi_bold], + t.atoms.text_contrast_medium, + web({ + marginBottom: 1, + }), + ], numberOfLines: 2, children: labelName }), !modui.noOverride && (_jsx(Text, { style: [ + a.font_semi_bold, + a.leading_snug, + gtMobile && [a.font_semi_bold], + t.atoms.text_contrast_high, + web({ + marginBottom: 1, + }), + ], children: override ? _jsx(Trans, { children: "Hide" }) : _jsx(Trans, { children: "Show" }) }))] })); } }), desc.source && blur.type === 'label' && !override && (_jsx(Button, { onPress: function (e) { + e.preventDefault(); + e.stopPropagation(); + control.open(); + }, label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Learn more about the moderation applied to this content"], ["Learn more about the moderation applied to this content"])))), style: [a.pt_sm], children: function (state) { return (_jsxs(Text, { style: [ + a.flex_1, + a.text_sm, + a.font_normal, + a.leading_snug, + t.atoms.text_contrast_medium, + a.text_left, + ], children: [desc.sourceType === 'user' ? (_jsx(Trans, { children: "Labeled by the author." })) : (_jsxs(Trans, { children: ["Labeled by ", sanitizeDisplayName(desc.source), "."] })), ' ', _jsx(Text, { style: [ + { color: t.palette.primary_500 }, + a.text_sm, + state.hovered && [web({ textDecoration: 'underline' })], + ], children: _jsx(Trans, { children: "Learn more." }) })] })); } })), override && _jsx(View, { style: childContainerStyle, children: children })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/moderation/Hider.js b/src/components/moderation/Hider.js new file mode 100644 index 0000000000..abf124b54c --- /dev/null +++ b/src/components/moderation/Hider.js @@ -0,0 +1,47 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { useModerationCauseDescription, } from '#/lib/moderation/useModerationCauseDescription'; +import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog'; +var Context = React.createContext({}); +Context.displayName = 'HiderContext'; +export var useHider = function () { return React.useContext(Context); }; +export function Outer(_a) { + var modui = _a.modui, isContentVisibleInitialState = _a.isContentVisibleInitialState, allowOverride = _a.allowOverride, children = _a.children; + var control = useModerationDetailsDialogControl(); + var blur = modui === null || modui === void 0 ? void 0 : modui.blurs[0]; + var _b = React.useState(isContentVisibleInitialState || !blur), isContentVisible = _b[0], setIsContentVisible = _b[1]; + var info = useModerationCauseDescription(blur); + var meta = { + isNoPwi: Boolean(modui === null || modui === void 0 ? void 0 : modui.blurs.find(function (cause) { + return cause.type === 'label' && + cause.labelDef.identifier === '!no-unauthenticated'; + })), + allowOverride: allowOverride !== null && allowOverride !== void 0 ? allowOverride : !(modui === null || modui === void 0 ? void 0 : modui.noOverride), + }; + var showInfoDialog = function () { + control.open(); + }; + var onSetContentVisible = function (show) { + if (!meta.allowOverride) + return; + setIsContentVisible(show); + }; + var ctx = { + isContentVisible: isContentVisible, + setIsContentVisible: onSetContentVisible, + showInfoDialog: showInfoDialog, + info: info, + meta: meta, + }; + return (_jsxs(Context.Provider, { value: ctx, children: [children, _jsx(ModerationDetailsDialog, { control: control, modcause: blur })] })); +} +export function Content(_a) { + var children = _a.children; + var ctx = useHider(); + return ctx.isContentVisible ? children : null; +} +export function Mask(_a) { + var children = _a.children; + var ctx = useHider(); + return ctx.isContentVisible ? null : children; +} diff --git a/src/components/moderation/LabelPreference.js b/src/components/moderation/LabelPreference.js new file mode 100644 index 0000000000..aaa5a342da --- /dev/null +++ b/src/components/moderation/LabelPreference.js @@ -0,0 +1,135 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useGlobalLabelStrings } from '#/lib/moderation/useGlobalLabelStrings'; +import { useLabelBehaviorDescription } from '#/lib/moderation/useLabelBehaviorDescription'; +import { getLabelStrings } from '#/lib/moderation/useLabelInfo'; +import { usePreferencesQuery, usePreferencesSetContentLabelMutation, } from '#/state/queries/preferences'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import * as ToggleButton from '#/components/forms/ToggleButton'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '../icons/CircleInfo'; +export function Outer(_a) { + var children = _a.children; + return (_jsx(View, { style: [ + a.flex_row, + a.gap_sm, + a.px_lg, + a.py_lg, + a.justify_between, + a.flex_wrap, + ], children: children })); +} +export function Content(_a) { + var children = _a.children, name = _a.name, description = _a.description; + var t = useTheme(); + var gtPhone = useBreakpoints().gtPhone; + return (_jsxs(View, { style: [a.gap_xs, a.flex_1], children: [_jsx(Text, { emoji: true, style: [a.font_semi_bold, gtPhone ? a.text_sm : a.text_md], children: name }), _jsx(Text, { emoji: true, style: [t.atoms.text_contrast_medium, a.leading_snug], children: description }), children] })); +} +export function Buttons(_a) { + var name = _a.name, values = _a.values, onChange = _a.onChange, ignoreLabel = _a.ignoreLabel, warnLabel = _a.warnLabel, hideLabel = _a.hideLabel, disabled = _a.disabled; + var _ = useLingui()._; + return (_jsx(View, { style: [{ minHeight: 35 }, a.w_full], children: _jsxs(ToggleButton.Group, { disabled: disabled, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Configure content filtering setting for category: ", ""], ["Configure content filtering setting for category: ", ""])), name)), values: values, onChange: onChange, children: [ignoreLabel && (_jsx(ToggleButton.Button, { name: "ignore", label: ignoreLabel, children: _jsx(ToggleButton.ButtonText, { children: ignoreLabel }) })), warnLabel && (_jsx(ToggleButton.Button, { name: "warn", label: warnLabel, children: _jsx(ToggleButton.ButtonText, { children: warnLabel }) })), hideLabel && (_jsx(ToggleButton.Button, { name: "hide", label: hideLabel, children: _jsx(ToggleButton.ButtonText, { children: hideLabel }) }))] }) })); +} +/** + * For use on the global Moderation screen to set prefs for a "global" label, + * not scoped to a single labeler. + */ +export function GlobalLabelPreference(_a) { + var _b, _c; + var labelDefinition = _a.labelDefinition, disabled = _a.disabled; + var _ = useLingui()._; + var identifier = labelDefinition.identifier; + var preferences = usePreferencesQuery().data; + var _d = usePreferencesSetContentLabelMutation(), mutate = _d.mutate, variables = _d.variables; + var savedPref = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs.labels[identifier]; + var pref = (_c = (_b = variables === null || variables === void 0 ? void 0 : variables.visibility) !== null && _b !== void 0 ? _b : savedPref) !== null && _c !== void 0 ? _c : 'warn'; + var allLabelStrings = useGlobalLabelStrings(); + var labelStrings = labelDefinition.identifier in allLabelStrings + ? allLabelStrings[labelDefinition.identifier] + : { + name: labelDefinition.identifier, + description: "Labeled \"".concat(labelDefinition.identifier, "\""), + }; + var labelOptions = { + hide: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Hide"], ["Hide"])))), + warn: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Warn"], ["Warn"])))), + ignore: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Show"], ["Show"])))), + }; + return (_jsxs(Outer, { children: [_jsx(Content, { name: labelStrings.name, description: labelStrings.description }), _jsx(Buttons, { name: labelStrings.name.toLowerCase(), values: [pref], onChange: function (values) { + mutate({ + label: identifier, + visibility: values[0], + labelerDid: undefined, + }); + }, ignoreLabel: labelOptions.ignore, warnLabel: labelOptions.warn, hideLabel: labelOptions.hide, disabled: disabled })] })); +} +/** + * For use on individual labeler pages + */ +export function LabelerLabelPreference(_a) { + var _b, _c, _d, _e; + var labelDefinition = _a.labelDefinition, disabled = _a.disabled, labelerDid = _a.labelerDid; + var _f = useLingui(), _ = _f._, i18n = _f.i18n; + var t = useTheme(); + var gtPhone = useBreakpoints().gtPhone; + var isGlobalLabel = !labelDefinition.definedBy; + var identifier = labelDefinition.identifier; + var preferences = usePreferencesQuery().data; + var _g = usePreferencesSetContentLabelMutation(), mutate = _g.mutate, variables = _g.variables; + var savedPref = labelerDid && !isGlobalLabel + ? (_b = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs.labelers.find(function (l) { return l.did === labelerDid; })) === null || _b === void 0 ? void 0 : _b.labels[identifier] + : preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs.labels[identifier]; + var pref = (_e = (_d = (_c = variables === null || variables === void 0 ? void 0 : variables.visibility) !== null && _c !== void 0 ? _c : savedPref) !== null && _d !== void 0 ? _d : labelDefinition.defaultSetting) !== null && _e !== void 0 ? _e : 'warn'; + // does the 'warn' setting make sense for this label? + var canWarn = !(labelDefinition.blurs === 'none' && labelDefinition.severity === 'none'); + // is this label adult only? + var adultOnly = labelDefinition.flags.includes('adult'); + // is this label disabled because it's adult only? + var adultDisabled = adultOnly && !(preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs.adultContentEnabled); + // are there any reasons we cant configure this label here? + var cantConfigure = isGlobalLabel || adultDisabled; + var showConfig = !disabled && (gtPhone || !cantConfigure); + // adjust the pref based on whether warn is available + var prefAdjusted = pref; + if (adultDisabled) { + prefAdjusted = 'hide'; + } + else if (!canWarn && pref === 'warn') { + prefAdjusted = 'ignore'; + } + // grab localized descriptions of the label and its settings + var currentPrefLabel = useLabelBehaviorDescription(labelDefinition, prefAdjusted); + var hideLabel = useLabelBehaviorDescription(labelDefinition, 'hide'); + var warnLabel = useLabelBehaviorDescription(labelDefinition, 'warn'); + var ignoreLabel = useLabelBehaviorDescription(labelDefinition, 'ignore'); + var globalLabelStrings = useGlobalLabelStrings(); + var labelStrings = getLabelStrings(i18n.locale, globalLabelStrings, labelDefinition); + return (_jsxs(Outer, { children: [_jsx(Content, { name: labelStrings.name, description: labelStrings.description, children: cantConfigure && (_jsxs(View, { style: [a.flex_row, a.gap_xs, a.align_center, a.mt_xs], children: [_jsx(CircleInfo, { size: "sm", fill: t.atoms.text_contrast_high.color }), _jsx(Text, { style: [ + t.atoms.text_contrast_medium, + a.font_semi_bold, + a.italic, + ], children: adultDisabled ? (_jsx(Trans, { children: "Adult content is disabled." })) : isGlobalLabel ? (_jsxs(Trans, { children: ["Configured in", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["moderation settings"], ["moderation settings"])))), to: "/moderation", style: a.text_sm, children: "moderation settings" }), "."] })) : null })] })) }), showConfig && (_jsx(_Fragment, { children: cantConfigure ? (_jsx(View, { style: [ + { minHeight: 35 }, + a.px_md, + a.py_md, + a.rounded_sm, + a.border, + t.atoms.border_contrast_low, + a.self_start, + ], children: _jsx(Text, { emoji: true, style: [a.font_semi_bold, t.atoms.text_contrast_low], children: currentPrefLabel }) })) : (_jsx(Buttons, { name: labelStrings.name.toLowerCase(), values: [pref], onChange: function (values) { + mutate({ + label: identifier, + visibility: values[0], + labelerDid: labelerDid, + }); + }, ignoreLabel: ignoreLabel, warnLabel: canWarn ? warnLabel : undefined, hideLabel: hideLabel })) }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/moderation/LabelsOnMe.js b/src/components/moderation/LabelsOnMe.js new file mode 100644 index 0000000000..c7d31d7992 --- /dev/null +++ b/src/components/moderation/LabelsOnMe.js @@ -0,0 +1,39 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useSession } from '#/state/session'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText, } from '#/components/Button'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { LabelsOnMeDialog, useLabelsOnMeDialogControl, } from '#/components/moderation/LabelsOnMeDialog'; +export function LabelsOnMe(_a) { + var type = _a.type, labels = _a.labels, size = _a.size, style = _a.style; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var control = useLabelsOnMeDialogControl(); + if (!labels || !currentAccount) { + return null; + } + labels = labels.filter(function (l) { return !l.val.startsWith('!'); }); + if (!labels.length) { + return null; + } + return (_jsxs(View, { style: [a.flex_row, style], children: [_jsx(LabelsOnMeDialog, { control: control, labels: labels, type: type }), _jsxs(Button, { variant: "solid", color: "secondary", size: size || 'small', label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["View information about these labels"], ["View information about these labels"])))), onPress: function () { + control.open(); + }, children: [_jsx(ButtonIcon, { position: "left", icon: CircleInfo }), _jsx(ButtonText, { style: [a.leading_snug], children: type === 'account' ? (_jsxs(Trans, { children: [_jsx(Plural, { value: labels.length, one: "# label has", other: "# labels have" }), ' ', "been placed on this account"] })) : (_jsxs(Trans, { children: [_jsx(Plural, { value: labels.length, one: "# label has", other: "# labels have" }), ' ', "been placed on this content"] })) })] })] })); +} +export function LabelsOnMyPost(_a) { + var post = _a.post, style = _a.style; + var currentAccount = useSession().currentAccount; + if (post.author.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + return null; + } + return (_jsx(LabelsOnMe, { type: "content", labels: post.labels, size: "tiny", style: style })); +} +var templateObject_1; diff --git a/src/components/moderation/LabelsOnMeDialog.js b/src/components/moderation/LabelsOnMeDialog.js new file mode 100644 index 0000000000..f522f63de9 --- /dev/null +++ b/src/components/moderation/LabelsOnMeDialog.js @@ -0,0 +1,177 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useState } from 'react'; +import { View } from 'react-native'; +import { ToolsOzoneReportDefs } from '@atproto/api'; +import { XRPCError } from '@atproto/xrpc'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation } from '@tanstack/react-query'; +import { useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { useLabelSubject } from '#/lib/moderation'; +import { useLabelInfo } from '#/lib/moderation/useLabelInfo'; +import { makeProfileLink } from '#/lib/routes/links'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { useAgent, useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { IS_ANDROID } from '#/env'; +import { Admonition } from '../Admonition'; +import { Divider } from '../Divider'; +import { Loader } from '../Loader'; +export { useDialogControl as useLabelsOnMeDialogControl } from '#/components/Dialog'; +export function LabelsOnMeDialog(props) { + return (_jsxs(Dialog.Outer, { control: props.control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(LabelsOnMeDialogInner, __assign({}, props))] })); +} +function LabelsOnMeDialogInner(props) { + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var _a = React.useState(undefined), appealingLabel = _a[0], setAppealingLabel = _a[1]; + var labels = props.labels; + var isAccount = props.type === 'account'; + var containsSelfLabel = React.useMemo(function () { return labels.some(function (l) { return l.src === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); }, [currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, labels]); + return (_jsxs(Dialog.ScrollableInner, { label: isAccount + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["The following labels were applied to your account."], ["The following labels were applied to your account."])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["The following labels were applied to your content."], ["The following labels were applied to your content."])))), children: [appealingLabel ? (_jsx(AppealForm, { label: appealingLabel, control: props.control, onPressBack: function () { return setAppealingLabel(undefined); } })) : (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight], children: isAccount ? (_jsx(Trans, { children: "Labels on your account" })) : (_jsx(Trans, { children: "Labels on your content" })) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: containsSelfLabel ? (_jsx(Trans, { children: "You may appeal non-self labels if you feel they were placed in error." })) : (_jsx(Trans, { children: "You may appeal these labels if you feel they were placed in error." })) }), _jsx(View, { style: [a.py_lg, a.gap_md], children: labels.map(function (label) { return (_jsx(Label, { label: label, isSelfLabel: label.src === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did), control: props.control, onPressAppeal: setAppealingLabel }, "".concat(label.val, "-").concat(label.src))); }) })] })), _jsx(Dialog.Close, {})] })); +} +function Label(_a) { + var label = _a.label, isSelfLabel = _a.isSelfLabel, control = _a.control, onPressAppeal = _a.onPressAppeal; + var t = useTheme(); + var _ = useLingui()._; + var _b = useLabelInfo(label), labeler = _b.labeler, strings = _b.strings; + var sourceName = labeler + ? sanitizeHandle(labeler.creator.handle, '@') + : label.src; + var timeDiff = useGetTimeAgo({ future: true }); + return (_jsxs(View, { style: [ + a.border, + t.atoms.border_contrast_low, + a.rounded_sm, + a.overflow_hidden, + ], children: [_jsxs(View, { style: [a.p_md, a.gap_sm, a.flex_row], children: [_jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsx(Text, { emoji: true, style: [a.font_semi_bold, a.text_md], children: strings.name }), _jsx(Text, { emoji: true, style: [t.atoms.text_contrast_medium, a.leading_snug], children: strings.description })] }), !isSelfLabel && (_jsx(View, { children: _jsx(Button, { variant: "solid", color: "secondary", size: "small", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Appeal"], ["Appeal"])))), onPress: function () { return onPressAppeal(label); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Appeal" }) }) }) }))] }), _jsx(Divider, {}), _jsx(View, { style: [a.px_md, a.py_sm, t.atoms.bg_contrast_25], children: isSelfLabel ? (_jsx(Text, { style: [t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "This label was applied by you." }) })) : (_jsxs(View, { style: [ + a.flex_row, + a.justify_between, + a.gap_xl, + { paddingBottom: 1 }, + ], children: [_jsx(Text, { style: [a.flex_1, a.leading_snug, t.atoms.text_contrast_medium], numberOfLines: 1, children: _jsxs(Trans, { children: ["Source:", ' ', _jsx(InlineLinkText, { label: sourceName, to: makeProfileLink(labeler ? labeler.creator : { did: label.src, handle: '' }), onPress: function () { return control.close(); }, children: sourceName })] }) }), label.exp && (_jsx(View, { children: _jsx(Text, { style: [ + a.leading_snug, + a.text_sm, + a.italic, + t.atoms.text_contrast_medium, + ], children: _jsxs(Trans, { children: ["Expires in ", timeDiff(Date.now(), label.exp)] }) }) }))] })) })] })); +} +function AppealForm(_a) { + var _this = this; + var label = _a.label, control = _a.control, onPressBack = _a.onPressBack; + var _ = useLingui()._; + var _b = useLabelInfo(label), labeler = _b.labeler, strings = _b.strings; + var gtMobile = useBreakpoints().gtMobile; + var _c = React.useState(''), details = _c[0], setDetails = _c[1]; + var subject = useLabelSubject({ label: label }).subject; + var isAccountReport = 'did' in subject; + var agent = useAgent(); + var sourceName = labeler + ? sanitizeHandle(labeler.creator.handle, '@') + : label.src; + var _d = useState(null), error = _d[0], setError = _d[1]; + var _e = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var $type; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + $type = !isAccountReport + ? 'com.atproto.repo.strongRef' + : 'com.atproto.admin.defs#repoRef'; + return [4 /*yield*/, agent.createModerationReport({ + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: __assign({ $type: $type }, subject), + reason: details, + }, { + encoding: 'application/json', + headers: { + 'atproto-proxy': "".concat(label.src, "#atproto_labeler"), + }, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onError: function (err) { + if (err instanceof XRPCError && err.error === 'AlreadyAppealed') { + setError(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["You've already appealed this label and it's being reviewed by our moderation team."], ["You've already appealed this label and it's being reviewed by our moderation team."]))))); + } + else { + setError(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Failed to submit appeal, please try again."], ["Failed to submit appeal, please try again."]))))); + } + logger.error('Failed to submit label appeal', { message: err }); + }, + onSuccess: function () { + control.close(); + Toast.show(_(msg({ message: 'Appeal submitted', context: 'toast' }))); + }, + }), mutate = _e.mutate, isPending = _e.isPending; + var onSubmit = React.useCallback(function () { return mutate(); }, [mutate]); + return (_jsxs(_Fragment, { children: [_jsxs(View, { children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold, a.pb_xs, a.leading_tight], children: _jsxs(Trans, { children: ["Appeal \"", strings.name, "\" label"] }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsxs(Trans, { children: ["This appeal will be sent to", ' ', _jsx(InlineLinkText, { label: sourceName, to: makeProfileLink(labeler ? labeler.creator : { did: label.src, handle: '' }), onPress: function () { return control.close(); }, style: [a.text_md, a.leading_snug], children: sourceName }), "."] }) })] }), error && (_jsx(Admonition, { type: "error", style: [a.mt_sm], children: error })), _jsx(View, { style: [a.my_md], children: _jsx(Dialog.Input, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Text input field"], ["Text input field"])))), placeholder: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Please explain why you think this label was incorrectly applied by ", ""], ["Please explain why you think this label was incorrectly applied by ", ""])), labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src)), value: details, onChangeText: setDetails, autoFocus: true, numberOfLines: 3, multiline: true, maxLength: 300 }) }), _jsxs(View, { style: gtMobile + ? [a.flex_row, a.justify_between] + : [{ flexDirection: 'column-reverse' }, a.gap_sm], children: [_jsx(Button, { testID: "backBtn", variant: "solid", color: "secondary", size: "large", onPress: onPressBack, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Back"], ["Back"])))), children: _jsx(ButtonText, { children: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Back"], ["Back"])))) }) }), _jsxs(Button, { testID: "submitBtn", variant: "solid", color: "primary", size: "large", onPress: onSubmit, label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Submit"], ["Submit"])))), children: [_jsx(ButtonText, { children: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Submit"], ["Submit"])))) }), isPending && _jsx(ButtonIcon, { icon: Loader })] })] }), IS_ANDROID && _jsx(View, { style: { height: 300 } })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11; diff --git a/src/components/moderation/ModerationDetailsDialog.js b/src/components/moderation/ModerationDetailsDialog.js new file mode 100644 index 0000000000..0ec059f288 --- /dev/null +++ b/src/components/moderation/ModerationDetailsDialog.js @@ -0,0 +1,139 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { useModerationCauseDescription } from '#/lib/moderation/useModerationCauseDescription'; +import { makeProfileLink } from '#/lib/routes/links'; +import { listUriToHref } from '#/lib/strings/url-helpers'; +import { useSession } from '#/state/session'; +import { atoms as a, useGutters, useTheme } from '#/alf'; +import * as Dialog from '#/components/Dialog'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +export { useDialogControl as useModerationDetailsDialogControl } from '#/components/Dialog'; +export function ModerationDetailsDialog(props) { + return (_jsxs(Dialog.Outer, { control: props.control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(ModerationDetailsDialogInner, __assign({}, props))] })); +} +function ModerationDetailsDialogInner(_a) { + var modcause = _a.modcause, control = _a.control; + var t = useTheme(); + var xGutters = useGutters([0, 'base']); + var _ = useLingui()._; + var desc = useModerationCauseDescription(modcause); + var currentAccount = useSession().currentAccount; + var timeDiff = useGetTimeAgo({ future: true }); + var name; + var description; + if (!modcause) { + name = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Content Warning"], ["Content Warning"])))); + description = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Moderator has chosen to set a general warning on the content."], ["Moderator has chosen to set a general warning on the content."])))); + } + else if (modcause.type === 'blocking') { + if (modcause.source.type === 'list') { + var list = modcause.source.list; + name = _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["User Blocked by List"], ["User Blocked by List"])))); + description = (_jsxs(Trans, { children: ["This user is included in the", ' ', _jsx(InlineLinkText, { label: list.name, to: listUriToHref(list.uri), style: [a.text_sm], children: list.name }), ' ', "list which you have blocked."] })); + } + else { + name = _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["User Blocked"], ["User Blocked"])))); + description = _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["You have blocked this user. You cannot view their content."], ["You have blocked this user. You cannot view their content."])))); + } + } + else if (modcause.type === 'blocked-by') { + name = _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["User Blocks You"], ["User Blocks You"])))); + description = _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["This user has blocked you. You cannot view their content."], ["This user has blocked you. You cannot view their content."])))); + } + else if (modcause.type === 'block-other') { + name = _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Content Not Available"], ["Content Not Available"])))); + description = _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["This content is not available because one of the users involved has blocked the other."], ["This content is not available because one of the users involved has blocked the other."])))); + } + else if (modcause.type === 'muted') { + if (modcause.source.type === 'list') { + var list = modcause.source.list; + name = _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Account Muted by List"], ["Account Muted by List"])))); + description = (_jsxs(Trans, { children: ["This user is included in the", ' ', _jsx(InlineLinkText, { label: list.name, to: listUriToHref(list.uri), style: [a.text_sm], children: list.name }), ' ', "list which you have muted."] })); + } + else { + name = _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Account Muted"], ["Account Muted"])))); + description = _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["You have muted this account."], ["You have muted this account."])))); + } + } + else if (modcause.type === 'mute-word') { + name = _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Post Hidden by Muted Word"], ["Post Hidden by Muted Word"])))); + description = _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["You've chosen to hide a word or tag within this post."], ["You've chosen to hide a word or tag within this post."])))); + } + else if (modcause.type === 'hidden') { + name = _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Post Hidden by You"], ["Post Hidden by You"])))); + description = _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["You have hidden this post."], ["You have hidden this post."])))); + } + else if (modcause.type === 'reply-hidden') { + var isYou = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === modcause.source.did; + name = isYou + ? _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Reply Hidden by You"], ["Reply Hidden by You"])))) + : _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Reply Hidden by Thread Author"], ["Reply Hidden by Thread Author"])))); + description = isYou + ? _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["You hid this reply."], ["You hid this reply."])))) + : _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["The author of this thread has hidden this reply."], ["The author of this thread has hidden this reply."])))); + } + else if (modcause.type === 'label') { + name = desc.name; + description = (_jsx(Text, { emoji: true, style: [t.atoms.text, a.text_md, a.leading_snug], children: desc.description })); + } + else { + // should never happen + name = ''; + description = ''; + } + var sourceName = desc.source || desc.sourceDisplayName || _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["an unknown labeler"], ["an unknown labeler"])))); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Moderation details"], ["Moderation details"])))), contentContainerStyle: { + paddingLeft: 0, + paddingRight: 0, + paddingBottom: 0, + }, children: [_jsxs(View, { style: [xGutters, a.pb_lg], children: [_jsx(Text, { emoji: true, style: [t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm], children: name }), _jsx(Text, { style: [t.atoms.text, a.text_sm, a.leading_snug], children: description })] }), (modcause === null || modcause === void 0 ? void 0 : modcause.type) === 'label' && (_jsx(View, { style: [ + xGutters, + a.py_md, + a.border_t, + !IS_NATIVE && t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + { + borderBottomLeftRadius: a.rounded_md.borderRadius, + borderBottomRightRadius: a.rounded_md.borderRadius, + }, + ], children: modcause.source.type === 'user' ? (_jsx(Text, { style: [t.atoms.text, a.text_md, a.leading_snug], children: _jsx(Trans, { children: "This label was applied by the author." }) })) : (_jsx(_Fragment, { children: _jsxs(View, { style: [ + a.flex_row, + a.justify_between, + a.gap_xl, + { paddingBottom: 1 }, + ], children: [_jsx(Text, { style: [ + a.flex_1, + a.leading_snug, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: _jsxs(Trans, { children: ["Source:", ' ', _jsx(InlineLinkText, { label: sourceName, to: makeProfileLink({ + did: modcause.label.src, + handle: '', + }), onPress: function () { return control.close(); }, children: sourceName })] }) }), modcause.label.exp && (_jsx(View, { children: _jsx(Text, { style: [ + a.leading_snug, + a.text_sm, + a.italic, + t.atoms.text_contrast_medium, + ], children: _jsxs(Trans, { children: ["Expires in ", timeDiff(Date.now(), modcause.label.exp)] }) }) }))] }) })) })), IS_NATIVE && _jsx(View, { style: { height: 40 } }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22; diff --git a/src/components/moderation/PostAlerts.js b/src/components/moderation/PostAlerts.js new file mode 100644 index 0000000000..57a0851155 --- /dev/null +++ b/src/components/moderation/PostAlerts.js @@ -0,0 +1,10 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { getModerationCauseKey, unique } from '#/lib/moderation'; +import * as Pills from '#/components/Pills'; +export function PostAlerts(_a) { + var modui = _a.modui, _b = _a.size, size = _b === void 0 ? 'sm' : _b, style = _a.style, additionalCauses = _a.additionalCauses; + if (!modui.alert && !modui.inform && !(additionalCauses === null || additionalCauses === void 0 ? void 0 : additionalCauses.length)) { + return null; + } + return (_jsxs(Pills.Row, { size: size, style: [size === 'sm' && { marginLeft: -3 }, style], children: [modui.alerts.filter(unique).map(function (cause) { return (_jsx(Pills.Label, { cause: cause, size: size, noBg: size === 'sm' }, getModerationCauseKey(cause))); }), modui.informs.filter(unique).map(function (cause) { return (_jsx(Pills.Label, { cause: cause, size: size, noBg: size === 'sm' }, getModerationCauseKey(cause))); }), additionalCauses === null || additionalCauses === void 0 ? void 0 : additionalCauses.map(function (cause) { return (_jsx(Pills.Label, { cause: cause, size: size, noBg: size === 'sm' }, getModerationCauseKey(cause))); })] })); +} diff --git a/src/components/moderation/PostHider.js b/src/components/moderation/PostHider.js new file mode 100644 index 0000000000..5bdc91efeb --- /dev/null +++ b/src/components/moderation/PostHider.js @@ -0,0 +1,102 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { LayoutAnimation, Pressable, StyleSheet, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { useModerationCauseDescription } from '#/lib/moderation/useModerationCauseDescription'; +import { addStyle } from '#/lib/styles'; +import { unstableCacheProfileView } from '#/state/queries/unstable-profile-cache'; +// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up +import { Link } from '#/view/com/util/Link'; +import { atoms as a, useTheme } from '#/alf'; +import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog'; +import { Text } from '#/components/Typography'; +export function PostHider(_a) { + var testID = _a.testID, href = _a.href, disabled = _a.disabled, modui = _a.modui, style = _a.style, hiderStyle = _a.hiderStyle, children = _a.children, iconSize = _a.iconSize, iconStyles = _a.iconStyles, profile = _a.profile, interpretFilterAsBlur = _a.interpretFilterAsBlur, props = __rest(_a, ["testID", "href", "disabled", "modui", "style", "hiderStyle", "children", "iconSize", "iconStyles", "profile", "interpretFilterAsBlur"]); + var queryClient = useQueryClient(); + var t = useTheme(); + var _ = useLingui()._; + var _b = useState(false), override = _b[0], setOverride = _b[1]; + var control = useModerationDetailsDialogControl(); + var blur = modui.blurs[0] || + (interpretFilterAsBlur ? getBlurrableFilter(modui) : undefined); + var desc = useModerationCauseDescription(blur); + var onBeforePress = useCallback(function () { + unstableCacheProfileView(queryClient, profile); + }, [queryClient, profile]); + if (!blur || (disabled && !modui.noOverride)) { + return (_jsx(Link, __assign({ testID: testID, style: style, href: href, accessible: false, onBeforePress: onBeforePress }, props, { children: children }))); + } + return !override ? (_jsxs(Pressable, { onPress: function () { + if (!modui.noOverride) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setOverride(function (v) { return !v; }); + } + }, accessibilityRole: "button", accessibilityLabel: override ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Hides the content"], ["Hides the content"])))) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Shows the content"], ["Shows the content"])))), accessibilityHint: "", style: [ + a.flex_row, + a.align_center, + a.gap_sm, + a.py_md, + { + paddingLeft: 6, + paddingRight: 18, + }, + override ? { paddingBottom: 0 } : undefined, + t.atoms.bg, + hiderStyle, + ], children: [_jsx(ModerationDetailsDialog, { control: control, modcause: blur }), _jsx(Pressable, { onPress: function () { + control.open(); + }, accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Learn more about this warning"], ["Learn more about this warning"])))), accessibilityHint: "", children: _jsx(View, { style: [ + t.atoms.bg_contrast_25, + a.align_center, + a.justify_center, + { + width: iconSize, + height: iconSize, + borderRadius: iconSize, + }, + iconStyles, + ], children: _jsx(desc.icon, { size: "sm", fill: t.atoms.text_contrast_medium.color }) }) }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.flex_1, a.leading_snug], numberOfLines: 1, children: desc.name }), !modui.noOverride && (_jsx(Text, { style: [{ color: t.palette.primary_500 }], children: override ? _jsx(Trans, { children: "Hide" }) : _jsx(Trans, { children: "Show" }) }))] })) : (_jsx(Link, __assign({ testID: testID, style: addStyle(style, styles.child), href: href, accessible: false }, props, { children: children }))); +} +function getBlurrableFilter(modui) { + // moderation causes get "downgraded" when they originate from embedded content + // a downgraded cause should *only* drive filtering in feeds, so we want to look + // for filters that arent downgraded + return modui.filters.find(function (filter) { return !filter.downgraded; }); +} +var styles = StyleSheet.create({ + child: { + borderWidth: 0, + borderTopWidth: 0, + borderRadius: 8, + }, +}); +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/moderation/ProfileHeaderAlerts.js b/src/components/moderation/ProfileHeaderAlerts.js new file mode 100644 index 0000000000..cd20422d54 --- /dev/null +++ b/src/components/moderation/ProfileHeaderAlerts.js @@ -0,0 +1,11 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { getModerationCauseKey, unique } from '#/lib/moderation'; +import * as Pills from '#/components/Pills'; +export function ProfileHeaderAlerts(_a) { + var moderation = _a.moderation, style = _a.style; + var modui = moderation.ui('profileView'); + if (!modui.alert && !modui.inform) { + return null; + } + return (_jsxs(Pills.Row, { size: "lg", style: style, children: [modui.alerts.filter(unique).map(function (cause) { return (_jsx(Pills.Label, { size: "lg", cause: cause }, getModerationCauseKey(cause))); }), modui.informs.filter(unique).map(function (cause) { return (_jsx(Pills.Label, { size: "lg", cause: cause }, getModerationCauseKey(cause))); })] })); +} diff --git a/src/components/moderation/ReportDialog/action.js b/src/components/moderation/ReportDialog/action.js new file mode 100644 index 0000000000..7c7356593c --- /dev/null +++ b/src/components/moderation/ReportDialog/action.js @@ -0,0 +1,144 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation } from '@tanstack/react-query'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { NEW_TO_OLD_REASONS_MAP } from './const'; +export function useSubmitReportMutation() { + var _ = useLingui()._; + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var labeler, labelerSupportedReasonTypes, reasonType, backwardsCompatibleReasonType, supportsNewReasonType, supportsOldReasonType, report; + var subject = _b.subject, state = _b.state; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!state.selectedOption) { + throw new Error(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Please select a reason for this report"], ["Please select a reason for this report"]))))); + } + if (!state.selectedLabeler) { + throw new Error(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please select a moderation service"], ["Please select a moderation service"]))))); + } + labeler = state.selectedLabeler; + labelerSupportedReasonTypes = labeler.reasonTypes || []; + reasonType = state.selectedOption.reason; + backwardsCompatibleReasonType = NEW_TO_OLD_REASONS_MAP[reasonType]; + supportsNewReasonType = labelerSupportedReasonTypes.includes(reasonType); + supportsOldReasonType = labelerSupportedReasonTypes.includes(backwardsCompatibleReasonType); + /* + * Only fall back for backwards compatibility if the labeler + * does not support the new reason type. If the labeler does not declare + * supported reason types, send the new version. + */ + if (supportsOldReasonType && !supportsNewReasonType) { + reasonType = backwardsCompatibleReasonType; + } + switch (subject.type) { + case 'account': { + report = { + reasonType: reasonType, + reason: state.details, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: subject.did, + }, + }; + break; + } + case 'status': + case 'post': + case 'list': + case 'feed': + case 'starterPack': { + report = { + reasonType: reasonType, + reason: state.details, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: subject.uri, + cid: subject.cid, + }, + }; + break; + } + case 'convoMessage': { + report = { + reasonType: reasonType, + reason: state.details, + subject: { + $type: 'chat.bsky.convo.defs#messageRef', + messageId: subject.message.id, + convoId: subject.convoId, + did: subject.message.sender.did, + }, + }; + break; + } + } + if (!__DEV__) return [3 /*break*/, 1]; + logger.info('Submitting report', { + labeler: { + handle: labeler.creator.handle, + }, + report: report, + }); + return [3 /*break*/, 3]; + case 1: return [4 /*yield*/, agent.createModerationReport(report, { + encoding: 'application/json', + headers: { + 'atproto-proxy': "".concat(labeler.creator.did, "#atproto_labeler"), + }, + })]; + case 2: + _c.sent(); + _c.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); + }, + }); +} +var templateObject_1, templateObject_2; diff --git a/src/components/moderation/ReportDialog/const.js b/src/components/moderation/ReportDialog/const.js new file mode 100644 index 0000000000..b3099e5135 --- /dev/null +++ b/src/components/moderation/ReportDialog/const.js @@ -0,0 +1,94 @@ +var _a, _b; +import { ComAtprotoModerationDefs as RootReportDefs, ToolsOzoneReportDefs as OzoneReportDefs, } from '@atproto/api'; +export var DMCA_LINK = 'https://bsky.social/about/support/copyright'; +export var SUPPORT_PAGE = 'https://bsky.social/about/support'; +export var NEW_TO_OLD_REASON_MAPPING = {}; +/** + * Mapping of new (Ozone namespace) reason types to old reason types. + * + * Matches the mapping defined in the Ozone codebase: + * @see https://github.com/bluesky-social/atproto/blob/4c15fb47cec26060bff2e710e95869a90c9d7fdd/packages/ozone/src/mod-service/profile.ts#L16-L64 + */ +export var NEW_TO_OLD_REASONS_MAP = (_a = {}, + _a[OzoneReportDefs.REASONAPPEAL] = RootReportDefs.REASONAPPEAL, + _a[OzoneReportDefs.REASONOTHER] = RootReportDefs.REASONOTHER, + _a[OzoneReportDefs.REASONVIOLENCEANIMAL] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONVIOLENCETHREATS] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONVIOLENCEGRAPHICCONTENT] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONVIOLENCEGLORIFICATION] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONVIOLENCETRAFFICKING] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONVIOLENCEOTHER] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONSEXUALABUSECONTENT] = RootReportDefs.REASONSEXUAL, + _a[OzoneReportDefs.REASONSEXUALNCII] = RootReportDefs.REASONSEXUAL, + _a[OzoneReportDefs.REASONSEXUALDEEPFAKE] = RootReportDefs.REASONSEXUAL, + _a[OzoneReportDefs.REASONSEXUALANIMAL] = RootReportDefs.REASONSEXUAL, + _a[OzoneReportDefs.REASONSEXUALUNLABELED] = RootReportDefs.REASONSEXUAL, + _a[OzoneReportDefs.REASONSEXUALOTHER] = RootReportDefs.REASONSEXUAL, + _a[OzoneReportDefs.REASONCHILDSAFETYCSAM] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONCHILDSAFETYGROOM] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONCHILDSAFETYPRIVACY] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONCHILDSAFETYHARASSMENT] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONCHILDSAFETYOTHER] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONHARASSMENTTROLL] = RootReportDefs.REASONRUDE, + _a[OzoneReportDefs.REASONHARASSMENTTARGETED] = RootReportDefs.REASONRUDE, + _a[OzoneReportDefs.REASONHARASSMENTHATESPEECH] = RootReportDefs.REASONRUDE, + _a[OzoneReportDefs.REASONHARASSMENTDOXXING] = RootReportDefs.REASONRUDE, + _a[OzoneReportDefs.REASONHARASSMENTOTHER] = RootReportDefs.REASONRUDE, + _a[OzoneReportDefs.REASONMISLEADINGBOT] = RootReportDefs.REASONMISLEADING, + _a[OzoneReportDefs.REASONMISLEADINGIMPERSONATION] = RootReportDefs.REASONMISLEADING, + _a[OzoneReportDefs.REASONMISLEADINGSPAM] = RootReportDefs.REASONSPAM, + _a[OzoneReportDefs.REASONMISLEADINGSCAM] = RootReportDefs.REASONMISLEADING, + _a[OzoneReportDefs.REASONMISLEADINGELECTIONS] = RootReportDefs.REASONMISLEADING, + _a[OzoneReportDefs.REASONMISLEADINGOTHER] = RootReportDefs.REASONMISLEADING, + _a[OzoneReportDefs.REASONRULESITESECURITY] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONRULEPROHIBITEDSALES] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONRULEBANEVASION] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONRULEOTHER] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONSELFHARMCONTENT] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONSELFHARMED] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONSELFHARMSTUNTS] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONSELFHARMSUBSTANCES] = RootReportDefs.REASONVIOLATION, + _a[OzoneReportDefs.REASONSELFHARMOTHER] = RootReportDefs.REASONVIOLATION, + _a); +/** + * Mapping of old reason types to new (Ozone namespace) reason types. + * @see https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity#backwards-compatibility + */ +export var OLD_TO_NEW_REASONS_MAP = (_b = {}, + _b[RootReportDefs.REASONSPAM] = [OzoneReportDefs.REASONMISLEADINGSPAM], + _b[RootReportDefs.REASONVIOLATION] = [OzoneReportDefs.REASONRULEOTHER], + _b[RootReportDefs.REASONMISLEADING] = [OzoneReportDefs.REASONMISLEADINGOTHER], + _b[RootReportDefs.REASONSEXUAL] = [OzoneReportDefs.REASONSEXUALUNLABELED], + _b[RootReportDefs.REASONRUDE] = [OzoneReportDefs.REASONHARASSMENTOTHER], + _b[RootReportDefs.REASONOTHER] = [OzoneReportDefs.REASONOTHER], + _b[RootReportDefs.REASONAPPEAL] = [OzoneReportDefs.REASONAPPEAL], + _b); +/** + * Set of report reasons that should optionally include additional details from + * the reporter. + */ +export var OTHER_REPORT_REASONS = new Set([ + OzoneReportDefs.REASONVIOLENCEOTHER, + OzoneReportDefs.REASONSEXUALOTHER, + OzoneReportDefs.REASONCHILDSAFETYOTHER, + OzoneReportDefs.REASONHARASSMENTOTHER, + OzoneReportDefs.REASONMISLEADINGOTHER, + OzoneReportDefs.REASONRULEOTHER, + OzoneReportDefs.REASONSELFHARMOTHER, + OzoneReportDefs.REASONOTHER, +]); +/** + * Set of report reasons that should only be sent to Bluesky's moderation service. + */ +export var BSKY_LABELER_ONLY_REPORT_REASONS = new Set([ + OzoneReportDefs.REASONCHILDSAFETYCSAM, + OzoneReportDefs.REASONCHILDSAFETYGROOM, + OzoneReportDefs.REASONCHILDSAFETYOTHER, + OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, +]); +/** + * Set of _parsed_ subject types that should only be sent to Bluesky's + * moderation service. + */ +export var BSKY_LABELER_ONLY_SUBJECT_TYPES = new Set(['convoMessage', 'status']); diff --git a/src/components/moderation/ReportDialog/copy.js b/src/components/moderation/ReportDialog/copy.js new file mode 100644 index 0000000000..1365cab97b --- /dev/null +++ b/src/components/moderation/ReportDialog/copy.js @@ -0,0 +1,67 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { useMemo } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +export function useCopyForSubject(subject) { + var _ = useLingui()._; + return useMemo(function () { + switch (subject.type) { + case 'account': { + return { + title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Report this user"], ["Report this user"])))), + subtitle: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Why should this user be reviewed?"], ["Why should this user be reviewed?"])))), + }; + } + case 'status': { + return { + title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Report this livestream"], ["Report this livestream"])))), + subtitle: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Why should this livestream be reviewed?"], ["Why should this livestream be reviewed?"])))), + }; + } + case 'post': { + return { + title: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Report this post"], ["Report this post"])))), + subtitle: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Why should this post be reviewed?"], ["Why should this post be reviewed?"])))), + }; + } + case 'list': { + return { + title: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Report this list"], ["Report this list"])))), + subtitle: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Why should this list be reviewed?"], ["Why should this list be reviewed?"])))), + }; + } + case 'feed': { + return { + title: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Report this feed"], ["Report this feed"])))), + subtitle: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Why should this feed be reviewed?"], ["Why should this feed be reviewed?"])))), + }; + } + case 'starterPack': { + return { + title: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Report this starter pack"], ["Report this starter pack"])))), + subtitle: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Why should this starter pack be reviewed?"], ["Why should this starter pack be reviewed?"])))), + }; + } + case 'convoMessage': { + switch (subject.view) { + case 'convo': { + return { + title: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Report this conversation"], ["Report this conversation"])))), + subtitle: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Why should this conversation be reviewed?"], ["Why should this conversation be reviewed?"])))), + }; + } + case 'message': { + return { + title: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Report this message"], ["Report this message"])))), + subtitle: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Why should this message be reviewed?"], ["Why should this message be reviewed?"])))), + }; + } + } + } + } + }, [_, subject]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16; diff --git a/src/components/moderation/ReportDialog/index.js b/src/components/moderation/ReportDialog/index.js new file mode 100644 index 0000000000..3578ae673a --- /dev/null +++ b/src/components/moderation/ReportDialog/index.js @@ -0,0 +1,465 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { Pressable, View } from 'react-native'; +import { BSKY_LABELER_DID } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { wait } from '#/lib/async/wait'; +import { getLabelingServiceTitle } from '#/lib/moderation'; +import { useCallOnce } from '#/lib/once'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useMyLabelersQuery } from '#/state/queries/preferences'; +import { CharProgress } from '#/view/com/composer/char-progress/CharProgress'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useGutters, useTheme } from '#/alf'; +import * as Admonition from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { useDelayedLoading } from '#/components/hooks/useDelayedLoading'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry } from '#/components/icons/ArrowRotate'; +import { Check_Stroke2_Corner0_Rounded as CheckThin, CheckThick_Stroke2_Corner0_Rounded as Check, } from '#/components/icons/Check'; +import { PaperPlane_Stroke2_Corner0_Rounded as PaperPlane } from '#/components/icons/PaperPlane'; +import { SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight } from '#/components/icons/SquareArrowTopRight'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { createStaticClick, InlineLinkText, Link } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +import { useSubmitReportMutation } from './action'; +import { BSKY_LABELER_ONLY_REPORT_REASONS, BSKY_LABELER_ONLY_SUBJECT_TYPES, NEW_TO_OLD_REASONS_MAP, SUPPORT_PAGE, } from './const'; +import { useCopyForSubject } from './copy'; +import { initialState, reducer } from './state'; +import { parseReportSubject } from './utils/parseReportSubject'; +import { useReportOptions, } from './utils/useReportOptions'; +export { useDialogControl as useReportDialogControl } from '#/components/Dialog'; +export function useGlobalReportDialogControl() { + return useGlobalDialogsControlContext().reportDialogControl; +} +export function GlobalReportDialog() { + var _a = useGlobalReportDialogControl(), value = _a.value, control = _a.control; + return _jsx(ReportDialog, { control: control, subject: value === null || value === void 0 ? void 0 : value.subject }); +} +export function ReportDialog(props) { + var ax = useAnalytics(); + var subject = React.useMemo(function () { return (props.subject ? parseReportSubject(props.subject) : undefined); }, [props.subject]); + var onClose = React.useCallback(function () { + ax.metric('reportDialog:close', {}); + }, [ax]); + return (_jsxs(Dialog.Outer, { control: props.control, onClose: onClose, children: [_jsx(Dialog.Handle, {}), subject ? _jsx(Inner, __assign({}, props, { subject: subject })) : _jsx(Invalid, {})] })); +} +/** + * This should only be shown if the dialog is configured incorrectly by a + * developer, but nevertheless we should have a graceful fallback. + */ +function Invalid() { + var _ = useLingui()._; + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Report dialog"], ["Report dialog"])))), children: [_jsx(Text, { style: [a.font_bold, a.text_xl, a.leading_snug, a.pb_xs], children: _jsx(Trans, { children: "Invalid report subject" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "Something wasn't quite right with the data you're trying to report. Please contact support." }) }), _jsx(Dialog.Close, {})] })); +} +function Inner(props) { + var _this = this; + var _a, _b, _c; + var ax = useAnalytics(); + var logger = ax.logger.useChild(ax.logger.Context.ReportDialog); + var t = useTheme(); + var _ = useLingui()._; + var ref = React.useRef(null); + var _d = useMyLabelersQuery({ excludeNonConfigurableLabelers: true }), allLabelers = _d.data, isLabelerLoading = _d.isLoading, labelersLoadError = _d.error, refetchLabelers = _d.refetch; + var isLoading = useDelayedLoading(500, isLabelerLoading); + var copy = useCopyForSubject(props.subject); + var _e = useReportOptions(), categories = _e.categories, getCategory = _e.getCategory; + var _f = React.useReducer(reducer, initialState), state = _f[0], dispatch = _f[1]; + /** + * Submission handling + */ + var submitReport = useSubmitReportMutation().mutateAsync; + var _g = React.useState(false), isPending = _g[0], setPending = _g[1]; + var _h = React.useState(false), isSuccess = _h[0], setSuccess = _h[1]; + // some reasons ONLY go to Bluesky + var isBskyOnlyReason = ((_a = state === null || state === void 0 ? void 0 : state.selectedOption) === null || _a === void 0 ? void 0 : _a.reason) + ? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason) + : false; + // some subjects ONLY go to Bluesky + var isBskyOnlySubject = BSKY_LABELER_ONLY_SUBJECT_TYPES.has(props.subject.type); + /** + * Labelers that support this `subject` and its NSID collection + */ + var supportedLabelers = React.useMemo(function () { + if (!allLabelers) + return []; + return allLabelers + .filter(function (l) { + var subjectTypes = l.subjectTypes; + if (subjectTypes === undefined) + return true; + if (props.subject.type === 'account') { + return subjectTypes.includes('account'); + } + else if (props.subject.type === 'convoMessage') { + return subjectTypes.includes('chat'); + } + else { + return subjectTypes.includes('record'); + } + }) + .filter(function (l) { + var collections = l.subjectCollections; + if (collections === undefined) + return true; + // all chat collections accepted, since only Bluesky handles chats + if (props.subject.type === 'convoMessage') + return true; + return collections.includes(props.subject.nsid); + }) + .filter(function (l) { + if (!state.selectedOption) + return false; + if (isBskyOnlyReason || isBskyOnlySubject) { + return l.creator.did === BSKY_LABELER_DID; + } + var supportedReasonTypes = l.reasonTypes; + if (supportedReasonTypes === undefined) + return true; + return ( + // supports new reason type + supportedReasonTypes.includes(state.selectedOption.reason) || // supports old reason type (backwards compat) + supportedReasonTypes.includes(NEW_TO_OLD_REASONS_MAP[state.selectedOption.reason])); + }); + }, [ + props, + allLabelers, + state.selectedOption, + isBskyOnlyReason, + isBskyOnlySubject, + ]); + var hasSupportedLabelers = !!supportedLabelers.length; + var hasSingleSupportedLabeler = supportedLabelers.length === 1; + /** + * We skip the select labeler step if there's only one possible labeler, and + * that labeler is Bluesky (which is the case for chat reports and certain + * reason types). We'll use this below to adjust the indexing and skip the + * step in the UI. + */ + var isAlwaysBskyLabeler = hasSingleSupportedLabeler && (isBskyOnlyReason || isBskyOnlySubject); + var onSubmit = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + var _a, _b, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + dispatch({ type: 'clearError' }); + logger.info('submitting'); + _e.label = 1; + case 1: + _e.trys.push([1, 3, 4, 5]); + setPending(true); + // wait at least 1s, make it feel substantial + return [4 /*yield*/, wait(1e3, submitReport({ + subject: props.subject, + state: state, + }))]; + case 2: + // wait at least 1s, make it feel substantial + _e.sent(); + setSuccess(true); + ax.metric('reportDialog:success', { + reason: (_b = (_a = state.selectedOption) === null || _a === void 0 ? void 0 : _a.reason) !== null && _b !== void 0 ? _b : '', + labeler: (_d = (_c = state.selectedLabeler) === null || _c === void 0 ? void 0 : _c.creator.handle) !== null && _d !== void 0 ? _d : '', + details: !!state.details, + }); + // give time for user feedback + setTimeout(function () { + props.control.close(function () { + var _a; + (_a = props.onAfterSubmit) === null || _a === void 0 ? void 0 : _a.call(props); + }); + }, 1e3); + return [3 /*break*/, 5]; + case 3: + e_1 = _e.sent(); + ax.metric('reportDialog:failure', {}); + logger.error(e_1, { + source: 'ReportDialog', + }); + dispatch({ + type: 'setError', + error: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Something went wrong. Please try again."], ["Something went wrong. Please try again."])))), + }); + return [3 /*break*/, 5]; + case 4: + setPending(false); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }, [_, submitReport, state, dispatch, props, setPending, setSuccess]); + useCallOnce(function () { + ax.metric('reportDialog:open', { + subjectType: props.subject.type, + }); + })(); + return (_jsxs(Dialog.ScrollableInner, { testID: "report:dialog", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Report dialog"], ["Report dialog"])))), ref: ref, style: [a.w_full, { maxWidth: 500 }], children: [_jsxs(View, { style: [a.gap_2xl, IS_NATIVE && a.pt_md], children: [_jsxs(StepOuter, { children: [_jsx(StepTitle, { index: 1, title: copy.subtitle, activeIndex1: state.activeStepIndex1 }), isLoading ? (_jsxs(View, { style: [a.gap_sm], children: [_jsx(OptionCardSkeleton, {}), _jsx(OptionCardSkeleton, {}), _jsx(OptionCardSkeleton, {}), _jsx(OptionCardSkeleton, {}), _jsx(OptionCardSkeleton, {}), _jsx(Pressable, { accessible: false })] })) : labelersLoadError || !allLabelers ? (_jsx(Admonition.Outer, { type: "error", children: _jsxs(Admonition.Row, { children: [_jsx(Admonition.Icon, {}), _jsx(Admonition.Content, { children: _jsx(Admonition.Text, { children: _jsx(Trans, { children: "Something went wrong, please try again" }) }) }), _jsxs(Admonition.Button, { color: "negative_subtle", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Retry loading report options"], ["Retry loading report options"])))), onPress: function () { return refetchLabelers(); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), _jsx(ButtonIcon, { icon: Retry })] })] }) })) : (_jsx(_Fragment, { children: state.selectedCategory ? (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_md], children: [_jsx(View, { style: [a.flex_1], children: _jsx(CategoryCard, { option: state.selectedCategory }) }), _jsx(Button, { testID: "report:clearCategory", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Change report category"], ["Change report category"])))), size: "tiny", variant: "solid", color: "secondary", shape: "round", onPress: function () { + dispatch({ type: 'clearCategory' }); + }, children: _jsx(ButtonIcon, { icon: X }) })] })) : (_jsxs(View, { style: [a.gap_sm], children: [categories.map(function (o) { return (_jsx(CategoryCard, { option: o, onSelect: function () { + dispatch({ + type: 'selectCategory', + option: o, + otherOption: getCategory('other').options[0], + }); + } }, o.key)); }), ['post', 'account'].includes(props.subject.type) && (_jsx(Link, { to: SUPPORT_PAGE, label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Need to report a copyright violation, legal request, or regulatory compliance issue?"], ["Need to report a copyright violation, legal request, or regulatory compliance issue?"])))), children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.w_full, + a.px_md, + a.py_sm, + a.rounded_sm, + a.border, + hovered || pressed + ? [t.atoms.border_contrast_high] + : [t.atoms.border_contrast_low], + ], children: [_jsx(Text, { style: [a.flex_1, a.italic, a.leading_snug], children: _jsx(Trans, { children: "Need to report a copyright violation, legal request, or regulatory compliance issue?" }) }), _jsx(SquareArrowTopRight, { size: "sm", fill: t.atoms.text.color })] })); + } }))] })) }))] }), _jsxs(StepOuter, { children: [_jsx(StepTitle, { index: 2, title: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Select a reason"], ["Select a reason"])))), activeIndex1: state.activeStepIndex1 }), state.selectedOption ? (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_md], children: [_jsx(View, { style: [a.flex_1], children: _jsx(OptionCard, { option: state.selectedOption }) }), _jsx(Button, { testID: "report:clearReportOption", label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Change report reason"], ["Change report reason"])))), size: "tiny", variant: "solid", color: "secondary", shape: "round", onPress: function () { + dispatch({ type: 'clearOption' }); + }, children: _jsx(ButtonIcon, { icon: X }) })] })) : state.selectedCategory ? (_jsx(View, { style: [a.gap_sm], children: getCategory(state.selectedCategory.key).options.map(function (o) { return (_jsx(OptionCard, { option: o, onSelect: function () { + dispatch({ type: 'selectOption', option: o }); + } }, o.reason)); }) })) : null] }), isAlwaysBskyLabeler ? (_jsx(ActionOnce, { check: function () { return !state.selectedLabeler; }, callback: function () { + dispatch({ + type: 'selectLabeler', + labeler: supportedLabelers[0], + }); + } })) : (_jsxs(StepOuter, { children: [_jsx(StepTitle, { index: 3, title: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Select moderation service"], ["Select moderation service"])))), activeIndex1: state.activeStepIndex1 }), state.activeStepIndex1 >= 3 && (_jsx(_Fragment, { children: state.selectedLabeler ? (_jsx(_Fragment, { children: hasSingleSupportedLabeler ? (_jsx(LabelerCard, { labeler: state.selectedLabeler })) : (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_md], children: [_jsx(View, { style: [a.flex_1], children: _jsx(LabelerCard, { labeler: state.selectedLabeler }) }), _jsx(Button, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Change moderation service"], ["Change moderation service"])))), size: "tiny", variant: "solid", color: "secondary", shape: "round", onPress: function () { + dispatch({ type: 'clearLabeler' }); + }, children: _jsx(ButtonIcon, { icon: X }) })] })) })) : (_jsx(_Fragment, { children: hasSupportedLabelers ? (_jsx(View, { style: [a.gap_sm], children: hasSingleSupportedLabeler ? (_jsxs(_Fragment, { children: [_jsx(LabelerCard, { labeler: supportedLabelers[0] }), _jsx(ActionOnce, { check: function () { return !state.selectedLabeler; }, callback: function () { + dispatch({ + type: 'selectLabeler', + labeler: supportedLabelers[0], + }); + } })] })) : (_jsx(_Fragment, { children: supportedLabelers.map(function (l) { return (_jsx(LabelerCard, { labeler: l, onSelect: function () { + dispatch({ type: 'selectLabeler', labeler: l }); + } }, l.creator.did)); }) })) })) : ( + // should never happen in our app + _jsx(Admonition.Admonition, { type: "warning", children: _jsx(Trans, { children: "Unfortunately, none of your subscribed labelers supports this report type." }) })) })) }))] })), _jsxs(StepOuter, { children: [_jsx(StepTitle, { index: isAlwaysBskyLabeler ? 3 : 4, title: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Submit report"], ["Submit report"])))), activeIndex1: isAlwaysBskyLabeler + ? state.activeStepIndex1 - 1 + : state.activeStepIndex1 }), state.activeStepIndex1 === 4 && (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.pb_xs, a.gap_xs], children: [_jsxs(Text, { style: [a.leading_snug, a.pb_xs], children: [_jsxs(Trans, { children: ["Your report will be sent to", ' ', _jsx(Text, { style: [a.font_semi_bold, a.leading_snug], children: (_b = state.selectedLabeler) === null || _b === void 0 ? void 0 : _b.creator.displayName }), "."] }), ' ', !state.detailsOpen ? (_jsx(InlineLinkText, __assign({ label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Add more details (optional)"], ["Add more details (optional)"])))) }, createStaticClick(function () { + dispatch({ type: 'showDetails' }); + }), { children: _jsx(Trans, { children: "Add more details (optional)" }) }))) : null] }), state.detailsOpen && (_jsxs(View, { children: [_jsx(Dialog.Input, { testID: "report:details", multiline: true, value: state.details, onChangeText: function (details) { + dispatch({ type: 'setDetails', details: details }); + }, label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Additional details (limit 300 characters)"], ["Additional details (limit 300 characters)"])))), style: { paddingRight: 60 }, numberOfLines: 4 }), _jsx(View, { style: [ + a.absolute, + a.flex_row, + a.align_center, + a.pr_md, + a.pb_sm, + { + bottom: 0, + right: 0, + }, + ], children: _jsx(CharProgress, { count: ((_c = state.details) === null || _c === void 0 ? void 0 : _c.length) || 0 }) })] }))] }), _jsxs(Button, { testID: "report:submit", label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Submit report"], ["Submit report"])))), size: "large", variant: "solid", color: "primary", disabled: isPending || isSuccess, onPress: onSubmit, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Submit report" }) }), _jsx(ButtonIcon, { icon: isSuccess ? CheckThin : isPending ? Loader : PaperPlane })] }), state.error && (_jsx(Admonition.Admonition, { type: "error", children: state.error }))] }))] })] }), _jsx(Dialog.Close, {})] })); +} +function ActionOnce(_a) { + var check = _a.check, callback = _a.callback; + React.useEffect(function () { + if (check()) { + callback(); + } + }, [check, callback]); + return null; +} +function StepOuter(_a) { + var children = _a.children; + return _jsx(View, { style: [a.gap_md, a.w_full], children: children }); +} +function StepTitle(_a) { + var index = _a.index, title = _a.title, activeIndex1 = _a.activeIndex1; + var t = useTheme(); + var active = activeIndex1 === index; + var completed = activeIndex1 > index; + return (_jsxs(View, { style: [a.flex_row, a.gap_sm, a.pr_3xl], children: [_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + a.border, + { + width: 24, + height: 24, + backgroundColor: active + ? t.palette.primary_500 + : completed + ? t.palette.primary_100 + : t.atoms.bg_contrast_25.backgroundColor, + borderColor: active + ? t.palette.primary_500 + : completed + ? t.palette.primary_400 + : t.atoms.border_contrast_low.borderColor, + }, + ], children: completed ? (_jsx(Check, { width: 12 })) : (_jsx(Text, { style: [ + a.font_bold, + a.text_center, + t.atoms.text, + { + color: active + ? 'white' + : completed + ? t.palette.primary_700 + : t.atoms.text_contrast_medium.color, + fontVariant: ['tabular-nums'], + width: 24, + height: 24, + lineHeight: 24, + }, + ], children: index })) }), _jsx(Text, { style: [ + a.flex_1, + a.font_bold, + a.text_lg, + a.leading_snug, + active ? t.atoms.text : t.atoms.text_contrast_medium, + { + top: 1, + }, + ], children: title })] })); +} +function CategoryCard(_a) { + var option = _a.option, onSelect = _a.onSelect; + var t = useTheme(); + var _ = useLingui()._; + var gutters = useGutters(['compact']); + var onPress = React.useCallback(function () { + onSelect === null || onSelect === void 0 ? void 0 : onSelect(option); + }, [onSelect, option]); + return (_jsx(Button, { testID: "report:category:".concat(option.title), label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Create report for ", ""], ["Create report for ", ""])), option.title)), onPress: onPress, disabled: !onSelect, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.w_full, + gutters, + a.py_sm, + a.rounded_sm, + a.border, + t.atoms.bg_contrast_25, + hovered || pressed + ? [t.atoms.border_contrast_high] + : [t.atoms.border_contrast_low], + ], children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold, a.leading_snug], children: option.title }), _jsx(Text, { style: [a.text_sm, , a.leading_snug, t.atoms.text_contrast_medium], children: option.description })] })); + } })); +} +function OptionCard(_a) { + var option = _a.option, onSelect = _a.onSelect; + var t = useTheme(); + var _ = useLingui()._; + var gutters = useGutters(['compact']); + var onPress = React.useCallback(function () { + onSelect === null || onSelect === void 0 ? void 0 : onSelect(option); + }, [onSelect, option]); + return (_jsx(Button, { testID: "report:option:".concat(option.title), label: _(msg({ + message: "Create report for ".concat(option.title), + comment: 'Accessibility label for button to create a moderation report for the selected option', + })), onPress: onPress, disabled: !onSelect, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(View, { style: [ + a.w_full, + gutters, + a.py_sm, + a.rounded_sm, + a.border, + t.atoms.bg_contrast_25, + hovered || pressed + ? [t.atoms.border_contrast_high] + : [t.atoms.border_contrast_low], + ], children: _jsx(Text, { style: [a.text_md, a.font_semi_bold, a.leading_snug], children: option.title }) })); + } })); +} +function OptionCardSkeleton() { + var t = useTheme(); + return (_jsx(View, { style: [ + a.w_full, + a.rounded_sm, + a.border, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + { height: 55 }, // magic, based on web + ] })); +} +function LabelerCard(_a) { + var labeler = _a.labeler, onSelect = _a.onSelect; + var t = useTheme(); + var _ = useLingui()._; + var onPress = React.useCallback(function () { + onSelect === null || onSelect === void 0 ? void 0 : onSelect(labeler); + }, [onSelect, labeler]); + var title = getLabelingServiceTitle({ + displayName: labeler.creator.displayName, + handle: labeler.creator.handle, + }); + return (_jsx(Button, { testID: "report:labeler:".concat(labeler.creator.handle), label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Send report to ", ""], ["Send report to ", ""])), title)), onPress: onPress, disabled: !onSelect, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.w_full, + a.p_sm, + a.flex_row, + a.align_center, + a.gap_sm, + a.rounded_md, + a.border, + t.atoms.bg_contrast_25, + hovered || pressed + ? [t.atoms.border_contrast_high] + : [t.atoms.border_contrast_low], + ], children: [_jsx(UserAvatar, { type: "labeler", size: 36, avatar: labeler.creator.avatar }), _jsxs(View, { style: [a.flex_1], children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold, a.leading_snug], children: title }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["By ", sanitizeHandle(labeler.creator.handle, '@')] }) })] })] })); + } })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16; diff --git a/src/components/moderation/ReportDialog/state.js b/src/components/moderation/ReportDialog/state.js new file mode 100644 index 0000000000..fe1e2351a2 --- /dev/null +++ b/src/components/moderation/ReportDialog/state.js @@ -0,0 +1,47 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { OTHER_REPORT_REASONS } from '#/components/moderation/ReportDialog/const'; +export var initialState = { + selectedCategory: undefined, + selectedOption: undefined, + selectedLabeler: undefined, + details: undefined, + detailsOpen: false, + activeStepIndex1: 1, +}; +export function reducer(state, action) { + var _a; + switch (action.type) { + case 'selectCategory': + return __assign(__assign({}, state), { selectedCategory: action.option, activeStepIndex1: action.option.key === 'other' ? 3 : 2, selectedOption: action.option.key === 'other' ? action.otherOption : undefined }); + case 'clearCategory': + return __assign(__assign({}, state), { selectedCategory: undefined, selectedOption: undefined, selectedLabeler: undefined, activeStepIndex1: 1, detailsOpen: false }); + case 'selectOption': + return __assign(__assign({}, state), { selectedOption: action.option, activeStepIndex1: 3, detailsOpen: OTHER_REPORT_REASONS.has(action.option.reason) }); + case 'clearOption': + return __assign(__assign({}, state), { selectedOption: undefined, selectedLabeler: undefined, activeStepIndex1: 2, detailsOpen: false }); + case 'selectLabeler': + return __assign(__assign({}, state), { selectedLabeler: action.labeler, activeStepIndex1: 4, detailsOpen: state.selectedOption + ? OTHER_REPORT_REASONS.has((_a = state.selectedOption) === null || _a === void 0 ? void 0 : _a.reason) + : false }); + case 'clearLabeler': + return __assign(__assign({}, state), { selectedLabeler: undefined, activeStepIndex1: 3 }); + case 'setDetails': + return __assign(__assign({}, state), { details: action.details }); + case 'setError': + return __assign(__assign({}, state), { error: action.error }); + case 'clearError': + return __assign(__assign({}, state), { error: undefined }); + case 'showDetails': + return __assign(__assign({}, state), { detailsOpen: true }); + } +} diff --git a/src/components/moderation/ReportDialog/types.js b/src/components/moderation/ReportDialog/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/moderation/ReportDialog/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.js b/src/components/moderation/ReportDialog/utils/parseReportSubject.js new file mode 100644 index 0000000000..763807b62e --- /dev/null +++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.js @@ -0,0 +1,88 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { AppBskyActorDefs, AppBskyFeedDefs, AppBskyFeedPost, AppBskyGraphDefs, } from '@atproto/api'; +import * as bsky from '#/types/bsky'; +export function parseReportSubject(subject) { + if (!subject) + return; + if ('convoId' in subject) { + return __assign({ type: 'convoMessage' }, subject); + } + if (AppBskyActorDefs.isProfileViewBasic(subject) || + AppBskyActorDefs.isProfileView(subject) || + AppBskyActorDefs.isProfileViewDetailed(subject)) { + return { + type: 'account', + did: subject.did, + nsid: 'app.bsky.actor.profile', + }; + } + else if (AppBskyActorDefs.isStatusView(subject)) { + if (!subject.uri || !subject.cid) + return; + return { + type: 'status', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.actor.status', + }; + } + else if (AppBskyGraphDefs.isListView(subject)) { + return { + type: 'list', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.graph.list', + }; + } + else if (AppBskyFeedDefs.isGeneratorView(subject)) { + return { + type: 'feed', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.feed.generator', + }; + } + else if (AppBskyGraphDefs.isStarterPackView(subject)) { + return { + type: 'starterPack', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.graph.starterPack', + }; + } + else if (AppBskyFeedDefs.isPostView(subject)) { + var record = subject.record; + var embed = bsky.post.parseEmbed(subject.embed); + if (bsky.dangerousIsType(record, AppBskyFeedPost.isRecord)) { + return { + type: 'post', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.feed.post', + attributes: { + reply: !!record.reply, + image: embed.type === 'images' || + (embed.type === 'post_with_media' && embed.media.type === 'images'), + video: embed.type === 'video' || + (embed.type === 'post_with_media' && embed.media.type === 'video'), + link: embed.type === 'link' || + (embed.type === 'post_with_media' && embed.media.type === 'link'), + quote: embed.type === 'post' || + (embed.type === 'post_with_media' && + (embed.view.type === 'post' || + embed.view.type === 'post_with_media')), + }, + }; + } + } +} diff --git a/src/components/moderation/ReportDialog/utils/useReportOptions.js b/src/components/moderation/ReportDialog/utils/useReportOptions.js new file mode 100644 index 0000000000..376e2fb12d --- /dev/null +++ b/src/components/moderation/ReportDialog/utils/useReportOptions.js @@ -0,0 +1,234 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { useMemo } from 'react'; +import { ToolsOzoneReportDefs as OzoneReportDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +export function useReportOptions() { + var _ = useLingui()._; + return useMemo(function () { + var categories = { + misleading: { + key: 'misleading', + title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Misleading"], ["Misleading"])))), + description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Spam or other inauthentic behavior or deception"], ["Spam or other inauthentic behavior or deception"])))), + options: [ + { + title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Spam"], ["Spam"])))), + reason: OzoneReportDefs.REASONMISLEADINGSPAM, + }, + { + title: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Scam"], ["Scam"])))), + reason: OzoneReportDefs.REASONMISLEADINGSCAM, + }, + { + title: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Fake account or bot"], ["Fake account or bot"])))), + reason: OzoneReportDefs.REASONMISLEADINGBOT, + }, + { + title: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Impersonation"], ["Impersonation"])))), + reason: OzoneReportDefs.REASONMISLEADINGIMPERSONATION, + }, + { + title: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["False information about elections"], ["False information about elections"])))), + reason: OzoneReportDefs.REASONMISLEADINGELECTIONS, + }, + { + title: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Other misleading content"], ["Other misleading content"])))), + reason: OzoneReportDefs.REASONMISLEADINGOTHER, + }, + ], + }, + sexualAdultContent: { + key: 'sexualAdultContent', + title: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Adult content"], ["Adult content"])))), + description: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Unlabeled, abusive, or non-consensual adult content"], ["Unlabeled, abusive, or non-consensual adult content"])))), + options: [ + { + title: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Unlabeled adult content"], ["Unlabeled adult content"])))), + reason: OzoneReportDefs.REASONSEXUALUNLABELED, + }, + { + title: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Adult sexual abuse content"], ["Adult sexual abuse content"])))), + reason: OzoneReportDefs.REASONSEXUALABUSECONTENT, + }, + { + title: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Non-consensual intimate imagery"], ["Non-consensual intimate imagery"])))), + reason: OzoneReportDefs.REASONSEXUALNCII, + }, + { + title: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Deepfake adult content"], ["Deepfake adult content"])))), + reason: OzoneReportDefs.REASONSEXUALDEEPFAKE, + }, + { + title: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Animal sexual abuse"], ["Animal sexual abuse"])))), + reason: OzoneReportDefs.REASONSEXUALANIMAL, + }, + { + title: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Other sexual violence content"], ["Other sexual violence content"])))), + reason: OzoneReportDefs.REASONSEXUALOTHER, + }, + ], + }, + harassmentHate: { + key: 'harassmentHate', + title: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Harassment or hate"], ["Harassment or hate"])))), + description: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Abusive or discriminatory behavior"], ["Abusive or discriminatory behavior"])))), + options: [ + { + title: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Trolling"], ["Trolling"])))), + reason: OzoneReportDefs.REASONHARASSMENTTROLL, + }, + { + title: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Targeted harassment"], ["Targeted harassment"])))), + reason: OzoneReportDefs.REASONHARASSMENTTARGETED, + }, + { + title: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Hate speech"], ["Hate speech"])))), + reason: OzoneReportDefs.REASONHARASSMENTHATESPEECH, + }, + { + title: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Doxxing"], ["Doxxing"])))), + reason: OzoneReportDefs.REASONHARASSMENTDOXXING, + }, + { + title: _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Other harassing or hateful content"], ["Other harassing or hateful content"])))), + reason: OzoneReportDefs.REASONHARASSMENTOTHER, + }, + ], + }, + violencePhysicalHarm: { + key: 'violencePhysicalHarm', + title: _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Violence"], ["Violence"])))), + description: _(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Violent or threatening content"], ["Violent or threatening content"])))), + options: [ + { + title: _(msg(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Animal welfare"], ["Animal welfare"])))), + reason: OzoneReportDefs.REASONVIOLENCEANIMAL, + }, + { + title: _(msg(templateObject_27 || (templateObject_27 = __makeTemplateObject(["Threats or incitement"], ["Threats or incitement"])))), + reason: OzoneReportDefs.REASONVIOLENCETHREATS, + }, + { + title: _(msg(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Graphic violent content"], ["Graphic violent content"])))), + reason: OzoneReportDefs.REASONVIOLENCEGRAPHICCONTENT, + }, + { + title: _(msg(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Glorification of violence"], ["Glorification of violence"])))), + reason: OzoneReportDefs.REASONVIOLENCEGLORIFICATION, + }, + { + title: _(msg(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Extremist content"], ["Extremist content"])))), + reason: OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, + }, + { + title: _(msg(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Human trafficking"], ["Human trafficking"])))), + reason: OzoneReportDefs.REASONVIOLENCETRAFFICKING, + }, + { + title: _(msg(templateObject_32 || (templateObject_32 = __makeTemplateObject(["Other violent content"], ["Other violent content"])))), + reason: OzoneReportDefs.REASONVIOLENCEOTHER, + }, + ], + }, + childSafety: { + key: 'childSafety', + title: _(msg(templateObject_33 || (templateObject_33 = __makeTemplateObject(["Child safety"], ["Child safety"])))), + description: _(msg(templateObject_34 || (templateObject_34 = __makeTemplateObject(["Harming or endangering minors"], ["Harming or endangering minors"])))), + options: [ + { + title: _(msg(templateObject_35 || (templateObject_35 = __makeTemplateObject(["Child Sexual Abuse Material (CSAM)"], ["Child Sexual Abuse Material (CSAM)"])))), + reason: OzoneReportDefs.REASONCHILDSAFETYCSAM, + }, + { + title: _(msg(templateObject_36 || (templateObject_36 = __makeTemplateObject(["Grooming or predatory behavior"], ["Grooming or predatory behavior"])))), + reason: OzoneReportDefs.REASONCHILDSAFETYGROOM, + }, + { + title: _(msg(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Privacy violation of a minor"], ["Privacy violation of a minor"])))), + reason: OzoneReportDefs.REASONCHILDSAFETYPRIVACY, + }, + { + title: _(msg(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Minor harassment or bullying"], ["Minor harassment or bullying"])))), + reason: OzoneReportDefs.REASONCHILDSAFETYHARASSMENT, + }, + { + title: _(msg(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Other child safety issue"], ["Other child safety issue"])))), + reason: OzoneReportDefs.REASONCHILDSAFETYOTHER, + }, + ], + }, + selfHarm: { + key: 'selfHarm', + title: _(msg(templateObject_40 || (templateObject_40 = __makeTemplateObject(["Self-harm or dangerous behaviors"], ["Self-harm or dangerous behaviors"])))), + description: _(msg(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Harmful or high-risk activities"], ["Harmful or high-risk activities"])))), + options: [ + { + title: _(msg(templateObject_42 || (templateObject_42 = __makeTemplateObject(["Content promoting or depicting self-harm"], ["Content promoting or depicting self-harm"])))), + reason: OzoneReportDefs.REASONSELFHARMCONTENT, + }, + { + title: _(msg(templateObject_43 || (templateObject_43 = __makeTemplateObject(["Eating disorders"], ["Eating disorders"])))), + reason: OzoneReportDefs.REASONSELFHARMED, + }, + { + title: _(msg(templateObject_44 || (templateObject_44 = __makeTemplateObject(["Dangerous challenges or activities"], ["Dangerous challenges or activities"])))), + reason: OzoneReportDefs.REASONSELFHARMSTUNTS, + }, + { + title: _(msg(templateObject_45 || (templateObject_45 = __makeTemplateObject(["Dangerous substances or drug abuse"], ["Dangerous substances or drug abuse"])))), + reason: OzoneReportDefs.REASONSELFHARMSUBSTANCES, + }, + { + title: _(msg(templateObject_46 || (templateObject_46 = __makeTemplateObject(["Other dangerous content"], ["Other dangerous content"])))), + reason: OzoneReportDefs.REASONSELFHARMOTHER, + }, + ], + }, + ruleBreaking: { + key: 'ruleBreaking', + title: _(msg(templateObject_47 || (templateObject_47 = __makeTemplateObject(["Breaking site rules"], ["Breaking site rules"])))), + description: _(msg(templateObject_48 || (templateObject_48 = __makeTemplateObject(["Banned activities or security violations"], ["Banned activities or security violations"])))), + options: [ + { + title: _(msg(templateObject_49 || (templateObject_49 = __makeTemplateObject(["Hacking or system attacks"], ["Hacking or system attacks"])))), + reason: OzoneReportDefs.REASONRULESITESECURITY, + }, + { + title: _(msg(templateObject_50 || (templateObject_50 = __makeTemplateObject(["Promoting or selling prohibited items or services"], ["Promoting or selling prohibited items or services"])))), + reason: OzoneReportDefs.REASONRULEPROHIBITEDSALES, + }, + { + title: _(msg(templateObject_51 || (templateObject_51 = __makeTemplateObject(["Banned user returning"], ["Banned user returning"])))), + reason: OzoneReportDefs.REASONRULEBANEVASION, + }, + { + title: _(msg(templateObject_52 || (templateObject_52 = __makeTemplateObject(["Other network rule-breaking"], ["Other network rule-breaking"])))), + reason: OzoneReportDefs.REASONRULEOTHER, + }, + ], + }, + other: { + key: 'other', + title: _(msg(templateObject_53 || (templateObject_53 = __makeTemplateObject(["Other"], ["Other"])))), + description: _(msg(templateObject_54 || (templateObject_54 = __makeTemplateObject(["An issue not included in these options"], ["An issue not included in these options"])))), + options: [ + { + title: _(msg(templateObject_55 || (templateObject_55 = __makeTemplateObject(["Other"], ["Other"])))), + reason: OzoneReportDefs.REASONOTHER, + }, + ], + }, + }; + return { + categories: Object.values(categories), + getCategory: function (reasonName) { + return categories[reasonName]; + }, + }; + }, [_]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26, templateObject_27, templateObject_28, templateObject_29, templateObject_30, templateObject_31, templateObject_32, templateObject_33, templateObject_34, templateObject_35, templateObject_36, templateObject_37, templateObject_38, templateObject_39, templateObject_40, templateObject_41, templateObject_42, templateObject_43, templateObject_44, templateObject_45, templateObject_46, templateObject_47, templateObject_48, templateObject_49, templateObject_50, templateObject_51, templateObject_52, templateObject_53, templateObject_54, templateObject_55; diff --git a/src/components/moderation/ScreenHider.js b/src/components/moderation/ScreenHider.js new file mode 100644 index 0000000000..8cad75b3b8 --- /dev/null +++ b/src/components/moderation/ScreenHider.js @@ -0,0 +1,92 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { TouchableWithoutFeedback, View, } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useWebMediaQueries } from '#/lib/hooks/useWebMediaQueries'; +import { useModerationCauseDescription } from '#/lib/moderation/useModerationCauseDescription'; +import { CenteredView } from '#/view/com/util/Views'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog'; +import { Text } from '#/components/Typography'; +export function ScreenHider(_a) { + var testID = _a.testID, screenDescription = _a.screenDescription, modui = _a.modui, style = _a.style, containerStyle = _a.containerStyle, children = _a.children; + var t = useTheme(); + var _ = useLingui()._; + var _b = React.useState(false), override = _b[0], setOverride = _b[1]; + var navigation = useNavigation(); + var isMobile = useWebMediaQueries().isMobile; + var control = useModerationDetailsDialogControl(); + var blur = modui.blurs[0]; + var desc = useModerationCauseDescription(blur); + if (!blur || override) { + return (_jsx(View, { testID: testID, style: style, children: children })); + } + var isNoPwi = !!modui.blurs.find(function (cause) { + return cause.type === 'label' && + cause.labelDef.identifier === '!no-unauthenticated'; + }); + return (_jsxs(CenteredView, { style: [ + a.flex_1, + { + paddingTop: 100, + paddingBottom: 150, + }, + t.atoms.bg, + containerStyle, + ], sideBorders: true, children: [_jsx(View, { style: [a.align_center, a.mb_md], children: _jsx(View, { style: [ + t.atoms.bg_contrast_975, + a.align_center, + a.justify_center, + { + borderRadius: 25, + width: 50, + height: 50, + }, + ], children: _jsx(desc.icon, { width: 24, fill: t.atoms.bg.backgroundColor }) }) }), _jsx(Text, { style: [ + a.text_4xl, + a.font_semi_bold, + a.text_center, + a.mb_md, + t.atoms.text, + ], children: isNoPwi ? (_jsx(Trans, { children: "Sign-in Required" })) : (_jsx(Trans, { children: "Content Warning" })) }), _jsxs(Text, { style: [ + a.text_lg, + a.mb_md, + a.px_lg, + a.text_center, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: [isNoPwi ? (_jsx(Trans, { children: "This account has requested that users sign in to view their profile." })) : (_jsxs(_Fragment, { children: [_jsxs(Trans, { children: ["This ", screenDescription, " has been flagged:"] }), ' ', _jsxs(Text, { style: [ + a.text_lg, + a.font_semi_bold, + a.leading_snug, + t.atoms.text, + a.ml_xs, + ], children: [desc.name, ".", ' '] }), _jsx(TouchableWithoutFeedback, { onPress: function () { + control.open(); + }, accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Learn more about this warning"], ["Learn more about this warning"])))), accessibilityHint: "", children: _jsx(Text, { style: [ + a.text_lg, + a.leading_snug, + { + color: t.palette.primary_500, + }, + web({ + cursor: 'pointer', + }), + ], children: _jsx(Trans, { children: "Learn More" }) }) }), _jsx(ModerationDetailsDialog, { control: control, modcause: blur })] })), ' '] }), isMobile && _jsx(View, { style: a.flex_1 }), _jsxs(View, { style: [a.flex_row, a.justify_center, a.my_md, a.gap_md], children: [_jsx(Button, { variant: "solid", color: "primary", size: "large", style: [a.rounded_full], label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Go back"], ["Go back"])))), onPress: function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go back" }) }) }), !modui.noOverride && (_jsx(Button, { variant: "solid", color: "secondary", size: "large", style: [a.rounded_full], label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Show anyway"], ["Show anyway"])))), onPress: function () { return setOverride(function (v) { return !v; }); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Show anyway" }) }) }))] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/verification/VerificationCheck.js b/src/components/verification/VerificationCheck.js new file mode 100644 index 0000000000..8207234447 --- /dev/null +++ b/src/components/verification/VerificationCheck.js @@ -0,0 +1,29 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { VerifiedCheck } from '#/components/icons/VerifiedCheck'; +import { VerifierCheck } from '#/components/icons/VerifierCheck'; +export function VerificationCheck(_a) { + var verifier = _a.verifier, rest = __rest(_a, ["verifier"]); + return verifier ? _jsx(VerifierCheck, __assign({}, rest)) : _jsx(VerifiedCheck, __assign({}, rest)); +} diff --git a/src/components/verification/VerificationCheckButton.js b/src/components/verification/VerificationCheckButton.js new file mode 100644 index 0000000000..c13c41aba4 --- /dev/null +++ b/src/components/verification/VerificationCheckButton.js @@ -0,0 +1,105 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { useFullVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +import { VerificationsDialog } from '#/components/verification/VerificationsDialog'; +import { VerifierDialog } from '#/components/verification/VerifierDialog'; +import { useAnalytics } from '#/analytics'; +export function shouldShowVerificationCheckButton(state) { + var ok = false; + if (state.profile.role === 'default') { + if (state.profile.isVerified) { + ok = true; + } + else if (state.profile.isViewer && state.profile.wasVerified) { + ok = true; + } + else if (state.viewer.role === 'verifier' && + state.viewer.hasIssuedVerification) { + ok = true; + } + } + else if (state.profile.role === 'verifier') { + if (state.profile.isViewer) { + ok = true; + } + else if (state.profile.isVerified) { + ok = true; + } + } + if (!state.profile.showBadge && + !state.profile.isViewer && + !(state.viewer.role === 'verifier' && state.viewer.hasIssuedVerification)) { + ok = false; + } + return ok; +} +export function VerificationCheckButton(_a) { + var profile = _a.profile, size = _a.size; + var state = useFullVerificationState({ + profile: profile, + }); + if (shouldShowVerificationCheckButton(state)) { + return _jsx(Badge, { profile: profile, verificationState: state, size: size }); + } + return null; +} +export function Badge(_a) { + var profile = _a.profile, state = _a.verificationState, size = _a.size; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var verificationsDialogControl = useDialogControl(); + var verifierDialogControl = useDialogControl(); + var gtPhone = useBreakpoints().gtPhone; + var dimensions = 12; + if (size === 'lg') { + dimensions = gtPhone ? 20 : 18; + } + else if (size === 'md') { + dimensions = 14; + } + var verifiedByHidden = !state.profile.showBadge && state.profile.isViewer; + return (_jsxs(_Fragment, { children: [_jsx(Button, { label: state.profile.isViewer + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["View your verifications"], ["View your verifications"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["View this user's verifications"], ["View this user's verifications"])))), hitSlop: 20, onPress: function (evt) { + evt.preventDefault(); + ax.metric('verification:badge:click', {}); + if (state.profile.role === 'verifier') { + verifierDialogControl.open(); + } + else { + verificationsDialogControl.open(); + } + }, children: function (_a) { + var hovered = _a.hovered; + return (_jsx(View, { style: [ + a.justify_end, + a.align_end, + a.transition_transform, + { + width: dimensions, + height: dimensions, + transform: [ + { + scale: hovered ? 1.1 : 1, + }, + ], + }, + ], children: _jsx(VerificationCheck, { width: dimensions, fill: verifiedByHidden + ? t.atoms.bg_contrast_100.backgroundColor + : state.profile.isVerified + ? t.palette.primary_500 + : t.atoms.bg_contrast_100.backgroundColor, verifier: state.profile.role === 'verifier' }) })); + } }), _jsx(VerificationsDialog, { control: verificationsDialogControl, profile: profile, verificationState: state }), _jsx(VerifierDialog, { control: verifierDialogControl, profile: profile, verificationState: state })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/verification/VerificationCreatePrompt.js b/src/components/verification/VerificationCreatePrompt.js new file mode 100644 index 0000000000..26b02645a1 --- /dev/null +++ b/src/components/verification/VerificationCreatePrompt.js @@ -0,0 +1,92 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { logger } from '#/logger'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useVerificationCreateMutation } from '#/state/queries/verification/useVerificationCreateMutation'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { VerifiedCheck } from '#/components/icons/VerifiedCheck'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import * as Prompt from '#/components/Prompt'; +export function VerificationCreatePrompt(_a) { + var _this = this; + var control = _a.control, profile = _a.profile; + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var _b = useVerificationCreateMutation(), create = _b.mutateAsync, isPending = _b.isPending; + var _c = useState(""), error = _c[0], setError = _c[1]; + var onConfirm = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, create({ profile: profile })]; + case 1: + _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Successfully verified"], ["Successfully verified"]))))); + control.close(); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Verification failed, please try again."], ["Verification failed, please try again."]))))); + logger.error('Failed to create a verification', { + safeMessage: e_1, + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }, [_, profile, create, control]); + return (_jsxs(Prompt.Outer, { control: control, children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm, a.pb_sm], children: [_jsx(VerifiedCheck, { width: 18 }), _jsx(Prompt.TitleText, { style: [a.pb_0], children: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Verify this account?"], ["Verify this account?"])))) })] }), _jsx(Prompt.DescriptionText, { children: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["This action can be undone at any time."], ["This action can be undone at any time."])))) }), moderationOpts ? (_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts })] })) : null, error && (_jsx(View, { style: [a.pt_lg], children: _jsx(Admonition, { type: "error", children: error }) })), _jsx(View, { style: [a.pt_xl], children: profile.displayName ? (_jsxs(Prompt.Actions, { children: [_jsxs(Button, { variant: "solid", color: "primary", size: gtMobile ? 'small' : 'large', label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Verify account"], ["Verify account"])))), onPress: onConfirm, children: [_jsx(ButtonText, { children: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Verify account"], ["Verify account"])))) }), isPending && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Prompt.Cancel, {})] })) : (_jsx(Admonition, { type: "warning", children: _jsx(Trans, { children: "This user does not have a display name, and therefore cannot be verified." }) })) }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/verification/VerificationRemovePrompt.js b/src/components/verification/VerificationRemovePrompt.js new file mode 100644 index 0000000000..8fd614335c --- /dev/null +++ b/src/components/verification/VerificationRemovePrompt.js @@ -0,0 +1,82 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { logger } from '#/logger'; +import { useVerificationsRemoveMutation } from '#/state/queries/verification/useVerificationsRemoveMutation'; +import * as Toast from '#/view/com/util/Toast'; +import * as Prompt from '#/components/Prompt'; +export { useDialogControl as usePromptControl } from '#/components/Dialog'; +export function VerificationRemovePrompt(_a) { + var _this = this; + var control = _a.control, profile = _a.profile, verifications = _a.verifications, onConfirmInner = _a.onConfirm; + var _ = useLingui()._; + var remove = useVerificationsRemoveMutation().mutateAsync; + var onConfirm = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + onConfirmInner === null || onConfirmInner === void 0 ? void 0 : onConfirmInner(); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, remove({ profile: profile, verifications: verifications })]; + case 2: + _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Removed verification"], ["Removed verification"]))))); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to remove verification"], ["Failed to remove verification"])))), 'xmark'); + logger.error('Failed to remove verification', { + safeMessage: e_1, + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [_, profile, verifications, remove, onConfirmInner]); + return (_jsx(Prompt.Basic, { control: control, title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Remove your verification for this account?"], ["Remove your verification for this account?"])))), onConfirm: onConfirm, confirmButtonCta: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Remove verification"], ["Remove verification"])))), confirmButtonColor: "negative" })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/verification/VerificationsDialog.js b/src/components/verification/VerificationsDialog.js new file mode 100644 index 0000000000..d695f26fb9 --- /dev/null +++ b/src/components/verification/VerificationsDialog.js @@ -0,0 +1,88 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { urls } from '#/lib/constants'; +import { getUserDisplayName } from '#/lib/getUserDisplayName'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useDialogControl } from '#/components/Dialog'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import { Link } from '#/components/Link'; +import * as ProfileCard from '#/components/ProfileCard'; +import { Text } from '#/components/Typography'; +import { VerificationRemovePrompt } from '#/components/verification/VerificationRemovePrompt'; +import { useAnalytics } from '#/analytics'; +export { useDialogControl } from '#/components/Dialog'; +export function VerificationsDialog(_a) { + var control = _a.control, profile = _a.profile, verificationState = _a.verificationState; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(Inner, { control: control, profile: profile, verificationState: verificationState })] })); +} +function Inner(_a) { + var profile = _a.profile, control = _a.control, state = _a.verificationState; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var userName = getUserDisplayName(profile); + var label = state.profile.isViewer + ? state.profile.isVerified + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You are verified"], ["You are verified"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Your verifications"], ["Your verifications"])))) + : state.profile.isVerified + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["", " is verified"], ["", " is verified"])), userName)) + : _(msg({ + message: "".concat(userName, "'s verifications"), + comment: "Possessive, meaning \"the verifications of {userName}\"", + })); + return (_jsxs(Dialog.ScrollableInner, { label: label, style: [ + gtMobile ? { width: 'auto', maxWidth: 400, minWidth: 200 } : a.w_full, + ], children: [_jsxs(View, { style: [a.gap_sm, a.pb_lg], children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold, a.pr_4xl, a.leading_tight], children: label }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: state.profile.isVerified ? (_jsx(Trans, { children: "This account has a checkmark because it's been verified by trusted sources." })) : (_jsx(Trans, { children: "This account has one or more attempted verifications, but it is not currently verified." })) })] }), profile.verification ? (_jsxs(View, { style: [a.pb_xl, a.gap_md], children: [_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Verified by:" }) }), _jsx(View, { style: [a.gap_lg], children: profile.verification.verifications.map(function (v) { return (_jsx(VerifierCard, { verification: v, subject: profile, outerDialogControl: control }, v.uri)); }) }), profile.verification.verifications.some(function (v) { return !v.isValid; }) && + state.profile.isViewer && (_jsx(Admonition, { type: "warning", style: [a.mt_xs], children: _jsx(Trans, { children: "Some of your verifications are invalid." }) }))] })) : null, _jsxs(View, { style: [ + a.w_full, + a.gap_sm, + a.justify_end, + gtMobile + ? [a.flex_row, a.flex_row_reverse, a.justify_start] + : [a.flex_col], + ], children: [_jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Close dialog"], ["Close dialog"])))), size: "small", variant: "solid", color: "primary", onPress: function () { + control.close(); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }), _jsx(Link, { overridePresentation: true, to: urls.website.blog.initialVerificationAnnouncement, label: _(msg({ + message: "Learn more about verification on Bluesky", + context: "english-only-resource", + })), size: "small", variant: "solid", color: "secondary", style: [a.justify_center], onPress: function () { + ax.metric('verification:learn-more', { + location: 'verificationsDialog', + }); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { context: "english-only-resource", children: "Learn more" }) }) })] }), _jsx(Dialog.Close, {})] })); +} +function VerifierCard(_a) { + var verification = _a.verification, subject = _a.subject, outerDialogControl = _a.outerDialogControl; + var t = useTheme(); + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var currentAccount = useSession().currentAccount; + var moderationOpts = useModerationOpts(); + var _c = useProfileQuery({ did: verification.issuer }), profile = _c.data, error = _c.error; + var verificationRemovePromptControl = useDialogControl(); + var canAdminister = verification.issuer === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + return (_jsxs(View, { style: { + opacity: verification.isValid ? 1 : 0.5, + }, children: [_jsx(ProfileCard.Outer, { children: _jsx(ProfileCard.Header, { children: error ? (_jsxs(_Fragment, { children: [_jsx(ProfileCard.AvatarPlaceholder, {}), _jsxs(View, { style: [a.flex_1], children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold, a.leading_snug], numberOfLines: 1, children: _jsx(Trans, { children: "Unknown verifier" }) }), _jsx(Text, { emoji: true, style: [a.leading_snug, t.atoms.text_contrast_medium], numberOfLines: 1, children: verification.issuer })] })] })) : profile && moderationOpts ? (_jsxs(_Fragment, { children: [_jsxs(ProfileCard.Link, { profile: profile, style: [a.flex_row, a.align_center, a.gap_sm, a.flex_1], onPress: function () { + outerDialogControl.close(); + }, children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts, disabledPreview: true }), _jsxs(View, { style: [a.flex_1], children: [_jsx(ProfileCard.Name, { profile: profile, moderationOpts: moderationOpts }), _jsx(Text, { emoji: true, style: [a.leading_snug, t.atoms.text_contrast_medium], numberOfLines: 1, children: i18n.date(new Date(verification.createdAt), { + dateStyle: 'long', + }) })] })] }), canAdminister && (_jsx(View, { children: _jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Remove verification"], ["Remove verification"])))), size: "small", variant: "outline", color: "negative", shape: "round", onPress: function () { + verificationRemovePromptControl.open(); + }, children: _jsx(ButtonIcon, { icon: TrashIcon }) }) }))] })) : (_jsxs(_Fragment, { children: [_jsx(ProfileCard.AvatarPlaceholder, {}), _jsx(ProfileCard.NameAndHandlePlaceholder, {})] })) }) }), _jsx(VerificationRemovePrompt, { control: verificationRemovePromptControl, profile: subject, verifications: [verification], onConfirm: function () { return outerDialogControl.close(); } })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/verification/VerifierDialog.js b/src/components/verification/VerifierDialog.js new file mode 100644 index 0000000000..f6ec330933 --- /dev/null +++ b/src/components/verification/VerifierDialog.js @@ -0,0 +1,66 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Text as RNText, View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { urls } from '#/lib/constants'; +import { getUserDisplayName } from '#/lib/getUserDisplayName'; +import { useSession } from '#/state/session'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { VerifierCheck } from '#/components/icons/VerifierCheck'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +export { useDialogControl } from '#/components/Dialog'; +export function VerifierDialog(_a) { + var control = _a.control, profile = _a.profile, verificationState = _a.verificationState; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(Inner, { control: control, profile: profile, verificationState: verificationState }), _jsx(Dialog.Close, {})] })); +} +function Inner(_a) { + var profile = _a.profile, control = _a.control; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var currentAccount = useSession().currentAccount; + var isSelf = profile.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var userName = getUserDisplayName(profile); + var label = isSelf + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You are a trusted verifier"], ["You are a trusted verifier"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["", " is a trusted verifier"], ["", " is a trusted verifier"])), userName)); + return (_jsx(Dialog.ScrollableInner, { label: label, style: [ + gtMobile ? { width: 'auto', maxWidth: 400, minWidth: 200 } : a.w_full, + ], children: _jsxs(View, { style: [a.gap_lg], children: [_jsx(View, { style: [ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + { minHeight: 100 }, + ], children: _jsx(Image, { accessibilityIgnoresInvertColors: true, source: require('../../../assets/images/initial_verification_announcement_1.png'), style: [ + { + aspectRatio: 353 / 160, + }, + ], alt: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts."], ["An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts."])))) }) }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold, a.pr_4xl, a.leading_tight], children: label }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsxs(Trans, { children: ["Accounts with a scalloped blue check mark", ' ', _jsx(RNText, { children: _jsx(VerifierCheck, { width: 14 }) }), ' ', "can verify others. These trusted verifiers are selected by Bluesky."] }) })] }), _jsxs(View, { style: [ + a.w_full, + a.gap_sm, + a.justify_end, + gtMobile ? [a.flex_row, a.justify_end] : [a.flex_col], + ], children: [_jsx(Link, { overridePresentation: true, to: urls.website.blog.initialVerificationAnnouncement, label: _(msg({ + message: "Learn more about verification on Bluesky", + context: "english-only-resource", + })), size: "small", color: "primary", style: [a.justify_center], onPress: function () { + ax.metric('verification:learn-more', { + location: 'verifierDialog', + }); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { context: "english-only-resource", children: "Learn more" }) }) }), _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Close dialog"], ["Close dialog"])))), size: "small", color: "secondary", onPress: function () { + control.close(); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/verification/index.js b/src/components/verification/index.js new file mode 100644 index 0000000000..aa083d2d16 --- /dev/null +++ b/src/components/verification/index.js @@ -0,0 +1,73 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { useMemo } from 'react'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useCurrentAccountProfile } from '#/state/queries/useCurrentAccountProfile'; +import { useSession } from '#/state/session'; +export function useFullVerificationState(_a) { + var profile = _a.profile; + var currentAccount = useSession().currentAccount; + var currentAccountProfile = useCurrentAccountProfile(); + var profileState = useSimpleVerificationState({ profile: profile }); + var viewerState = useSimpleVerificationState({ + profile: currentAccountProfile, + }); + return useMemo(function () { + var _a; + var verifications = ((_a = profile.verification) === null || _a === void 0 ? void 0 : _a.verifications) || []; + var wasVerified = profileState.role === 'default' && + !profileState.isVerified && + verifications.length > 0; + var hasIssuedVerification = Boolean(viewerState && + viewerState.role === 'verifier' && + profileState.role === 'default' && + verifications.find(function (v) { return v.issuer === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); })); + return { + profile: __assign(__assign({}, profileState), { wasVerified: wasVerified, isViewer: profile.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did), showBadge: profileState.showBadge }), + viewer: viewerState.role === 'verifier' + ? { + role: 'verifier', + isVerified: viewerState.isVerified, + hasIssuedVerification: hasIssuedVerification, + } + : { + role: 'default', + isVerified: viewerState.isVerified, + }, + }; + }, [profile, currentAccount, profileState, viewerState]); +} +export function useSimpleVerificationState(_a) { + var _b; + var profile = _a.profile; + var preferences = usePreferencesQuery(); + var prefs = useMemo(function () { var _a; return ((_a = preferences.data) === null || _a === void 0 ? void 0 : _a.verificationPrefs) || { hideBadges: false }; }, [(_b = preferences.data) === null || _b === void 0 ? void 0 : _b.verificationPrefs]); + return useMemo(function () { + if (!profile || !profile.verification) { + return { + role: 'default', + isVerified: false, + showBadge: false, + }; + } + var _a = profile.verification, verifiedStatus = _a.verifiedStatus, trustedVerifierStatus = _a.trustedVerifierStatus; + var isVerifiedUser = ['valid', 'invalid'].includes(verifiedStatus); + var isVerifierUser = ['valid', 'invalid'].includes(trustedVerifierStatus); + var isVerified = (isVerifiedUser && verifiedStatus === 'valid') || + (isVerifierUser && trustedVerifierStatus === 'valid'); + return { + role: isVerifierUser ? 'verifier' : 'default', + isVerified: isVerified, + showBadge: prefs.hideBadges ? false : isVerified, + }; + }, [profile, prefs]); +} diff --git a/src/components/video/PlayButtonIcon.js b/src/components/video/PlayButtonIcon.js new file mode 100644 index 0000000000..b0df594089 --- /dev/null +++ b/src/components/video/PlayButtonIcon.js @@ -0,0 +1,23 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +import { Play_Filled_Corner0_Rounded as PlayIcon } from '#/components/icons/Play'; +export function PlayButtonIcon(_a) { + var _b = _a.size, size = _b === void 0 ? 32 : _b; + var t = useTheme(); + var bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975; + var fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25; + return (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.rounded_full, + { + backgroundColor: bg, + shadowColor: 'black', + shadowRadius: 32, + shadowOpacity: 0.5, + elevation: 24, + width: size + size / 1.5, + height: size + size / 1.5, + opacity: 0.7, + }, + ] }), _jsx(PlayIcon, { width: size, fill: fg, style: a.absolute })] })); +} diff --git a/src/env/common.js b/src/env/common.js new file mode 100644 index 0000000000..b736d6afb1 --- /dev/null +++ b/src/env/common.js @@ -0,0 +1,111 @@ +import packageJson from '#/../package.json'; +/** + * The semver version of the app, as defined in `package.json.` + * + * N.B. The fallback is needed for Render.com deployments + */ +export var RELEASE_VERSION = process.env.EXPO_PUBLIC_RELEASE_VERSION || packageJson.version; +/** + * The env the app is running in e.g. development, testflight, production, e2e + */ +export var ENV = process.env.EXPO_PUBLIC_ENV; +/** + * Indicates whether the app is running in TestFlight + */ +export var IS_TESTFLIGHT = ENV === 'testflight'; +/** + * Indicates whether the app is `__DEV__` + */ +export var IS_DEV = __DEV__; +/** + * Indicates whether the app is running in a test environment + */ +export var IS_E2E = ENV === 'e2e'; +/** + * Indicates whether the app is `__DEV__` or TestFlight + */ +export var IS_INTERNAL = IS_DEV || IS_TESTFLIGHT; +/** + * The commit hash that the current bundle was made from. The user can + * see the commit hash in the app's settings along with the other version info. + * Useful for debugging/reporting. + */ +export var BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER || 'dev'; +/** + * This will always be in the format of YYMMDDHH, so that it always increases + * for each build. This should only be used for analytics reporting and shouldn't + * be used to identify a specific bundle. + */ +export var BUNDLE_DATE = process.env.EXPO_PUBLIC_BUNDLE_DATE === undefined + ? 0 + : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE); +/** + * The log level for the app. + */ +export var LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info'); +/** + * Enable debug logs for specific logger instances + */ +export var LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''; +/** + * The DID of the Bluesky appview to proxy to + */ +export var BLUESKY_PROXY_DID = process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID || 'did:web:api.bsky.app'; +/** + * The DID of the chat service to proxy to + */ +export var CHAT_PROXY_DID = process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat'; +/** + * Metrics API host + */ +export var METRICS_API_HOST = process.env.EXPO_PUBLIC_METRICS_API_HOST || 'https://events.bsky.app'; +/** + * Growthbook API host + */ +export var GROWTHBOOK_API_HOST = process.env.EXPO_PUBLIC_GROWTHBOOK_API_HOST || "".concat(METRICS_API_HOST, "/gb"); +/** + * Growthbook client key + */ +export var GROWTHBOOK_CLIENT_KEY = process.env.EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY || 'sdk-7gkUkGy9wguUjyFe'; +/** + * Sentry DSN for telemetry + */ +export var SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN; +/** + * Bitdrift API key. If undefined, Bitdrift should be disabled. + */ +export var BITDRIFT_API_KEY = process.env.EXPO_PUBLIC_BITDRIFT_API_KEY; +/** + * GCP project ID which is required for native device attestation. On web, this + * should be unset and evaluate to 0. + */ +export var GCP_PROJECT_ID = process.env.EXPO_PUBLIC_GCP_PROJECT_ID === undefined + ? 0 + : Number(process.env.EXPO_PUBLIC_GCP_PROJECT_ID); +/** + * URLs for the app config web worker. Can be a + * locally running server, see `env.example` for more. + */ +export var GEOLOCATION_DEV_URL = process.env.GEOLOCATION_DEV_URL; +export var GEOLOCATION_PROD_URL = "https://ip.bsky.app"; +export var GEOLOCATION_URL = IS_DEV + ? (GEOLOCATION_DEV_URL !== null && GEOLOCATION_DEV_URL !== void 0 ? GEOLOCATION_DEV_URL : GEOLOCATION_PROD_URL) + : GEOLOCATION_PROD_URL; +/** + * URLs for the live-event config web worker. Can be a + * locally running server, see `env.example` for more. + */ +export var LIVE_EVENTS_DEV_URL = process.env.LIVE_EVENTS_DEV_URL; +export var LIVE_EVENTS_PROD_URL = "https://live-events.workers.bsky.app"; +export var LIVE_EVENTS_URL = IS_DEV + ? (LIVE_EVENTS_DEV_URL !== null && LIVE_EVENTS_DEV_URL !== void 0 ? LIVE_EVENTS_DEV_URL : LIVE_EVENTS_PROD_URL) + : LIVE_EVENTS_PROD_URL; +/** + * URLs for the app-config web worker. Can be a + * locally running server, see `env.example` for more. + */ +export var APP_CONFIG_DEV_URL = process.env.APP_CONFIG_DEV_URL; +export var APP_CONFIG_PROD_URL = "https://app-config.workers.bsky.app"; +export var APP_CONFIG_URL = IS_DEV + ? (APP_CONFIG_DEV_URL !== null && APP_CONFIG_DEV_URL !== void 0 ? APP_CONFIG_DEV_URL : APP_CONFIG_PROD_URL) + : APP_CONFIG_PROD_URL; diff --git a/src/env/index.js b/src/env/index.js new file mode 100644 index 0000000000..7a130686ce --- /dev/null +++ b/src/env/index.js @@ -0,0 +1,40 @@ +import { Platform } from 'react-native'; +import { nativeBuildVersion } from 'expo-application'; +import { BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION } from '#/env/common'; +export * from '#/env/common'; +// for some reason Platform.OS === 'ios' AND Platform.Version is undefined in our CI unit tests -sfn +var iOSMajorVersion = Platform.OS === 'ios' && typeof Platform.Version === 'string' + ? parseInt(Platform.Version.split('.')[0], 10) + : 0; +/** + * The semver version of the app, specified in our `package.json`.file. On + * iOs/Android, the native build version is appended to the semver version, so + * that it can be used to identify a specific build. + */ +export var APP_VERSION = "".concat(RELEASE_VERSION, ".").concat(nativeBuildVersion); +/** + * The short commit hash and environment of the current bundle. + */ +export var APP_METADATA = "".concat(BUNDLE_IDENTIFIER.slice(0, 7), " (").concat(__DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod', ")"); +/** + * Platform detection + */ +export var IS_IOS = Platform.OS === 'ios'; +export var IS_ANDROID = Platform.OS === 'android'; +export var IS_NATIVE = true; +export var IS_WEB = false; +/** + * Web-specific platform detection + */ +export var IS_WEB_TOUCH_DEVICE = true; +export var IS_WEB_MOBILE = false; +export var IS_WEB_MOBILE_IOS = false; +export var IS_WEB_MOBILE_ANDROID = false; +export var IS_WEB_SAFARI = false; +export var IS_WEB_FIREFOX = false; +/** + * Misc + */ +export var IS_HIGH_DPI = true; +// ideally we'd use isLiquidGlassAvailable() from expo-glass-effect but checking iOS version is good enough for now +export var IS_LIQUID_GLASS = iOSMajorVersion >= 26; diff --git a/src/env/index.web.js b/src/env/index.web.js new file mode 100644 index 0000000000..b49bd5ae29 --- /dev/null +++ b/src/env/index.web.js @@ -0,0 +1,36 @@ +var _a; +import { BUNDLE_IDENTIFIER, RELEASE_VERSION } from '#/env/common'; +export * from '#/env/common'; +/** + * The semver version of the app, specified in our `package.json`.file. On + * iOs/Android, the native build version is appended to the semver version, so + * that it can be used to identify a specific build. + */ +export var APP_VERSION = RELEASE_VERSION; +/** + * The short commit hash and environment of the current bundle. + */ +export var APP_METADATA = "".concat(BUNDLE_IDENTIFIER.slice(0, 7), " (").concat(__DEV__ ? 'dev' : 'prod', ")"); +/** + * Platform detection + */ +export var IS_IOS = false; +export var IS_ANDROID = false; +export var IS_NATIVE = false; +export var IS_WEB = true; +/** + * Web-specific platform detection + */ +export var IS_WEB_TOUCH_DEVICE = window.matchMedia('(pointer: coarse)').matches; +export var IS_WEB_MOBILE = (_a = window.matchMedia('only screen and (max-width: 1300px)')) === null || _a === void 0 ? void 0 : _a.matches; +export var IS_WEB_MOBILE_IOS = /iPhone/.test(navigator.userAgent); +export var IS_WEB_MOBILE_ANDROID = /android/i.test(navigator.userAgent) && IS_WEB_TOUCH_DEVICE; +export var IS_WEB_SAFARI = /^((?!chrome|android).)*safari/i.test( +// https://stackoverflow.com/questions/7944460/detect-safari-browser +navigator.userAgent); +export var IS_WEB_FIREFOX = /firefox|fxios/i.test(navigator.userAgent); +/** + * Misc + */ +export var IS_HIGH_DPI = window.matchMedia('(min-resolution: 2dppx)').matches; +export var IS_LIQUID_GLASS = false; diff --git a/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.js b/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.js new file mode 100644 index 0000000000..506c864bf1 --- /dev/null +++ b/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.js @@ -0,0 +1,71 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useTrendingSettings } from '#/state/preferences/trending'; +import { atoms as a, useLayoutBreakpoints } from '#/alf'; +import { Button } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '#/components/icons/Times'; +import { TrendingInterstitial } from '#/components/interstitials/Trending'; +import * as Toast from '#/components/Toast'; +import { LiveEventFeedCardWide } from '#/features/liveEvents/components/LiveEventFeedCardWide'; +import { useUserPreferencedLiveEvents } from '#/features/liveEvents/context'; +import { useUpdateLiveEventPreferences } from '#/features/liveEvents/preferences'; +export function DiscoverFeedLiveEventFeedsAndTrendingBanner() { + var events = useUserPreferencedLiveEvents(); + var rightNavVisible = useLayoutBreakpoints().rightNavVisible; + var trendingDisabled = useTrendingSettings().trendingDisabled; + if (!events.feeds.length) { + if (!rightNavVisible && !trendingDisabled) { + // only show trending on mobile when live event banner is not shown + return _jsx(TrendingInterstitial, {}); + } + else { + // no feed, no trending + return null; + } + } + // On desktop, we show in the sidebar + if (rightNavVisible) + return null; + return events.feeds.map(function (feed) { return _jsx(Inner, { feed: feed }, feed.id); }); +} +function Inner(_a) { + var feed = _a.feed; + var _ = useLingui()._; + var layout = feed.layouts.wide; + var _b = useUpdateLiveEventPreferences({ + feed: feed, + metricContext: 'discover', + onUpdateSuccess: function (_a) { + var undoAction = _a.undoAction; + Toast.show(_jsxs(Toast.Outer, { children: [_jsx(Toast.Icon, {}), _jsx(Toast.Text, { children: undoAction ? (_jsx(Trans, { children: "Live event hidden" })) : (_jsx(Trans, { children: "Live event unhidden" })) }), undoAction && (_jsx(Toast.Action, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Undo"], ["Undo"])))), onPress: function () { + if (undoAction) { + update(undoAction); + } + }, children: _jsx(Trans, { children: "Undo" }) }))] }), { type: 'success' }); + }, + }), update = _b.mutate, variables = _b.variables; + if (variables) + return null; + return (_jsx(_Fragment, { children: _jsx(View, { style: [a.px_lg, a.pt_md, a.pb_xs], children: _jsxs(View, { children: [_jsx(LiveEventFeedCardWide, { feed: feed, metricContext: "discover" }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Dismiss live event banner"], ["Dismiss live event banner"])))), size: "tiny", shape: "round", style: [a.absolute, a.z_10, { top: 6, right: 6 }], onPress: function () { + update({ type: 'hideFeed', id: feed.id }); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.rounded_full, + { + backgroundColor: layout.overlayColor, + opacity: hovered || pressed ? 0.8 : 0.6, + }, + ] }), _jsx(CloseIcon, { size: "xs", fill: layout.textColor, style: [a.z_20] })] })); + } })] }) }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.js b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.js new file mode 100644 index 0000000000..ae8e62a315 --- /dev/null +++ b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.js @@ -0,0 +1,10 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +import { LiveEventFeedCardWide } from '#/features/liveEvents/components/LiveEventFeedCardWide'; +import { useLiveEvents } from '#/features/liveEvents/context'; +export function ExploreScreenLiveEventFeedsBanner() { + var t = useTheme(); + var events = useLiveEvents(); + return events.feeds.map(function (feed) { return (_jsx(View, { style: [a.p_lg, a.border_b, t.atoms.border_contrast_low], children: _jsx(LiveEventFeedCardWide, { feed: feed, metricContext: "explore" }) }, feed.id)); }); +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.js b/src/features/liveEvents/components/LiveEventFeedCardCompact.js new file mode 100644 index 0000000000..7b134c73ba --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.js @@ -0,0 +1,77 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useCallOnce } from '#/lib/once'; +import { isBskyCustomFeedUrl } from '#/lib/strings/url-helpers'; +import { atoms as a, utils } from '#/alf'; +import { Live_Stroke2_Corner0_Rounded as LiveIcon } from '#/components/icons/Live'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +var roundedStyles = [a.rounded_md, a.curve_continuous]; +export function LiveEventFeedCardCompact(_a) { + var feed = _a.feed, metricContext = _a.metricContext; + var _ = useLingui()._; + var ax = useAnalytics(); + var layout = feed.layouts.compact; + var overlayColor = layout.overlayColor; + var textColor = layout.textColor; + var url = useMemo(function () { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname; + } + return '/'; + }, [feed.url]); + useCallOnce(function () { + ax.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }); + })(); + return (_jsx(Link, { to: url, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Live event happening now: ", ""], ["Live event happening now: ", ""])), feed.title)), style: [a.w_full], onPress: function () { + ax.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(View, { style: [roundedStyles, a.shadow_md, a.w_full], children: _jsxs(View, { style: [a.w_full, a.align_start, a.overflow_hidden, roundedStyles], children: [_jsx(Image, { accessibilityIgnoresInvertColors: true, source: { uri: layout.image }, placeholder: { blurhash: layout.blurhash }, style: [a.absolute, a.inset_0, a.w_full, a.h_full], contentFit: "cover", placeholderContentFit: "cover" }), _jsx(LinearGradient, { colors: [overlayColor, utils.alpha(overlayColor, 0)], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, style: [ + a.absolute, + a.inset_0, + a.transition_opacity, + { + transitionDuration: '200ms', + opacity: hovered || pressed ? 0.6 : 0, + }, + ] }), _jsxs(View, { style: [a.w_full, a.justify_end], children: [_jsx(LinearGradient, { colors: [ + overlayColor, + utils.alpha(overlayColor, 0.7), + utils.alpha(overlayColor, 0), + ], locations: [0, 0.8, 1], start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, style: [a.absolute, a.inset_0] }), _jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.align_center, + a.gap_xs, + a.z_10, + a.px_lg, + a.py_md, + ], children: [_jsx(LiveIcon, { size: "md", fill: textColor }), _jsx(Text, { numberOfLines: 1, style: [ + a.flex_1, + a.leading_snug, + a.font_bold, + a.text_lg, + a.pr_xl, + { color: textColor }, + ], children: layout.title })] })] })] }) })); + } })); +} +var templateObject_1; diff --git a/src/features/liveEvents/components/LiveEventFeedCardWide.js b/src/features/liveEvents/components/LiveEventFeedCardWide.js new file mode 100644 index 0000000000..35c976cf3d --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardWide.js @@ -0,0 +1,79 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useCallOnce } from '#/lib/once'; +import { isBskyCustomFeedUrl } from '#/lib/strings/url-helpers'; +import { atoms as a, useBreakpoints, utils } from '#/alf'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +var roundedStyles = [a.rounded_lg, a.curve_continuous]; +export function LiveEventFeedCardWide(_a) { + var feed = _a.feed, metricContext = _a.metricContext; + var ax = useAnalytics(); + var _ = useLingui()._; + var gtPhone = useBreakpoints().gtPhone; + var layout = feed.layouts.wide; + var overlayColor = layout.overlayColor; + var textColor = layout.textColor; + var url = useMemo(function () { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname; + } + return '/'; + }, [feed.url]); + useCallOnce(function () { + ax.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }); + })(); + return (_jsx(Link, { to: url, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Live event happening now: ", ""], ["Live event happening now: ", ""])), feed.title)), style: [a.w_full], onPress: function () { + ax.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(View, { style: [roundedStyles, a.shadow_md, a.w_full], children: _jsxs(View, { style: [ + a.align_start, + roundedStyles, + a.overflow_hidden, + { + aspectRatio: gtPhone ? 576 / 144 : 369 / 100, + }, + ], children: [_jsx(Image, { accessibilityIgnoresInvertColors: true, source: { uri: layout.image }, placeholder: { blurhash: layout.blurhash }, style: [a.absolute, a.inset_0, a.w_full, a.h_full], contentFit: "cover", placeholderContentFit: "cover" }), _jsx(LinearGradient, { colors: [overlayColor, utils.alpha(overlayColor, 0)], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, style: [ + a.absolute, + a.inset_0, + a.transition_opacity, + { + transitionDuration: '200ms', + opacity: hovered || pressed ? 0.6 : 0, + }, + ] }), _jsxs(View, { style: [a.flex_1, a.justify_end], children: [_jsx(LinearGradient, { colors: [overlayColor, utils.alpha(overlayColor, 0)], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, style: [a.absolute, a.inset_0] }), _jsxs(View, { style: [ + a.z_10, + gtPhone ? [a.pl_xl, a.pb_lg] : [a.pl_lg, a.pb_md], + { paddingRight: 64 }, + ], children: [_jsx(Text, { style: [ + a.leading_snug, + gtPhone ? a.text_xs : a.text_2xs, + { color: textColor, opacity: 0.8 }, + ], children: feed.preview ? (_jsx(Trans, { children: "Preview" })) : (_jsx(Trans, { children: "Happening now" })) }), _jsx(Text, { style: [ + a.leading_snug, + a.font_bold, + gtPhone ? a.text_3xl : a.text_lg, + { color: textColor }, + ], children: layout.title })] })] })] }) })); + } })); +} +var templateObject_1; diff --git a/src/features/liveEvents/components/LiveEventFeedOptionsMenu.js b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.js new file mode 100644 index 0000000000..c18f739f19 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.js @@ -0,0 +1,59 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { atoms as a, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Span, Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +import { useUpdateLiveEventPreferences } from '#/features/liveEvents/preferences'; +export { useDialogControl } from '#/components/Dialog'; +export function LiveEventFeedOptionsMenu(_a) { + var control = _a.control, feed = _a.feed, metricContext = _a.metricContext; + var _ = useLingui()._; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Configure live event banner"], ["Configure live event banner"])))), style: [web({ maxWidth: 400 })], children: [_jsx(Inner, { control: control, feed: feed, metricContext: metricContext }), _jsx(Dialog.Close, {})] })] })); +} +function Inner(_a) { + var control = _a.control, feed = _a.feed, metricContext = _a.metricContext; + var _ = useLingui()._; + var _b = useUpdateLiveEventPreferences({ + feed: feed, + metricContext: metricContext, + onUpdateSuccess: function (_a) { + var undoAction = _a.undoAction; + Toast.show(_jsxs(Toast.Outer, { children: [_jsx(Toast.Icon, {}), _jsx(Toast.Text, { children: _jsx(Trans, { children: "Your live event preferences have been updated." }) }), undoAction && (_jsx(Toast.Action, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Undo"], ["Undo"])))), onPress: function () { + if (undoAction) { + update(undoAction); + } + }, children: _jsx(Trans, { children: "Undo" }) }))] }), { type: 'success' }); + /* + * If there is no `undoAction`, it means that the action was already + * undone, and therefore the menu would have been closed prior to the + * undo happening. + */ + if (undoAction) { + control.close(); + } + }, + }), isPending = _b.isPending, update = _b.mutate, rawError = _b.error, variables = _b.variables; + var cleanError = useCleanError(); + var error = rawError ? cleanError(rawError) : undefined; + var isHidingFeed = (variables === null || variables === void 0 ? void 0 : variables.type) === 'hideFeed' && isPending; + var isHidingAllFeeds = (variables === null || variables === void 0 ? void 0 : variables.type) === 'toggleHideAllFeeds' && isPending; + return (_jsxs(View, { style: [a.gap_lg], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { children: "Live event options" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface." }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsxs(Trans, { children: ["If you choose to hide all events, you can always re-enable them from", ' ', _jsx(Span, { style: [a.font_semi_bold], children: "Settings \u2192 Content & Media" }), "."] }) })] }), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Hide this event"], ["Hide this event"])))), size: "large", color: "primary_subtle", onPress: function () { + update({ type: 'hideFeed', id: feed.id }); + }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Hide this event" }) }), isHidingFeed && _jsx(ButtonIcon, { icon: Loader })] }), _jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Hide all events"], ["Hide all events"])))), size: "large", color: "secondary", onPress: function () { + update({ type: 'toggleHideAllFeeds' }); + }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Hide all events" }) }), isHidingAllFeeds && _jsx(ButtonIcon, { icon: Loader })] }), IS_NATIVE && (_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Cancel"], ["Cancel"])))), size: "large", color: "secondary_inverted", onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) }))] }), error && (_jsx(Admonition, { type: "error", children: error.clean || error.raw || _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["An unknown error occurred."], ["An unknown error occurred."])))) }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.js b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.js new file mode 100644 index 0000000000..29cfd289e6 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.js @@ -0,0 +1,27 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import * as Toggle from '#/components/forms/Toggle'; +import { Live_Stroke2_Corner0_Rounded as LiveIcon } from '#/components/icons/Live'; +import { useLiveEventPreferences, useUpdateLiveEventPreferences, } from '#/features/liveEvents/preferences'; +export function LiveEventFeedsSettingsToggle() { + var _a; + var _ = useLingui()._; + var prefs = useLiveEventPreferences().data; + var _b = useUpdateLiveEventPreferences({ + metricContext: 'settings', + }), isPending = _b.isPending, updatedPrefs = _b.data, update = _b.mutate; + var hideAllFeeds = !!((_a = (updatedPrefs || prefs)) === null || _a === void 0 ? void 0 : _a.hideAllFeeds); + return (_jsx(Toggle.Item, { name: "enable_live_event_banner", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Show live events in your Discover Feed"], ["Show live events in your Discover Feed"])))), value: !hideAllFeeds, onChange: function () { + if (!isPending) { + update({ type: 'toggleHideAllFeeds' }); + } + }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: LiveIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Show live events in your Discover Feed" }) }), _jsx(Toggle.Platform, {})] }) })); +} +var templateObject_1; diff --git a/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.js b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.js new file mode 100644 index 0000000000..3e56e1da03 --- /dev/null +++ b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.js @@ -0,0 +1,54 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a } from '#/alf'; +import { Button } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '#/components/icons/Times'; +import * as Toast from '#/components/Toast'; +import { LiveEventFeedCardCompact } from '#/features/liveEvents/components/LiveEventFeedCardCompact'; +import { useUserPreferencedLiveEvents } from '#/features/liveEvents/context'; +import { useUpdateLiveEventPreferences } from '#/features/liveEvents/preferences'; +export function SidebarLiveEventFeedsBanner() { + var events = useUserPreferencedLiveEvents(); + return events.feeds.map(function (feed) { return _jsx(Inner, { feed: feed }, feed.id); }); +} +function Inner(_a) { + var feed = _a.feed; + var _ = useLingui()._; + var layout = feed.layouts.wide; + var _b = useUpdateLiveEventPreferences({ + feed: feed, + metricContext: 'sidebar', + onUpdateSuccess: function (_a) { + var undoAction = _a.undoAction; + Toast.show(_jsxs(Toast.Outer, { children: [_jsx(Toast.Icon, {}), _jsx(Toast.Text, { children: undoAction ? (_jsx(Trans, { children: "Live event hidden" })) : (_jsx(Trans, { children: "Live event unhidden" })) }), undoAction && (_jsx(Toast.Action, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Undo"], ["Undo"])))), onPress: function () { + if (undoAction) { + update(undoAction); + } + }, children: _jsx(Trans, { children: "Undo" }) }))] }), { type: 'success' }); + }, + }), update = _b.mutate, variables = _b.variables; + if (variables) + return null; + return (_jsxs(View, { style: [a.relative], children: [_jsx(LiveEventFeedCardCompact, { feed: feed, metricContext: "sidebar" }), _jsx(View, { style: [a.justify_center, a.absolute, { top: 0, right: 6, bottom: 0 }], children: _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Dismiss live event banner"], ["Dismiss live event banner"])))), size: "tiny", shape: "round", style: [a.z_10], onPress: function () { + update({ type: 'hideFeed', id: feed.id }); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.rounded_full, + { + backgroundColor: layout.overlayColor, + opacity: hovered || pressed ? 0.8 : 0.6, + }, + ] }), _jsx(CloseIcon, { size: "xs", fill: layout.textColor, style: [a.z_20] })] })); + } }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/features/liveEvents/context.js b/src/features/liveEvents/context.js new file mode 100644 index 0000000000..96246f56d8 --- /dev/null +++ b/src/features/liveEvents/context.js @@ -0,0 +1,187 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { hasMutedWord } from '@atproto/api'; +import { QueryClient, useQuery } from '@tanstack/react-query'; +import { useOnAppStateChange } from '#/lib/appState'; +import { useIsBskyTeam } from '#/lib/hooks/useIsBskyTeam'; +import { convertBskyAppUrlIfNeeded, isBskyCustomFeedUrl, makeRecordUri, } from '#/lib/strings/url-helpers'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { IS_DEV, LIVE_EVENTS_URL } from '#/env'; +import { useLiveEventPreferences } from '#/features/liveEvents/preferences'; +import { useDevMode } from '#/storage/hooks/dev-mode'; +var qc = new QueryClient(); +var liveEventsQueryKey = ['live-events']; +export var DEFAULT_LIVE_EVENTS = { + feeds: [], +}; +function fetchLiveEvents() { + return __awaiter(this, void 0, void 0, function () { + var res, data, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 3, , 4]); + return [4 /*yield*/, fetch("".concat(LIVE_EVENTS_URL, "/config"))]; + case 1: + res = _b.sent(); + if (!res.ok) + return [2 /*return*/, null]; + return [4 /*yield*/, res.json()]; + case 2: + data = _b.sent(); + return [2 /*return*/, data]; + case 3: + _a = _b.sent(); + return [2 /*return*/, null]; + case 4: return [2 /*return*/]; + } + }); + }); +} +var Context = createContext(DEFAULT_LIVE_EVENTS); +export function Provider(_a) { + var _b; + var children = _a.children; + var isDevMode = useDevMode()[0]; + var isBskyTeam = useIsBskyTeam(); + var preferences = usePreferencesQuery().data; + var mutedWords = useMemo(function () { var _a, _b; return (_b = (_a = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs) === null || _a === void 0 ? void 0 : _a.mutedWords) !== null && _b !== void 0 ? _b : []; }, [(_b = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs) === null || _b === void 0 ? void 0 : _b.mutedWords]); + var _c = useQuery({ + // keep this, prefectching handles initial load + staleTime: 1000 * 15, + queryKey: liveEventsQueryKey, + refetchInterval: 1000 * 60 * 5, // refetch every 5 minutes + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, fetchLiveEvents()]; + }); + }); + }, + }, qc), data = _c.data, refetch = _c.refetch; + useOnAppStateChange(function (state) { + if (state === 'active') + void refetch(); + }); + var ctx = useMemo(function () { + if (!data) + return DEFAULT_LIVE_EVENTS; + var skipMuteFilter = isBskyTeam || IS_DEV; + var feeds = data.feeds.filter(function (f) { + var _a, _b, _c, _d; + if (f.preview && !isBskyTeam) + return false; + if (!skipMuteFilter && mutedWords.length > 0) { + var text = [ + f.title, + (_b = (_a = f.layouts) === null || _a === void 0 ? void 0 : _a.wide) === null || _b === void 0 ? void 0 : _b.title, + (_d = (_c = f.layouts) === null || _c === void 0 ? void 0 : _c.compact) === null || _d === void 0 ? void 0 : _d.title, + ] + .filter(Boolean) + .join(' '); + if (hasMutedWord({ mutedWords: mutedWords, text: text })) + return false; + } + return true; + }); + return __assign(__assign({}, data), { + // only one at a time for now, unless bsky team and dev mode + feeds: isBskyTeam && isDevMode ? feeds : feeds.slice(0, 1) }); + }, [data, isBskyTeam, isDevMode, mutedWords]); + return _jsx(Context.Provider, { value: ctx, children: children }); +} +export function prefetchLiveEvents() { + return __awaiter(this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, fetchLiveEvents()]; + case 1: + data = _a.sent(); + if (data) { + qc.setQueryData(liveEventsQueryKey, data); + } + return [2 /*return*/]; + } + }); + }); +} +export function useLiveEvents() { + var ctx = useContext(Context); + if (!ctx) { + throw new Error('useLiveEventsContext must be used within a Provider'); + } + return ctx; +} +export function useUserPreferencedLiveEvents() { + var events = useLiveEvents(); + var _a = useLiveEventPreferences(), data = _a.data, isLoading = _a.isLoading; + if (isLoading) + return DEFAULT_LIVE_EVENTS; + var hideAllFeeds = data.hideAllFeeds, hiddenFeedIds = data.hiddenFeedIds; + return __assign(__assign({}, events), { feeds: hideAllFeeds + ? [] + : events.feeds.filter(function (f) { + var hidden = (f === null || f === void 0 ? void 0 : f.id) ? hiddenFeedIds.includes((f === null || f === void 0 ? void 0 : f.id) || '') : false; + return !hidden; + }) }); +} +export function useActiveLiveEventFeedUris() { + var feeds = useLiveEvents().feeds; + return new Set(feeds + // insurance + .filter(function (f) { return isBskyCustomFeedUrl(f.url); }) + .map(function (f) { + var uri = convertBskyAppUrlIfNeeded(f.url); + var _a = uri.split('/').filter(Boolean), _0 = _a[0], did = _a[1], _1 = _a[2], rkey = _a[3]; + var urip = makeRecordUri(did, 'app.bsky.feed.generator', rkey); + return urip.toString(); + })); +} diff --git a/src/features/liveEvents/preferences.js b/src/features/liveEvents/preferences.js new file mode 100644 index 0000000000..df282e905a --- /dev/null +++ b/src/features/liveEvents/preferences.js @@ -0,0 +1,183 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useEffect } from 'react'; +import { AppBskyActorDefs, asPredicate } from '@atproto/api'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { preferencesQueryKey, usePreferencesQuery, } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import * as env from '#/env'; +export function useLiveEventPreferences() { + var _a; + var query = usePreferencesQuery(); + useWebOnlyDebugLiveEventPreferences(); + return __assign(__assign({}, query), { data: ((_a = query.data) === null || _a === void 0 ? void 0 : _a.liveEventPreferences) || { + hideAllFeeds: false, + hiddenFeedIds: [], + } }); +} +function useWebOnlyDebugLiveEventPreferences() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + useEffect(function () { + if (env.IS_DEV && IS_WEB && typeof window !== 'undefined') { + // @ts-ignore + window.__updateLiveEventPreferences = function (action) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.updateLiveEventPreferences(action) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }; + } + }, [agent, queryClient]); +} +export function useUpdateLiveEventPreferences(props) { + var _this = this; + var ax = useAnalytics(); + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + onSettled: function (data, error, variables) { + var _a; + /* + * `onSettled` runs after the mutation completes, success or no. The idea + * here is that we want to invert the action that was just passed in, and + * provide it as an `undoAction` to the `onUpdateSuccess` callback. + * + * If the operation was not a success, we don't provide the `undoAction`. + * + * Upon the first call of the mutation, the `__canUndo` flag is undefined, + * so we allow the undo. However, when we create the `undoAction`, we + * set its `__canUndo` flag to false, so that if the user were to call + * the undo action, we would not provide another undo for that. + */ + var canUndo = variables.__canUndo === undefined ? true : false; + var undoAction = null; + switch (variables.type) { + case 'hideFeed': + undoAction = { type: 'unhideFeed', id: variables.id, __canUndo: false }; + break; + case 'unhideFeed': + undoAction = { type: 'hideFeed', id: variables.id, __canUndo: false }; + break; + case 'toggleHideAllFeeds': + undoAction = { type: 'toggleHideAllFeeds', __canUndo: false }; + break; + } + if (data && !error) { + (_a = props === null || props === void 0 ? void 0 : props.onUpdateSuccess) === null || _a === void 0 ? void 0 : _a.call(props, { + undoAction: canUndo ? undoAction : null, + }); + } + }, + mutationFn: function (action) { return __awaiter(_this, void 0, void 0, function () { + var updated, prefs; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.updateLiveEventPreferences(action)]; + case 1: + updated = _a.sent(); + prefs = updated.find(function (p) { + return asPredicate(AppBskyActorDefs.validateLiveEventPreferences)(p); + }); + switch (action.type) { + case 'hideFeed': + case 'unhideFeed': { + if (!props.feed) { + ax.logger.error("useUpdateLiveEventPreferences: feed is missing, but required for hiding/unhiding", { + action: action, + }); + break; + } + ax.metric(action.type === 'hideFeed' + ? 'liveEvents:feedBanner:hide' + : 'liveEvents:feedBanner:unhide', { + feed: props.feed.url, + context: props.metricContext, + }); + break; + } + case 'toggleHideAllFeeds': { + if (prefs.hideAllFeeds) { + ax.metric('liveEvents:hideAllFeedBanners', { + context: props.metricContext, + }); + } + else { + ax.metric('liveEvents:unhideAllFeedBanners', { + context: props.metricContext, + }); + } + break; + } + } + // triggers a refetch + queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }); + return [2 /*return*/, prefs]; + } + }); + }); }, + }); +} diff --git a/src/features/liveEvents/types.js b/src/features/liveEvents/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/features/liveEvents/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/features/liveNow/components/EditLiveDialog.js b/src/features/liveNow/components/EditLiveDialog.js new file mode 100644 index 0000000000..a3b0ef2bc9 --- /dev/null +++ b/src/features/liveNow/components/EditLiveDialog.js @@ -0,0 +1,85 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { AppBskyActorStatus, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { differenceInMinutes } from 'date-fns'; +import { useDebouncedValue } from '#/lib/hooks/useDebouncedValue'; +import { cleanError } from '#/lib/strings/errors'; +import { definitelyUrl } from '#/lib/strings/url-helpers'; +import { useTickEveryMinute } from '#/state/shell'; +import { atoms as a, platform, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextField from '#/components/forms/TextField'; +import { Clock_Stroke2_Corner0_Rounded as ClockIcon } from '#/components/icons/Clock'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { displayDuration, useLiveLinkMetaQuery, useRemoveLiveStatusMutation, useUpsertLiveStatusMutation, } from '#/features/liveNow'; +import { LinkPreview } from '#/features/liveNow/components/LinkPreview'; +export function EditLiveDialog(_a) { + var control = _a.control, status = _a.status, embed = _a.embed; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { status: status, embed: embed })] })); +} +function DialogInner(_a) { + var _b; + var status = _a.status, embed = _a.embed; + var control = Dialog.useDialogContext(); + var _c = useLingui(), _ = _c._, i18n = _c.i18n; + var t = useTheme(); + var _d = useState(embed.external.uri), liveLink = _d[0], setLiveLink = _d[1]; + var _e = useState(''), liveLinkError = _e[0], setLiveLinkError = _e[1]; + var tick = useTickEveryMinute(); + var liveLinkUrl = definitelyUrl(liveLink); + var debouncedUrl = useDebouncedValue(liveLinkUrl, 500); + var isDirty = liveLinkUrl !== embed.external.uri; + var _f = useLiveLinkMetaQuery(debouncedUrl), linkMeta = _f.data, hasValidLinkMeta = _f.isSuccess, linkMetaLoading = _f.isLoading, linkMetaError = _f.error; + var record = useMemo(function () { + if (!AppBskyActorStatus.isRecord(status.record)) + return null; + var validation = AppBskyActorStatus.validateRecord(status.record); + if (validation.success) { + return validation.value; + } + return null; + }, [status]); + var _g = useUpsertLiveStatusMutation((_b = record === null || record === void 0 ? void 0 : record.durationMinutes) !== null && _b !== void 0 ? _b : 0, linkMeta, record === null || record === void 0 ? void 0 : record.createdAt), goLive = _g.mutate, isGoingLive = _g.isPending, goLiveError = _g.error; + var _h = useRemoveLiveStatusMutation(), removeLiveStatus = _h.mutate, isRemovingLiveStatus = _h.isPending, removeLiveStatusError = _h.error; + var _j = useMemo(function () { + var _a; + void tick; + var expiry = new Date((_a = status.expiresAt) !== null && _a !== void 0 ? _a : new Date()); + return { + expiryDateTime: expiry, + minutesUntilExpiry: differenceInMinutes(expiry, new Date()), + }; + }, [tick, status.expiresAt]), minutesUntilExpiry = _j.minutesUntilExpiry, expiryDateTime = _j.expiryDateTime; + var submitDisabled = isGoingLive || + !hasValidLinkMeta || + debouncedUrl !== liveLinkUrl || + isRemovingLiveStatus; + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You are Live"], ["You are Live"])))), style: web({ maxWidth: 420 }), children: [_jsxs(View, { style: [a.gap_lg], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_2xl], children: _jsx(Trans, { children: "You are Live" }) }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(ClockIcon, { style: [t.atoms.text_contrast_high], size: "sm" }), _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_high], children: typeof (record === null || record === void 0 ? void 0 : record.durationMinutes) === 'number' ? (_jsxs(Trans, { children: ["Expires in ", displayDuration(i18n, minutesUntilExpiry), " at", ' ', i18n.date(expiryDateTime, { + hour: 'numeric', + minute: '2-digit', + hour12: true, + })] })) : (_jsx(Trans, { children: "No expiry set" })) })] })] }), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Live link" }) }), _jsx(TextField.Root, { isInvalid: !!liveLinkError || !!linkMetaError, children: _jsx(TextField.Input, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Live link"], ["Live link"])))), placeholder: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["www.mylivestream.tv"], ["www.mylivestream.tv"])))), value: liveLink, onChangeText: setLiveLink, onFocus: function () { return setLiveLinkError(''); }, onBlur: function () { + if (!definitelyUrl(liveLink)) { + setLiveLinkError('Invalid URL'); + } + }, returnKeyType: "done", autoCapitalize: "none", autoComplete: "url", autoCorrect: false, onSubmitEditing: function () { + if (isDirty && !submitDisabled) { + goLive(); + } + } }) })] }), (liveLinkError || linkMetaError) && (_jsx(Admonition, { type: "error", children: liveLinkError ? (_jsx(Trans, { children: "This is not a valid link" })) : (cleanError(linkMetaError)) })), _jsx(LinkPreview, { linkMeta: linkMeta, loading: linkMetaLoading })] }), goLiveError && (_jsx(Admonition, { type: "error", children: cleanError(goLiveError) })), removeLiveStatusError && (_jsx(Admonition, { type: "error", children: cleanError(removeLiveStatusError) })), _jsxs(View, { style: platform({ + native: [a.gap_md, a.pt_lg], + web: [a.flex_row_reverse, a.gap_md, a.align_center], + }), children: [isDirty ? (_jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Save"], ["Save"])))), size: platform({ native: 'large', web: 'small' }), color: "primary", variant: "solid", onPress: function () { return goLive(); }, disabled: submitDisabled, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Save" }) }), isGoingLive && _jsx(ButtonIcon, { icon: Loader })] })) : (_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Close"], ["Close"])))), size: platform({ native: 'large', web: 'small' }), color: "primary", variant: "solid", onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })), _jsxs(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Remove live status"], ["Remove live status"])))), onPress: function () { return removeLiveStatus(); }, size: platform({ native: 'large', web: 'small' }), color: "negative_subtle", variant: "solid", disabled: isRemovingLiveStatus || isGoingLive, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Remove live status" }) }), isRemovingLiveStatus && _jsx(ButtonIcon, { icon: Loader })] })] })] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/features/liveNow/components/GoLiveDialog.js b/src/features/liveNow/components/GoLiveDialog.js new file mode 100644 index 0000000000..341ea3117f --- /dev/null +++ b/src/features/liveNow/components/GoLiveDialog.js @@ -0,0 +1,79 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useDebouncedValue } from '#/lib/hooks/useDebouncedValue'; +import { cleanError } from '#/lib/strings/errors'; +import { definitelyUrl } from '#/lib/strings/url-helpers'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useTickEveryMinute } from '#/state/shell'; +import { atoms as a, ios, native, platform, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextField from '#/components/forms/TextField'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import * as Select from '#/components/Select'; +import { Text } from '#/components/Typography'; +import { displayDuration, getLiveServiceNames, useLiveLinkMetaQuery, useLiveNowConfig, useUpsertLiveStatusMutation, } from '#/features/liveNow'; +import { LinkPreview } from './LinkPreview'; +export function GoLiveDialog(_a) { + var control = _a.control, profile = _a.profile; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { profile: profile })] })); +} +// Possible durations: max 4 hours, 5 minute intervals +var DURATIONS = Array.from({ length: (4 * 60) / 5 }).map(function (_, i) { return (i + 1) * 5; }); +function DialogInner(_a) { + var profile = _a.profile; + var control = Dialog.useDialogContext(); + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var t = useTheme(); + var _c = useState(''), liveLink = _c[0], setLiveLink = _c[1]; + var _d = useState(''), liveLinkError = _d[0], setLiveLinkError = _d[1]; + var _e = useState(60), duration = _e[0], setDuration = _e[1]; + var moderationOpts = useModerationOpts(); + var tick = useTickEveryMinute(); + var liveNowConfig = useLiveNowConfig(); + var allowedServices = getLiveServiceNames(liveNowConfig.currentAccountAllowedHosts).formatted; + var time = useCallback(function (offset) { + void tick; + var date = new Date(); + date.setMinutes(date.getMinutes() + offset); + return i18n.date(date, { hour: 'numeric', minute: '2-digit', hour12: true }); + }, [tick, i18n]); + var onChangeDuration = useCallback(function (newDuration) { + setDuration(Number(newDuration)); + }, []); + var liveLinkUrl = definitelyUrl(liveLink); + var debouncedUrl = useDebouncedValue(liveLinkUrl, 500); + var _f = useLiveLinkMetaQuery(debouncedUrl), linkMeta = _f.data, hasValidLinkMeta = _f.isSuccess, linkMetaLoading = _f.isLoading, linkMetaError = _f.error; + var _g = useUpsertLiveStatusMutation(duration, linkMeta), goLive = _g.mutate, isGoingLive = _g.isPending, goLiveError = _g.error; + var isSourceInvalid = !!liveLinkError || !!linkMetaError; + var hasLink = !!debouncedUrl && !isSourceInvalid; + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go Live"], ["Go Live"])))), style: web({ maxWidth: 420 }), children: [_jsxs(View, { style: [a.gap_xl], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_2xl], children: _jsx(Trans, { children: "Go Live" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "Add a temporary live status to your profile. When someone clicks on your avatar, they\u2019ll see information about your live event." }) })] }), moderationOpts && (_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts, liveOverride: true, disabledPreview: true }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts })] })), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Live link" }) }), _jsx(TextField.Root, { isInvalid: isSourceInvalid, children: _jsx(TextField.Input, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Live link"], ["Live link"])))), placeholder: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["www.mylivestream.tv"], ["www.mylivestream.tv"])))), value: liveLink, onChangeText: setLiveLink, onFocus: function () { return setLiveLinkError(''); }, onBlur: function () { + if (!definitelyUrl(liveLink)) { + setLiveLinkError('Invalid URL'); + } + }, returnKeyType: "done", autoCapitalize: "none", autoComplete: "url", autoCorrect: false }) })] }), liveLinkError || linkMetaError ? (_jsx(Admonition, { type: "error", children: liveLinkError ? (_jsx(Trans, { children: "This is not a valid link" })) : (cleanError(linkMetaError)) })) : (_jsx(Admonition, { type: "tip", children: _jsxs(Trans, { children: ["The following services are enabled for your account:", ' ', allowedServices] }) })), _jsx(LinkPreview, { linkMeta: linkMeta, loading: linkMetaLoading })] }), hasLink && (_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Go live for" }) }), _jsxs(Select.Root, { value: String(duration), onValueChange: onChangeDuration, children: [_jsxs(Select.Trigger, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Select duration"], ["Select duration"])))), children: [_jsxs(Text, { style: [ios(a.py_xs)], children: [displayDuration(i18n, duration), ' ', _jsx(Text, { style: [t.atoms.text_contrast_low], children: time(duration) })] }), _jsx(Select.Icon, {})] }), _jsx(Select.Content, { renderItem: function (item, _i, selectedValue) { + var label = displayDuration(i18n, item); + return (_jsxs(Select.Item, { value: String(item), label: label, children: [_jsx(Select.ItemIndicator, {}), _jsxs(Select.ItemText, { children: [label, ' ', _jsx(Text, { style: [ + native(a.text_md), + web(a.ml_xs), + selectedValue === String(item) + ? t.atoms.text_contrast_medium + : t.atoms.text_contrast_low, + a.font_normal, + ], children: time(item) })] })] })); + }, items: DURATIONS, valueExtractor: function (d) { return String(d); } })] })] })), goLiveError && (_jsx(Admonition, { type: "error", children: cleanError(goLiveError) })), _jsxs(View, { style: platform({ + native: [a.gap_md, a.pt_lg], + web: [a.flex_row_reverse, a.gap_md, a.align_center], + }), children: [hasLink && (_jsxs(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Go Live"], ["Go Live"])))), size: platform({ native: 'large', web: 'small' }), color: "primary", variant: "solid", onPress: function () { return goLive(); }, disabled: isGoingLive || !hasValidLinkMeta || debouncedUrl !== liveLinkUrl, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Go Live" }) }), isGoingLive && _jsx(ButtonIcon, { icon: Loader })] })), _jsx(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: function () { return control.close(); }, size: platform({ native: 'large', web: 'small' }), color: "secondary", variant: platform({ native: 'solid', web: 'ghost' }), children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] })] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/features/liveNow/components/GoLiveDisabledDialog.js b/src/features/liveNow/components/GoLiveDisabledDialog.js new file mode 100644 index 0000000000..8c53833270 --- /dev/null +++ b/src/features/liveNow/components/GoLiveDisabledDialog.js @@ -0,0 +1,125 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { ToolsOzoneReportDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation } from '@tanstack/react-query'; +import { BLUESKY_MOD_SERVICE_HEADERS } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { atoms as a, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +export function GoLiveDisabledDialog(_a) { + var control = _a.control, status = _a.status; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { control: control, status: status })] })); +} +export function DialogInner(_a) { + var _this = this; + var control = _a.control, status = _a.status; + var _ = useLingui()._; + var agent = useAgent(); + var _b = useState(''), details = _b[0], setDetails = _b[1]; + var _c = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!((_a = agent.session) === null || _a === void 0 ? void 0 : _a.did)) { + throw new Error('Not logged in'); + } + if (!status.uri || !status.cid) { + throw new Error('Status is missing uri or cid'); + } + if (!__DEV__) return [3 /*break*/, 1]; + logger.info('Submitting go live appeal', { + details: details, + }); + return [3 /*break*/, 3]; + case 1: return [4 /*yield*/, agent.createModerationReport({ + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: status.uri, + cid: status.cid, + }, + reason: details, + }, { + encoding: 'application/json', + headers: BLUESKY_MOD_SERVICE_HEADERS, + })]; + case 2: + _b.sent(); + _b.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); }, + onError: function () { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to submit appeal, please try again."], ["Failed to submit appeal, please try again."])))), { + type: 'error', + }); + }, + onSuccess: function () { + control.close(); + Toast.show(_(msg({ message: 'Appeal submitted', context: 'toast' })), { + type: 'success', + }); + }, + }), mutate = _c.mutate, isPending = _c.isPending; + var onSubmit = useCallback(function () { return mutate(); }, [mutate]); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Appeal livestream suspension"], ["Appeal livestream suspension"])))), style: [web({ maxWidth: 400 })], children: [_jsxs(View, { style: [a.gap_lg], children: [_jsxs(View, { style: [a.gap_md], children: [_jsx(Text, { style: [ + a.flex_1, + a.text_2xl, + a.font_semi_bold, + a.leading_snug, + a.pr_4xl, + ], children: _jsx(Trans, { children: "Going live is currently disabled for your account" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "You are currently blocked from using the Go Live feature. To appeal this moderation decision, please submit the form below." }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "This appeal will be sent to Bluesky's moderation service." }) })] }), _jsxs(View, { style: [a.gap_md], children: [_jsx(Dialog.Input, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Text input field"], ["Text input field"])))), placeholder: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Please explain why you think your Go Live access was incorrectly disabled."], ["Please explain why you think your Go Live access was incorrectly disabled."])))), value: details, onChangeText: setDetails, autoFocus: true, numberOfLines: 3, multiline: true, maxLength: 300 }), _jsxs(Button, { testID: "submitBtn", variant: "solid", color: "primary", size: "large", onPress: onSubmit, label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Submit"], ["Submit"])))), children: [_jsx(ButtonText, { children: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Submit"], ["Submit"])))) }), isPending && _jsx(ButtonIcon, { icon: Loader })] })] })] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/features/liveNow/components/LinkPreview.js b/src/features/liveNow/components/LinkPreview.js new file mode 100644 index 0000000000..336124a073 --- /dev/null +++ b/src/features/liveNow/components/LinkPreview.js @@ -0,0 +1,39 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { Trans } from '@lingui/react/macro'; +import { toNiceDomain } from '#/lib/strings/url-helpers'; +import { LoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { atoms as a, useTheme } from '#/alf'; +import { Globe_Stroke2_Corner0_Rounded as GlobeIcon } from '#/components/icons/Globe'; +import { Image_Stroke2_Corner0_Rounded as ImageIcon } from '#/components/icons/Image'; +import { Text } from '#/components/Typography'; +export function LinkPreview(_a) { + var linkMeta = _a.linkMeta, loading = _a.loading; + var t = useTheme(); + var _b = useState(false), imageLoadError = _b[0], setImageLoadError = _b[1]; + if (!linkMeta && !loading) { + return null; + } + return (_jsxs(View, { style: [ + a.w_full, + a.border, + t.atoms.border_contrast_low, + t.atoms.bg, + a.flex_row, + a.rounded_sm, + a.overflow_hidden, + a.align_stretch, + ], children: [_jsxs(View, { style: [ + t.atoms.bg_contrast_25, + { minHeight: 64, width: 114 }, + a.justify_center, + a.align_center, + a.gap_xs, + ], children: [(linkMeta === null || linkMeta === void 0 ? void 0 : linkMeta.image) && (_jsx(Image, { source: linkMeta.image, accessibilityIgnoresInvertColors: true, transition: 200, style: [a.absolute, a.inset_0], contentFit: "cover", onLoad: function () { return setImageLoadError(false); }, onError: function () { return setImageLoadError(true); } })), linkMeta && (!linkMeta.image || imageLoadError) && (_jsxs(_Fragment, { children: [_jsx(ImageIcon, { style: [t.atoms.text_contrast_low] }), _jsx(Text, { style: [t.atoms.text_contrast_low, a.text_xs, a.text_center], children: _jsx(Trans, { children: "No image" }) })] }))] }), _jsx(View, { style: [a.flex_1, a.justify_center, a.py_sm, a.gap_xs, a.px_md], children: linkMeta ? (_jsxs(_Fragment, { children: [_jsx(Text, { numberOfLines: 2, style: [a.leading_snug, a.font_semi_bold, a.text_md], children: linkMeta.title || linkMeta.url }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_2xs], children: [_jsx(GlobeIcon, { size: "xs", style: [t.atoms.text_contrast_low] }), _jsx(Text, { numberOfLines: 1, style: [ + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: toNiceDomain(linkMeta.url) })] })] })) : (_jsxs(_Fragment, { children: [_jsx(LoadingPlaceholder, { height: 16, width: 128 }), _jsx(LoadingPlaceholder, { height: 12, width: 72 })] })) })] })); +} diff --git a/src/features/liveNow/components/LiveIndicator.js b/src/features/liveNow/components/LiveIndicator.js new file mode 100644 index 0000000000..d59cb67e74 --- /dev/null +++ b/src/features/liveNow/components/LiveIndicator.js @@ -0,0 +1,32 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, tokens, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +export function LiveIndicator(_a) { + var _b = _a.size, size = _b === void 0 ? 'small' : _b, style = _a.style; + var t = useTheme(); + var fontSize = { + tiny: { fontSize: 7, letterSpacing: tokens.TRACKING }, + small: a.text_2xs, + large: a.text_xs, + }[size]; + return (_jsx(View, { style: [ + a.absolute, + a.w_full, + a.align_center, + a.pointer_events_none, + { bottom: size === 'large' ? -8 : -5 }, + style, + ], children: _jsx(View, { style: { + backgroundColor: t.palette.negative_500, + paddingVertical: size === 'large' ? 2 : 1, + paddingHorizontal: size === 'large' ? 4 : 3, + borderRadius: size === 'large' ? 5 : tokens.borderRadius.xs, + }, children: _jsx(Text, { style: [ + a.text_center, + a.font_semi_bold, + fontSize, + { color: t.palette.white }, + ], children: _jsx(Trans, { comment: "Live status indicator on avatar. Should be extremely short, not much space for more than 4 characters", children: "LIVE" }) }) }) })); +} diff --git a/src/features/liveNow/components/LiveStatusDialog.js b/src/features/liveNow/components/LiveStatusDialog.js new file mode 100644 index 0000000000..adc3aecb54 --- /dev/null +++ b/src/features/liveNow/components/LiveStatusDialog.js @@ -0,0 +1,117 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { useOpenLink } from '#/lib/hooks/useOpenLink'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { toNiceDomain } from '#/lib/strings/url-helpers'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { unstableCacheProfileView } from '#/state/queries/profile'; +import { android, atoms as a, platform, tokens, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon } from '#/components/icons/CircleInfo'; +import { Globe_Stroke2_Corner0_Rounded } from '#/components/icons/Globe'; +import { SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon } from '#/components/icons/SquareArrowTopRight'; +import { createStaticClick, SimpleInlineLinkText } from '#/components/Link'; +import { useGlobalReportDialogControl } from '#/components/moderation/ReportDialog'; +import * as ProfileCard from '#/components/ProfileCard'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { LiveIndicator } from '#/features/liveNow/components/LiveIndicator'; +export function LiveStatusDialog(_a) { + var control = _a.control, profile = _a.profile, embed = _a.embed, status = _a.status; + var navigation = useNavigation(); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, { difference: !!embed.external.thumb }), _jsx(DialogInner, { status: status, profile: profile, embed: embed, navigation: navigation })] })); +} +function DialogInner(_a) { + var profile = _a.profile, embed = _a.embed, navigation = _a.navigation, status = _a.status; + var _ = useLingui()._; + var control = Dialog.useDialogContext(); + var onPressOpenProfile = useCallback(function () { + control.close(function () { + navigation.push('Profile', { + name: profile.handle, + }); + }); + }, [navigation, profile.handle, control]); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["", " is live"], ["", " is live"])), sanitizeHandle(profile.handle))), contentContainerStyle: [a.pt_0, a.px_0], style: [web({ maxWidth: 420 }), a.overflow_hidden], children: [_jsx(LiveStatus, { status: status, profile: profile, embed: embed, onPressOpenProfile: onPressOpenProfile }), _jsx(Dialog.Close, {})] })); +} +export function LiveStatus(_a) { + var status = _a.status, profile = _a.profile, embed = _a.embed, _b = _a.padding, padding = _b === void 0 ? 'xl' : _b, onPressOpenProfile = _a.onPressOpenProfile; + var ax = useAnalytics(); + var _ = useLingui()._; + var t = useTheme(); + var queryClient = useQueryClient(); + var openLink = useOpenLink(); + var moderationOpts = useModerationOpts(); + var reportDialogControl = useGlobalReportDialogControl(); + var dialogContext = Dialog.useDialogContext(); + return (_jsxs(_Fragment, { children: [embed.external.thumb && (_jsxs(View, { style: [ + t.atoms.bg_contrast_25, + a.w_full, + a.aspect_card, + android([ + a.overflow_hidden, + { + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }, + ]), + ], children: [_jsx(Image, { source: embed.external.thumb, contentFit: "cover", style: [a.absolute, a.inset_0], accessibilityIgnoresInvertColors: true }), _jsx(LiveIndicator, { size: "large", style: [ + a.absolute, + { top: tokens.space.lg, left: tokens.space.lg }, + a.align_start, + ] })] })), _jsxs(View, { style: [ + a.gap_lg, + padding === 'xl' + ? [a.px_xl, !embed.external.thumb ? a.pt_2xl : a.pt_lg] + : a.p_lg, + ], children: [_jsxs(View, { style: [a.w_full, a.justify_center, a.gap_2xs], children: [_jsx(Text, { numberOfLines: 3, style: [a.leading_snug, a.font_semi_bold, a.text_xl], children: embed.external.title || embed.external.uri }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_2xs], children: [_jsx(Globe_Stroke2_Corner0_Rounded, { size: "xs", style: [t.atoms.text_contrast_medium] }), _jsx(Text, { numberOfLines: 1, style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: toNiceDomain(embed.external.uri) })] })] }), _jsxs(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Watch now"], ["Watch now"])))), size: platform({ native: 'large', web: 'small' }), color: "primary", variant: "solid", onPress: function () { + ax.metric('live:card:watch', { subject: profile.did }); + openLink(embed.external.uri, false); + }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Watch now" }) }), _jsx(ButtonIcon, { icon: SquareArrowTopRightIcon })] }), _jsx(View, { style: [t.atoms.border_contrast_low, a.border_t, a.w_full] }), moderationOpts && (_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts, disabledPreview: true }), _jsx(View, { style: [a.flex_1, web({ minWidth: 100 })], children: _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts }) }), _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Open profile"], ["Open profile"])))), size: "small", color: "secondary", variant: "solid", onPress: function () { + ax.metric('live:card:openProfile', { subject: profile.did }); + unstableCacheProfileView(queryClient, profile); + onPressOpenProfile(); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Open profile" }) }) })] })), _jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.justify_between, + a.w_full, + a.pt_sm, + ], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs, a.flex_1], children: [_jsx(CircleInfoIcon, { size: "sm", fill: t.atoms.text_contrast_low.color }), _jsx(Text, { style: [t.atoms.text_contrast_low, a.text_sm], children: _jsx(Trans, { children: "Live feature is in beta" }) })] }), status && (_jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Report this livestream"], ["Report this livestream"])))) }, createStaticClick(function () { + function open() { + reportDialogControl.open({ + subject: __assign(__assign({}, status), { $type: 'app.bsky.actor.defs#statusView' }), + }); + } + if (dialogContext.isWithinDialog) { + dialogContext.close(open); + } + else { + open(); + } + }), { style: [a.text_sm, a.underline, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Report" }) })))] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/features/liveNow/index.js b/src/features/liveNow/index.js new file mode 100644 index 0000000000..5ef394add9 --- /dev/null +++ b/src/features/liveNow/index.js @@ -0,0 +1,391 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMemo } from 'react'; +import { AppBskyEmbedExternal, AtUri, ComAtprotoRepoPutRecord, } from '@atproto/api'; +import { retry } from '@atproto/common-web'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { isAfter, parseISO } from 'date-fns'; +import { uploadBlob } from '#/lib/api'; +import { imageToThumb } from '#/lib/api/resolve'; +import { getLinkMeta } from '#/lib/link-meta/link-meta'; +import { useAppConfig } from '#/state/appConfig'; +import { updateProfileShadow, useMaybeProfileShadow, } from '#/state/cache/profile-shadow'; +import { useAgent, useSession } from '#/state/session'; +import { useTickEveryMinute } from '#/state/shell'; +import * as Toast from '#/view/com/util/Toast'; +import { useDialogContext } from '#/components/Dialog'; +import { useAnalytics } from '#/analytics'; +import { getLiveNowHost, getLiveServiceNames } from '#/features/liveNow/utils'; +export * from '#/features/liveNow/utils'; +export var DEFAULT_ALLOWED_DOMAINS = [ + 'twitch.tv', + 'stream.place', + 'bluecast.app', +]; +export function useLiveNowConfig() { + var ax = useAnalytics(); + var liveNow = useAppConfig().liveNow; + var currentAccount = useSession().currentAccount; + return useMemo(function () { + var _a; + var disabled = ax.features.enabled(ax.features.LiveNowBetaDisable); + var defaultAllowedHosts = new Set(DEFAULT_ALLOWED_DOMAINS.concat(liveNow.allow)); + var allowedHostsExceptionsByDid = new Map(); + for (var _i = 0, _b = liveNow.exceptions; _i < _b.length; _i++) { + var ex = _b[_i]; + allowedHostsExceptionsByDid.set(ex.did, new Set(DEFAULT_ALLOWED_DOMAINS.concat(ex.allow))); + } + if (!(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) || disabled) { + return { + canGoLive: false, + currentAccountAllowedHosts: new Set(), + defaultAllowedHosts: defaultAllowedHosts, + allowedHostsExceptionsByDid: allowedHostsExceptionsByDid, + }; + } + return { + canGoLive: true, + currentAccountAllowedHosts: (_a = allowedHostsExceptionsByDid.get(currentAccount.did)) !== null && _a !== void 0 ? _a : defaultAllowedHosts, + defaultAllowedHosts: defaultAllowedHosts, + allowedHostsExceptionsByDid: allowedHostsExceptionsByDid, + }; + }, [ax, liveNow, currentAccount]); +} +export function useActorStatus(actor) { + var shadowed = useMaybeProfileShadow(actor); + var tick = useTickEveryMinute(); + var config = useLiveNowConfig(); + return useMemo(function () { + void tick; // revalidate every minute + if (shadowed && 'status' in shadowed && shadowed.status) { + var isValid = isStatusValidForViewers(shadowed.status, config); + var isDisabled = shadowed.status.isDisabled || false; + var isActive = isStatusStillActive(shadowed.status.expiresAt); + if (isValid && !isDisabled && isActive) { + return { + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled: false, + isActive: true, + status: 'app.bsky.actor.status#live', + embed: shadowed.status.embed, // temp_isStatusValid asserts this + expiresAt: shadowed.status.expiresAt, // isStatusStillActive asserts this + record: shadowed.status.record, + }; + } + return { + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled: isDisabled, + isActive: false, + status: 'app.bsky.actor.status#live', + embed: shadowed.status.embed, // temp_isStatusValid asserts this + expiresAt: shadowed.status.expiresAt, // isStatusStillActive asserts this + record: shadowed.status.record, + }; + } + else { + return { + status: '', + isDisabled: false, + isActive: false, + record: {}, + }; + } + }, [shadowed, config, tick]); +} +export function isStatusStillActive(timeStr) { + if (!timeStr) + return false; + var now = new Date(); + var expiry = parseISO(timeStr); + return isAfter(expiry, now); +} +/** + * Validates whether the live status is valid for display in the app. Does NOT + * validate if the status is valid for the acting user e.g. as they go live. + */ +export function isStatusValidForViewers(status, config) { + if (status.status !== 'app.bsky.actor.status#live') + return false; + if (!status.uri) + return false; // should not happen, just backwards compat + try { + var liveDid = new AtUri(status.uri).host; + if (AppBskyEmbedExternal.isView(status.embed)) { + var host = getLiveNowHost(status.embed.external.uri); + var exception = config.allowedHostsExceptionsByDid.get(liveDid); + var isValidException = exception ? exception.has(host) : false; + var isValidForAnyone = config.defaultAllowedHosts.has(host); + return isValidException || isValidForAnyone; + } + else { + return false; + } + } + catch (_a) { + return false; + } +} +export function useLiveLinkMetaQuery(url) { + var _this = this; + var liveNowConfig = useLiveNowConfig(); + var _ = useLingui()._; + var agent = useAgent(); + return useQuery({ + enabled: !!url, + queryKey: ['link-meta', url], + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var host, formatted; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!url) + return [2 /*return*/, undefined]; + host = getLiveNowHost(url); + if (!liveNowConfig.currentAccountAllowedHosts.has(host)) { + formatted = getLiveServiceNames(liveNowConfig.currentAccountAllowedHosts).formatted; + throw new Error(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["This service is not supported while the Live feature is in beta. Allowed services: ", "."], ["This service is not supported while the Live feature is in beta. Allowed services: ", "."])), formatted))); + } + return [4 /*yield*/, getLinkMeta(agent, url)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }); +} +export function useUpsertLiveStatusMutation(duration, linkMeta, createdAt) { + var _this = this; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + var control = useDialogContext(); + var _ = useLingui()._; + return useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var embed, thumb, img, blob, e_1, record, upsert; + var _this = this; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) + throw new Error('Not logged in'); + if (!linkMeta) return [3 /*break*/, 7]; + thumb = void 0; + if (!linkMeta.image) return [3 /*break*/, 6]; + _c.label = 1; + case 1: + _c.trys.push([1, 5, , 6]); + return [4 /*yield*/, imageToThumb(linkMeta.image)]; + case 2: + img = _c.sent(); + if (!img) return [3 /*break*/, 4]; + return [4 /*yield*/, uploadBlob(agent, img.source.path, img.source.mime)]; + case 3: + blob = _c.sent(); + thumb = blob.data.blob; + _c.label = 4; + case 4: return [3 /*break*/, 6]; + case 5: + e_1 = _c.sent(); + ax.logger.error("Failed to upload thumbnail for live status", { + url: linkMeta.url, + image: linkMeta.image, + safeMessage: e_1, + }); + return [3 /*break*/, 6]; + case 6: + embed = { + $type: 'app.bsky.embed.external', + external: { + $type: 'app.bsky.embed.external#external', + title: (_a = linkMeta.title) !== null && _a !== void 0 ? _a : '', + description: (_b = linkMeta.description) !== null && _b !== void 0 ? _b : '', + uri: linkMeta.url, + thumb: thumb, + }, + }; + _c.label = 7; + case 7: + record = { + $type: 'app.bsky.actor.status', + createdAt: createdAt !== null && createdAt !== void 0 ? createdAt : new Date().toISOString(), + status: 'app.bsky.actor.status#live', + durationMinutes: duration, + embed: embed, + }; + upsert = function () { return __awaiter(_this, void 0, void 0, function () { + var repo, collection, existing; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + repo = currentAccount.did; + collection = 'app.bsky.actor.status'; + return [4 /*yield*/, agent.com.atproto.repo + .getRecord({ repo: repo, collection: collection, rkey: 'self' }) + .catch(function (_e) { return undefined; })]; + case 1: + existing = _a.sent(); + return [4 /*yield*/, agent.com.atproto.repo.putRecord({ + repo: repo, + collection: collection, + rkey: 'self', + record: record, + swapRecord: (existing === null || existing === void 0 ? void 0 : existing.data.cid) || null, + })]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }; + return [4 /*yield*/, retry(upsert, { + maxRetries: 5, + retryable: function (e) { return e instanceof ComAtprotoRepoPutRecord.InvalidSwapError; }, + })]; + case 8: + _c.sent(); + return [2 /*return*/, { + record: record, + image: linkMeta === null || linkMeta === void 0 ? void 0 : linkMeta.image, + }]; + } + }); + }); }, + onError: function (e) { + ax.logger.error("Failed to upsert live status", { + url: linkMeta === null || linkMeta === void 0 ? void 0 : linkMeta.url, + image: linkMeta === null || linkMeta === void 0 ? void 0 : linkMeta.image, + safeMessage: e, + }); + }, + onSuccess: function (_a) { + var record = _a.record, image = _a.image; + if (createdAt) { + ax.metric('live:edit', { duration: record.durationMinutes }); + } + else { + ax.metric('live:create', { duration: record.durationMinutes }); + } + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You are now live!"], ["You are now live!"]))))); + control.close(function () { + if (!currentAccount) + return; + var expiresAt = new Date(record.createdAt); + expiresAt.setMinutes(expiresAt.getMinutes() + record.durationMinutes); + updateProfileShadow(queryClient, currentAccount.did, { + status: { + $type: 'app.bsky.actor.defs#statusView', + status: 'app.bsky.actor.status#live', + isActive: true, + expiresAt: expiresAt.toISOString(), + embed: record.embed && image + ? { + $type: 'app.bsky.embed.external#view', + external: __assign(__assign({}, record.embed.external), { $type: 'app.bsky.embed.external#viewExternal', thumb: image }), + } + : undefined, + record: record, + }, + }); + }); + }, + }); +} +export function useRemoveLiveStatusMutation() { + var _this = this; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + var control = useDialogContext(); + var _ = useLingui()._; + return useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!currentAccount) + throw new Error('Not logged in'); + return [4 /*yield*/, agent.app.bsky.actor.status.delete({ + repo: currentAccount.did, + rkey: 'self', + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onError: function (e) { + ax.logger.error("Failed to remove live status", { + safeMessage: e, + }); + }, + onSuccess: function () { + ax.metric('live:remove', {}); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You are no longer live"], ["You are no longer live"]))))); + control.close(function () { + if (!currentAccount) + return; + updateProfileShadow(queryClient, currentAccount.did, { + status: undefined, + }); + }); + }, + }); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/features/liveNow/utils.js b/src/features/liveNow/utils.js new file mode 100644 index 0000000000..b5bc94850b --- /dev/null +++ b/src/features/liveNow/utils.js @@ -0,0 +1,58 @@ +import { plural } from '@lingui/core/macro'; +import psl from 'psl'; +export function displayDuration(i18n, durationInMinutes) { + var roundedDurationInMinutes = Math.round(durationInMinutes); + var hours = Math.floor(roundedDurationInMinutes / 60); + var minutes = roundedDurationInMinutes % 60; + var minutesString = i18n._(plural(minutes, { one: '# minute', other: '# minutes' })); + return hours > 0 + ? i18n._(minutes > 0 + ? plural(hours, { + one: "# hour ".concat(minutesString), + other: "# hours ".concat(minutesString), + }) + : plural(hours, { + one: '# hour', + other: '# hours', + })) + : minutesString; +} +var serviceUrlToNameMap = { + 'twitch.tv': 'Twitch', + 'youtube.com': 'YouTube', + 'nba.com': 'NBA', + 'nba.smart.link': 'nba.smart.link', + 'espn.com': 'ESPN', + 'stream.place': 'Streamplace', + 'skylight.social': 'Skylight', + 'bluecast.app': 'Bluecast', +}; +export function getLiveServiceNames(domains) { + var names = Array.from(new Set(Array.from(domains.values()) + .map(function (d) { return sanitizeLiveNowHost(d); }) + .map(function (d) { return serviceUrlToNameMap[d] || d; }))); + return { + names: names, + formatted: names.join(', '), + }; +} +export function sanitizeLiveNowHost(hostname) { + // special case this one + if (hostname === 'nba.smart.link') { + return hostname; + } + var parsed = psl.parse(hostname); + if (parsed.error || !parsed.listed || !parsed.domain) { + // fall back to dumb version + return hostname.replace(/^www\./, ''); + } + return parsed.domain; +} +/** + * Extracts the apex domain from a given URL, for use when matching allowed + * Live Now hosts. + */ +export function getLiveNowHost(url) { + var hostname = new URL(url).hostname; + return sanitizeLiveNowHost(hostname); +} diff --git a/src/features/nuxs/components/Dot.js b/src/features/nuxs/components/Dot.js new file mode 100644 index 0000000000..9c665c8cd8 --- /dev/null +++ b/src/features/nuxs/components/Dot.js @@ -0,0 +1,20 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +/** + * The little blue dot used to nudge a user towards a certain feature. The dot + * is absolutely positioned, and is intended to be configured by passing in + * positional styles via `top`, `bottom`, `left`, and `right` props. + */ +export function Dot(_a) { + var top = _a.top, bottom = _a.bottom, left = _a.left, right = _a.right; + var t = useTheme(); + return (_jsx(View, { style: [a.absolute, { top: top, bottom: bottom, left: left, right: right }], children: _jsx(View, { style: [ + a.rounded_full, + { + height: 8, + width: 8, + backgroundColor: t.palette.primary_500, + }, + ] }) })); +} diff --git a/src/features/nuxs/components/Gradient.js b/src/features/nuxs/components/Gradient.js new file mode 100644 index 0000000000..72f97af64e --- /dev/null +++ b/src/features/nuxs/components/Gradient.js @@ -0,0 +1,16 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { LinearGradient } from 'expo-linear-gradient'; +import { atoms as a, useTheme, utils } from '#/alf'; +/** + * A gradient overlay using the primary color at low opacity. This component is + * absolutely positioned and intended to be composed within other components, + * with optional styling allowed, such as adjusting border radius. + */ +export function Gradient(_a) { + var style = _a.style; + var t = useTheme(); + return (_jsx(LinearGradient, { colors: [ + utils.alpha(t.palette.primary_500, 0.2), + utils.alpha(t.palette.primary_500, 0.1), + ], locations: [0, 1], start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, style: [a.absolute, a.inset_0, style] })); +} diff --git a/src/geolocation/const.js b/src/geolocation/const.js new file mode 100644 index 0000000000..639a89dfd1 --- /dev/null +++ b/src/geolocation/const.js @@ -0,0 +1,9 @@ +import { GEOLOCATION_URL } from '#/env'; +export var GEOLOCATION_SERVICE_URL = "".concat(GEOLOCATION_URL, "/geolocation"); +/** + * Default geolocation config. + */ +export var FALLBACK_GEOLOCATION_SERVICE_RESPONSE = { + countryCode: undefined, + regionCode: undefined, +}; diff --git a/src/geolocation/debug.js b/src/geolocation/debug.js new file mode 100644 index 0000000000..c67b015682 --- /dev/null +++ b/src/geolocation/debug.js @@ -0,0 +1,65 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var _a; +import * as aaDebug from '#/ageAssurance/debug'; +import { IS_DEV } from '#/env'; +var localEnabled = false; +export var enabled = IS_DEV && (localEnabled || aaDebug.geolocation); +export var geolocation = (_a = aaDebug.geolocation) !== null && _a !== void 0 ? _a : { + countryCode: 'US', + regionCode: 'TX', +}; +var deviceLocalEnabled = false; +export var deviceGeolocation = aaDebug.deviceGeolocation || + (deviceLocalEnabled + ? { + countryCode: 'US', + regionCode: 'TX', + } + : undefined); +export function resolve(data) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, new Promise(function (y) { return setTimeout(y, 500); })]; // simulate network + case 1: + _a.sent(); // simulate network + return [2 /*return*/, data]; + } + }); + }); +} diff --git a/src/geolocation/device.js b/src/geolocation/device.js new file mode 100644 index 0000000000..deffed3cd7 --- /dev/null +++ b/src/geolocation/device.js @@ -0,0 +1,209 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback, useEffect, useRef } from 'react'; +import { Platform } from 'react-native'; +import * as Location from 'expo-location'; +import { createPermissionHook } from 'expo-modules-core'; +import { IS_NATIVE } from '#/env'; +import * as debug from '#/geolocation/debug'; +import { logger } from '#/geolocation/logger'; +import { normalizeDeviceLocation } from '#/geolocation/util'; +import { device } from '#/storage'; +/** + * Location.useForegroundPermissions on web just errors if the + * navigator.permissions API is not available. We need to catch and ignore it, + * since it's effectively denied. + * + * @see https://github.com/expo/expo/blob/72f1562ed9cce5ff6dfe04aa415b71632a3d4b87/packages/expo-location/src/Location.ts#L290-L293 + */ +var useForegroundPermissions = createPermissionHook({ + getMethod: function () { + return Location.getForegroundPermissionsAsync().catch(function (error) { + logger.debug('useForegroundPermission: error getting location permissions', { safeMessage: error }); + return { + status: Location.PermissionStatus.DENIED, + granted: false, + canAskAgain: false, + expires: 0, + }; + }); + }, + requestMethod: function () { + return Location.requestForegroundPermissionsAsync().catch(function (error) { + logger.debug('useForegroundPermission: error requesting location permissions', { safeMessage: error }); + return { + status: Location.PermissionStatus.DENIED, + granted: false, + canAskAgain: false, + expires: 0, + }; + }); + }, +}); +export function getDeviceGeolocation() { + return __awaiter(this, void 0, void 0, function () { + var geocode, locations, location_1, normalized, e_1; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (debug.enabled && debug.deviceGeolocation) + return [2 /*return*/, debug.resolve(debug.deviceGeolocation)]; + _c.label = 1; + case 1: + _c.trys.push([1, 4, , 5]); + return [4 /*yield*/, Location.getCurrentPositionAsync()]; + case 2: + geocode = _c.sent(); + return [4 /*yield*/, Location.reverseGeocodeAsync({ + latitude: geocode.coords.latitude, + longitude: geocode.coords.longitude, + })]; + case 3: + locations = _c.sent(); + location_1 = locations.at(0); + normalized = location_1 ? normalizeDeviceLocation(location_1) : undefined; + if ((normalized === null || normalized === void 0 ? void 0 : normalized.regionCode) && normalized.regionCode.length > 5) { + /* + * We want short codes only, and we're still seeing some full names here. + * 5 is just a heuristic for a region that is probably not formatted as a + * short code. + */ + logger.error('getDeviceGeolocation: invalid regionCode', { + os: Platform.OS, + version: Platform.Version, + regionCode: normalized.regionCode, + }); + } + return [2 /*return*/, { + countryCode: (_a = normalized === null || normalized === void 0 ? void 0 : normalized.countryCode) !== null && _a !== void 0 ? _a : undefined, + regionCode: (_b = normalized === null || normalized === void 0 ? void 0 : normalized.regionCode) !== null && _b !== void 0 ? _b : undefined, + }]; + case 4: + e_1 = _c.sent(); + logger.error('getDeviceGeolocation: failed', { safeMessage: e_1 }); + return [2 /*return*/, { + countryCode: undefined, + regionCode: undefined, + }]; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function useRequestDeviceGeolocation() { + var _this = this; + return useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var status; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, Location.requestForegroundPermissionsAsync()]; + case 1: + status = _b.sent(); + if (!status.granted) return [3 /*break*/, 3]; + _a = { + granted: true + }; + return [4 /*yield*/, getDeviceGeolocation()]; + case 2: return [2 /*return*/, (_a.location = _b.sent(), + _a)]; + case 3: return [2 /*return*/, { + granted: false, + }]; + } + }); + }); }, []); +} +/** + * Hook to get and sync the device geolocation from the device GPS and store it + * using device storage. If permissions are not granted, it will clear any cached + * storage value. + */ +export function useSyncDeviceGeolocationOnStartup(sync) { + var synced = useRef(false); + var status = useForegroundPermissions()[0]; + useEffect(function () { + if (!IS_NATIVE) + return; + function get() { + return __awaiter(this, void 0, void 0, function () { + var location_2, hasCachedValue; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // no need to set this more than once per session + if (synced.current) + return [2 /*return*/]; + logger.debug('useSyncDeviceGeolocationOnStartup: checking perms'); + if (!(status === null || status === void 0 ? void 0 : status.granted)) return [3 /*break*/, 2]; + return [4 /*yield*/, getDeviceGeolocation()]; + case 1: + location_2 = _a.sent(); + if (location_2) { + logger.debug('useSyncDeviceGeolocationOnStartup: got location'); + sync(location_2); + synced.current = true; + } + return [3 /*break*/, 3]; + case 2: + hasCachedValue = device.get(['deviceGeolocation']) !== undefined; + /** + * If we have a cached value, but user has revoked permissions, + * quietly (will take effect lazily) clear this out. + */ + if (hasCachedValue) { + logger.debug('useSyncDeviceGeolocationOnStartup: clearing cached location, perms revoked'); + device.set(['deviceGeolocation'], undefined); + } + _a.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); + } + get().catch(function (e) { + logger.error('useSyncDeviceGeolocationOnStartup: failed to get location', { + safeMessage: e, + }); + }); + }, [status, sync]); +} +export function useIsDeviceGeolocationGranted() { + var status = useForegroundPermissions()[0]; + return (status === null || status === void 0 ? void 0 : status.granted) === true; +} diff --git a/src/geolocation/index.js b/src/geolocation/index.js new file mode 100644 index 0000000000..8c46eb38c8 --- /dev/null +++ b/src/geolocation/index.js @@ -0,0 +1,41 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useEffect, useMemo, } from 'react'; +import { useSyncDeviceGeolocationOnStartup } from '#/geolocation/device'; +import { useGeolocationServiceResponse } from '#/geolocation/service'; +import { mergeGeolocations } from '#/geolocation/util'; +import { device, useStorage } from '#/storage'; +export { useIsDeviceGeolocationGranted, useRequestDeviceGeolocation, } from '#/geolocation/device'; +export { resolve } from '#/geolocation/service'; +export * from '#/geolocation/types'; +var GeolocationContext = createContext({ + countryCode: undefined, + regionCode: undefined, +}); +var DeviceGeolocationAPIContext = createContext({ + setDeviceGeolocation: function () { }, +}); +export function useGeolocation() { + return useContext(GeolocationContext); +} +export function useDeviceGeolocationApi() { + return useContext(DeviceGeolocationAPIContext); +} +export function Provider(_a) { + var children = _a.children; + var geolocationService = useGeolocationServiceResponse(); + var _b = useStorage(device, [ + 'deviceGeolocation', + ]), deviceGeolocation = _b[0], setDeviceGeolocation = _b[1]; + var geolocation = useMemo(function () { + return mergeGeolocations(deviceGeolocation, geolocationService); + }, [deviceGeolocation, geolocationService]); + useEffect(function () { + /** + * Save this for out-of-band-reads during future cold starts of the app. + * Needs to be available for the data prefetching we do on boot. + */ + device.set(['mergedGeolocation'], geolocation); + }, [geolocation]); + useSyncDeviceGeolocationOnStartup(setDeviceGeolocation); + return (_jsx(GeolocationContext.Provider, { value: geolocation, children: _jsx(DeviceGeolocationAPIContext.Provider, { value: useMemo(function () { return ({ setDeviceGeolocation: setDeviceGeolocation }); }, [setDeviceGeolocation]), children: children }) })); +} diff --git a/src/geolocation/logger.js b/src/geolocation/logger.js new file mode 100644 index 0000000000..04452e4734 --- /dev/null +++ b/src/geolocation/logger.js @@ -0,0 +1,2 @@ +import { Logger } from '#/logger'; +export var logger = Logger.create(Logger.Context.Geolocation); diff --git a/src/geolocation/service.js b/src/geolocation/service.js new file mode 100644 index 0000000000..5393bed637 --- /dev/null +++ b/src/geolocation/service.js @@ -0,0 +1,183 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useEffect, useState } from 'react'; +import EventEmitter from 'eventemitter3'; +import { networkRetry } from '#/lib/async/retry'; +import { FALLBACK_GEOLOCATION_SERVICE_RESPONSE, GEOLOCATION_SERVICE_URL, } from '#/geolocation/const'; +import * as debug from '#/geolocation/debug'; +import { logger } from '#/geolocation/logger'; +import { device } from '#/storage'; +var events = new EventEmitter(); +var EVENT = 'geolocation-service-response-updated'; +var emitGeolocationServiceResponseUpdate = function (data) { + events.emit(EVENT, data); +}; +var onGeolocationServiceResponseUpdate = function (listener) { + events.on(EVENT, listener); + return function () { + events.off(EVENT, listener); + }; +}; +function fetchGeolocationServiceData(url) { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (debug.enabled) + return [2 /*return*/, debug.resolve(debug.geolocation)]; + return [4 /*yield*/, fetch(url)]; + case 1: + res = _a.sent(); + if (!res.ok) { + throw new Error("fetchGeolocationServiceData failed ".concat(res.status)); + } + return [2 /*return*/, res.json()]; + } + }); + }); +} +/** + * Local promise used within this file only. + */ +var geolocationServicePromise; +/** + * Begin the process of resolving geolocation config. This is called right away + * at app start, and the promise is awaited later before proceeding with app + * startup. + */ +export function resolve() { + return __awaiter(this, void 0, void 0, function () { + var cached, success; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!geolocationServicePromise) return [3 /*break*/, 4]; + cached = device.get(['geolocationServiceResponse']); + if (!cached) return [3 /*break*/, 1]; + logger.debug("resolve(): using cache"); + return [3 /*break*/, 3]; + case 1: + logger.debug("resolve(): no cache"); + return [4 /*yield*/, geolocationServicePromise]; + case 2: + success = (_a.sent()).success; + if (success) { + logger.debug("resolve(): resolved"); + } + else { + logger.info("resolve(): failed"); + } + _a.label = 3; + case 3: return [3 /*break*/, 5]; + case 4: + logger.debug("resolve(): initiating"); + /** + * THIS PROMISE SHOULD NEVER `reject()`! We want the app to proceed with + * startup, even if geolocation resolution fails. + */ + geolocationServicePromise = new Promise(function (resolve) { return __awaiter(_this, void 0, void 0, function () { + function cacheResponseOrThrow(response) { + if (response) { + device.set(['geolocationServiceResponse'], response); + emitGeolocationServiceResponseUpdate(response); + } + else { + // endpoint should throw on all failures, this is insurance + throw new Error("fetchGeolocationServiceData returned no data"); + } + } + var success, config, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + success = false; + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, fetchGeolocationServiceData(GEOLOCATION_SERVICE_URL)]; + case 2: + config = _a.sent(); + cacheResponseOrThrow(config); + success = true; + return [3 /*break*/, 5]; + case 3: + e_1 = _a.sent(); + logger.debug("resolve(): fetchGeolocationServiceData failed initial request", { + safeMessage: e_1.message, + }); + // retry 3 times, but don't await, proceed with default + networkRetry(3, function () { + return fetchGeolocationServiceData(GEOLOCATION_SERVICE_URL); + }) + .then(function (config) { + cacheResponseOrThrow(config); + }) + .catch(function (e) { + // complete fail closed + logger.debug("resolve(): fetchGeolocationServiceData failed retries", { + safeMessage: e.message, + }); + }); + return [3 /*break*/, 5]; + case 4: + resolve({ success: success }); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }); + _a.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function useGeolocationServiceResponse() { + var _a = useState(function () { + var initial = device.get(['geolocationServiceResponse']) || + FALLBACK_GEOLOCATION_SERVICE_RESPONSE; + return initial; + }), config = _a[0], setConfig = _a[1]; + useEffect(function () { + return onGeolocationServiceResponseUpdate(function (config) { + setConfig(config); + }); + }, []); + return config; +} diff --git a/src/geolocation/types.js b/src/geolocation/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/geolocation/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/geolocation/util.js b/src/geolocation/util.js new file mode 100644 index 0000000000..f9f0fbf9f9 --- /dev/null +++ b/src/geolocation/util.js @@ -0,0 +1,142 @@ +var _a; +import { IS_ANDROID } from '#/env'; +import { logger } from '#/geolocation/logger'; +import { device } from '#/storage'; +/** + * Maps full US region names to their short codes. + * + * Context: in some cases, like on Android, we get the full region name instead + * of the short code. We may need to expand this in the future to other + * countries, hence the prefix. + */ +export var USRegionNameToRegionCode = (_a = { + Alabama: 'AL', + Alaska: 'AK', + Arizona: 'AZ', + Arkansas: 'AR', + California: 'CA', + Colorado: 'CO', + Connecticut: 'CT', + Delaware: 'DE', + Florida: 'FL', + Georgia: 'GA', + Hawaii: 'HI', + Idaho: 'ID', + Illinois: 'IL', + Indiana: 'IN', + Iowa: 'IA', + Kansas: 'KS', + Kentucky: 'KY', + Louisiana: 'LA', + Maine: 'ME', + Maryland: 'MD', + Massachusetts: 'MA', + Michigan: 'MI', + Minnesota: 'MN', + Mississippi: 'MS', + Missouri: 'MO', + Montana: 'MT', + Nebraska: 'NE', + Nevada: 'NV' + }, + _a['New Hampshire'] = 'NH', + _a['New Jersey'] = 'NJ', + _a['New Mexico'] = 'NM', + _a['New York'] = 'NY', + _a['North Carolina'] = 'NC', + _a['North Dakota'] = 'ND', + _a.Ohio = 'OH', + _a.Oklahoma = 'OK', + _a.Oregon = 'OR', + _a.Pennsylvania = 'PA', + _a['Rhode Island'] = 'RI', + _a['South Carolina'] = 'SC', + _a['South Dakota'] = 'SD', + _a.Tennessee = 'TN', + _a.Texas = 'TX', + _a.Utah = 'UT', + _a.Vermont = 'VT', + _a.Virginia = 'VA', + _a.Washington = 'WA', + _a['West Virginia'] = 'WV', + _a.Wisconsin = 'WI', + _a.Wyoming = 'WY', + _a); +/** + * Normalizes a `LocationGeocodedAddress` into a `Geolocation`. + * + * We don't want or care about the full location data, so we trim it down and + * normalize certain fields, like region, into the format we need. + */ +export function normalizeDeviceLocation(location) { + var _a; + var isoCountryCode = location.isoCountryCode, region = location.region; + var regionCode = region !== null && region !== void 0 ? region : undefined; + /* + * Android doesn't give us ISO 3166-2 short codes. We need these for US + */ + if (IS_ANDROID) { + if (region && isoCountryCode === 'US') { + /* + * We need short codes for US states. If we can't remap it, just drop it + * entirely for now. + */ + regionCode = (_a = USRegionNameToRegionCode[region]) !== null && _a !== void 0 ? _a : undefined; + } + else { + /* + * Outside the US, we don't need regionCodes for now, so just drop it. + */ + regionCode = undefined; + } + } + return { + countryCode: isoCountryCode !== null && isoCountryCode !== void 0 ? isoCountryCode : undefined, + regionCode: regionCode, + }; +} +/** + * Combines precise location data with the geolocation config fetched from the + * IP service, with preference to the precise data. + */ +export function mergeGeolocations(device, geolocationService) { + var _a, _b; + var geolocation = { + countryCode: (_a = geolocationService === null || geolocationService === void 0 ? void 0 : geolocationService.countryCode) !== null && _a !== void 0 ? _a : undefined, + regionCode: (_b = geolocationService === null || geolocationService === void 0 ? void 0 : geolocationService.regionCode) !== null && _b !== void 0 ? _b : undefined, + }; + // prefer GPS + if (device === null || device === void 0 ? void 0 : device.countryCode) { + geolocation = device; + } + logger.debug('merged geolocation data', { + device: device, + service: geolocationService, + merged: geolocation, + }); + return geolocation; +} +/** + * Gets the IP-based geolocation as a string in the format of + * "countryCode-regionCode", or just "countryCode" if regionCode is not + * available. + * + * IMPORTANT: this method should only return IP-based data, not the user's GPS + * based data. IP-based data we can already infer from requests, but for + * consistency between frontend and backend, we sometimes want to share the + * value we have on the frontend with the backend. + */ +export function getIPGeolocationString() { + var geo = device.get(['geolocationServiceResponse']); + if (!geo) + return; + var countryCode = geo.countryCode, regionCode = geo.regionCode; + if (countryCode) { + if (regionCode) { + return "".concat(countryCode, "-").concat(regionCode); + } + else { + return countryCode; + } + } +} diff --git a/src/lib/ScrollContext.js b/src/lib/ScrollContext.js new file mode 100644 index 0000000000..1f5d3d27e6 --- /dev/null +++ b/src/lib/ScrollContext.js @@ -0,0 +1,24 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +var ScrollContext = createContext({ + onBeginDrag: undefined, + onEndDrag: undefined, + onScroll: undefined, + onMomentumEnd: undefined, +}); +ScrollContext.displayName = 'ScrollContext'; +export function useScrollHandlers() { + return useContext(ScrollContext); +} +// Note: this completely *overrides* the parent handlers. +// It's up to you to compose them with the parent ones via useScrollHandlers() if needed. +export function ScrollProvider(_a) { + var children = _a.children, onBeginDrag = _a.onBeginDrag, onEndDrag = _a.onEndDrag, onScroll = _a.onScroll, onMomentumEnd = _a.onMomentumEnd; + var handlers = useMemo(function () { return ({ + onBeginDrag: onBeginDrag, + onEndDrag: onEndDrag, + onScroll: onScroll, + onMomentumEnd: onMomentumEnd, + }); }, [onBeginDrag, onEndDrag, onScroll, onMomentumEnd]); + return (_jsx(ScrollContext.Provider, { value: handlers, children: children })); +} diff --git a/src/lib/ThemeContext.js b/src/lib/ThemeContext.js new file mode 100644 index 0000000000..b52e5c9292 --- /dev/null +++ b/src/lib/ThemeContext.js @@ -0,0 +1,23 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext } from 'react'; +import { darkTheme, defaultTheme, dimTheme } from './themes'; +export var ThemeContext = createContext(defaultTheme); +ThemeContext.displayName = 'ThemeContext'; +export var useTheme = function () { return useContext(ThemeContext); }; +function getTheme(theme) { + switch (theme) { + case 'light': + return defaultTheme; + case 'dim': + return dimTheme; + case 'dark': + return darkTheme; + default: + return defaultTheme; + } +} +export var ThemeProvider = function (_a) { + var theme = _a.theme, children = _a.children; + var themeValue = getTheme(theme); + return (_jsx(ThemeContext.Provider, { value: themeValue, children: children })); +}; diff --git a/src/lib/__tests__/parseLinkingUrl.test.js b/src/lib/__tests__/parseLinkingUrl.test.js new file mode 100644 index 0000000000..15f870ec09 --- /dev/null +++ b/src/lib/__tests__/parseLinkingUrl.test.js @@ -0,0 +1,18 @@ +import { describe, expect, it } from '@jest/globals'; +import { parseLinkingUrl } from '../parseLinkingUrl'; +describe('parseLinkingUrl', function () { + it('should correctly parse bluesky:// URLs', function () { + var url = 'bluesky://intent/age-assurance?result=success&actorDid=did:example:123'; + var urlp = parseLinkingUrl(url); + expect(urlp.protocol).toBe('bluesky:'); + expect(urlp.host).toBe(''); + expect(urlp.pathname).toBe('/intent/age-assurance'); + }); + it('should correctly parse standard URLs', function () { + var url = 'https://bsky.app/intent/age-assurance?result=success&actorDid=did:example:123'; + var urlp = parseLinkingUrl(url); + expect(urlp.protocol).toBe('https:'); + expect(urlp.host).toBe('bsky.app'); + expect(urlp.pathname).toBe('/intent/age-assurance'); + }); +}); diff --git a/src/lib/__tests__/persisted-query-storage.test.js b/src/lib/__tests__/persisted-query-storage.test.js new file mode 100644 index 0000000000..6938e73f93 --- /dev/null +++ b/src/lib/__tests__/persisted-query-storage.test.js @@ -0,0 +1,182 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +jest.mock('@bsky.app/react-native-mmkv', function () { return ({ + MMKV: /** @class */ (function () { + function MMKVMock() { + this._store = new Map(); + } + MMKVMock.prototype.getString = function (key) { + return this._store.get(key); + }; + MMKVMock.prototype.set = function (key, value) { + this._store.set(key, value); + }; + MMKVMock.prototype.delete = function (key) { + this._store.delete(key); + }; + MMKVMock.prototype.clearAll = function () { + this._store.clear(); + }; + return MMKVMock; + }()), +}); }); +import { createPersistedQueryStorage } from '../persisted-query-storage'; +describe('createPersistedQueryStorage', function () { + it('should create isolated storage instances', function () { return __awaiter(void 0, void 0, void 0, function () { + var storage1, storage2, _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + storage1 = createPersistedQueryStorage('store1'); + storage2 = createPersistedQueryStorage('store2'); + return [4 /*yield*/, storage1.setItem('key', 'value1')]; + case 1: + _c.sent(); + return [4 /*yield*/, storage2.setItem('key', 'value2')]; + case 2: + _c.sent(); + _a = expect; + return [4 /*yield*/, storage1.getItem('key')]; + case 3: + _a.apply(void 0, [_c.sent()]).toBe('value1'); + _b = expect; + return [4 /*yield*/, storage2.getItem('key')]; + case 4: + _b.apply(void 0, [_c.sent()]).toBe('value2'); + return [2 /*return*/]; + } + }); + }); }); + describe('storage operations', function () { + var storage; + beforeEach(function () { + storage = createPersistedQueryStorage('test_store'); + }); + it('should return null for non-existent keys', function () { return __awaiter(void 0, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, storage.getItem('non-existent-key')]; + case 1: + result = _a.sent(); + expect(result).toBeNull(); + return [2 /*return*/]; + } + }); + }); }); + it('should store and retrieve a value', function () { return __awaiter(void 0, void 0, void 0, function () { + var testValue, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + testValue = JSON.stringify({ data: 'test' }); + return [4 /*yield*/, storage.setItem('test-key', testValue)]; + case 1: + _a.sent(); + return [4 /*yield*/, storage.getItem('test-key')]; + case 2: + result = _a.sent(); + expect(result).toBe(testValue); + return [2 /*return*/]; + } + }); + }); }); + it('should remove a value', function () { return __awaiter(void 0, void 0, void 0, function () { + var testValue, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + testValue = JSON.stringify({ data: 'test' }); + return [4 /*yield*/, storage.setItem('test-key', testValue)]; + case 1: + _a.sent(); + return [4 /*yield*/, storage.removeItem('test-key')]; + case 2: + _a.sent(); + return [4 /*yield*/, storage.getItem('test-key')]; + case 3: + result = _a.sent(); + expect(result).toBeNull(); + return [2 /*return*/]; + } + }); + }); }); + it('should handle complex JSON data', function () { return __awaiter(void 0, void 0, void 0, function () { + var complexData, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + complexData = JSON.stringify({ + queries: [ + { key: 'query1', data: { nested: { value: 123 } } }, + { key: 'query2', data: { array: [1, 2, 3] } }, + ], + timestamp: Date.now(), + }); + return [4 /*yield*/, storage.setItem('complex-key', complexData)]; + case 1: + _a.sent(); + return [4 /*yield*/, storage.getItem('complex-key')]; + case 2: + result = _a.sent(); + expect(result).toBe(complexData); + expect(JSON.parse(result)).toEqual(JSON.parse(complexData)); + return [2 /*return*/]; + } + }); + }); }); + it('should overwrite existing values', function () { return __awaiter(void 0, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, storage.setItem('test-key', 'value1')]; + case 1: + _a.sent(); + return [4 /*yield*/, storage.setItem('test-key', 'value2')]; + case 2: + _a.sent(); + return [4 /*yield*/, storage.getItem('test-key')]; + case 3: + result = _a.sent(); + expect(result).toBe('value2'); + return [2 /*return*/]; + } + }); + }); }); + }); +}); diff --git a/src/lib/api/feed-manip.js b/src/lib/api/feed-manip.js new file mode 100644 index 0000000000..f3b46218f8 --- /dev/null +++ b/src/lib/api/feed-manip.js @@ -0,0 +1,424 @@ +import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, AppBskyFeedPost, } from '@atproto/api'; +import * as bsky from '#/types/bsky'; +import { isPostInLanguage } from '../../locale/helpers'; +import { FALLBACK_MARKER_POST } from './feed/home'; +var FeedViewPostsSlice = /** @class */ (function () { + function FeedViewPostsSlice(feedPost) { + var _a, _b, _c, _d; + var post = feedPost.post, reply = feedPost.reply, reason = feedPost.reason; + this.items = []; + this.isIncompleteThread = false; + this.isFallbackMarker = false; + this.isOrphan = false; + this.isThreadMuted = (_b = (_a = post.viewer) === null || _a === void 0 ? void 0 : _a.threadMuted) !== null && _b !== void 0 ? _b : false; + this.feedPostUri = post.uri; + if (AppBskyFeedDefs.isPostView(reply === null || reply === void 0 ? void 0 : reply.root)) { + this.rootUri = reply.root.uri; + } + else { + this.rootUri = post.uri; + } + this._feedPost = feedPost; + this._reactKey = "slice-".concat(post.uri, "-").concat(feedPost.reason && 'indexedAt' in feedPost.reason + ? feedPost.reason.indexedAt + : post.indexedAt); + if (feedPost.post.uri === FALLBACK_MARKER_POST.post.uri) { + this.isFallbackMarker = true; + return; + } + if (!AppBskyFeedPost.isRecord(post.record) || + !bsky.validate(post.record, AppBskyFeedPost.validateRecord)) { + return; + } + var parent = reply === null || reply === void 0 ? void 0 : reply.parent; + var isParentBlocked = AppBskyFeedDefs.isBlockedPost(parent); + var isParentNotFound = AppBskyFeedDefs.isNotFoundPost(parent); + var parentAuthor; + if (AppBskyFeedDefs.isPostView(parent)) { + parentAuthor = parent.author; + } + this.items.push({ + post: post, + record: post.record, + parentAuthor: parentAuthor, + isParentBlocked: isParentBlocked, + isParentNotFound: isParentNotFound, + }); + if (!reply) { + if (post.record.reply) { + // This reply wasn't properly hydrated by the AppView. + this.isOrphan = true; + this.items[0].isParentNotFound = true; + } + return; + } + if (reason) { + return; + } + if (!AppBskyFeedDefs.isPostView(parent) || + !AppBskyFeedPost.isRecord(parent.record) || + !bsky.validate(parent.record, AppBskyFeedPost.validateRecord)) { + this.isOrphan = true; + return; + } + var root = reply.root; + var rootIsView = AppBskyFeedDefs.isPostView(root) || + AppBskyFeedDefs.isBlockedPost(root) || + AppBskyFeedDefs.isNotFoundPost(root); + /* + * If the parent is also the root, we just so happen to have the data we + * need to compute if the parent's parent (grandparent) is blocked. This + * doesn't always happen, of course, but we can take advantage of it when + * it does. + */ + var grandparent = rootIsView && ((_c = parent.record.reply) === null || _c === void 0 ? void 0 : _c.parent.uri) === root.uri + ? root + : undefined; + var grandparentAuthor = reply.grandparentAuthor; + var isGrandparentBlocked = Boolean(grandparent && AppBskyFeedDefs.isBlockedPost(grandparent)); + var isGrandparentNotFound = Boolean(grandparent && AppBskyFeedDefs.isNotFoundPost(grandparent)); + this.items.unshift({ + post: parent, + record: parent.record, + parentAuthor: grandparentAuthor, + isParentBlocked: isGrandparentBlocked, + isParentNotFound: isGrandparentNotFound, + }); + if (isGrandparentBlocked) { + this.isOrphan = true; + // Keep going, it might still have a root, and we need this for thread + // de-deduping + } + if (!AppBskyFeedDefs.isPostView(root) || + !AppBskyFeedPost.isRecord(root.record) || + !bsky.validate(root.record, AppBskyFeedPost.validateRecord)) { + this.isOrphan = true; + return; + } + if (root.uri === parent.uri) { + return; + } + this.items.unshift({ + post: root, + record: root.record, + isParentBlocked: false, + isParentNotFound: false, + parentAuthor: undefined, + }); + if (((_d = parent.record.reply) === null || _d === void 0 ? void 0 : _d.parent.uri) !== root.uri) { + this.isIncompleteThread = true; + } + } + Object.defineProperty(FeedViewPostsSlice.prototype, "isQuotePost", { + get: function () { + var embed = this._feedPost.post.embed; + return (AppBskyEmbedRecord.isView(embed) || + AppBskyEmbedRecordWithMedia.isView(embed)); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(FeedViewPostsSlice.prototype, "isReply", { + get: function () { + return (AppBskyFeedPost.isRecord(this._feedPost.post.record) && + !!this._feedPost.post.record.reply); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(FeedViewPostsSlice.prototype, "reason", { + get: function () { + return '__source' in this._feedPost + ? this._feedPost.__source + : this._feedPost.reason; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(FeedViewPostsSlice.prototype, "feedContext", { + get: function () { + return this._feedPost.feedContext; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(FeedViewPostsSlice.prototype, "reqId", { + get: function () { + return this._feedPost.reqId; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(FeedViewPostsSlice.prototype, "isRepost", { + get: function () { + var reason = this._feedPost.reason; + return AppBskyFeedDefs.isReasonRepost(reason); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(FeedViewPostsSlice.prototype, "likeCount", { + get: function () { + var _a; + return (_a = this._feedPost.post.likeCount) !== null && _a !== void 0 ? _a : 0; + }, + enumerable: false, + configurable: true + }); + FeedViewPostsSlice.prototype.containsUri = function (uri) { + return !!this.items.find(function (item) { return item.post.uri === uri; }); + }; + FeedViewPostsSlice.prototype.getAuthors = function () { + var feedPost = this._feedPost; + var author = feedPost.post.author; + var parentAuthor; + var grandparentAuthor; + var rootAuthor; + if (feedPost.reply) { + if (AppBskyFeedDefs.isPostView(feedPost.reply.parent)) { + parentAuthor = feedPost.reply.parent.author; + } + if (feedPost.reply.grandparentAuthor) { + grandparentAuthor = feedPost.reply.grandparentAuthor; + } + if (AppBskyFeedDefs.isPostView(feedPost.reply.root)) { + rootAuthor = feedPost.reply.root.author; + } + } + return { + author: author, + parentAuthor: parentAuthor, + grandparentAuthor: grandparentAuthor, + rootAuthor: rootAuthor, + }; + }; + return FeedViewPostsSlice; +}()); +export { FeedViewPostsSlice }; +var FeedTuner = /** @class */ (function () { + function FeedTuner(tunerFns) { + this.tunerFns = tunerFns; + this.seenKeys = new Set(); + this.seenUris = new Set(); + this.seenRootUris = new Set(); + } + FeedTuner.prototype.tune = function (feed, _a) { + var _this = this; + var _b = _a === void 0 ? { + dryRun: false, + } : _a, dryRun = _b.dryRun; + var slices = feed + .map(function (item) { return new FeedViewPostsSlice(item); }) + .filter(function (s) { return s.items.length > 0 || s.isFallbackMarker; }); + // run the custom tuners + for (var _i = 0, _c = this.tunerFns; _i < _c.length; _i++) { + var tunerFn = _c[_i]; + slices = tunerFn(this, slices.slice(), dryRun); + } + slices = slices.filter(function (slice) { + if (_this.seenKeys.has(slice._reactKey)) { + return false; + } + // Some feeds, like Following, dedupe by thread, so you only see the most recent reply. + // However, we don't want per-thread dedupe for author feeds (where we need to show every post) + // or for feedgens (where we want to let the feed serve multiple replies if it chooses to). + // To avoid showing the same context (root and/or parent) more than once, we do last resort + // per-post deduplication. It hides already seen posts as long as this doesn't break the thread. + for (var i = 0; i < slice.items.length; i++) { + var item = slice.items[i]; + if (_this.seenUris.has(item.post.uri)) { + if (i === 0) { + // Omit contiguous seen leading items. + // For example, [A -> B -> C], [A -> D -> E], [A -> D -> F] + // would turn into [A -> B -> C], [D -> E], [F]. + slice.items.splice(0, 1); + i--; + } + if (i === slice.items.length - 1) { + // If the last item in the slice was already seen, omit the whole slice. + // This means we'd miss its parents, but the user can "show more" to see them. + // For example, [A ... E -> F], [A ... D -> E], [A ... C -> D], [A -> B -> C] + // would get collapsed into [A ... E -> F], with B/C/D considered seen. + return false; + } + } + else { + if (!dryRun) { + // Reposting a reply elevates it to top-level, so its parent/root won't be displayed. + // Disable in-thread dedupe for this case since we don't want to miss them later. + var disableDedupe = slice.isReply && slice.isRepost; + if (!disableDedupe) { + _this.seenUris.add(item.post.uri); + } + } + } + } + if (!dryRun) { + _this.seenKeys.add(slice._reactKey); + } + return true; + }); + return slices; + }; + FeedTuner.removeReplies = function (tuner, slices, _dryRun) { + for (var i = 0; i < slices.length; i++) { + var slice = slices[i]; + if (slice.isReply && + !slice.isRepost && + // This is not perfect but it's close as we can get to + // detecting threads without having to peek ahead. + !areSameAuthor(slice.getAuthors())) { + slices.splice(i, 1); + i--; + } + } + return slices; + }; + FeedTuner.removeReposts = function (tuner, slices, _dryRun) { + for (var i = 0; i < slices.length; i++) { + if (slices[i].isRepost) { + slices.splice(i, 1); + i--; + } + } + return slices; + }; + FeedTuner.removeQuotePosts = function (tuner, slices, _dryRun) { + for (var i = 0; i < slices.length; i++) { + if (slices[i].isQuotePost) { + slices.splice(i, 1); + i--; + } + } + return slices; + }; + FeedTuner.removeOrphans = function (tuner, slices, _dryRun) { + for (var i = 0; i < slices.length; i++) { + if (slices[i].isOrphan) { + slices.splice(i, 1); + i--; + } + } + return slices; + }; + FeedTuner.removeMutedThreads = function (tuner, slices, _dryRun) { + for (var i = 0; i < slices.length; i++) { + if (slices[i].isThreadMuted) { + slices.splice(i, 1); + i--; + } + } + return slices; + }; + FeedTuner.dedupThreads = function (tuner, slices, dryRun) { + for (var i = 0; i < slices.length; i++) { + var rootUri = slices[i].rootUri; + if (!slices[i].isRepost && tuner.seenRootUris.has(rootUri)) { + slices.splice(i, 1); + i--; + } + else { + if (!dryRun) { + tuner.seenRootUris.add(rootUri); + } + } + } + return slices; + }; + FeedTuner.followedRepliesOnly = function (_a) { + var userDid = _a.userDid; + return function (tuner, slices, _dryRun) { + for (var i = 0; i < slices.length; i++) { + var slice = slices[i]; + if (slice.isReply && + !slice.isRepost && + !shouldDisplayReplyInFollowing(slice.getAuthors(), userDid)) { + slices.splice(i, 1); + i--; + } + } + return slices; + }; + }; + /** + * This function filters a list of FeedViewPostsSlice items based on whether they contain text in a + * preferred language. + * @param {string[]} preferredLangsCode2 - An array of preferred language codes in ISO 639-1 or ISO 639-2 format. + * @returns A function that takes in a `FeedTuner` and an array of `FeedViewPostsSlice` objects and + * returns an array of `FeedViewPostsSlice` objects. + */ + FeedTuner.preferredLangOnly = function (preferredLangsCode2) { + return function (tuner, slices, _dryRun) { + // early return if no languages have been specified + if (!preferredLangsCode2.length || preferredLangsCode2.length === 0) { + return slices; + } + var candidateSlices = slices.filter(function (slice) { + for (var _i = 0, _a = slice.items; _i < _a.length; _i++) { + var item = _a[_i]; + if (isPostInLanguage(item.post, preferredLangsCode2)) { + return true; + } + } + // if item does not fit preferred language, remove it + return false; + }); + // if the language filter cleared out the entire page, return the original set + // so that something always shows + if (candidateSlices.length === 0) { + return slices; + } + return candidateSlices; + }; + }; + return FeedTuner; +}()); +export { FeedTuner }; +function areSameAuthor(authors) { + var author = authors.author, parentAuthor = authors.parentAuthor, grandparentAuthor = authors.grandparentAuthor, rootAuthor = authors.rootAuthor; + var authorDid = author.did; + if (parentAuthor && parentAuthor.did !== authorDid) { + return false; + } + if (grandparentAuthor && grandparentAuthor.did !== authorDid) { + return false; + } + if (rootAuthor && rootAuthor.did !== authorDid) { + return false; + } + return true; +} +function shouldDisplayReplyInFollowing(authors, userDid) { + var author = authors.author, parentAuthor = authors.parentAuthor, grandparentAuthor = authors.grandparentAuthor, rootAuthor = authors.rootAuthor; + if (!isSelfOrFollowing(author, userDid)) { + // Only show replies from self or people you follow. + return false; + } + if ((!parentAuthor || parentAuthor.did === author.did) && + (!rootAuthor || rootAuthor.did === author.did) && + (!grandparentAuthor || grandparentAuthor.did === author.did)) { + // Always show self-threads. + return true; + } + // From this point on we need at least one more reason to show it. + if (parentAuthor && + parentAuthor.did !== author.did && + isSelfOrFollowing(parentAuthor, userDid)) { + return true; + } + if (grandparentAuthor && + grandparentAuthor.did !== author.did && + isSelfOrFollowing(grandparentAuthor, userDid)) { + return true; + } + if (rootAuthor && + rootAuthor.did !== author.did && + isSelfOrFollowing(rootAuthor, userDid)) { + return true; + } + return false; +} +function isSelfOrFollowing(profile, userDid) { + var _a; + return Boolean(profile.did === userDid || ((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following)); +} diff --git a/src/lib/api/feed/author.js b/src/lib/api/feed/author.js new file mode 100644 index 0000000000..d506732207 --- /dev/null +++ b/src/lib/api/feed/author.js @@ -0,0 +1,142 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AppBskyFeedDefs, } from '@atproto/api'; +var AuthorFeedAPI = /** @class */ (function () { + function AuthorFeedAPI(_a) { + var agent = _a.agent, feedParams = _a.feedParams; + this.agent = agent; + this._params = feedParams; + } + Object.defineProperty(AuthorFeedAPI.prototype, "params", { + get: function () { + var params = __assign({}, this._params); + params.includePins = params.filter === 'posts_and_author_threads'; + return params; + }, + enumerable: false, + configurable: true + }); + AuthorFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.agent.getAuthorFeed(__assign(__assign({}, this.params), { limit: 1 }))]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.feed[0]]; + } + }); + }); + }; + AuthorFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var cursor = _b.cursor, limit = _b.limit; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, this.agent.getAuthorFeed(__assign(__assign({}, this.params), { cursor: cursor, limit: limit }))]; + case 1: + res = _c.sent(); + if (res.success) { + return [2 /*return*/, { + cursor: res.data.cursor, + feed: this._filter(res.data.feed), + }]; + } + return [2 /*return*/, { + feed: [], + }]; + } + }); + }); + }; + AuthorFeedAPI.prototype._filter = function (feed) { + var _this = this; + if (this.params.filter === 'posts_and_author_threads') { + return feed.filter(function (post) { + var isReply = post.reply; + var isRepost = AppBskyFeedDefs.isReasonRepost(post.reason); + var isPin = AppBskyFeedDefs.isReasonPin(post.reason); + if (!isReply) + return true; + if (isRepost || isPin) + return true; + return isReply && isAuthorReplyChain(_this.params.actor, post, feed); + }); + } + return feed; + }; + return AuthorFeedAPI; +}()); +export { AuthorFeedAPI }; +function isAuthorReplyChain(actor, post, posts) { + var _a; + // current post is by a different user (shouldn't happen) + if (post.post.author.did !== actor) + return false; + var replyParent = (_a = post.reply) === null || _a === void 0 ? void 0 : _a.parent; + if (AppBskyFeedDefs.isPostView(replyParent)) { + // reply parent is by a different user + if (replyParent.author.did !== actor) + return false; + // A top-level post that matches the parent of the current post. + var parentPost = posts.find(function (p) { return p.post.uri === replyParent.uri; }); + /* + * Either we haven't fetched the parent at the top level, or the only + * record we have is on feedItem.reply.parent, which we've already checked + * above. + */ + if (!parentPost) + return true; + // Walk up to parent + return isAuthorReplyChain(actor, parentPost, posts); + } + // Just default to showing it + return true; +} diff --git a/src/lib/api/feed/custom.js b/src/lib/api/feed/custom.js new file mode 100644 index 0000000000..5dd57bdca1 --- /dev/null +++ b/src/lib/api/feed/custom.js @@ -0,0 +1,193 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { BskyAgent, jsonStringToLex, } from '@atproto/api'; +import { getAppLanguageAsContentLanguage, getContentLanguages, } from '#/state/preferences/languages'; +import { createBskyTopicsHeader, isBlueskyOwnedFeed } from './utils'; +var CustomFeedAPI = /** @class */ (function () { + function CustomFeedAPI(_a) { + var agent = _a.agent, feedParams = _a.feedParams, userInterests = _a.userInterests; + this.agent = agent; + this.params = feedParams; + this.userInterests = userInterests; + } + CustomFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + var contentLangs, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + contentLangs = getContentLanguages().join(','); + return [4 /*yield*/, this.agent.app.bsky.feed.getFeed(__assign(__assign({}, this.params), { limit: 1 }), { headers: { 'Accept-Language': contentLangs } })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.feed[0]]; + } + }); + }); + }; + CustomFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var contentLangs, agent, isBlueskyOwned, res, _c; + var cursor = _b.cursor, limit = _b.limit; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + contentLangs = getContentLanguages().join(','); + agent = this.agent; + isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed); + if (!agent.did) return [3 /*break*/, 2]; + return [4 /*yield*/, this.agent.app.bsky.feed.getFeed(__assign(__assign({}, this.params), { cursor: cursor, limit: limit }), { + headers: __assign(__assign({}, (isBlueskyOwned + ? createBskyTopicsHeader(this.userInterests) + : {})), { 'Accept-Language': contentLangs }), + })]; + case 1: + _c = _d.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, loggedOutFetch(__assign(__assign({}, this.params), { cursor: cursor, limit: limit }))]; + case 3: + _c = _d.sent(); + _d.label = 4; + case 4: + res = _c; + if (res.success) { + // NOTE + // some custom feeds fail to enforce the pagination limit + // so we manually truncate here + // -prf + if (res.data.feed.length > limit) { + res.data.feed = res.data.feed.slice(0, limit); + } + return [2 /*return*/, { + cursor: res.data.feed.length ? res.data.cursor : undefined, + feed: res.data.feed, + }]; + } + return [2 /*return*/, { + feed: [], + }]; + } + }); + }); + }; + return CustomFeedAPI; +}()); +export { CustomFeedAPI }; +// HACK +// we want feeds to give language-specific results immediately when a +// logged-out user changes their language. this comes with two problems: +// 1. not all languages have content, and +// 2. our public caching layer isnt correctly busting against the accept-language header +// for now we handle both of these with a manual workaround +// -prf +function loggedOutFetch(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var contentLangs, labelersHeader, res, data, _c, _d, _e, _f; + var _g, _h; + var feed = _b.feed, limit = _b.limit, cursor = _b.cursor; + return __generator(this, function (_j) { + switch (_j.label) { + case 0: + contentLangs = getAppLanguageAsContentLanguage(); + labelersHeader = { + 'atproto-accept-labelers': BskyAgent.appLabelers + .map(function (l) { return "".concat(l, ";redact"); }) + .join(', '), + }; + return [4 /*yield*/, fetch("https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=".concat(feed).concat(cursor ? "&cursor=".concat(cursor) : '', "&limit=").concat(limit, "&lang=").concat(contentLangs), { + method: 'GET', + headers: __assign({ 'Accept-Language': contentLangs }, labelersHeader), + })]; + case 1: + res = _j.sent(); + if (!res.ok) return [3 /*break*/, 3]; + _d = jsonStringToLex; + return [4 /*yield*/, res.text()]; + case 2: + _c = _d.apply(void 0, [_j.sent()]); + return [3 /*break*/, 4]; + case 3: + _c = null; + _j.label = 4; + case 4: + data = _c; + if ((_g = data === null || data === void 0 ? void 0 : data.feed) === null || _g === void 0 ? void 0 : _g.length) { + return [2 /*return*/, { + success: true, + data: data, + }]; + } + return [4 /*yield*/, fetch("https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=".concat(feed).concat(cursor ? "&cursor=".concat(cursor) : '', "&limit=").concat(limit), { method: 'GET', headers: __assign({ 'Accept-Language': '' }, labelersHeader) })]; + case 5: + // no data, try again with language headers removed + res = _j.sent(); + if (!res.ok) return [3 /*break*/, 7]; + _f = jsonStringToLex; + return [4 /*yield*/, res.text()]; + case 6: + _e = _f.apply(void 0, [_j.sent()]); + return [3 /*break*/, 8]; + case 7: + _e = null; + _j.label = 8; + case 8: + data = _e; + if ((_h = data === null || data === void 0 ? void 0 : data.feed) === null || _h === void 0 ? void 0 : _h.length) { + return [2 /*return*/, { + success: true, + data: data, + }]; + } + return [2 /*return*/, { + success: false, + data: { feed: [] }, + }]; + } + }); + }); +} diff --git a/src/lib/api/feed/demo.js b/src/lib/api/feed/demo.js new file mode 100644 index 0000000000..832195dc07 --- /dev/null +++ b/src/lib/api/feed/demo.js @@ -0,0 +1,59 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { DEMO_FEED } from '#/lib/demo'; +var DemoFeedAPI = /** @class */ (function () { + function DemoFeedAPI(_a) { + var agent = _a.agent; + this.agent = agent; + } + DemoFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, DEMO_FEED.feed[0]]; + }); + }); + }; + DemoFeedAPI.prototype.fetch = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, DEMO_FEED]; + }); + }); + }; + return DemoFeedAPI; +}()); +export { DemoFeedAPI }; diff --git a/src/lib/api/feed/following.js b/src/lib/api/feed/following.js new file mode 100644 index 0000000000..e1a8248ca2 --- /dev/null +++ b/src/lib/api/feed/following.js @@ -0,0 +1,84 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var FollowingFeedAPI = /** @class */ (function () { + function FollowingFeedAPI(_a) { + var agent = _a.agent; + this.agent = agent; + } + FollowingFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.agent.getTimeline({ + limit: 1, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.feed[0]]; + } + }); + }); + }; + FollowingFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var cursor = _b.cursor, limit = _b.limit; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, this.agent.getTimeline({ + cursor: cursor, + limit: limit, + })]; + case 1: + res = _c.sent(); + if (res.success) { + return [2 /*return*/, { + cursor: res.data.cursor, + feed: res.data.feed, + }]; + } + return [2 /*return*/, { + feed: [], + }]; + } + }); + }); + }; + return FollowingFeedAPI; +}()); +export { FollowingFeedAPI }; diff --git a/src/lib/api/feed/home.js b/src/lib/api/feed/home.js new file mode 100644 index 0000000000..10514de9ef --- /dev/null +++ b/src/lib/api/feed/home.js @@ -0,0 +1,134 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { PROD_DEFAULT_FEED } from '#/lib/constants'; +import { CustomFeedAPI } from './custom'; +import { FollowingFeedAPI } from './following'; +// HACK +// the feed API does not include any facilities for passing down +// non-post elements. adding that is a bit of a heavy lift, and we +// have just one temporary usecase for it: flagging when the home feed +// falls back to discover. +// we use this fallback marker post to drive this instead. see Feed.tsx +// for the usage. +// -prf +export var FALLBACK_MARKER_POST = { + post: { + uri: 'fallback-marker-post', + cid: 'fake', + record: {}, + author: { + did: 'did:fake', + handle: 'fake.com', + }, + indexedAt: new Date().toISOString(), + }, +}; +var HomeFeedAPI = /** @class */ (function () { + function HomeFeedAPI(_a) { + var userInterests = _a.userInterests, agent = _a.agent; + this.usingDiscover = false; + this.itemCursor = 0; + this.agent = agent; + this.following = new FollowingFeedAPI({ agent: agent }); + this.discover = new CustomFeedAPI({ + agent: agent, + feedParams: { feed: PROD_DEFAULT_FEED('whats-hot') }, + }); + this.userInterests = userInterests; + } + HomeFeedAPI.prototype.reset = function () { + this.following = new FollowingFeedAPI({ agent: this.agent }); + this.discover = new CustomFeedAPI({ + agent: this.agent, + feedParams: { feed: PROD_DEFAULT_FEED('whats-hot') }, + userInterests: this.userInterests, + }); + this.usingDiscover = false; + this.itemCursor = 0; + }; + HomeFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (this.usingDiscover) { + return [2 /*return*/, this.discover.peekLatest()]; + } + return [2 /*return*/, this.following.peekLatest()]; + }); + }); + }; + HomeFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var returnCursor, posts, res, res; + var cursor = _b.cursor, limit = _b.limit; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!cursor) { + this.reset(); + } + posts = []; + if (!!this.usingDiscover) return [3 /*break*/, 2]; + return [4 /*yield*/, this.following.fetch({ cursor: cursor, limit: limit })]; + case 1: + res = _c.sent(); + returnCursor = res.cursor; + posts = posts.concat(res.feed); + if (!returnCursor) { + cursor = ''; + posts.push(FALLBACK_MARKER_POST); + this.usingDiscover = true; + } + _c.label = 2; + case 2: + if (!(this.usingDiscover && !__DEV__)) return [3 /*break*/, 4]; + return [4 /*yield*/, this.discover.fetch({ cursor: cursor, limit: limit })]; + case 3: + res = _c.sent(); + returnCursor = res.cursor; + posts = posts.concat(res.feed); + _c.label = 4; + case 4: return [2 /*return*/, { + cursor: returnCursor, + feed: posts, + }]; + } + }); + }); + }; + return HomeFeedAPI; +}()); +export { HomeFeedAPI }; diff --git a/src/lib/api/feed/likes.js b/src/lib/api/feed/likes.js new file mode 100644 index 0000000000..8a362b50cf --- /dev/null +++ b/src/lib/api/feed/likes.js @@ -0,0 +1,92 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var LikesFeedAPI = /** @class */ (function () { + function LikesFeedAPI(_a) { + var agent = _a.agent, feedParams = _a.feedParams; + this.agent = agent; + this.params = feedParams; + } + LikesFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.agent.getActorLikes(__assign(__assign({}, this.params), { limit: 1 }))]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.feed[0]]; + } + }); + }); + }; + LikesFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res, isEmptyPage; + var cursor = _b.cursor, limit = _b.limit; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, this.agent.getActorLikes(__assign(__assign({}, this.params), { cursor: cursor, limit: limit }))]; + case 1: + res = _c.sent(); + if (res.success) { + isEmptyPage = res.data.feed.length === 0; + return [2 /*return*/, { + cursor: isEmptyPage ? undefined : res.data.cursor, + feed: res.data.feed, + }]; + } + return [2 /*return*/, { + feed: [], + }]; + } + }); + }); + }; + return LikesFeedAPI; +}()); +export { LikesFeedAPI }; diff --git a/src/lib/api/feed/list.js b/src/lib/api/feed/list.js new file mode 100644 index 0000000000..19701ba72b --- /dev/null +++ b/src/lib/api/feed/list.js @@ -0,0 +1,91 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var ListFeedAPI = /** @class */ (function () { + function ListFeedAPI(_a) { + var agent = _a.agent, feedParams = _a.feedParams; + this.agent = agent; + this.params = feedParams; + } + ListFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.agent.app.bsky.feed.getListFeed(__assign(__assign({}, this.params), { limit: 1 }))]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.feed[0]]; + } + }); + }); + }; + ListFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var cursor = _b.cursor, limit = _b.limit; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, this.agent.app.bsky.feed.getListFeed(__assign(__assign({}, this.params), { cursor: cursor, limit: limit }))]; + case 1: + res = _c.sent(); + if (res.success) { + return [2 /*return*/, { + cursor: res.data.cursor, + feed: res.data.feed, + }]; + } + return [2 /*return*/, { + feed: [], + }]; + } + }); + }); + }; + return ListFeedAPI; +}()); +export { ListFeedAPI }; diff --git a/src/lib/api/feed/merge.js b/src/lib/api/feed/merge.js new file mode 100644 index 0000000000..4ddade1328 --- /dev/null +++ b/src/lib/api/feed/merge.js @@ -0,0 +1,385 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import shuffle from 'lodash.shuffle'; +import { bundleAsync } from '#/lib/async/bundle'; +import { timeout } from '#/lib/async/timeout'; +import { feedUriToHref } from '#/lib/strings/url-helpers'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { FeedTuner } from '../feed-manip'; +import { createBskyTopicsHeader, isBlueskyOwnedFeed } from './utils'; +var REQUEST_WAIT_MS = 500; // 500ms +var POST_AGE_CUTOFF = 60e3 * 60 * 24; // 24hours +var MergeFeedAPI = /** @class */ (function () { + function MergeFeedAPI(_a) { + var agent = _a.agent, feedParams = _a.feedParams, feedTuners = _a.feedTuners, userInterests = _a.userInterests; + this.customFeeds = []; + this.feedCursor = 0; + this.itemCursor = 0; + this.sampleCursor = 0; + this.agent = agent; + this.params = feedParams; + this.feedTuners = feedTuners; + this.userInterests = userInterests; + this.following = new MergeFeedSource_Following({ + agent: this.agent, + feedTuners: this.feedTuners, + }); + } + MergeFeedAPI.prototype.reset = function () { + var _this = this; + this.following = new MergeFeedSource_Following({ + agent: this.agent, + feedTuners: this.feedTuners, + }); + this.customFeeds = []; + this.feedCursor = 0; + this.itemCursor = 0; + this.sampleCursor = 0; + if (this.params.mergeFeedSources) { + this.customFeeds = shuffle(this.params.mergeFeedSources.map(function (feedUri) { + return new MergeFeedSource_Custom({ + agent: _this.agent, + feedUri: feedUri, + feedTuners: _this.feedTuners, + userInterests: _this.userInterests, + }); + })); + } + else { + this.customFeeds = []; + } + }; + MergeFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.agent.getTimeline({ + limit: 1, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.feed[0]]; + } + }); + }); + }; + MergeFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var promises, feeds, outOfFollows, _i, feeds_1, feed, posts, slice; + var cursor = _b.cursor, limit = _b.limit; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!cursor) { + this.reset(); + } + promises = []; + if (!(this.following.numReady < limit)) return [3 /*break*/, 2]; + return [4 /*yield*/, this.following.fetchNext(60)]; + case 1: + _c.sent(); + _c.label = 2; + case 2: + feeds = this.customFeeds.slice(this.feedCursor, this.feedCursor + 3); + this.feedCursor += 3; + if (this.feedCursor > this.customFeeds.length) { + this.feedCursor = 0; + } + outOfFollows = !this.following.hasMore && this.following.numReady < limit; + if (this.params.mergeFeedEnabled || outOfFollows) { + for (_i = 0, feeds_1 = feeds; _i < feeds_1.length; _i++) { + feed = feeds_1[_i]; + if (feed.numReady < 5) { + promises.push(feed.fetchNext(10)); + } + } + } + // wait for requests (all capped at a fixed timeout) + return [4 /*yield*/, Promise.all(promises) + // assemble a response by sampling from feeds with content + ]; + case 3: + // wait for requests (all capped at a fixed timeout) + _c.sent(); + posts = []; + while (posts.length < limit) { + slice = this.sampleItem(); + if (slice[0]) { + posts.push(slice[0]); + } + else { + break; + } + } + return [2 /*return*/, { + cursor: String(this.itemCursor), + feed: posts, + }]; + } + }); + }); + }; + MergeFeedAPI.prototype.sampleItem = function () { + var i = this.itemCursor++; + var candidateFeeds = this.customFeeds.filter(function (f) { return f.numReady > 0; }); + var canSample = candidateFeeds.length > 0; + var hasFollows = this.following.hasMore; + var hasFollowsReady = this.following.numReady > 0; + // this condition establishes the frequency that custom feeds are woven into follows + var shouldSample = this.params.mergeFeedEnabled && + i >= 15 && + candidateFeeds.length >= 2 && + (i % 4 === 0 || i % 5 === 0); + if (!canSample && !hasFollows) { + // no data available + return []; + } + if (shouldSample || !hasFollows) { + // time to sample, or the user isnt following anybody + return candidateFeeds[this.sampleCursor++ % candidateFeeds.length].take(1); + } + if (!hasFollowsReady) { + // stop here so more follows can be fetched + return []; + } + // provide follow + return this.following.take(1); + }; + return MergeFeedAPI; +}()); +export { MergeFeedAPI }; +var MergeFeedSource = /** @class */ (function () { + function MergeFeedSource(_a) { + var agent = _a.agent, feedTuners = _a.feedTuners; + var _this = this; + this.cursor = undefined; + this.queue = []; + this.hasMore = true; + this._fetchNextInner = bundleAsync(function (n) { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this._getFeed(this.cursor, n)]; + case 1: + res = _a.sent(); + if (res.success) { + this.cursor = res.data.cursor; + if (res.data.feed.length) { + this.queue = this.queue.concat(res.data.feed); + } + else { + this.hasMore = false; + } + } + else { + this.hasMore = false; + } + return [2 /*return*/]; + } + }); + }); }); + this.agent = agent; + this.feedTuners = feedTuners; + } + Object.defineProperty(MergeFeedSource.prototype, "numReady", { + get: function () { + return this.queue.length; + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(MergeFeedSource.prototype, "needsFetch", { + get: function () { + return this.hasMore && this.queue.length === 0; + }, + enumerable: false, + configurable: true + }); + MergeFeedSource.prototype.take = function (n) { + return this.queue.splice(0, n); + }; + MergeFeedSource.prototype.fetchNext = function (n) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Promise.race([this._fetchNextInner(n), timeout(REQUEST_WAIT_MS)])]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + MergeFeedSource.prototype._getFeed = function (_cursor, _limit) { + throw new Error('Must be overridden'); + }; + return MergeFeedSource; +}()); +var MergeFeedSource_Following = /** @class */ (function (_super) { + __extends(MergeFeedSource_Following, _super); + function MergeFeedSource_Following() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.tuner = new FeedTuner(_this.feedTuners); + return _this; + } + MergeFeedSource_Following.prototype.fetchNext = function (n) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this._fetchNextInner(n)]; + }); + }); + }; + MergeFeedSource_Following.prototype._getFeed = function (cursor, limit) { + return __awaiter(this, void 0, void 0, function () { + var res, slices; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.agent.getTimeline({ cursor: cursor, limit: limit }) + // run the tuner pre-emptively to ensure better mixing + ]; + case 1: + res = _a.sent(); + slices = this.tuner.tune(res.data.feed, { + dryRun: false, + }); + res.data.feed = slices.map(function (slice) { return slice._feedPost; }); + return [2 /*return*/, res]; + } + }); + }); + }; + return MergeFeedSource_Following; +}(MergeFeedSource)); +var MergeFeedSource_Custom = /** @class */ (function (_super) { + __extends(MergeFeedSource_Custom, _super); + function MergeFeedSource_Custom(_a) { + var agent = _a.agent, feedUri = _a.feedUri, feedTuners = _a.feedTuners, userInterests = _a.userInterests; + var _this = _super.call(this, { + agent: agent, + feedTuners: feedTuners, + }) || this; + _this.agent = agent; + _this.feedUri = feedUri; + _this.userInterests = userInterests; + _this.sourceInfo = { + $type: 'reasonFeedSource', + uri: feedUri, + href: feedUriToHref(feedUri), + }; + _this.minDate = new Date(Date.now() - POST_AGE_CUTOFF); + return _this; + } + MergeFeedSource_Custom.prototype._getFeed = function (cursor, limit) { + return __awaiter(this, void 0, void 0, function () { + var contentLangs, isBlueskyOwned, res, _i, _a, post, _b; + var _this = this; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + _c.trys.push([0, 2, , 3]); + contentLangs = getContentLanguages().join(','); + isBlueskyOwned = isBlueskyOwnedFeed(this.feedUri); + return [4 /*yield*/, this.agent.app.bsky.feed.getFeed({ + cursor: cursor, + limit: limit, + feed: this.feedUri, + }, { + headers: __assign(__assign({}, (isBlueskyOwned + ? createBskyTopicsHeader(this.userInterests) + : {})), { 'Accept-Language': contentLangs }), + }) + // NOTE + // some custom feeds fail to enforce the pagination limit + // so we manually truncate here + // -prf + ]; + case 1: + res = _c.sent(); + // NOTE + // some custom feeds fail to enforce the pagination limit + // so we manually truncate here + // -prf + if (limit && res.data.feed.length > limit) { + res.data.feed = res.data.feed.slice(0, limit); + } + // filter out older posts + res.data.feed = res.data.feed.filter(function (post) { return new Date(post.post.indexedAt) > _this.minDate; }); + // attach source info + for (_i = 0, _a = res.data.feed; _i < _a.length; _i++) { + post = _a[_i]; + // @ts-ignore + post.__source = this.sourceInfo; + } + return [2 /*return*/, res]; + case 2: + _b = _c.sent(); + // dont bubble custom-feed errors + return [2 /*return*/, { success: false, headers: {}, data: { feed: [] } }]; + case 3: return [2 /*return*/]; + } + }); + }); + }; + return MergeFeedSource_Custom; +}(MergeFeedSource)); diff --git a/src/lib/api/feed/posts.js b/src/lib/api/feed/posts.js new file mode 100644 index 0000000000..b738718ebe --- /dev/null +++ b/src/lib/api/feed/posts.js @@ -0,0 +1,93 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { logger } from '#/logger'; +var PostListFeedAPI = /** @class */ (function () { + function PostListFeedAPI(_a) { + var agent = _a.agent, feedParams = _a.feedParams; + this.peek = null; + this.agent = agent; + if (feedParams.uris.length > 25) { + logger.warn("Too many URIs provided - expected 25, got ".concat(feedParams.uris.length)); + } + this.params = { + uris: feedParams.uris.slice(0, 25), + }; + } + PostListFeedAPI.prototype.peekLatest = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (this.peek) + return [2 /*return*/, this.peek]; + throw new Error('Has not fetched yet'); + }); + }); + }; + PostListFeedAPI.prototype.fetch = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, this.agent.app.bsky.feed.getPosts(__assign({}, this.params))]; + case 1: + res = _c.sent(); + if (res.success) { + this.peek = { post: res.data.posts[0] }; + return [2 /*return*/, { + feed: res.data.posts.map(function (post) { return ({ post: post }); }), + }]; + } + return [2 /*return*/, { + feed: [], + }]; + } + }); + }); + }; + return PostListFeedAPI; +}()); +export { PostListFeedAPI }; diff --git a/src/lib/api/feed/types.js b/src/lib/api/feed/types.js new file mode 100644 index 0000000000..969f34fefe --- /dev/null +++ b/src/lib/api/feed/types.js @@ -0,0 +1,6 @@ +export function isReasonFeedSource(v) { + return (!!v && + typeof v === 'object' && + '$type' in v && + v.$type === 'reasonFeedSource'); +} diff --git a/src/lib/api/feed/utils.js b/src/lib/api/feed/utils.js new file mode 100644 index 0000000000..aad1558dc8 --- /dev/null +++ b/src/lib/api/feed/utils.js @@ -0,0 +1,22 @@ +var _a; +import { AtUri } from '@atproto/api'; +import { BSKY_FEED_OWNER_DIDS } from '#/lib/constants'; +import { IS_WEB } from '#/env'; +var debugTopics = ''; +if (IS_WEB && typeof window !== 'undefined') { + var params = new URLSearchParams(window.location.search); + debugTopics = (_a = params.get('debug_topics')) !== null && _a !== void 0 ? _a : ''; +} +export function createBskyTopicsHeader(userInterests) { + return { + 'X-Bsky-Topics': debugTopics || userInterests || '', + }; +} +export function aggregateUserInterests(preferences) { + var _a, _b; + return ((_b = (_a = preferences === null || preferences === void 0 ? void 0 : preferences.interests) === null || _a === void 0 ? void 0 : _a.tags) === null || _b === void 0 ? void 0 : _b.join(',')) || ''; +} +export function isBlueskyOwnedFeed(feedUri) { + var uri = new AtUri(feedUri); + return BSKY_FEED_OWNER_DIDS.includes(uri.host); +} diff --git a/src/lib/api/index.js b/src/lib/api/index.js new file mode 100644 index 0000000000..06ae4575b0 --- /dev/null +++ b/src/lib/api/index.js @@ -0,0 +1,525 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AtUri, BlobRef, RichText, } from '@atproto/api'; +import { TID } from '@atproto/common-web'; +import * as dcbor from '@ipld/dag-cbor'; +import { t } from '@lingui/core/macro'; +import { sha256 } from 'js-sha256'; +import { CID } from 'multiformats/cid'; +import * as Hasher from 'multiformats/hashes/hasher'; +import { isNetworkError } from '#/lib/strings/errors'; +import { shortenLinks, stripInvalidMentions } from '#/lib/strings/rich-text-manip'; +import { logger } from '#/logger'; +import { compressImage } from '#/state/gallery'; +import { fetchResolveGifQuery, fetchResolveLinkQuery, } from '#/state/queries/resolve-link'; +import { createThreadgateRecord, threadgateAllowUISettingToAllowRecordValue, } from '#/state/queries/threadgate'; +import { createGIFDescription } from '../gif-alt-text'; +import { uploadBlob } from './upload-blob'; +export { uploadBlob }; +export function post(agent, queryClient, opts) { + return __awaiter(this, void 0, void 0, function () { + var thread, replyPromise, langs, did, writes, uris, now, tid, i, draft, rtPromise, embedPromise, labels, rkey, uri, rt, embed, reply, record, ref, e_1; + var _a; + var _b, _c, _d, _e; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + thread = opts.thread; + (_b = opts.onStateChange) === null || _b === void 0 ? void 0 : _b.call(opts, t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Processing..."], ["Processing..."])))); + if (opts.replyTo) { + // Not awaited to avoid waterfalls. + replyPromise = resolveReply(agent, opts.replyTo); + } + langs = opts.langs; + if (opts.langs) { + langs = opts.langs.slice(0, 3); + } + did = agent.assertDid; + writes = []; + uris = []; + now = new Date(); + i = 0; + _f.label = 1; + case 1: + if (!(i < thread.posts.length)) return [3 /*break*/, 7]; + draft = thread.posts[i]; + rtPromise = resolveRT(agent, draft.richtext); + embedPromise = resolveEmbed(agent, queryClient, draft, opts.onStateChange); + labels = void 0; + if (draft.labels.length) { + labels = { + $type: 'com.atproto.label.defs#selfLabels', + values: draft.labels.map(function (val) { return ({ val: val }); }), + }; + } + // The sorting behavior for multiple posts sharing the same createdAt time is + // undefined, so what we'll do here is increment the time by 1 for every post + now.setMilliseconds(now.getMilliseconds() + 1); + tid = TID.next(tid); + rkey = tid.toString(); + uri = "at://".concat(did, "/app.bsky.feed.post/").concat(rkey); + uris.push(uri); + return [4 /*yield*/, rtPromise]; + case 2: + rt = _f.sent(); + return [4 /*yield*/, embedPromise]; + case 3: + embed = _f.sent(); + return [4 /*yield*/, replyPromise]; + case 4: + reply = _f.sent(); + record = { + // IMPORTANT: $type has to exist, CID is calculated with the `$type` field + // present and will produce the wrong CID if you omit it. + $type: 'app.bsky.feed.post', + createdAt: now.toISOString(), + text: rt.text, + facets: rt.facets, + reply: reply, + embed: embed, + langs: langs, + labels: labels, + }; + writes.push({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.feed.post', + rkey: rkey, + value: record, + }); + if (i === 0 && thread.threadgate.some(function (tg) { return tg.type !== 'everybody'; })) { + writes.push({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.feed.threadgate', + rkey: rkey, + value: createThreadgateRecord({ + createdAt: now.toISOString(), + post: uri, + allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate), + }), + }); + } + if (((_c = thread.postgate.embeddingRules) === null || _c === void 0 ? void 0 : _c.length) || + ((_d = thread.postgate.detachedEmbeddingUris) === null || _d === void 0 ? void 0 : _d.length)) { + writes.push({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.feed.postgate', + rkey: rkey, + value: __assign(__assign({}, thread.postgate), { $type: 'app.bsky.feed.postgate', createdAt: now.toISOString(), post: uri }), + }); + } + _a = {}; + return [4 /*yield*/, computeCid(record)]; + case 5: + ref = (_a.cid = _f.sent(), + _a.uri = uri, + _a); + replyPromise = { + root: (_e = reply === null || reply === void 0 ? void 0 : reply.root) !== null && _e !== void 0 ? _e : ref, + parent: ref, + }; + _f.label = 6; + case 6: + i++; + return [3 /*break*/, 1]; + case 7: + _f.trys.push([7, 9, , 10]); + return [4 /*yield*/, agent.com.atproto.repo.applyWrites({ + repo: agent.assertDid, + writes: writes, + validate: true, + })]; + case 8: + _f.sent(); + return [3 /*break*/, 10]; + case 9: + e_1 = _f.sent(); + logger.error("Failed to create post", { + safeMessage: e_1.message, + }); + if (isNetworkError(e_1)) { + throw new Error(t(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Post failed to upload. Please check your Internet connection and try again."], ["Post failed to upload. Please check your Internet connection and try again."])))); + } + else { + throw e_1; + } + return [3 /*break*/, 10]; + case 10: return [2 /*return*/, { uris: uris }]; + } + }); + }); +} +function resolveRT(agent, richtext) { + return __awaiter(this, void 0, void 0, function () { + var trimmedText, rt; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + trimmedText = richtext.text + // Trim leading whitespace-only lines (but don't break ASCII art). + .replace(/^(\s*\n)+/, '') + // Trim any trailing whitespace. + .trimEnd(); + rt = new RichText({ text: trimmedText }, { cleanNewlines: true }); + return [4 /*yield*/, rt.detectFacets(agent)]; + case 1: + _a.sent(); + rt = shortenLinks(rt); + rt = stripInvalidMentions(rt); + return [2 /*return*/, rt]; + } + }); + }); +} +function resolveReply(agent, replyTo) { + return __awaiter(this, void 0, void 0, function () { + var replyToUrip, parentPost, parentRef; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + replyToUrip = new AtUri(replyTo); + return [4 /*yield*/, agent.getPost({ + repo: replyToUrip.host, + rkey: replyToUrip.rkey, + })]; + case 1: + parentPost = _b.sent(); + if (parentPost) { + parentRef = { + uri: parentPost.uri, + cid: parentPost.cid, + }; + return [2 /*return*/, { + root: ((_a = parentPost.value.reply) === null || _a === void 0 ? void 0 : _a.root) || parentRef, + parent: parentRef, + }]; + } + return [2 /*return*/]; + } + }); + }); +} +function resolveEmbed(agent, queryClient, draft, onStateChange) { + return __awaiter(this, void 0, void 0, function () { + var _a, resolvedMedia_1, resolvedQuote, resolvedMedia, resolvedLink; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!draft.embed.quote) return [3 /*break*/, 2]; + return [4 /*yield*/, Promise.all([ + resolveMedia(agent, queryClient, draft.embed, onStateChange), + resolveRecord(agent, queryClient, draft.embed.quote.uri), + ])]; + case 1: + _a = _b.sent(), resolvedMedia_1 = _a[0], resolvedQuote = _a[1]; + if (resolvedMedia_1) { + return [2 /*return*/, { + $type: 'app.bsky.embed.recordWithMedia', + record: { + $type: 'app.bsky.embed.record', + record: resolvedQuote, + }, + media: resolvedMedia_1, + }]; + } + return [2 /*return*/, { + $type: 'app.bsky.embed.record', + record: resolvedQuote, + }]; + case 2: return [4 /*yield*/, resolveMedia(agent, queryClient, draft.embed, onStateChange)]; + case 3: + resolvedMedia = _b.sent(); + if (resolvedMedia) { + return [2 /*return*/, resolvedMedia]; + } + if (!draft.embed.link) return [3 /*break*/, 5]; + return [4 /*yield*/, fetchResolveLinkQuery(queryClient, agent, draft.embed.link.uri)]; + case 4: + resolvedLink = _b.sent(); + if (resolvedLink.type === 'record') { + return [2 /*return*/, { + $type: 'app.bsky.embed.record', + record: resolvedLink.record, + }]; + } + _b.label = 5; + case 5: return [2 /*return*/, undefined]; + } + }); + }); +} +function resolveMedia(agent, queryClient, embedDraft, onStateChange) { + return __awaiter(this, void 0, void 0, function () { + var imagesDraft, images, videoDraft, captions, width, height, aspectRatio, gifDraft, resolvedGif, blob, _a, path, mime, response, resolvedLink, blob, _b, path, mime, response; + var _this = this; + var _c, _d, _e; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + if (!(((_c = embedDraft.media) === null || _c === void 0 ? void 0 : _c.type) === 'images')) return [3 /*break*/, 2]; + imagesDraft = embedDraft.media.images; + logger.debug("Uploading images", { + count: imagesDraft.length, + }); + onStateChange === null || onStateChange === void 0 ? void 0 : onStateChange(t(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Uploading images..."], ["Uploading images..."])))); + return [4 /*yield*/, Promise.all(imagesDraft.map(function (image, i) { return __awaiter(_this, void 0, void 0, function () { + var _a, path, width, height, mime, res; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + logger.debug("Compressing image #".concat(i)); + return [4 /*yield*/, compressImage(image)]; + case 1: + _a = _b.sent(), path = _a.path, width = _a.width, height = _a.height, mime = _a.mime; + logger.debug("Uploading image #".concat(i)); + return [4 /*yield*/, uploadBlob(agent, path, mime)]; + case 2: + res = _b.sent(); + return [2 /*return*/, { + image: res.data.blob, + alt: image.alt, + aspectRatio: { width: width, height: height }, + }]; + } + }); + }); }))]; + case 1: + images = _f.sent(); + return [2 /*return*/, { + $type: 'app.bsky.embed.images', + images: images, + }]; + case 2: + if (!(((_d = embedDraft.media) === null || _d === void 0 ? void 0 : _d.type) === 'video' && + embedDraft.media.video.status === 'done')) return [3 /*break*/, 4]; + videoDraft = embedDraft.media.video; + return [4 /*yield*/, Promise.all(videoDraft.captions + .filter(function (caption) { return caption.lang !== ''; }) + .map(function (caption) { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.uploadBlob(caption.file, { + encoding: 'text/vtt', + })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, { lang: caption.lang, file: data.blob }]; + } + }); + }); })) + // lexicon numbers must be floats + ]; + case 3: + captions = _f.sent(); + width = Math.round(videoDraft.asset.width); + height = Math.round(videoDraft.asset.height); + aspectRatio = width > 0 && height > 0 ? { width: width, height: height } : undefined; + if (!aspectRatio) { + logger.error("Invalid aspect ratio - got { width: ".concat(videoDraft.asset.width, ", height: ").concat(videoDraft.asset.height, " }")); + } + return [2 /*return*/, { + $type: 'app.bsky.embed.video', + video: videoDraft.pendingPublish.blobRef, + alt: videoDraft.altText || undefined, + captions: captions.length === 0 ? undefined : captions, + aspectRatio: aspectRatio, + presentation: videoDraft.video.mimeType === 'image/gif' ? 'gif' : 'default', + }]; + case 4: + if (!(((_e = embedDraft.media) === null || _e === void 0 ? void 0 : _e.type) === 'gif')) return [3 /*break*/, 8]; + gifDraft = embedDraft.media; + return [4 /*yield*/, fetchResolveGifQuery(queryClient, agent, gifDraft.gif)]; + case 5: + resolvedGif = _f.sent(); + blob = void 0; + if (!resolvedGif.thumb) return [3 /*break*/, 7]; + onStateChange === null || onStateChange === void 0 ? void 0 : onStateChange(t(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Uploading link thumbnail..."], ["Uploading link thumbnail..."])))); + _a = resolvedGif.thumb.source, path = _a.path, mime = _a.mime; + return [4 /*yield*/, uploadBlob(agent, path, mime)]; + case 6: + response = _f.sent(); + blob = response.data.blob; + _f.label = 7; + case 7: return [2 /*return*/, { + $type: 'app.bsky.embed.external', + external: { + uri: resolvedGif.uri, + title: resolvedGif.title, + description: createGIFDescription(resolvedGif.title, gifDraft.alt), + thumb: blob, + }, + }]; + case 8: + if (!embedDraft.link) return [3 /*break*/, 12]; + return [4 /*yield*/, fetchResolveLinkQuery(queryClient, agent, embedDraft.link.uri)]; + case 9: + resolvedLink = _f.sent(); + if (!(resolvedLink.type === 'external')) return [3 /*break*/, 12]; + blob = void 0; + if (!resolvedLink.thumb) return [3 /*break*/, 11]; + onStateChange === null || onStateChange === void 0 ? void 0 : onStateChange(t(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Uploading link thumbnail..."], ["Uploading link thumbnail..."])))); + _b = resolvedLink.thumb.source, path = _b.path, mime = _b.mime; + return [4 /*yield*/, uploadBlob(agent, path, mime)]; + case 10: + response = _f.sent(); + blob = response.data.blob; + _f.label = 11; + case 11: return [2 /*return*/, { + $type: 'app.bsky.embed.external', + external: { + uri: resolvedLink.uri, + title: resolvedLink.title, + description: resolvedLink.description, + thumb: blob, + }, + }]; + case 12: return [2 /*return*/, undefined]; + } + }); + }); +} +function resolveRecord(agent, queryClient, uri) { + return __awaiter(this, void 0, void 0, function () { + var resolvedLink; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, fetchResolveLinkQuery(queryClient, agent, uri)]; + case 1: + resolvedLink = _a.sent(); + if (resolvedLink.type !== 'record') { + throw Error(t(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Expected uri to resolve to a record"], ["Expected uri to resolve to a record"])))); + } + return [2 /*return*/, resolvedLink.record]; + } + }); + }); +} +// The built-in hashing functions from multiformats (`multiformats/hashes/sha2`) +// are meant for Node.js, this is the cross-platform equivalent. +var mf_sha256 = Hasher.from({ + name: 'sha2-256', + code: 0x12, + encode: function (input) { + var digest = sha256.arrayBuffer(input); + return new Uint8Array(digest); + }, +}); +function computeCid(record) { + return __awaiter(this, void 0, void 0, function () { + var prepared, encoded, digest, cid; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + prepared = prepareForHashing(record); + encoded = dcbor.encode(prepared); + return [4 /*yield*/, mf_sha256.digest(encoded) + // 3. Create a CIDv1, specifying DAG-CBOR as content (code 0x71) + ]; + case 1: + digest = _a.sent(); + cid = CID.createV1(0x71, digest); + // 4. Get the Base32 representation of the CID (`b` prefix) + return [2 /*return*/, cid.toString()]; + } + }); + }); +} +// Returns a transformed version of the object for use in DAG-CBOR. +function prepareForHashing(v) { + // IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing, + // the API client will convert this for you but we're hashing in the client, + // so we need it *now*. + if (v instanceof BlobRef) { + return v.ipld(); + } + // Walk through arrays + if (Array.isArray(v)) { + var pure_1 = true; + var mapped = v.map(function (value) { + if (value !== (value = prepareForHashing(value))) { + pure_1 = false; + } + return value; + }); + return pure_1 ? v : mapped; + } + // Walk through plain objects + if (isPlainObject(v)) { + var obj = {}; + var pure = true; + for (var key in v) { + var value = v[key]; + // `value` is undefined + if (value === undefined) { + pure = false; + continue; + } + // `prepareObject` returned a value that's different from what we had before + if (value !== (value = prepareForHashing(value))) { + pure = false; + } + obj[key] = value; + } + // Return as is if we haven't needed to tamper with anything + return pure ? v : obj; + } + return v; +} +function isPlainObject(v) { + if (typeof v !== 'object' || v === null) { + return false; + } + var proto = Object.getPrototypeOf(v); + return proto === Object.prototype || proto === null; +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/lib/api/resolve.js b/src/lib/api/resolve.js new file mode 100644 index 0000000000..0a7058925f --- /dev/null +++ b/src/lib/api/resolve.js @@ -0,0 +1,302 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AtUri } from '@atproto/api'; +import { POST_IMG_MAX } from '#/lib/constants'; +import { getLinkMeta } from '#/lib/link-meta/link-meta'; +import { resolveShortLink } from '#/lib/link-meta/resolve-short-link'; +import { downloadAndResize } from '#/lib/media/manip'; +import { createStarterPackUri, parseStarterPackUri, } from '#/lib/strings/starter-pack'; +import { isBskyCustomFeedUrl, isBskyListUrl, isBskyPostUrl, isBskyStarterPackUrl, isBskyStartUrl, isShortLink, } from '#/lib/strings/url-helpers'; +import { createComposerImage } from '#/state/gallery'; +import { createGIFDescription } from '../gif-alt-text'; +import { convertBskyAppUrlIfNeeded, makeRecordUri } from '../strings/url-helpers'; +var EmbeddingDisabledError = /** @class */ (function (_super) { + __extends(EmbeddingDisabledError, _super); + function EmbeddingDisabledError() { + return _super.call(this, 'Embedding is disabled for this record') || this; + } + return EmbeddingDisabledError; +}(Error)); +export { EmbeddingDisabledError }; +export function resolveLink(agent, uri) { + return __awaiter(this, void 0, void 0, function () { + // Forked from useGetPost. TODO: move into RQ. + function getPost(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var urip, res_1, res; + var uri = _b.uri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + urip = new AtUri(uri); + if (!!urip.host.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.resolveHandle({ + handle: urip.host, + }) + // @ts-expect-error TODO new-sdk-migration + ]; + case 1: + res_1 = _c.sent(); + // @ts-expect-error TODO new-sdk-migration + urip.host = res_1.data.did; + _c.label = 2; + case 2: return [4 /*yield*/, agent.getPosts({ + uris: [urip.toString()], + })]; + case 3: + res = _c.sent(); + if (res.success && res.data.posts[0]) { + return [2 /*return*/, res.data.posts[0]]; + } + throw new Error('getPost: post not found'); + } + }); + }); + } + // Forked from useFetchDid. TODO: move into RQ. + function fetchDid(handleOrDid) { + return __awaiter(this, void 0, void 0, function () { + var identifier, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + identifier = handleOrDid; + if (!!identifier.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.resolveHandle({ handle: identifier })]; + case 1: + res = _a.sent(); + identifier = res.data.did; + _a.label = 2; + case 2: return [2 /*return*/, identifier]; + } + }); + }); + } + var _a, _0, user, _1, rkey, recordUri, post, _b, _0, handleOrDid, _1, rkey, did, feed, res, _c, _0, handleOrDid, _1, rkey, did, list, res, parsed, did, starterPack, res; + var _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + if (!isShortLink(uri)) return [3 /*break*/, 2]; + return [4 /*yield*/, resolveShortLink(uri)]; + case 1: + uri = _e.sent(); + _e.label = 2; + case 2: + if (!isBskyPostUrl(uri)) return [3 /*break*/, 4]; + uri = convertBskyAppUrlIfNeeded(uri); + _a = uri.split('/').filter(Boolean), _0 = _a[0], user = _a[1], _1 = _a[2], rkey = _a[3]; + recordUri = makeRecordUri(user, 'app.bsky.feed.post', rkey); + return [4 /*yield*/, getPost({ uri: recordUri })]; + case 3: + post = _e.sent(); + if ((_d = post.viewer) === null || _d === void 0 ? void 0 : _d.embeddingDisabled) { + throw new EmbeddingDisabledError(); + } + return [2 /*return*/, { + type: 'record', + record: { + cid: post.cid, + uri: post.uri, + }, + kind: 'post', + view: post, + }]; + case 4: + if (!isBskyCustomFeedUrl(uri)) return [3 /*break*/, 7]; + uri = convertBskyAppUrlIfNeeded(uri); + _b = uri.split('/').filter(Boolean), _0 = _b[0], handleOrDid = _b[1], _1 = _b[2], rkey = _b[3]; + return [4 /*yield*/, fetchDid(handleOrDid)]; + case 5: + did = _e.sent(); + feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey); + return [4 /*yield*/, agent.app.bsky.feed.getFeedGenerator({ feed: feed })]; + case 6: + res = _e.sent(); + return [2 /*return*/, { + type: 'record', + record: { + uri: res.data.view.uri, + cid: res.data.view.cid, + }, + kind: 'feed', + view: res.data.view, + }]; + case 7: + if (!isBskyListUrl(uri)) return [3 /*break*/, 10]; + uri = convertBskyAppUrlIfNeeded(uri); + _c = uri.split('/').filter(Boolean), _0 = _c[0], handleOrDid = _c[1], _1 = _c[2], rkey = _c[3]; + return [4 /*yield*/, fetchDid(handleOrDid)]; + case 8: + did = _e.sent(); + list = makeRecordUri(did, 'app.bsky.graph.list', rkey); + return [4 /*yield*/, agent.app.bsky.graph.getList({ list: list })]; + case 9: + res = _e.sent(); + return [2 /*return*/, { + type: 'record', + record: { + uri: res.data.list.uri, + cid: res.data.list.cid, + }, + kind: 'list', + view: res.data.list, + }]; + case 10: + if (!(isBskyStartUrl(uri) || isBskyStarterPackUrl(uri))) return [3 /*break*/, 13]; + parsed = parseStarterPackUri(uri); + if (!parsed) { + throw new Error('Unexpectedly called getStarterPackAsEmbed with a non-starterpack url'); + } + return [4 /*yield*/, fetchDid(parsed.name)]; + case 11: + did = _e.sent(); + starterPack = createStarterPackUri({ did: did, rkey: parsed.rkey }); + return [4 /*yield*/, agent.app.bsky.graph.getStarterPack({ starterPack: starterPack })]; + case 12: + res = _e.sent(); + return [2 /*return*/, { + type: 'record', + record: { + uri: res.data.starterPack.uri, + cid: res.data.starterPack.cid, + }, + kind: 'starter-pack', + view: res.data.starterPack, + }]; + case 13: return [2 /*return*/, resolveExternal(agent, uri) + // Forked from useGetPost. TODO: move into RQ. + ]; + } + }); + }); +} +export function resolveGif(agent, gif) { + return __awaiter(this, void 0, void 0, function () { + var uri; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + uri = "".concat(gif.media_formats.gif.url, "?hh=").concat(gif.media_formats.gif.dims[1], "&ww=").concat(gif.media_formats.gif.dims[0]); + _a = { + type: 'external', + uri: uri, + title: gif.content_description, + description: createGIFDescription(gif.content_description) + }; + return [4 /*yield*/, imageToThumb(gif.media_formats.preview.url)]; + case 1: return [2 /*return*/, (_a.thumb = _b.sent(), + _a)]; + } + }); + }); +} +function resolveExternal(agent, uri) { + return __awaiter(this, void 0, void 0, function () { + var result, _a; + var _b; + var _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: return [4 /*yield*/, getLinkMeta(agent, uri)]; + case 1: + result = _e.sent(); + _b = { + type: 'external', + uri: result.url, + title: (_c = result.title) !== null && _c !== void 0 ? _c : '', + description: (_d = result.description) !== null && _d !== void 0 ? _d : '' + }; + if (!result.image) return [3 /*break*/, 3]; + return [4 /*yield*/, imageToThumb(result.image)]; + case 2: + _a = _e.sent(); + return [3 /*break*/, 4]; + case 3: + _a = undefined; + _e.label = 4; + case 4: return [2 /*return*/, (_b.thumb = _a, + _b)]; + } + }); + }); +} +export function imageToThumb(imageUri) { + return __awaiter(this, void 0, void 0, function () { + var img, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 4, , 5]); + return [4 /*yield*/, downloadAndResize({ + uri: imageUri, + width: POST_IMG_MAX.width, + height: POST_IMG_MAX.height, + mode: 'contain', + maxSize: POST_IMG_MAX.size, + timeout: 15e3, + })]; + case 1: + img = _b.sent(); + if (!img) return [3 /*break*/, 3]; + return [4 /*yield*/, createComposerImage(img)]; + case 2: return [2 /*return*/, _b.sent()]; + case 3: return [3 /*break*/, 5]; + case 4: + _a = _b.sent(); + return [3 /*break*/, 5]; + case 5: return [2 /*return*/]; + } + }); + }); +} diff --git a/src/lib/api/upload-blob.js b/src/lib/api/upload-blob.js new file mode 100644 index 0000000000..b737253cd3 --- /dev/null +++ b/src/lib/api/upload-blob.js @@ -0,0 +1,144 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { copyAsync } from 'expo-file-system/legacy'; +import { safeDeleteAsync } from '#/lib/media/manip'; +/** + * @param encoding Allows overriding the blob's type + */ +export function uploadBlob(agent, input, encoding) { + return __awaiter(this, void 0, void 0, function () { + var blob, blob, blob; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(typeof input === 'string' && input.startsWith('file:'))) return [3 /*break*/, 2]; + return [4 /*yield*/, asBlob(input)]; + case 1: + blob = _a.sent(); + return [2 /*return*/, agent.uploadBlob(blob, { encoding: encoding })]; + case 2: + if (!(typeof input === 'string' && input.startsWith('/'))) return [3 /*break*/, 4]; + return [4 /*yield*/, asBlob("file://".concat(input))]; + case 3: + blob = _a.sent(); + return [2 /*return*/, agent.uploadBlob(blob, { encoding: encoding })]; + case 4: + if (!(typeof input === 'string' && input.startsWith('data:'))) return [3 /*break*/, 6]; + return [4 /*yield*/, fetch(input).then(function (r) { return r.blob(); })]; + case 5: + blob = _a.sent(); + return [2 /*return*/, agent.uploadBlob(blob, { encoding: encoding })]; + case 6: + if (input instanceof Blob) { + return [2 /*return*/, agent.uploadBlob(input, { encoding: encoding })]; + } + throw new TypeError("Invalid uploadBlob input: ".concat(typeof input)); + } + }); + }); +} +function asBlob(uri) { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + return [2 /*return*/, withSafeFile(uri, function (safeUri) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function () { return resolve(xhr.response); }; + xhr.onerror = function () { return reject(new Error('Failed to load blob')); }; + xhr.responseType = 'blob'; + xhr.open('GET', safeUri, true); + xhr.send(null); + })]; + case 1: + // Note + // Android does not support `fetch()` on `file://` URIs. for this reason, we + // use XMLHttpRequest instead of simply calling: + // return fetch(safeUri.replace('file:///', 'file:/')).then(r => r.blob()) + return [2 /*return*/, _a.sent()]; + } + }); + }); })]; + }); + }); +} +// HACK +// React native has a bug that inflates the size of jpegs on upload +// we get around that by renaming the file ext to .bin +// see https://github.com/facebook/react-native/issues/27099 +// -prf +function withSafeFile(uri, fn) { + return __awaiter(this, void 0, void 0, function () { + var newPath, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!(uri.endsWith('.jpeg') || uri.endsWith('.jpg'))) return [3 /*break*/, 10]; + newPath = uri.replace(/\.jpe?g$/, '.bin'); + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 5]); + return [4 /*yield*/, copyAsync({ from: uri, to: newPath })]; + case 2: + _b.sent(); + return [3 /*break*/, 5]; + case 3: + _a = _b.sent(); + return [4 /*yield*/, fn(uri)]; + case 4: + // Failed to copy the file, just use the original + return [2 /*return*/, _b.sent()]; + case 5: + _b.trys.push([5, , 7, 9]); + return [4 /*yield*/, fn(newPath)]; + case 6: return [2 /*return*/, _b.sent()]; + case 7: + // Remove the temporary file + return [4 /*yield*/, safeDeleteAsync(newPath)]; + case 8: + // Remove the temporary file + _b.sent(); + return [7 /*endfinally*/]; + case 9: return [3 /*break*/, 11]; + case 10: return [2 /*return*/, fn(uri)]; + case 11: return [2 /*return*/]; + } + }); + }); +} diff --git a/src/lib/api/upload-blob.web.js b/src/lib/api/upload-blob.web.js new file mode 100644 index 0000000000..aa1fd71963 --- /dev/null +++ b/src/lib/api/upload-blob.web.js @@ -0,0 +1,65 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +/** + * @note It is recommended, on web, to use the `file` instance of the file + * selector input element, rather than a `data:` URL, to avoid + * loading the file into memory. `File` extends `Blob` "file" instances can + * be passed directly to this function. + */ +export function uploadBlob(agent, input, encoding) { + return __awaiter(this, void 0, void 0, function () { + var blob; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(typeof input === 'string' && + (input.startsWith('data:') || input.startsWith('blob:')))) return [3 /*break*/, 2]; + return [4 /*yield*/, fetch(input).then(function (r) { return r.blob(); })]; + case 1: + blob = _a.sent(); + return [2 /*return*/, agent.uploadBlob(blob, { encoding: encoding })]; + case 2: + if (input instanceof Blob) { + return [2 /*return*/, agent.uploadBlob(input, { + encoding: encoding, + })]; + } + throw new TypeError("Invalid uploadBlob input: ".concat(typeof input)); + } + }); + }); +} diff --git a/src/lib/appState.js b/src/lib/appState.js new file mode 100644 index 0000000000..c33a99db3b --- /dev/null +++ b/src/lib/appState.js @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react'; +import { AppState } from 'react-native'; +export var getCurrentState = function () { return AppState.currentState; }; +export function onAppStateChange(cb) { + var prev = AppState.currentState; + return AppState.addEventListener('change', function (next) { + if (next === prev) + return; + prev = next; + cb(next); + }); +} +export function useOnAppStateChange(cb) { + useEffect(function () { + var sub = onAppStateChange(function (next) { return cb(next); }); + return function () { return sub.remove(); }; + }, [cb]); +} +export function useAppState() { + var _a = useState(AppState.currentState), state = _a[0], setState = _a[1]; + useOnAppStateChange(setState); + return state; +} diff --git a/src/lib/async/accumulate.js b/src/lib/async/accumulate.js new file mode 100644 index 0000000000..f384a894fb --- /dev/null +++ b/src/lib/async/accumulate.js @@ -0,0 +1,65 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +export function accumulate(fn_1) { + return __awaiter(this, arguments, void 0, function (fn, pageLimit) { + var cursor, acc, i, res; + if (pageLimit === void 0) { pageLimit = 100; } + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + acc = []; + i = 0; + _a.label = 1; + case 1: + if (!(i < pageLimit)) return [3 /*break*/, 4]; + return [4 /*yield*/, fn(cursor)]; + case 2: + res = _a.sent(); + cursor = res.cursor; + acc = acc.concat(res.items); + if (!cursor) { + return [3 /*break*/, 4]; + } + _a.label = 3; + case 3: + i++; + return [3 /*break*/, 1]; + case 4: return [2 /*return*/, acc]; + } + }); + }); +} diff --git a/src/lib/async/bundle.js b/src/lib/async/bundle.js new file mode 100644 index 0000000000..2f6db2b1c3 --- /dev/null +++ b/src/lib/async/bundle.js @@ -0,0 +1,70 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +/** + * A helper which ensures that multiple calls to an async function + * only produces one in-flight request at a time. + */ +export function bundleAsync(fn) { + var _this = this; + var promise; + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (promise) { + return [2 /*return*/, promise]; + } + promise = fn.apply(void 0, args); + _a.label = 1; + case 1: + _a.trys.push([1, , 3, 4]); + return [4 /*yield*/, promise]; + case 2: return [2 /*return*/, _a.sent()]; + case 3: + promise = undefined; + return [7 /*endfinally*/]; + case 4: return [2 /*return*/]; + } + }); + }); + }; +} diff --git a/src/lib/async/cancelable.js b/src/lib/async/cancelable.js new file mode 100644 index 0000000000..7a3e7f701f --- /dev/null +++ b/src/lib/async/cancelable.js @@ -0,0 +1,35 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +export function cancelable(f, signal) { + return function (args) { + return new Promise(function (resolve, reject) { + signal.addEventListener('abort', function () { + reject(new AbortError()); + }); + f(args).then(resolve, reject); + }); + }; +} +var AbortError = /** @class */ (function (_super) { + __extends(AbortError, _super); + function AbortError() { + var _this = _super.call(this, 'Aborted') || this; + _this.name = 'AbortError'; + return _this; + } + return AbortError; +}(Error)); +export { AbortError }; diff --git a/src/lib/async/retry.js b/src/lib/async/retry.js new file mode 100644 index 0000000000..728f4d1308 --- /dev/null +++ b/src/lib/async/retry.js @@ -0,0 +1,76 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { timeout } from '#/lib/async/timeout'; +import { isNetworkError } from '#/lib/strings/errors'; +export function retry(retries, shouldRetry, action, delay) { + return __awaiter(this, void 0, void 0, function () { + var lastErr, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(retries > 0)) return [3 /*break*/, 8]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 7]); + return [4 /*yield*/, action()]; + case 2: return [2 /*return*/, _a.sent()]; + case 3: + e_1 = _a.sent(); + lastErr = e_1; + if (!shouldRetry(e_1)) return [3 /*break*/, 6]; + if (!delay) return [3 /*break*/, 5]; + return [4 /*yield*/, timeout(delay)]; + case 4: + _a.sent(); + _a.label = 5; + case 5: + retries--; + return [3 /*break*/, 0]; + case 6: throw e_1; + case 7: return [3 /*break*/, 0]; + case 8: throw lastErr; + } + }); + }); +} +export function networkRetry(retries, fn, delay) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, retry(retries, isNetworkError, fn, delay)]; + }); + }); +} diff --git a/src/lib/async/timeout.js b/src/lib/async/timeout.js new file mode 100644 index 0000000000..e0d20fba8e --- /dev/null +++ b/src/lib/async/timeout.js @@ -0,0 +1,3 @@ +export function timeout(ms) { + return new Promise(function (r) { return setTimeout(r, ms); }); +} diff --git a/src/lib/async/until.js b/src/lib/async/until.js new file mode 100644 index 0000000000..07d894ca07 --- /dev/null +++ b/src/lib/async/until.js @@ -0,0 +1,72 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { timeout } from './timeout'; +export function until(retries, delay, cond, fn) { + return __awaiter(this, void 0, void 0, function () { + var v, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(retries > 0)) return [3 /*break*/, 6]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fn()]; + case 2: + v = _a.sent(); + if (cond(v, undefined)) { + return [2 /*return*/, true]; + } + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + // TODO: change the type signature of cond to accept undefined + // however this breaks every existing usage of until -sfn + if (cond(undefined, e_1)) { + return [2 /*return*/, true]; + } + return [3 /*break*/, 4]; + case 4: return [4 /*yield*/, timeout(delay)]; + case 5: + _a.sent(); + retries--; + return [3 /*break*/, 0]; + case 6: return [2 /*return*/, false]; + } + }); + }); +} diff --git a/src/lib/async/wait.js b/src/lib/async/wait.js new file mode 100644 index 0000000000..5e29962ff6 --- /dev/null +++ b/src/lib/async/wait.js @@ -0,0 +1,46 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +export function wait(delay, fn) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Promise.all([fn, new Promise(function (y) { return setTimeout(y, delay); })]).then(function (arr) { return arr[0]; })]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); +} diff --git a/src/lib/batchedUpdates.js b/src/lib/batchedUpdates.js new file mode 100644 index 0000000000..90420ba165 --- /dev/null +++ b/src/lib/batchedUpdates.js @@ -0,0 +1 @@ +export { unstable_batchedUpdates as batchedUpdates } from 'react-native'; diff --git a/src/lib/batchedUpdates.web.js b/src/lib/batchedUpdates.web.js new file mode 100644 index 0000000000..020427ebef --- /dev/null +++ b/src/lib/batchedUpdates.web.js @@ -0,0 +1 @@ +export { unstable_batchedUpdates as batchedUpdates } from 'react-dom'; diff --git a/src/lib/broadcast/index.js b/src/lib/broadcast/index.js new file mode 100644 index 0000000000..73dac0fc23 --- /dev/null +++ b/src/lib/broadcast/index.js @@ -0,0 +1,2 @@ +import Stub from '#/lib/broadcast/stub'; +export default Stub; diff --git a/src/lib/broadcast/index.web.js b/src/lib/broadcast/index.web.js new file mode 100644 index 0000000000..496eb4660a --- /dev/null +++ b/src/lib/broadcast/index.web.js @@ -0,0 +1,2 @@ +import Stub from '#/lib/broadcast/stub'; +export default 'BroadcastChannel' in window ? window.BroadcastChannel : Stub; diff --git a/src/lib/broadcast/stub.js b/src/lib/broadcast/stub.js new file mode 100644 index 0000000000..29695954a2 --- /dev/null +++ b/src/lib/broadcast/stub.js @@ -0,0 +1,12 @@ +var BroadcastChannel = /** @class */ (function () { + function BroadcastChannel(name) { + this.name = name; + this.onmessage = function () { }; + } + BroadcastChannel.prototype.postMessage = function (_data) { }; + BroadcastChannel.prototype.close = function () { }; + BroadcastChannel.prototype.addEventListener = function (_type, _listener) { }; + BroadcastChannel.prototype.removeEventListener = function (_type, _listener) { }; + return BroadcastChannel; +}()); +export default BroadcastChannel; diff --git a/src/lib/constants.js b/src/lib/constants.js new file mode 100644 index 0000000000..9864445faa --- /dev/null +++ b/src/lib/constants.js @@ -0,0 +1,197 @@ +import { Platform } from 'react-native'; +import { BSKY_LABELER_DID } from '@atproto/api'; +import { BLUESKY_PROXY_DID, CHAT_PROXY_DID } from '#/env'; +export var LOCAL_DEV_SERVICE = Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'; +export var STAGING_SERVICE = 'https://staging.bsky.dev'; +export var BSKY_SERVICE = 'https://bsky.social'; +export var BSKY_SERVICE_DID = 'did:web:bsky.social'; +export var PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'; +export var DEFAULT_SERVICE = BSKY_SERVICE; +var HELP_DESK_LANG = 'en-us'; +export var HELP_DESK_URL = "https://blueskyweb.zendesk.com/hc/".concat(HELP_DESK_LANG); +export var EMBED_SERVICE = 'https://embed.bsky.app'; +export var EMBED_SCRIPT = "".concat(EMBED_SERVICE, "/static/embed.js"); +export var BSKY_DOWNLOAD_URL = 'https://bsky.app/download'; +export var STARTER_PACK_MAX_SIZE = 150; +export var CARD_ASPECT_RATIO = 1200 / 630; +// HACK +// Yes, this is exactly what it looks like. It's a hard-coded constant +// reflecting the number of new users in the last week. We don't have +// time to add a route to the servers for this so we're just going to hard +// code and update this number with each release until we can get the +// server route done. +// -prf +export var JOINED_THIS_WEEK = 560000; // estimate as of 12/18/24 +export var DISCOVER_DEBUG_DIDS = { + 'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at + 'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team + 'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com + 'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team + 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol + 'did:plc:vjug55kidv6sye7ykr5faxxn': true, // emilyliu.me + 'did:plc:tgqseeot47ymot4zro244fj3': true, // iwsmith.bsky.social + 'did:plc:2dzyut5lxna5ljiaasgeuffz': true, // darrin.bsky.team +}; +var BASE_FEEDBACK_FORM_URL = "".concat(HELP_DESK_URL, "/requests/new"); +export function FEEDBACK_FORM_URL(_a) { + var email = _a.email, handle = _a.handle; + var str = BASE_FEEDBACK_FORM_URL; + if (email) { + str += "?tf_anonymous_requester_email=".concat(encodeURIComponent(email)); + if (handle) { + str += "&tf_17205412673421=".concat(encodeURIComponent(handle)); + } + } + return str; +} +export var MAX_DISPLAY_NAME = 64; +export var MAX_DESCRIPTION = 256; +export var MAX_GRAPHEME_LENGTH = 300; +export var MAX_DRAFT_GRAPHEME_LENGTH = 1000; +export var MAX_DM_GRAPHEME_LENGTH = 1000; +// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html +// but increasing limit per user feedback +export var MAX_ALT_TEXT = 2000; +export var MAX_REPORT_REASON_GRAPHEME_LENGTH = 2000; +export function IS_TEST_USER(handle) { + return handle && (handle === null || handle === void 0 ? void 0 : handle.endsWith('.test')); +} +export function IS_PROD_SERVICE(url) { + return url && url !== STAGING_SERVICE && !url.startsWith(LOCAL_DEV_SERVICE); +} +export var PROD_DEFAULT_FEED = function (rkey) { + return "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/".concat(rkey); +}; +export var STAGING_DEFAULT_FEED = function (rkey) { + return "at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/".concat(rkey); +}; +export var PROD_FEEDS = [ + "feedgen|".concat(PROD_DEFAULT_FEED('whats-hot')), + "feedgen|".concat(PROD_DEFAULT_FEED('thevids')), +]; +export var STAGING_FEEDS = [ + "feedgen|".concat(STAGING_DEFAULT_FEED('whats-hot')), + "feedgen|".concat(STAGING_DEFAULT_FEED('thevids')), +]; +export var POST_IMG_MAX = { + width: 2000, + height: 2000, + size: 1000000, +}; +export var STAGING_LINK_META_PROXY = 'https://cardyb.staging.bsky.dev/v1/extract?url='; +export var PROD_LINK_META_PROXY = 'https://cardyb.bsky.app/v1/extract?url='; +export function LINK_META_PROXY(serviceUrl) { + if (IS_PROD_SERVICE(serviceUrl)) { + return PROD_LINK_META_PROXY; + } + return STAGING_LINK_META_PROXY; +} +export var STATUS_PAGE_URL = 'https://status.bsky.app/'; +// Hitslop constants +export var createHitslop = function (size) { return ({ + top: size, + left: size, + bottom: size, + right: size, +}); }; +export var HITSLOP_10 = createHitslop(10); +export var HITSLOP_20 = createHitslop(20); +export var HITSLOP_30 = createHitslop(30); +export var LANG_DROPDOWN_HITSLOP = { top: 10, bottom: 10, left: 4, right: 4 }; +export var BACK_HITSLOP = HITSLOP_30; +export var MAX_POST_LINES = 25; +export var BSKY_APP_ACCOUNT_DID = 'did:plc:z72i7hdynmk6r22z27h6tvur'; +export var BSKY_FEED_OWNER_DIDS = [ + BSKY_APP_ACCOUNT_DID, + 'did:plc:vpkhqolt662uhesyj6nxm7ys', + 'did:plc:q6gjnaw2blty4crticxkmujt', +]; +export var DISCOVER_FEED_URI = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'; +export var VIDEO_FEED_URI = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/thevids'; +export var STAGING_VIDEO_FEED_URI = 'at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/thevids'; +export var VIDEO_FEED_URIS = [VIDEO_FEED_URI, STAGING_VIDEO_FEED_URI]; +export var DISCOVER_SAVED_FEED = { + type: 'feed', + value: DISCOVER_FEED_URI, + pinned: true, +}; +export var TIMELINE_SAVED_FEED = { + type: 'timeline', + value: 'following', + pinned: true, +}; +export var VIDEO_SAVED_FEED = { + type: 'feed', + value: VIDEO_FEED_URI, + pinned: true, +}; +export var RECOMMENDED_SAVED_FEEDS = [DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED]; +export var KNOWN_SHUTDOWN_FEEDS = [ + 'at://did:plc:wqowuobffl66jv3kpsvo7ak4/app.bsky.feed.generator/the-algorithm', // for you by skygaze +]; +export var GIF_SERVICE = 'https://gifs.bsky.app'; +export var GIF_SEARCH = function (params) { + return "".concat(GIF_SERVICE, "/tenor/v2/search?").concat(params); +}; +export var GIF_FEATURED = function (params) { + return "".concat(GIF_SERVICE, "/tenor/v2/featured?").concat(params); +}; +export var MAX_LABELERS = 20; +export var VIDEO_SERVICE = 'https://video.bsky.app'; +export var VIDEO_SERVICE_DID = 'did:web:video.bsky.app'; +export var VIDEO_MAX_DURATION_MS = 3 * 60 * 1000; // 3 minutes in milliseconds +/** + * Maximum size of a video in megabytes, _not_ mebibytes. Backend uses + * ISO megabytes. + */ +export var VIDEO_MAX_SIZE = 1000 * 1000 * 100; // 100mb +export var SUPPORTED_MIME_TYPES = [ + 'video/mp4', + 'video/mpeg', + 'video/webm', + 'video/quicktime', + 'image/gif', +]; +export var EMOJI_REACTION_LIMIT = 5; +export var urls = { + website: { + blog: { + findFriendsAnnouncement: 'https://bsky.social/about/blog/12-16-2025-find-friends', + initialVerificationAnnouncement: "https://bsky.social/about/blog/04-21-2025-verification", + searchTipsAndTricks: 'https://bsky.social/about/blog/05-31-2024-search', + }, + support: { + findFriendsPrivacyPolicy: 'https://bsky.social/about/support/find-friends-privacy-policy', + }, + }, +}; +export var PUBLIC_APPVIEW = 'https://api.bsky.app'; +export var PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app'; +export var PUBLIC_STAGING_APPVIEW_DID = 'did:web:api.staging.bsky.dev'; +export var DEV_ENV_APPVIEW = "http://localhost:2584"; // always the same +export var DEV_ENV_APPVIEW_DID = "did:plc:dw4kbjf5mn7nhenabiqpkyh3"; // always the same +// temp hack for e2e - esb +export var BLUESKY_PROXY_HEADER = { + value: "".concat(BLUESKY_PROXY_DID, "#bsky_appview"), + get: function () { + return this.value; + }, + set: function (value) { + this.value = value; + }, +}; +export var DM_SERVICE_HEADERS = { + 'atproto-proxy': "".concat(CHAT_PROXY_DID, "#bsky_chat"), +}; +export var BLUESKY_MOD_SERVICE_HEADERS = { + 'atproto-proxy': "".concat(BSKY_LABELER_DID, "#atproto_labeler"), +}; +export var BLUESKY_NOTIF_SERVICE_HEADERS = { + 'atproto-proxy': "".concat(BLUESKY_PROXY_DID, "#bsky_notif"), +}; +export var webLinks = { + tos: "https://bsky.social/about/support/tos", + privacy: "https://bsky.social/about/support/privacy-policy", + community: "https://bsky.social/about/support/community-guidelines", + communityDeprecated: "https://bsky.social/about/support/community-guidelines-deprecated", +}; diff --git a/src/lib/currency.js b/src/lib/currency.js new file mode 100644 index 0000000000..88e542f43d --- /dev/null +++ b/src/lib/currency.js @@ -0,0 +1,301 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import React from 'react'; +import { deviceLocales } from '#/locale/deviceLocales'; +import { useLanguagePrefs } from '#/state/preferences'; +import { useGeolocation } from '#/geolocation'; +/** + * From react-native-localize + * + * MIT License + * Copyright (c) 2017-present, Mathieu Acthernoene + * + * @see https://github.com/zoontek/react-native-localize/blob/master/LICENSE + * @see https://github.com/zoontek/react-native-localize/blob/ee5bf25e0bb8f3b8e4f3fd055f67ad46269c81ea/src/constants.ts + */ +export var countryCodeToCurrency = { + ad: 'eur', + ae: 'aed', + af: 'afn', + ag: 'xcd', + ai: 'xcd', + al: 'all', + am: 'amd', + an: 'ang', + ao: 'aoa', + ar: 'ars', + as: 'usd', + at: 'eur', + au: 'aud', + aw: 'awg', + ax: 'eur', + az: 'azn', + ba: 'bam', + bb: 'bbd', + bd: 'bdt', + be: 'eur', + bf: 'xof', + bg: 'bgn', + bh: 'bhd', + bi: 'bif', + bj: 'xof', + bl: 'eur', + bm: 'bmd', + bn: 'bnd', + bo: 'bob', + bq: 'usd', + br: 'brl', + bs: 'bsd', + bt: 'btn', + bv: 'nok', + bw: 'bwp', + by: 'byn', + bz: 'bzd', + ca: 'cad', + cc: 'aud', + cd: 'cdf', + cf: 'xaf', + cg: 'xaf', + ch: 'chf', + ci: 'xof', + ck: 'nzd', + cl: 'clp', + cm: 'xaf', + cn: 'cny', + co: 'cop', + cr: 'crc', + cu: 'cup', + cv: 'cve', + cw: 'ang', + cx: 'aud', + cy: 'eur', + cz: 'czk', + de: 'eur', + dj: 'djf', + dk: 'dkk', + dm: 'xcd', + do: 'dop', + dz: 'dzd', + ec: 'usd', + ee: 'eur', + eg: 'egp', + eh: 'mad', + er: 'ern', + es: 'eur', + et: 'etb', + fi: 'eur', + fj: 'fjd', + fk: 'fkp', + fm: 'usd', + fo: 'dkk', + fr: 'eur', + ga: 'xaf', + gb: 'gbp', + gd: 'xcd', + ge: 'gel', + gf: 'eur', + gg: 'gbp', + gh: 'ghs', + gi: 'gip', + gl: 'dkk', + gm: 'gmd', + gn: 'gnf', + gp: 'eur', + gq: 'xaf', + gr: 'eur', + gs: 'gbp', + gt: 'gtq', + gu: 'usd', + gw: 'xof', + gy: 'gyd', + hk: 'hkd', + hm: 'aud', + hn: 'hnl', + hr: 'hrk', + ht: 'htg', + hu: 'huf', + id: 'idr', + ie: 'eur', + il: 'ils', + im: 'gbp', + in: 'inr', + io: 'usd', + iq: 'iqd', + ir: 'irr', + is: 'isk', + it: 'eur', + je: 'gbp', + jm: 'jmd', + jo: 'jod', + jp: 'jpy', + ke: 'kes', + kg: 'kgs', + kh: 'khr', + ki: 'aud', + km: 'kmf', + kn: 'xcd', + kp: 'kpw', + kr: 'krw', + kw: 'kwd', + ky: 'kyd', + kz: 'kzt', + la: 'lak', + lb: 'lbp', + lc: 'xcd', + li: 'chf', + lk: 'lkr', + lr: 'lrd', + ls: 'lsl', + lt: 'eur', + lu: 'eur', + lv: 'eur', + ly: 'lyd', + ma: 'mad', + mc: 'eur', + md: 'mdl', + me: 'eur', + mf: 'eur', + mg: 'mga', + mh: 'usd', + mk: 'mkd', + ml: 'xof', + mm: 'mmk', + mn: 'mnt', + mo: 'mop', + mp: 'usd', + mq: 'eur', + mr: 'mro', + ms: 'xcd', + mt: 'eur', + mu: 'mur', + mv: 'mvr', + mw: 'mwk', + mx: 'mxn', + my: 'myr', + mz: 'mzn', + na: 'nad', + nc: 'xpf', + ne: 'xof', + nf: 'aud', + ng: 'ngn', + ni: 'nio', + nl: 'eur', + no: 'nok', + np: 'npr', + nr: 'aud', + nu: 'nzd', + nz: 'nzd', + om: 'omr', + pa: 'pab', + pe: 'pen', + pf: 'xpf', + pg: 'pgk', + ph: 'php', + pk: 'pkr', + pl: 'pln', + pm: 'eur', + pn: 'nzd', + pr: 'usd', + ps: 'ils', + pt: 'eur', + pw: 'usd', + py: 'pyg', + qa: 'qar', + re: 'eur', + ro: 'ron', + rs: 'rsd', + ru: 'rub', + rw: 'rwf', + sa: 'sar', + sb: 'sbd', + sc: 'scr', + sd: 'sdg', + se: 'sek', + sg: 'sgd', + sh: 'shp', + si: 'eur', + sj: 'nok', + sk: 'eur', + sl: 'sll', + sm: 'eur', + sn: 'xof', + so: 'sos', + sr: 'srd', + ss: 'ssp', + st: 'std', + sv: 'svc', + sx: 'ang', + sy: 'syp', + sz: 'szl', + tc: 'usd', + td: 'xaf', + tf: 'eur', + tg: 'xof', + th: 'thb', + tj: 'tjs', + tk: 'nzd', + tl: 'usd', + tm: 'tmt', + tn: 'tnd', + to: 'top', + tr: 'try', + tt: 'ttd', + tv: 'aud', + tw: 'twd', + tz: 'tzs', + ua: 'uah', + ug: 'ugx', + um: 'usd', + us: 'usd', + uy: 'uyu', + uz: 'uzs', + va: 'eur', + vc: 'xcd', + ve: 'vef', + vg: 'usd', + vi: 'usd', + vn: 'vnd', + vu: 'vuv', + wf: 'xpf', + ws: 'wst', + ye: 'yer', + yt: 'eur', + za: 'zar', + zm: 'zmw', + zw: 'zwl', +}; +/** + * Best-guess currency formatting. + * + * Attempts to use `getLocales` from `expo-localization` if available, + * otherwise falls back to the `persisted.appLanguage` setting, and geolocation + * API for region. + */ +export function useFormatCurrency(options) { + var geolocation = useGeolocation(); + var appLanguage = useLanguagePrefs().appLanguage; + return React.useMemo(function () { + var locale = deviceLocales.at(0); + var languageTag = (locale === null || locale === void 0 ? void 0 : locale.languageTag) || appLanguage || 'en-US'; + var countryCode = ((locale === null || locale === void 0 ? void 0 : locale.regionCode) || + (geolocation === null || geolocation === void 0 ? void 0 : geolocation.countryCode) || + 'us').toLowerCase(); + var currency = countryCodeToCurrency[countryCode] || 'usd'; + var format = new Intl.NumberFormat(languageTag, __assign(__assign({}, (options || {})), { style: 'currency', currency: currency })).format; + return { + format: format, + currency: currency, + countryCode: countryCode, + languageTag: languageTag, + }; + }, [geolocation, appLanguage, options]); +} diff --git a/src/lib/custom-animations/AccordionAnimation.js b/src/lib/custom-animations/AccordionAnimation.js new file mode 100644 index 0000000000..311297477c --- /dev/null +++ b/src/lib/custom-animations/AccordionAnimation.js @@ -0,0 +1,44 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { View, } from 'react-native'; +import Animated, { Easing, FadeInUp, FadeOutUp, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { IS_IOS, IS_WEB } from '#/env'; +function WebAccordion(_a) { + var isExpanded = _a.isExpanded, _b = _a.duration, duration = _b === void 0 ? 300 : _b, style = _a.style, children = _a.children; + var heightValue = useSharedValue(0); + var animatedStyle = useAnimatedStyle(function () { + var targetHeight = isExpanded ? heightValue.get() : 0; + return { + height: withTiming(targetHeight, { + duration: duration, + easing: Easing.out(Easing.cubic), + }), + overflow: 'hidden', + }; + }); + var onLayout = function (e) { + if (heightValue.get() === 0) { + heightValue.set(e.nativeEvent.layout.height); + } + }; + return (_jsx(Animated.View, { style: [animatedStyle, style], children: _jsx(View, { onLayout: onLayout, children: children }) })); +} +function MobileAccordion(_a) { + var isExpanded = _a.isExpanded, _b = _a.duration, duration = _b === void 0 ? 200 : _b, style = _a.style, children = _a.children; + if (!isExpanded) + return null; + return (_jsx(Animated.View, { style: style, entering: FadeInUp.duration(duration), exiting: FadeOutUp.duration(duration / 2), pointerEvents: IS_IOS ? 'auto' : 'box-none', children: children })); +} +export function AccordionAnimation(props) { + return IS_WEB ? _jsx(WebAccordion, __assign({}, props)) : _jsx(MobileAccordion, __assign({}, props)); +} diff --git a/src/lib/custom-animations/CountWheel.js b/src/lib/custom-animations/CountWheel.js new file mode 100644 index 0000000000..d0a3310660 --- /dev/null +++ b/src/lib/custom-animations/CountWheel.js @@ -0,0 +1,125 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import Animated, { Easing, LayoutAnimationConfig, useReducedMotion, withTiming, } from 'react-native-reanimated'; +import { decideShouldRoll } from '#/lib/custom-animations/util'; +import { s } from '#/lib/styles'; +import { Text } from '#/view/com/util/text/Text'; +import { atoms as a, useTheme } from '#/alf'; +import { useFormatPostStatCount } from '#/components/PostControls/util'; +var animationConfig = { + duration: 400, + easing: Easing.out(Easing.cubic), +}; +function EnteringUp() { + 'worklet'; + var animations = { + opacity: withTiming(1, animationConfig), + transform: [{ translateY: withTiming(0, animationConfig) }], + }; + var initialValues = { + opacity: 0, + transform: [{ translateY: 18 }], + }; + return { + animations: animations, + initialValues: initialValues, + }; +} +function EnteringDown() { + 'worklet'; + var animations = { + opacity: withTiming(1, animationConfig), + transform: [{ translateY: withTiming(0, animationConfig) }], + }; + var initialValues = { + opacity: 0, + transform: [{ translateY: -18 }], + }; + return { + animations: animations, + initialValues: initialValues, + }; +} +function ExitingUp() { + 'worklet'; + var animations = { + opacity: withTiming(0, animationConfig), + transform: [ + { + translateY: withTiming(-18, animationConfig), + }, + ], + }; + var initialValues = { + opacity: 1, + transform: [{ translateY: 0 }], + }; + return { + animations: animations, + initialValues: initialValues, + }; +} +function ExitingDown() { + 'worklet'; + var animations = { + opacity: withTiming(0, animationConfig), + transform: [{ translateY: withTiming(18, animationConfig) }], + }; + var initialValues = { + opacity: 1, + transform: [{ translateY: 0 }], + }; + return { + animations: animations, + initialValues: initialValues, + }; +} +export function CountWheel(_a) { + var likeCount = _a.likeCount, big = _a.big, isLiked = _a.isLiked, hasBeenToggled = _a.hasBeenToggled; + var t = useTheme(); + var shouldAnimate = !useReducedMotion() && hasBeenToggled; + var shouldRoll = decideShouldRoll(isLiked, likeCount); + // Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting + // animation + // The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would + // be unnecessary + var _b = React.useState(0), key = _b[0], setKey = _b[1]; + var _c = React.useState(likeCount), prevCount = _c[0], setPrevCount = _c[1]; + var prevIsLiked = React.useRef(isLiked); + var formatPostStatCount = useFormatPostStatCount(); + var formattedCount = formatPostStatCount(likeCount); + var formattedPrevCount = formatPostStatCount(prevCount); + React.useEffect(function () { + if (isLiked === prevIsLiked.current) { + return; + } + var newPrevCount = isLiked ? likeCount - 1 : likeCount + 1; + setKey(function (prev) { return prev + 1; }); + setPrevCount(newPrevCount); + prevIsLiked.current = isLiked; + }, [isLiked, likeCount]); + var enteringAnimation = shouldAnimate && shouldRoll + ? isLiked + ? EnteringUp + : EnteringDown + : undefined; + var exitingAnimation = shouldAnimate && shouldRoll + ? isLiked + ? ExitingUp + : ExitingDown + : undefined; + return (_jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: likeCount > 0 ? (_jsxs(View, { style: [a.justify_center], children: [_jsx(Animated.View, { entering: enteringAnimation, children: _jsx(Text, { testID: "likeCount", style: [ + big ? a.text_md : a.text_sm, + a.user_select_none, + isLiked + ? [a.font_semi_bold, s.likeColor] + : { color: t.palette.contrast_500 }, + ], children: formattedCount }) }, key), shouldAnimate && (likeCount > 1 || !isLiked) ? (_jsx(Animated.View, { entering: exitingAnimation, style: [a.absolute, { width: 50, opacity: 0 }], "aria-disabled": true, children: _jsx(Text, { style: [ + big ? a.text_md : a.text_sm, + a.user_select_none, + isLiked + ? [a.font_semi_bold, s.likeColor] + : { color: t.palette.contrast_500 }, + ], children: formattedPrevCount }) }, key + 2)) : null] })) : null })); +} diff --git a/src/lib/custom-animations/CountWheel.web.js b/src/lib/custom-animations/CountWheel.web.js new file mode 100644 index 0000000000..fb1516e5e4 --- /dev/null +++ b/src/lib/custom-animations/CountWheel.web.js @@ -0,0 +1,78 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { useReducedMotion } from 'react-native-reanimated'; +import { decideShouldRoll } from '#/lib/custom-animations/util'; +import { s } from '#/lib/styles'; +import { Text } from '#/view/com/util/text/Text'; +import { atoms as a, useTheme } from '#/alf'; +import { useFormatPostStatCount } from '#/components/PostControls/util'; +var animationConfig = { + duration: 400, + easing: 'cubic-bezier(0.4, 0, 0.2, 1)', + fill: 'forwards', +}; +var enteringUpKeyframe = [ + { opacity: 0, transform: 'translateY(18px)' }, + { opacity: 1, transform: 'translateY(0)' }, +]; +var enteringDownKeyframe = [ + { opacity: 0, transform: 'translateY(-18px)' }, + { opacity: 1, transform: 'translateY(0)' }, +]; +var exitingUpKeyframe = [ + { opacity: 1, transform: 'translateY(0)' }, + { opacity: 0, transform: 'translateY(-18px)' }, +]; +var exitingDownKeyframe = [ + { opacity: 1, transform: 'translateY(0)' }, + { opacity: 0, transform: 'translateY(18px)' }, +]; +export function CountWheel(_a) { + var likeCount = _a.likeCount, big = _a.big, isLiked = _a.isLiked, hasBeenToggled = _a.hasBeenToggled; + var t = useTheme(); + var shouldAnimate = !useReducedMotion() && hasBeenToggled; + var shouldRoll = decideShouldRoll(isLiked, likeCount); + var countView = React.useRef(null); + var prevCountView = React.useRef(null); + var _b = React.useState(likeCount), prevCount = _b[0], setPrevCount = _b[1]; + var prevIsLiked = React.useRef(isLiked); + var formatPostStatCount = useFormatPostStatCount(); + var formattedCount = formatPostStatCount(likeCount); + var formattedPrevCount = formatPostStatCount(prevCount); + React.useEffect(function () { + var _a, _b, _c, _d; + if (isLiked === prevIsLiked.current) { + return; + } + var newPrevCount = isLiked ? likeCount - 1 : likeCount + 1; + if (shouldAnimate && shouldRoll) { + (_b = (_a = countView.current) === null || _a === void 0 ? void 0 : _a.animate) === null || _b === void 0 ? void 0 : _b.call(_a, isLiked ? enteringUpKeyframe : enteringDownKeyframe, animationConfig); + (_d = (_c = prevCountView.current) === null || _c === void 0 ? void 0 : _c.animate) === null || _d === void 0 ? void 0 : _d.call(_c, isLiked ? exitingUpKeyframe : exitingDownKeyframe, animationConfig); + setPrevCount(newPrevCount); + } + prevIsLiked.current = isLiked; + }, [isLiked, likeCount, shouldAnimate, shouldRoll]); + if (likeCount < 1) { + return null; + } + return (_jsxs(View, { children: [_jsx(View + // @ts-expect-error is div + , { + // @ts-expect-error is div + ref: countView, children: _jsx(Text, { testID: "likeCount", style: [ + big ? a.text_md : a.text_sm, + a.user_select_none, + isLiked + ? [a.font_semi_bold, s.likeColor] + : { color: t.palette.contrast_500 }, + ], children: formattedCount }) }), shouldAnimate && (likeCount > 1 || !isLiked) ? (_jsx(View, { style: { position: 'absolute', opacity: 0 }, "aria-disabled": true, + // @ts-expect-error is div + ref: prevCountView, children: _jsx(Text, { style: [ + big ? a.text_md : a.text_sm, + a.user_select_none, + isLiked + ? [a.font_semi_bold, s.likeColor] + : { color: t.palette.contrast_500 }, + ], children: formattedPrevCount }) })) : null] })); +} diff --git a/src/lib/custom-animations/GestureActionView.js b/src/lib/custom-animations/GestureActionView.js new file mode 100644 index 0000000000..c2ccfa8600 --- /dev/null +++ b/src/lib/custom-animations/GestureActionView.js @@ -0,0 +1,284 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Dimensions, StyleSheet, View } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { clamp, interpolate, interpolateColor, runOnJS, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useReducedMotion, useSharedValue, withSequence, withTiming, } from 'react-native-reanimated'; +import { useHaptics } from '#/lib/haptics'; +var MAX_WIDTH = Dimensions.get('screen').width; +var ICON_SIZE = 32; +export function GestureActionView(_a) { + var _b, _c, _d, _e; + var children = _a.children, actions = _a.actions; + if ((actions.leftSecond && !actions.leftFirst) || + (actions.rightSecond && !actions.rightFirst)) { + throw new Error('You must provide the first action before the second action'); + } + var _f = React.useState(null), activeAction = _f[0], setActiveAction = _f[1]; + var haptic = useHaptics(); + var isReducedMotion = useReducedMotion(); + var transX = useSharedValue(0); + var clampedTransX = useDerivedValue(function () { + var min = actions.leftFirst ? -MAX_WIDTH : 0; + var max = actions.rightFirst ? MAX_WIDTH : 0; + return clamp(transX.get(), min, max); + }); + var iconScale = useSharedValue(1); + var isActive = useSharedValue(false); + var hitFirst = useSharedValue(false); + var hitSecond = useSharedValue(false); + var runPopAnimation = function () { + 'worklet'; + if (isReducedMotion) { + return; + } + iconScale.set(function () { + return withSequence(withTiming(1.2, { duration: 175 }), withTiming(1, { duration: 100 })); + }); + }; + useAnimatedReaction(function () { return transX; }, function () { + if (transX.get() === 0) { + runOnJS(setActiveAction)(null); + } + else if (transX.get() < 0) { + if (actions.leftSecond && + transX.get() <= -actions.leftSecond.threshold) { + if (activeAction !== 'leftSecond') { + runOnJS(setActiveAction)('leftSecond'); + } + } + else if (activeAction !== 'leftFirst') { + runOnJS(setActiveAction)('leftFirst'); + } + } + else if (transX.get() > 0) { + if (actions.rightSecond && + transX.get() > actions.rightSecond.threshold) { + if (activeAction !== 'rightSecond') { + runOnJS(setActiveAction)('rightSecond'); + } + } + else if (activeAction !== 'rightFirst') { + runOnJS(setActiveAction)('rightFirst'); + } + } + }); + // NOTE(haileyok): + // Absurdly high value so it doesn't interfere with the pan gestures above (i.e., scroll) + // reanimated doesn't offer great support for disabling y/x axes :/ + var effectivelyDisabledOffset = 200; + var panGesture = Gesture.Pan() + .activeOffsetX([ + actions.leftFirst ? -10 : -effectivelyDisabledOffset, + actions.rightFirst ? 10 : effectivelyDisabledOffset, + ]) + .activeOffsetY([-effectivelyDisabledOffset, effectivelyDisabledOffset]) + .onStart(function () { + 'worklet'; + isActive.set(true); + }) + .onChange(function (e) { + 'worklet'; + transX.set(e.translationX); + if (e.translationX < 0) { + // Left side + if (actions.leftSecond) { + if (e.translationX <= -actions.leftSecond.threshold && + !hitSecond.get()) { + runPopAnimation(); + runOnJS(haptic)(); + hitSecond.set(true); + } + else if (hitSecond.get() && + e.translationX > -actions.leftSecond.threshold) { + runPopAnimation(); + hitSecond.set(false); + } + } + if (!hitSecond.get() && actions.leftFirst) { + if (e.translationX <= -actions.leftFirst.threshold && + !hitFirst.get()) { + runPopAnimation(); + runOnJS(haptic)(); + hitFirst.set(true); + } + else if (hitFirst.get() && + e.translationX > -actions.leftFirst.threshold) { + hitFirst.set(false); + } + } + } + else if (e.translationX > 0) { + // Right side + if (actions.rightSecond) { + if (e.translationX >= actions.rightSecond.threshold && + !hitSecond.get()) { + runPopAnimation(); + runOnJS(haptic)(); + hitSecond.set(true); + } + else if (hitSecond.get() && + e.translationX < actions.rightSecond.threshold) { + runPopAnimation(); + hitSecond.set(false); + } + } + if (!hitSecond.get() && actions.rightFirst) { + if (e.translationX >= actions.rightFirst.threshold && + !hitFirst.get()) { + runPopAnimation(); + runOnJS(haptic)(); + hitFirst.set(true); + } + else if (hitFirst.get() && + e.translationX < actions.rightFirst.threshold) { + hitFirst.set(false); + } + } + } + }) + .onEnd(function (e) { + 'worklet'; + if (e.translationX < 0) { + if (hitSecond.get() && actions.leftSecond) { + runOnJS(actions.leftSecond.action)(); + } + else if (hitFirst.get() && actions.leftFirst) { + runOnJS(actions.leftFirst.action)(); + } + } + else if (e.translationX > 0) { + if (hitSecond.get() && actions.rightSecond) { + runOnJS(actions.rightSecond.action)(); + } + else if (hitSecond.get() && actions.rightFirst) { + runOnJS(actions.rightFirst.action)(); + } + } + transX.set(function () { return withTiming(0, { duration: 200 }); }); + hitFirst.set(false); + hitSecond.set(false); + isActive.set(false); + }); + var composedGesture = Gesture.Simultaneous(panGesture); + var animatedSliderStyle = useAnimatedStyle(function () { + return { + transform: [{ translateX: clampedTransX.get() }], + }; + }); + var leftSideInterpolation = React.useMemo(function () { + var _a, _b, _c, _d; + return createInterpolation({ + firstColor: (_a = actions.leftFirst) === null || _a === void 0 ? void 0 : _a.color, + secondColor: (_b = actions.leftSecond) === null || _b === void 0 ? void 0 : _b.color, + firstThreshold: (_c = actions.leftFirst) === null || _c === void 0 ? void 0 : _c.threshold, + secondThreshold: (_d = actions.leftSecond) === null || _d === void 0 ? void 0 : _d.threshold, + side: 'left', + }); + }, [actions.leftFirst, actions.leftSecond]); + var rightSideInterpolation = React.useMemo(function () { + var _a, _b, _c, _d; + return createInterpolation({ + firstColor: (_a = actions.rightFirst) === null || _a === void 0 ? void 0 : _a.color, + secondColor: (_b = actions.rightSecond) === null || _b === void 0 ? void 0 : _b.color, + firstThreshold: (_c = actions.rightFirst) === null || _c === void 0 ? void 0 : _c.threshold, + secondThreshold: (_d = actions.rightSecond) === null || _d === void 0 ? void 0 : _d.threshold, + side: 'right', + }); + }, [actions.rightFirst, actions.rightSecond]); + var interpolation = React.useMemo(function () { + if (!actions.leftFirst) { + return rightSideInterpolation; + } + else if (!actions.rightFirst) { + return leftSideInterpolation; + } + else { + return { + inputRange: __spreadArray(__spreadArray([], leftSideInterpolation.inputRange, true), rightSideInterpolation.inputRange, true), + outputRange: __spreadArray(__spreadArray([], leftSideInterpolation.outputRange, true), rightSideInterpolation.outputRange, true), + }; + } + }, [ + leftSideInterpolation, + rightSideInterpolation, + actions.leftFirst, + actions.rightFirst, + ]); + var animatedBackgroundStyle = useAnimatedStyle(function () { + return { + backgroundColor: interpolateColor(clampedTransX.get(), interpolation.inputRange, + // @ts-expect-error - Weird type expected by reanimated, but this is okay + interpolation.outputRange), + }; + }); + var animatedIconStyle = useAnimatedStyle(function () { + var absTransX = Math.abs(clampedTransX.get()); + return { + opacity: interpolate(absTransX, [0, 75], [0.15, 1]), + transform: [{ scale: iconScale.get() }], + }; + }); + return (_jsx(GestureDetector, { gesture: composedGesture, children: _jsxs(View, { children: [_jsx(Animated.View, { style: [StyleSheet.absoluteFill, animatedBackgroundStyle], children: _jsx(View, { style: { + flex: 1, + marginHorizontal: 12, + justifyContent: 'center', + alignItems: activeAction === 'leftFirst' || activeAction === 'leftSecond' + ? 'flex-end' + : 'flex-start', + }, children: _jsx(Animated.View, { style: [animatedIconStyle], children: activeAction === 'leftFirst' && ((_b = actions.leftFirst) === null || _b === void 0 ? void 0 : _b.icon) ? (_jsx(actions.leftFirst.icon, { height: ICON_SIZE, width: ICON_SIZE, style: { + color: 'white', + } })) : activeAction === 'leftSecond' && ((_c = actions.leftSecond) === null || _c === void 0 ? void 0 : _c.icon) ? (_jsx(actions.leftSecond.icon, { height: ICON_SIZE, width: ICON_SIZE, style: { color: 'white' } })) : activeAction === 'rightFirst' && ((_d = actions.rightFirst) === null || _d === void 0 ? void 0 : _d.icon) ? (_jsx(actions.rightFirst.icon, { height: ICON_SIZE, width: ICON_SIZE, style: { color: 'white' } })) : activeAction === 'rightSecond' && + ((_e = actions.rightSecond) === null || _e === void 0 ? void 0 : _e.icon) ? (_jsx(actions.rightSecond.icon, { height: ICON_SIZE, width: ICON_SIZE, style: { color: 'white' } })) : null }) }) }), _jsx(Animated.View, { style: animatedSliderStyle, children: children })] }) })); +} +function createInterpolation(_a) { + var firstColor = _a.firstColor, secondColor = _a.secondColor, firstThreshold = _a.firstThreshold, secondThreshold = _a.secondThreshold, side = _a.side; + if ((secondThreshold && !secondColor) || (!secondThreshold && secondColor)) { + throw new Error('You must provide a second color if you provide a second threshold'); + } + if (!firstThreshold) { + return { + inputRange: [0], + outputRange: ['transparent'], + }; + } + var offset = side === 'left' ? -20 : 20; + if (side === 'left') { + firstThreshold = -firstThreshold; + if (secondThreshold) { + secondThreshold = -secondThreshold; + } + } + var res; + if (secondThreshold) { + res = { + inputRange: [ + 0, + firstThreshold, + firstThreshold + offset - 20, + secondThreshold, + ], + outputRange: ['transparent', firstColor, firstColor, secondColor], + }; + } + else { + res = { + inputRange: [0, firstThreshold], + outputRange: ['transparent', firstColor], + }; + } + if (side === 'left') { + // Reverse the input/output ranges + res.inputRange.reverse(); + res.outputRange.reverse(); + } + return res; +} diff --git a/src/lib/custom-animations/GestureActionView.web.js b/src/lib/custom-animations/GestureActionView.web.js new file mode 100644 index 0000000000..45975d49cb --- /dev/null +++ b/src/lib/custom-animations/GestureActionView.web.js @@ -0,0 +1,4 @@ +export function GestureActionView(_a) { + var children = _a.children; + return children; +} diff --git a/src/lib/custom-animations/LikeIcon.js b/src/lib/custom-animations/LikeIcon.js new file mode 100644 index 0000000000..d46dfb65cb --- /dev/null +++ b/src/lib/custom-animations/LikeIcon.js @@ -0,0 +1,85 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import Animated, { Keyframe, LayoutAnimationConfig, useReducedMotion, } from 'react-native-reanimated'; +import { s } from '#/lib/styles'; +import { useTheme } from '#/alf'; +import { Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, } from '#/components/icons/Heart2'; +var keyframe = new Keyframe({ + 0: { + transform: [{ scale: 1 }], + }, + 10: { + transform: [{ scale: 0.7 }], + }, + 40: { + transform: [{ scale: 1.2 }], + }, + 100: { + transform: [{ scale: 1 }], + }, +}); +var circle1Keyframe = new Keyframe({ + 0: { + opacity: 0, + transform: [{ scale: 0 }], + }, + 10: { + opacity: 0.4, + }, + 40: { + transform: [{ scale: 1.5 }], + }, + 95: { + opacity: 0.4, + }, + 100: { + opacity: 0, + transform: [{ scale: 1.5 }], + }, +}); +var circle2Keyframe = new Keyframe({ + 0: { + opacity: 0, + transform: [{ scale: 0 }], + }, + 10: { + opacity: 1, + }, + 40: { + transform: [{ scale: 0 }], + }, + 95: { + opacity: 1, + }, + 100: { + opacity: 0, + transform: [{ scale: 1.5 }], + }, +}); +export function AnimatedLikeIcon(_a) { + var isLiked = _a.isLiked, big = _a.big, hasBeenToggled = _a.hasBeenToggled; + var t = useTheme(); + var size = big ? 22 : 18; + var shouldAnimate = !useReducedMotion() && hasBeenToggled; + return (_jsx(View, { children: _jsxs(LayoutAnimationConfig, { skipEntering: true, children: [isLiked ? (_jsx(Animated.View, { entering: shouldAnimate ? keyframe.duration(300) : undefined, children: _jsx(HeartIconFilled, { style: s.likeColor, width: size }) })) : (_jsx(HeartIconOutline, { style: [{ color: t.palette.contrast_500 }, { pointerEvents: 'none' }], width: size })), isLiked && shouldAnimate ? (_jsxs(_Fragment, { children: [_jsx(Animated.View, { entering: circle1Keyframe.duration(300), style: { + position: 'absolute', + backgroundColor: s.likeColor.color, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + } }), _jsx(Animated.View, { entering: circle2Keyframe.duration(300), style: { + position: 'absolute', + backgroundColor: t.atoms.bg.backgroundColor, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + } })] })) : null] }) })); +} diff --git a/src/lib/custom-animations/LikeIcon.web.js b/src/lib/custom-animations/LikeIcon.web.js new file mode 100644 index 0000000000..4b1540d614 --- /dev/null +++ b/src/lib/custom-animations/LikeIcon.web.js @@ -0,0 +1,87 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { useReducedMotion } from 'react-native-reanimated'; +import { s } from '#/lib/styles'; +import { useTheme } from '#/alf'; +import { Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, } from '#/components/icons/Heart2'; +var animationConfig = { + duration: 600, + easing: 'cubic-bezier(0.4, 0, 0.2, 1)', + fill: 'forwards', +}; +var keyframe = [ + { transform: 'scale(1)' }, + { transform: 'scale(0.7)' }, + { transform: 'scale(1.2)' }, + { transform: 'scale(1)' }, +]; +var circle1Keyframe = [ + { opacity: 0, transform: 'scale(0)' }, + { opacity: 0.4 }, + { transform: 'scale(1.5)' }, + { opacity: 0.4 }, + { opacity: 0, transform: 'scale(1.5)' }, +]; +var circle2Keyframe = [ + { opacity: 0, transform: 'scale(0)' }, + { opacity: 1 }, + { transform: 'scale(0)' }, + { opacity: 1 }, + { opacity: 0, transform: 'scale(1.5)' }, +]; +export function AnimatedLikeIcon(_a) { + var isLiked = _a.isLiked, big = _a.big, hasBeenToggled = _a.hasBeenToggled; + var t = useTheme(); + var size = big ? 22 : 18; + var shouldAnimate = !useReducedMotion() && hasBeenToggled; + var prevIsLiked = React.useRef(isLiked); + var likeIconRef = React.useRef(null); + var circle1Ref = React.useRef(null); + var circle2Ref = React.useRef(null); + React.useEffect(function () { + var _a, _b, _c, _d, _e, _f; + if (prevIsLiked.current === isLiked) { + return; + } + if (shouldAnimate && isLiked) { + (_b = (_a = likeIconRef.current) === null || _a === void 0 ? void 0 : _a.animate) === null || _b === void 0 ? void 0 : _b.call(_a, keyframe, animationConfig); + (_d = (_c = circle1Ref.current) === null || _c === void 0 ? void 0 : _c.animate) === null || _d === void 0 ? void 0 : _d.call(_c, circle1Keyframe, animationConfig); + (_f = (_e = circle2Ref.current) === null || _e === void 0 ? void 0 : _e.animate) === null || _f === void 0 ? void 0 : _f.call(_e, circle2Keyframe, animationConfig); + } + prevIsLiked.current = isLiked; + }, [shouldAnimate, isLiked]); + return (_jsxs(View, { children: [isLiked ? ( + // @ts-expect-error is div + _jsx(View, { ref: likeIconRef, children: _jsx(HeartIconFilled, { style: s.likeColor, width: size }) })) : (_jsx(HeartIconOutline, { style: [{ color: t.palette.contrast_500 }, { pointerEvents: 'none' }], width: size })), _jsx(View + // @ts-expect-error is div + , { + // @ts-expect-error is div + ref: circle1Ref, style: { + position: 'absolute', + backgroundColor: s.likeColor.color, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + opacity: 0, + } }), _jsx(View + // @ts-expect-error is div + , { + // @ts-expect-error is div + ref: circle2Ref, style: { + position: 'absolute', + backgroundColor: t.atoms.bg.backgroundColor, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + opacity: 0, + } })] })); +} diff --git a/src/lib/custom-animations/PressableScale.js b/src/lib/custom-animations/PressableScale.js new file mode 100644 index 0000000000..030da3dfb9 --- /dev/null +++ b/src/lib/custom-animations/PressableScale.js @@ -0,0 +1,49 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { Pressable, } from 'react-native'; +import Animated, { cancelAnimation, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { IS_NATIVE, IS_WEB_TOUCH_DEVICE } from '#/env'; +var DEFAULT_TARGET_SCALE = IS_NATIVE || IS_WEB_TOUCH_DEVICE ? 0.98 : 1; +var AnimatedPressable = Animated.createAnimatedComponent(Pressable); +export function PressableScale(_a) { + var _b = _a.targetScale, targetScale = _b === void 0 ? DEFAULT_TARGET_SCALE : _b, children = _a.children, style = _a.style, onPressIn = _a.onPressIn, onPressOut = _a.onPressOut, rest = __rest(_a, ["targetScale", "children", "style", "onPressIn", "onPressOut"]); + var reducedMotion = useReducedMotion(); + var scale = useSharedValue(1); + var animatedStyle = useAnimatedStyle(function () { return ({ + transform: [{ scale: scale.get() }], + }); }); + return (_jsx(AnimatedPressable, __assign({ accessibilityRole: "button", onPressIn: function (e) { + if (onPressIn) { + onPressIn(e); + } + cancelAnimation(scale); + scale.set(function () { return withTiming(targetScale, { duration: 100 }); }); + }, onPressOut: function (e) { + if (onPressOut) { + onPressOut(e); + } + cancelAnimation(scale); + scale.set(function () { return withTiming(1, { duration: 100 }); }); + }, style: [!reducedMotion && animatedStyle, style] }, rest, { children: children }))); +} diff --git a/src/lib/custom-animations/ScaleAndFade.js b/src/lib/custom-animations/ScaleAndFade.js new file mode 100644 index 0000000000..9eaf3817f6 --- /dev/null +++ b/src/lib/custom-animations/ScaleAndFade.js @@ -0,0 +1,31 @@ +import { withTiming } from 'react-native-reanimated'; +export function ScaleAndFadeIn() { + 'worklet'; + var animations = { + opacity: withTiming(1), + transform: [{ scale: withTiming(1) }], + }; + var initialValues = { + opacity: 0, + transform: [{ scale: 0.7 }], + }; + return { + animations: animations, + initialValues: initialValues, + }; +} +export function ScaleAndFadeOut() { + 'worklet'; + var animations = { + opacity: withTiming(0), + transform: [{ scale: withTiming(0.7) }], + }; + var initialValues = { + opacity: 1, + transform: [{ scale: 1 }], + }; + return { + animations: animations, + initialValues: initialValues, + }; +} diff --git a/src/lib/custom-animations/ShrinkAndPop.js b/src/lib/custom-animations/ShrinkAndPop.js new file mode 100644 index 0000000000..4b08398f60 --- /dev/null +++ b/src/lib/custom-animations/ShrinkAndPop.js @@ -0,0 +1,20 @@ +import { withDelay, withSequence, withTiming } from 'react-native-reanimated'; +export function ShrinkAndPop() { + 'worklet'; + var animations = { + opacity: withDelay(125, withTiming(0, { duration: 125 })), + transform: [ + { + scale: withSequence(withTiming(0.7, { duration: 75 }), withTiming(1.1, { duration: 150 })), + }, + ], + }; + var initialValues = { + opacity: 1, + transform: [{ scale: 1 }], + }; + return { + animations: animations, + initialValues: initialValues, + }; +} diff --git a/src/lib/custom-animations/util.js b/src/lib/custom-animations/util.js new file mode 100644 index 0000000000..5b008de38a --- /dev/null +++ b/src/lib/custom-animations/util.js @@ -0,0 +1,24 @@ +// It should roll when: +// - We're going from 1 to 0 (roll backwards) +// - The count is anywhere between 1 and 999 +// - The count is going up and is a multiple of 100 +// - The count is going down and is 1 less than a multiple of 100 +export function decideShouldRoll(isSet, count) { + var shouldRoll = false; + if (!isSet && count === 1) { + shouldRoll = true; + } + else if (count > 1 && count < 1000) { + shouldRoll = true; + } + else if (count > 0) { + var mod = count % 100; + if (isSet && mod === 0) { + shouldRoll = true; + } + else if (!isSet && mod === 99) { + shouldRoll = true; + } + } + return shouldRoll; +} diff --git a/src/lib/demo.js b/src/lib/demo.js new file mode 100644 index 0000000000..b4e5e25a1f --- /dev/null +++ b/src/lib/demo.js @@ -0,0 +1,198 @@ +import { subDays, subMinutes } from 'date-fns'; +var DID = "did:plc:z72i7hdynmk6r22z27h6tvur"; +var NOW = new Date(); +var POST_1_DATE = subMinutes(NOW, 2).toISOString(); +var POST_2_DATE = subMinutes(NOW, 4).toISOString(); +var POST_3_DATE = subMinutes(NOW, 5).toISOString(); +export var DEMO_FEED = { + feed: [ + { + post: { + uri: 'at://did:plc:pvooorihapc2lf2pijehgrdf/app.bsky.feed.post/3lniysofyll2d', + cid: 'bafyreihwh3wxxme732ylbylhhdyz7ex6t4jtu6s3gjxxvnnh4feddhg3ku', + author: { + did: 'did:plc:pvooorihapc2lf2pijehgrdf', + handle: 'forkedriverband.bsky.social', + displayName: 'Forked River Band', + avatar: 'https://bsky.social/about/adi/post_1_avi.jpg', + viewer: { + muted: false, + blockedBy: false, + following: "at://".concat(DID, "/app.bsky.graph.follow/post1"), + }, + labels: [], + createdAt: POST_1_DATE, + verification: { + verifications: [ + { + issuer: DID, + uri: "at://".concat(DID, "/app.bsky.graph.verification/post1"), + isValid: true, + createdAt: subDays(NOW, 11).toISOString(), + }, + ], + verifiedStatus: 'valid', + trustedVerifierStatus: 'none', + }, + }, + record: { + $type: 'app.bsky.feed.post', + createdAt: POST_1_DATE, + // embed: { + // $type: 'app.bsky.embed.images', + // images: [ + // { + // alt: 'Fake flier for Sebastapol Bluegrass Fest', + // aspectRatio: { + // height: 1350, + // width: 900, + // }, + // image: { + // $type: 'blob', + // ref: { + // $link: + // 'bafkreig7gnirmz5guhhjutf3mqbjjzxzi3w4wvs5qy2gnxma5g3brbaidi', + // }, + // mimeType: 'image/jpeg', + // size: 562871, + // }, + // }, + // ], + // }, + langs: ['en'], + text: 'Sonoma County folks: Come tip your hats our way and see us play new and old bluegrass tunes at Sebastopol Solstice Fest on June 20th.', + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://bsky.social/about/adi/post_1_image.jpg', + fullsize: 'https://bsky.social/about/adi/post_1_image.jpg', + alt: 'Fake flier for Sebastapol Bluegrass Fest', + aspectRatio: { + height: 1350, + width: 900, + }, + }, + ], + }, + replyCount: 1, + repostCount: 4, + likeCount: 18, + quoteCount: 0, + indexedAt: POST_1_DATE, + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + }, + { + post: { + uri: 'at://did:plc:fhhqii56ppgyh5qcm2b3mokf/app.bsky.feed.post/3lnizc7fug52c', + cid: 'bafyreienuabsr55rycirdf4ewue5tjcseg5lzqompcsh2brqzag6hvxllm', + author: { + did: 'did:plc:fhhqii56ppgyh5qcm2b3mokf', + handle: 'dinh-designs.bsky.social', + displayName: 'Rich Dinh Designs', + avatar: 'https://bsky.social/about/adi/post_2_avi.jpg', + viewer: { + muted: false, + blockedBy: false, + following: "at://".concat(DID, "/app.bsky.graph.follow/post2"), + }, + labels: [], + createdAt: POST_2_DATE, + }, + record: { + $type: 'app.bsky.feed.post', + createdAt: POST_2_DATE, + // embed: { + // $type: 'app.bsky.embed.images', + // images: [ + // { + // alt: 'Placeholder image of interior design', + // aspectRatio: { + // height: 872, + // width: 598, + // }, + // image: { + // $type: 'blob', + // ref: { + // $link: + // 'bafkreidcjc6bjb4jjjejruin5cldhj5zovsuu4tydulenyprneziq5rfeu', + // }, + // mimeType: 'image/jpeg', + // size: 296003, + // }, + // }, + // ], + // }, + langs: ['en'], + text: 'Details from our install at the Lucas residence in Joshua Tree. We populated the space with rich, earthy tones and locally-sourced materials to suit the landscape.', + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://bsky.social/about/adi/post_2_image.jpg', + fullsize: 'https://bsky.social/about/adi/post_2_image.jpg', + alt: 'Placeholder image of interior design', + aspectRatio: { + height: 872, + width: 598, + }, + }, + ], + }, + replyCount: 3, + repostCount: 1, + likeCount: 4, + quoteCount: 0, + indexedAt: POST_2_DATE, + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + }, + { + post: { + uri: 'at://did:plc:h7fwnfejmmifveeea5eyxgkc/app.bsky.feed.post/3lnizna3g4f2t', + cid: 'bafyreiepn7obmlshliori4j34texpaukrqkyyu7cq6nmpzk4lkis7nqeae', + author: { + did: 'did:plc:h7fwnfejmmifveeea5eyxgkc', + handle: 'rodyalbuerne.bsky.social', + displayName: 'Rody Albuerne', + avatar: 'https://bsky.social/about/adi/post_3_avi.jpg', + viewer: { + muted: false, + blockedBy: false, + following: "at://".concat(DID, "/app.bsky.graph.follow/post3"), + }, + labels: [], + createdAt: POST_3_DATE, + }, + record: { + $type: 'app.bsky.feed.post', + createdAt: POST_3_DATE, + langs: ['en'], + text: 'Tinkering with the basics of traditional wooden joinery in my shop lately. Starting small with this ox, made using simple mortise and tenon joints.', + }, + replyCount: 11, + repostCount: 97, + likeCount: 399, + quoteCount: 0, + indexedAt: POST_3_DATE, + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + }, + ], +}; +export var BOTTOM_BAR_AVI = 'https://bsky.social/about/adi/user_avi.jpg'; diff --git a/src/lib/deviceName.js b/src/lib/deviceName.js new file mode 100644 index 0000000000..80ef9829f3 --- /dev/null +++ b/src/lib/deviceName.js @@ -0,0 +1,17 @@ +import * as Device from 'expo-device'; +import * as env from '#/env'; +export var FALLBACK_ANDROID = 'Android'; +export var FALLBACK_IOS = 'iOS'; +export var FALLBACK_WEB = 'Web'; +export function getDeviceName() { + var deviceName = Device.deviceName; + if (env.IS_ANDROID) { + return deviceName || FALLBACK_ANDROID; + } + else if (env.IS_IOS) { + return deviceName || FALLBACK_IOS; + } + else { + return FALLBACK_WEB; // could append browser info here + } +} diff --git a/src/lib/functions.js b/src/lib/functions.js new file mode 100644 index 0000000000..494886984d --- /dev/null +++ b/src/lib/functions.js @@ -0,0 +1,87 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +export function choose(value, choices) { + return choices[value]; +} +export function dedupArray(arr) { + var s = new Set(arr); + return __spreadArray([], s, true); +} +/** + * Taken from @tanstack/query-core utils.ts + * Modified to support Date object comparisons + * + * This function returns `a` if `b` is deeply equal. + * If not, it will replace any deeply equal children of `b` with those of `a`. + * This can be used for structural sharing between JSON values for example. + */ +export function replaceEqualDeep(a, b) { + if (a === b) { + return a; + } + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime() ? a : b; + } + var array = isPlainArray(a) && isPlainArray(b); + if (array || (isPlainObject(a) && isPlainObject(b))) { + var aItems = array ? a : Object.keys(a); + var aSize = aItems.length; + var bItems = array ? b : Object.keys(b); + var bSize = bItems.length; + var copy = array ? [] : {}; + var equalItems = 0; + for (var i = 0; i < bSize; i++) { + var key = array ? i : bItems[i]; + if (!array && + a[key] === undefined && + b[key] === undefined && + aItems.includes(key)) { + copy[key] = undefined; + equalItems++; + } + else { + copy[key] = replaceEqualDeep(a[key], b[key]); + if (copy[key] === a[key] && a[key] !== undefined) { + equalItems++; + } + } + } + return aSize === bSize && equalItems === aSize ? a : copy; + } + return b; +} +export function isPlainArray(value) { + return Array.isArray(value) && value.length === Object.keys(value).length; +} +// Copied from: https://github.com/jonschlinkert/is-plain-object +export function isPlainObject(o) { + if (!hasObjectPrototype(o)) { + return false; + } + // If has no constructor + var ctor = o.constructor; + if (ctor === undefined) { + return true; + } + // If has modified prototype + var prot = ctor.prototype; + if (!hasObjectPrototype(prot)) { + return false; + } + // If constructor does not have an Object-specific method + if (!prot.hasOwnProperty('isPrototypeOf')) { + return false; + } + // Most likely a plain Object + return true; +} +function hasObjectPrototype(o) { + return Object.prototype.toString.call(o) === '[object Object]'; +} diff --git a/src/lib/generate-starterpack.js b/src/lib/generate-starterpack.js new file mode 100644 index 0000000000..0688ada2fd --- /dev/null +++ b/src/lib/generate-starterpack.js @@ -0,0 +1,193 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation } from '@tanstack/react-query'; +import { until } from '#/lib/async/until'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { enforceLen } from '#/lib/strings/helpers'; +import { useAgent } from '#/state/session'; +export var createStarterPackList = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var list; + var name = _b.name, description = _b.description, descriptionFacets = _b.descriptionFacets, profiles = _b.profiles, agent = _b.agent; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (profiles.length === 0) + throw new Error('No profiles given'); + return [4 /*yield*/, agent.app.bsky.graph.list.create({ repo: agent.session.did }, { + name: name, + description: description, + descriptionFacets: descriptionFacets, + avatar: undefined, + createdAt: new Date().toISOString(), + purpose: 'app.bsky.graph.defs#referencelist', + })]; + case 1: + list = _c.sent(); + if (!list) + throw new Error('List creation failed'); + return [4 /*yield*/, agent.com.atproto.repo.applyWrites({ + repo: agent.session.did, + writes: profiles.map(function (p) { return createListItem({ did: p.did, listUri: list.uri }); }), + })]; + case 2: + _c.sent(); + return [2 /*return*/, list]; + } + }); +}); }; +export function useGenerateStarterPackMutation(_a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var _ = useLingui()._; + var agent = useAgent(); + return useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var profile, profiles, displayName, starterPackName, list; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Promise.all([ + (function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.actor.getProfile({ + actor: agent.session.did, + })]; + case 1: + profile = (_a.sent()).data; + return [2 /*return*/]; + } + }); + }); })(), + (function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.actor.searchActors({ + q: encodeURIComponent('*'), + limit: 49, + })]; + case 1: + profiles = (_a.sent()).data.actors.filter(function (p) { var _a; return (_a = p.viewer) === null || _a === void 0 ? void 0 : _a.following; }); + return [2 /*return*/]; + } + }); + }); })(), + ])]; + case 1: + _a.sent(); + if (!profile || !profiles) { + throw new Error('ERROR_DATA'); + } + // We include ourselves when we make the list + if (profiles.length < 7) { + throw new Error('NOT_ENOUGH_FOLLOWERS'); + } + displayName = enforceLen(profile.displayName + ? sanitizeDisplayName(profile.displayName) + : "@".concat(sanitizeHandle(profile.handle)), 25, true); + starterPackName = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["", "'s Starter Pack"], ["", "'s Starter Pack"])), displayName)); + return [4 /*yield*/, createStarterPackList({ + name: starterPackName, + profiles: profiles, + agent: agent, + })]; + case 2: + list = _a.sent(); + return [4 /*yield*/, agent.app.bsky.graph.starterpack.create({ + repo: agent.session.did, + }, { + name: starterPackName, + list: list.uri, + createdAt: new Date().toISOString(), + })]; + case 3: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + onSuccess: function (data) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, whenAppViewReady(agent, data.uri, function (v) { + return typeof (v === null || v === void 0 ? void 0 : v.data.starterPack.uri) === 'string'; + })]; + case 1: + _a.sent(); + onSuccess(data); + return [2 /*return*/]; + } + }); + }); }, + onError: function (error) { + onError(error); + }, + }); +} +function createListItem(_a) { + var did = _a.did, listUri = _a.listUri; + return { + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + value: { + $type: 'app.bsky.graph.listitem', + subject: did, + list: listUri, + createdAt: new Date().toISOString(), + }, + }; +} +function whenAppViewReady(agent, uri, fn) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, until(5, // 5 tries + 1e3, // 1s delay between tries + fn, function () { return agent.app.bsky.graph.getStarterPack({ starterPack: uri }); })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +var templateObject_1; diff --git a/src/lib/getUserDisplayName.js b/src/lib/getUserDisplayName.js new file mode 100644 index 0000000000..54e90689c1 --- /dev/null +++ b/src/lib/getUserDisplayName.js @@ -0,0 +1,5 @@ +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +export function getUserDisplayName(props) { + return sanitizeDisplayName(props.displayName || sanitizeHandle(props.handle, '@')); +} diff --git a/src/lib/gif-alt-text.js b/src/lib/gif-alt-text.js new file mode 100644 index 0000000000..6446010a2d --- /dev/null +++ b/src/lib/gif-alt-text.js @@ -0,0 +1,31 @@ +// Kind of a hack. We needed some way to distinguish these. +var USER_ALT_PREFIX = 'Alt: '; +var DEFAULT_ALT_PREFIX = 'ALT: '; +export function createGIFDescription(tenorDescription, preferredAlt) { + if (preferredAlt === void 0) { preferredAlt = ''; } + preferredAlt = preferredAlt.trim(); + if (preferredAlt !== '') { + return USER_ALT_PREFIX + preferredAlt; + } + else { + return DEFAULT_ALT_PREFIX + tenorDescription; + } +} +export function parseAltFromGIFDescription(description) { + if (description.startsWith(USER_ALT_PREFIX)) { + return { + isPreferred: true, + alt: description.replace(USER_ALT_PREFIX, ''), + }; + } + else if (description.startsWith(DEFAULT_ALT_PREFIX)) { + return { + isPreferred: false, + alt: description.replace(DEFAULT_ALT_PREFIX, ''), + }; + } + return { + isPreferred: false, + alt: description, + }; +} diff --git a/src/lib/haptics.js b/src/lib/haptics.js new file mode 100644 index 0000000000..9d60ef0788 --- /dev/null +++ b/src/lib/haptics.js @@ -0,0 +1,24 @@ +import React from 'react'; +import * as Device from 'expo-device'; +import { impactAsync, ImpactFeedbackStyle } from 'expo-haptics'; +import { useHapticsDisabled } from '#/state/preferences/disable-haptics'; +import { IS_IOS, IS_WEB } from '#/env'; +export function useHaptics() { + var isHapticsDisabled = useHapticsDisabled(); + return React.useCallback(function (strength) { + if (strength === void 0) { strength = 'Medium'; } + if (isHapticsDisabled || IS_WEB) { + return; + } + // Users said the medium impact was too strong on Android; see APP-537s + var style = IS_IOS + ? ImpactFeedbackStyle[strength] + : ImpactFeedbackStyle.Light; + impactAsync(style); + // DEV ONLY - show a toast when a haptic is meant to fire on simulator + if (__DEV__ && !Device.isDevice) { + // disabled because it's annoying + // Toast.show(`Buzzz!`) + } + }, [isHapticsDisabled]); +} diff --git a/src/lib/hooks/__tests__/useTimeAgo.test.js b/src/lib/hooks/__tests__/useTimeAgo.test.js new file mode 100644 index 0000000000..8e17e46b39 --- /dev/null +++ b/src/lib/hooks/__tests__/useTimeAgo.test.js @@ -0,0 +1,203 @@ +import { describe, expect, it } from '@jest/globals'; +import { addDays, subDays, subHours, subMinutes, subSeconds } from 'date-fns'; +import { dateDiff } from '../useTimeAgo'; +var base = new Date('2024-06-17T00:00:00Z'); +describe('dateDiff', function () { + it("works with numbers", function () { + var earlier = subDays(base, 3); + expect(dateDiff(earlier, Number(base))).toEqual({ + value: 3, + unit: 'day', + earlier: earlier, + later: base, + }); + }); + it("works with strings", function () { + var earlier = subDays(base, 3); + expect(dateDiff(earlier, base.toString())).toEqual({ + value: 3, + unit: 'day', + earlier: earlier, + later: base, + }); + }); + it("works with dates", function () { + var earlier = subDays(base, 3); + expect(dateDiff(earlier, base)).toEqual({ + value: 3, + unit: 'day', + earlier: earlier, + later: base, + }); + }); + it("equal values return now", function () { + expect(dateDiff(base, base)).toEqual({ + value: 0, + unit: 'now', + earlier: base, + later: base, + }); + }); + it("future dates return now", function () { + var earlier = addDays(base, 3); + expect(dateDiff(earlier, base)).toEqual({ + value: 0, + unit: 'now', + earlier: earlier, + later: base, + }); + }); + it("values < 5 seconds ago return now", function () { + var then = subSeconds(base, 4); + expect(dateDiff(then, base)).toEqual({ + value: 0, + unit: 'now', + earlier: then, + later: base, + }); + }); + it("values >= 5 seconds ago return seconds", function () { + var then = subSeconds(base, 5); + expect(dateDiff(then, base)).toEqual({ + value: 5, + unit: 'second', + earlier: then, + later: base, + }); + }); + it("values < 1 min return seconds", function () { + var then = subSeconds(base, 59); + expect(dateDiff(then, base)).toEqual({ + value: 59, + unit: 'second', + earlier: then, + later: base, + }); + }); + it("values >= 1 min return minutes", function () { + var then = subSeconds(base, 60); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'minute', + earlier: then, + later: base, + }); + }); + it("minutes round down", function () { + var then = subSeconds(base, 119); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'minute', + earlier: then, + later: base, + }); + }); + it("values < 1 hour return minutes", function () { + var then = subMinutes(base, 59); + expect(dateDiff(then, base)).toEqual({ + value: 59, + unit: 'minute', + earlier: then, + later: base, + }); + }); + it("values >= 1 hour return hours", function () { + var then = subMinutes(base, 60); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'hour', + earlier: then, + later: base, + }); + }); + it("hours round down", function () { + var then = subMinutes(base, 119); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'hour', + earlier: then, + later: base, + }); + }); + it("values < 1 day return hours", function () { + var then = subHours(base, 23); + expect(dateDiff(then, base)).toEqual({ + value: 23, + unit: 'hour', + earlier: then, + later: base, + }); + }); + it("values >= 1 day return days", function () { + var then = subHours(base, 24); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'day', + earlier: then, + later: base, + }); + }); + it("days round down", function () { + var then = subHours(base, 47); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'day', + earlier: then, + later: base, + }); + }); + it("values < 30 days return days", function () { + var then = subDays(base, 29); + expect(dateDiff(then, base)).toEqual({ + value: 29, + unit: 'day', + earlier: then, + later: base, + }); + }); + it("values >= 30 days return months", function () { + var then = subDays(base, 30); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'month', + earlier: then, + later: base, + }); + }); + it("months round down", function () { + var then = subDays(base, 59); + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'month', + earlier: then, + later: base, + }); + }); + it("values are rounded by increments of 30", function () { + var then = subDays(base, 61); + expect(dateDiff(then, base)).toEqual({ + value: 2, + unit: 'month', + earlier: then, + later: base, + }); + }); + it("values < 360 days return months", function () { + var then = subDays(base, 359); + expect(dateDiff(then, base)).toEqual({ + value: 11, + unit: 'month', + earlier: then, + later: base, + }); + }); + it("values >= 360 days return the earlier value", function () { + var then = subDays(base, 360); + expect(dateDiff(then, base)).toEqual({ + value: 12, + unit: 'month', + earlier: then, + later: base, + }); + }); +}); diff --git a/src/lib/hooks/useAccountSwitcher.js b/src/lib/hooks/useAccountSwitcher.js new file mode 100644 index 0000000000..2ffdd62504 --- /dev/null +++ b/src/lib/hooks/useAccountSwitcher.js @@ -0,0 +1,107 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback, useState } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { logger } from '#/logger'; +import { useSessionApi } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import * as Toast from '#/view/com/util/Toast'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +export function useAccountSwitcher() { + var _this = this; + var ax = useAnalytics(); + var _a = useState(null), pendingDid = _a[0], setPendingDid = _a[1]; + var _ = useLingui()._; + var resumeSession = useSessionApi().resumeSession; + var requestSwitchToAccount = useLoggedOutViewControls().requestSwitchToAccount; + var onPressSwitchAccount = useCallback(function (account, logContext) { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (pendingDid) { + // The session API isn't resilient to race conditions so let's just ignore this. + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 5, 6, 7]); + setPendingDid(account.did); + if (!account.accessJwt) return [3 /*break*/, 3]; + if (IS_WEB) { + // We're switching accounts, which remounts the entire app. + // On mobile, this gets us Home, but on the web we also need reset the URL. + // We can't change the URL via a navigate() call because the navigator + // itself is about to unmount, and it calls pushState() too late. + // So we change the URL ourselves. The navigator will pick it up on remount. + history.pushState(null, '', '/'); + } + return [4 /*yield*/, resumeSession(account, true)]; + case 2: + _a.sent(); + ax.metric('account:loggedIn', { logContext: logContext, withPassword: false }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Signed in as @", ""], ["Signed in as @", ""])), account.handle))); + return [3 /*break*/, 4]; + case 3: + requestSwitchToAccount({ requestedAccount: account.did }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please sign in as @", ""], ["Please sign in as @", ""])), account.handle)), 'circle-exclamation'); + _a.label = 4; + case 4: return [3 /*break*/, 7]; + case 5: + e_1 = _a.sent(); + logger.error("switch account: selectAccount failed", { + message: e_1.message, + }); + requestSwitchToAccount({ requestedAccount: account.did }); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Please sign in as @", ""], ["Please sign in as @", ""])), account.handle)), 'circle-exclamation'); + return [3 /*break*/, 7]; + case 6: + setPendingDid(null); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); }, [_, ax, resumeSession, requestSwitchToAccount, pendingDid]); + return { onPressSwitchAccount: onPressSwitchAccount, pendingDid: pendingDid }; +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/lib/hooks/useAnimatedValue.js b/src/lib/hooks/useAnimatedValue.js new file mode 100644 index 0000000000..61e7322851 --- /dev/null +++ b/src/lib/hooks/useAnimatedValue.js @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Animated } from 'react-native'; +export function useAnimatedValue(initialValue) { + var lazyRef = React.useRef(undefined); + if (lazyRef.current === undefined) { + lazyRef.current = new Animated.Value(initialValue); + } + return lazyRef.current; +} diff --git a/src/lib/hooks/useBottomBarOffset.js b/src/lib/hooks/useBottomBarOffset.js new file mode 100644 index 0000000000..512e8a4035 --- /dev/null +++ b/src/lib/hooks/useBottomBarOffset.js @@ -0,0 +1,11 @@ +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useWebMediaQueries } from '#/lib/hooks/useWebMediaQueries'; +import { clamp } from '#/lib/numbers'; +import { IS_WEB } from '#/env'; +export function useBottomBarOffset(modifier) { + if (modifier === void 0) { modifier = 0; } + var isTabletOrDesktop = useWebMediaQueries().isTabletOrDesktop; + var bottomInset = useSafeAreaInsets().bottom; + return ((IS_WEB && isTabletOrDesktop ? 0 : clamp(60 + bottomInset, 60, 75)) + + modifier); +} diff --git a/src/lib/hooks/useCleanError.js b/src/lib/hooks/useCleanError.js new file mode 100644 index 0000000000..38069cd426 --- /dev/null +++ b/src/lib/hooks/useCleanError.js @@ -0,0 +1,79 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { useCallback } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +export function useCleanError() { + var _ = useLingui()._; + return useCallback(function (error) { + if (!error) + return { + raw: undefined, + clean: undefined, + }; + var raw = error.toString(); + if (isNetworkError(raw)) { + return { + raw: raw, + clean: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Unable to connect. Please check your internet connection and try again."], ["Unable to connect. Please check your internet connection and try again."])))), + }; + } + if (raw.includes('Upstream Failure') || + raw.includes('NotEnoughResources') || + raw.includes('pipethrough network error')) { + return { + raw: raw, + clean: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["The server appears to be experiencing issues. Please try again in a few moments."], ["The server appears to be experiencing issues. Please try again in a few moments."])))), + }; + } + /** + * @see https://github.com/bluesky-social/atproto/blob/255cfcebb54332a7129af768a93004e22c6858e3/packages/pds/src/actor-store/preference/transactor.ts#L24 + */ + if (raw.includes('Do not have authorization to set preferences') && + raw.includes('app.bsky.actor.defs#personalDetailsPref')) { + return { + raw: raw, + clean: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You cannot update your birthdate while using an app password. Please sign in with your main password to update your birthdate."], ["You cannot update your birthdate while using an app password. Please sign in with your main password to update your birthdate."])))), + }; + } + if (raw.includes('Bad token scope') || raw.includes('Bad token method')) { + return { + raw: raw, + clean: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["This feature is not available while using an app password. Please sign in with your main password."], ["This feature is not available while using an app password. Please sign in with your main password."])))), + }; + } + if (raw.includes('Rate Limit Exceeded')) { + return { + raw: raw, + clean: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["You've reached the maximum number of requests allowed. Please try again later."], ["You've reached the maximum number of requests allowed. Please try again later."])))), + }; + } + if (raw.startsWith('Error: ')) { + raw = raw.slice('Error: '.length); + } + return { + raw: raw, + clean: undefined, + }; + }, [_]); +} +var NETWORK_ERRORS = [ + 'Abort', + 'Network request failed', + 'Failed to fetch', + 'Load failed', + 'Upstream service unreachable', +]; +export function isNetworkError(e) { + var str = String(e); + for (var _i = 0, NETWORK_ERRORS_1 = NETWORK_ERRORS; _i < NETWORK_ERRORS_1.length; _i++) { + var err = NETWORK_ERRORS_1[_i]; + if (str.includes(err)) { + return true; + } + } + return false; +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/lib/hooks/useColorSchemeStyle.js b/src/lib/hooks/useColorSchemeStyle.js new file mode 100644 index 0000000000..ad2a42eea0 --- /dev/null +++ b/src/lib/hooks/useColorSchemeStyle.js @@ -0,0 +1,5 @@ +import { useTheme } from '#/lib/ThemeContext'; +export function useColorSchemeStyle(lightStyle, darkStyle) { + var colorScheme = useTheme().colorScheme; + return colorScheme === 'dark' ? darkStyle : lightStyle; +} diff --git a/src/lib/hooks/useCreateSupportLink.js b/src/lib/hooks/useCreateSupportLink.js new file mode 100644 index 0000000000..121c903071 --- /dev/null +++ b/src/lib/hooks/useCreateSupportLink.js @@ -0,0 +1,37 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { useCallback } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useSession } from '#/state/session'; +export var ZENDESK_SUPPORT_URL = 'https://blueskyweb.zendesk.com/hc/requests/new'; +export var SupportCode; +(function (SupportCode) { + SupportCode["AA_DID"] = "AA_DID"; + SupportCode["AA_BIRTHDATE"] = "AA_BIRTHDATE"; +})(SupportCode || (SupportCode = {})); +/** + * {@link https://support.zendesk.com/hc/en-us/articles/4408839114522-Creating-pre-filled-ticket-forms} + */ +export function useCreateSupportLink() { + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + return useCallback(function (_a) { + var code = _a.code, email = _a.email; + var url = new URL(ZENDESK_SUPPORT_URL); + if (currentAccount) { + url.search = new URLSearchParams({ + tf_anonymous_requester_email: email || currentAccount.email || '', // email will be defined + tf_description: "[Code: ".concat(code, "] \u2014 ") + _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Please write your message below:"], ["Please write your message below:"])))), + /** + * Custom field specific to {@link ZENDESK_SUPPORT_URL} form + */ + tf_17205412673421: currentAccount.handle + " (".concat(currentAccount.did, ")"), + }).toString(); + } + return url.toString(); + }, [_, currentAccount]); +} +var templateObject_1; diff --git a/src/lib/hooks/useDebouncedValue.js b/src/lib/hooks/useDebouncedValue.js new file mode 100644 index 0000000000..a86888621d --- /dev/null +++ b/src/lib/hooks/useDebouncedValue.js @@ -0,0 +1,13 @@ +import { useEffect, useState } from 'react'; +/** + * Returns a debounced version of the input value that only updates after the + * specified delay has passed without any changes to the input value. + */ +export function useDebouncedValue(val, delayMs) { + var _a = useState(val), prev = _a[0], setPrev = _a[1]; + useEffect(function () { + var timeout = setTimeout(function () { return setPrev(val); }, delayMs); + return function () { return clearTimeout(timeout); }; + }, [val, delayMs]); + return prev; +} diff --git a/src/lib/hooks/useDedupe.js b/src/lib/hooks/useDedupe.js new file mode 100644 index 0000000000..93d797cfaf --- /dev/null +++ b/src/lib/hooks/useDedupe.js @@ -0,0 +1,16 @@ +import { useCallback, useRef } from 'react'; +export function useDedupe(timeout) { + if (timeout === void 0) { timeout = 250; } + var canDo = useRef(true); + return useCallback(function (cb) { + if (canDo.current) { + canDo.current = false; + setTimeout(function () { + canDo.current = true; + }, timeout); + cb(); + return true; + } + return false; + }, [timeout]); +} diff --git a/src/lib/hooks/useDraggableScrollView.js b/src/lib/hooks/useDraggableScrollView.js new file mode 100644 index 0000000000..734bbf5d28 --- /dev/null +++ b/src/lib/hooks/useDraggableScrollView.js @@ -0,0 +1,60 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { Platform } from 'react-native'; +import { mergeRefs } from '#/lib/merge-refs'; +export function useDraggableScroll(_a) { + var _b = _a === void 0 ? {} : _a, outerRef = _b.outerRef, _c = _b.cursor, cursor = _c === void 0 ? 'grab' : _c; + var ref = useRef(null); + useEffect(function () { + if (Platform.OS !== 'web' || !ref.current) { + return; + } + var slider = ref.current; + var isDragging = false; + var isMouseDown = false; + var startX = 0; + var scrollLeft = 0; + var mouseDown = function (e) { + isMouseDown = true; + startX = e.pageX - slider.offsetLeft; + scrollLeft = slider.scrollLeft; + slider.style.cursor = cursor; + }; + var mouseUp = function () { + if (isDragging) { + slider.addEventListener('click', function (e) { return e.stopPropagation(); }, { once: true }); + } + isMouseDown = false; + isDragging = false; + slider.style.cursor = 'default'; + }; + var mouseMove = function (e) { + var _a, _b; + if (!isMouseDown) { + return; + } + // Require n pixels momement before start of drag (3 in this case ) + var x = e.pageX - slider.offsetLeft; + if (Math.abs(x - startX) < 3) { + return; + } + isDragging = true; + e.preventDefault(); + var walk = x - startX; + slider.scrollLeft = scrollLeft - walk; + if (slider.contains(document.activeElement)) + (_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.blur) === null || _b === void 0 ? void 0 : _b.call(_a); + }; + slider.addEventListener('mousedown', mouseDown); + window.addEventListener('mouseup', mouseUp); + window.addEventListener('mousemove', mouseMove); + return function () { + slider.removeEventListener('mousedown', mouseDown); + window.removeEventListener('mouseup', mouseUp); + window.removeEventListener('mousemove', mouseMove); + }; + }, [cursor]); + var refs = useMemo(function () { return mergeRefs(outerRef ? [ref, outerRef] : [ref]); }, [ref, outerRef]); + return { + refs: refs, + }; +} diff --git a/src/lib/hooks/useGoBack.js b/src/lib/hooks/useGoBack.js new file mode 100644 index 0000000000..e0458e915c --- /dev/null +++ b/src/lib/hooks/useGoBack.js @@ -0,0 +1,23 @@ +import { StackActions, useNavigation } from '@react-navigation/native'; +import { router } from '#/routes'; +export function useGoBack(onGoBack) { + var navigation = useNavigation(); + return function () { + var _a; + onGoBack === null || onGoBack === void 0 ? void 0 : onGoBack(); + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('HomeTab'); + // Checking the state for routes ensures that web doesn't encounter errors while going back + if ((_a = navigation.getState()) === null || _a === void 0 ? void 0 : _a.routes) { + navigation.dispatch(StackActions.push.apply(StackActions, router.matchPath('/'))); + } + else { + navigation.navigate('HomeTab'); + navigation.dispatch(StackActions.popToTop()); + } + } + }; +} diff --git a/src/lib/hooks/useHideBottomBarBorder.js b/src/lib/hooks/useHideBottomBarBorder.js new file mode 100644 index 0000000000..1f38f75db6 --- /dev/null +++ b/src/lib/hooks/useHideBottomBarBorder.js @@ -0,0 +1,34 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useState } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; +var HideBottomBarBorderContext = createContext(false); +HideBottomBarBorderContext.displayName = 'HideBottomBarBorderContext'; +var HideBottomBarBorderSetterContext = createContext(null); +HideBottomBarBorderSetterContext.displayName = + 'HideBottomBarBorderSetterContext'; +export function useHideBottomBarBorderSetter() { + var hideBottomBarBorder = useContext(HideBottomBarBorderSetterContext); + if (!hideBottomBarBorder) { + throw new Error('useHideBottomBarBorderSetter must be used within a HideBottomBarBorderProvider'); + } + return hideBottomBarBorder; +} +export function useHideBottomBarBorderForScreen() { + var hideBorder = useHideBottomBarBorderSetter(); + useFocusEffect(useCallback(function () { + var cleanup = hideBorder(); + return function () { return cleanup(); }; + }, [hideBorder])); +} +export function useHideBottomBarBorder() { + return useContext(HideBottomBarBorderContext); +} +export function Provider(_a) { + var children = _a.children; + var _b = useState(0), refCount = _b[0], setRefCount = _b[1]; + var setter = useCallback(function () { + setRefCount(function (prev) { return prev + 1; }); + return function () { return setRefCount(function (prev) { return prev - 1; }); }; + }, []); + return (_jsx(HideBottomBarBorderSetterContext.Provider, { value: setter, children: _jsx(HideBottomBarBorderContext.Provider, { value: refCount > 0, children: children }) })); +} diff --git a/src/lib/hooks/useInitialNumToRender.js b/src/lib/hooks/useInitialNumToRender.js new file mode 100644 index 0000000000..9abb294ca6 --- /dev/null +++ b/src/lib/hooks/useInitialNumToRender.js @@ -0,0 +1,16 @@ +import { useWindowDimensions } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useBottomBarOffset } from '#/lib/hooks/useBottomBarOffset'; +var MIN_POST_HEIGHT = 100; +export function useInitialNumToRender(_a) { + var _b = _a === void 0 ? {} : _a, _c = _b.minItemHeight, minItemHeight = _c === void 0 ? MIN_POST_HEIGHT : _c, _d = _b.screenHeightOffset, screenHeightOffset = _d === void 0 ? 0 : _d; + var screenHeight = useWindowDimensions().height; + var topInset = useSafeAreaInsets().top; + var bottomBarHeight = useBottomBarOffset(); + var finalHeight = screenHeight - screenHeightOffset - topInset - bottomBarHeight; + var minItems = Math.floor(finalHeight / minItemHeight); + if (minItems < 1) { + return 1; + } + return minItems; +} diff --git a/src/lib/hooks/useIntentHandler.js b/src/lib/hooks/useIntentHandler.js new file mode 100644 index 0000000000..a270234fe2 --- /dev/null +++ b/src/lib/hooks/useIntentHandler.js @@ -0,0 +1,196 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { Alert } from 'react-native'; +import * as Linking from 'expo-linking'; +import * as WebBrowser from 'expo-web-browser'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { parseLinkingUrl } from '#/lib/parseLinkingUrl'; +import { useSession } from '#/state/session'; +import { useCloseAllActiveElements } from '#/state/util'; +import { useIntentDialogs } from '#/components/intents/IntentDialogs'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS, IS_NATIVE } from '#/env'; +import { Referrer } from '../../../modules/expo-bluesky-swiss-army'; +import { useApplyPullRequestOTAUpdate } from './useOTAUpdates'; +var VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/; +// This needs to stay outside of react to persist between account switches +var previousIntentUrl = ''; +export function useIntentHandler() { + var _this = this; + var incomingUrl = Linking.useLinkingURL(); + var ax = useAnalytics(); + var composeIntent = useComposeIntent(); + var verifyEmailIntent = useVerifyEmailIntent(); + var currentAccount = useSession().currentAccount; + var tryApplyUpdate = useApplyPullRequestOTAUpdate().tryApplyUpdate; + React.useEffect(function () { + var handleIncomingURL = function (url) { return __awaiter(_this, void 0, void 0, function () { + var referrerInfo, urlp, _a, intent, intentType, isIntent, params, code, channel; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!IS_IOS) return [3 /*break*/, 2]; + // Close in-app browser if it's open (iOS only) + return [4 /*yield*/, WebBrowser.dismissBrowser().catch(function () { })]; + case 1: + // Close in-app browser if it's open (iOS only) + _b.sent(); + _b.label = 2; + case 2: + referrerInfo = Referrer.getReferrerInfo(); + if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { + ax.metric('deepLink:referrerReceived', { + to: url, + referrer: referrerInfo === null || referrerInfo === void 0 ? void 0 : referrerInfo.referrer, + hostname: referrerInfo === null || referrerInfo === void 0 ? void 0 : referrerInfo.hostname, + }); + } + urlp = parseLinkingUrl(url); + _a = urlp.pathname.split('/'), intent = _a[1], intentType = _a[2]; + isIntent = intent === 'intent'; + params = urlp.searchParams; + if (!isIntent) + return [2 /*return*/]; + switch (intentType) { + case 'compose': { + composeIntent({ + text: params.get('text'), + imageUrisStr: params.get('imageUris'), + videoUri: params.get('videoUri'), + }); + return [2 /*return*/]; + } + case 'verify-email': { + code = params.get('code'); + if (!code) + return [2 /*return*/]; + verifyEmailIntent(code); + return [2 /*return*/]; + } + case 'age-assurance': { + // Handled in `#/ageAssurance/components/RedirectOverlay.tsx` + return [2 /*return*/]; + } + case 'apply-ota': { + channel = params.get('channel'); + if (!channel) { + Alert.alert('Error', 'No channel provided to look for.'); + } + else { + tryApplyUpdate(channel); + } + return [2 /*return*/]; + } + default: { + return [2 /*return*/]; + } + } + return [2 /*return*/]; + } + }); + }); }; + if (incomingUrl) { + if (previousIntentUrl === incomingUrl) { + return; + } + handleIncomingURL(incomingUrl); + previousIntentUrl = incomingUrl; + } + }, [ + incomingUrl, + ax, + composeIntent, + verifyEmailIntent, + currentAccount, + tryApplyUpdate, + ]); +} +export function useComposeIntent() { + var closeAllActiveElements = useCloseAllActiveElements(); + var openComposer = useOpenComposer().openComposer; + var hasSession = useSession().hasSession; + return React.useCallback(function (_a) { + var text = _a.text, imageUrisStr = _a.imageUrisStr, videoUri = _a.videoUri; + if (!hasSession) + return; + closeAllActiveElements(); + // Whenever a video URI is present, we don't support adding images right now. + if (videoUri) { + var _b = videoUri.split('|'), uri = _b[0], width = _b[1], height = _b[2]; + openComposer({ + text: text !== null && text !== void 0 ? text : undefined, + videoUri: { uri: uri, width: Number(width), height: Number(height) }, + logContext: 'Deeplink', + }); + return; + } + var imageUris = imageUrisStr === null || imageUrisStr === void 0 ? void 0 : imageUrisStr.split(',').filter(function (part) { + // For some security, we're going to filter out any image uri that is external. We don't want someone to + // be able to provide some link like "bluesky://intent/compose?imageUris=https://IHaveYourIpNow.com/image.jpeg + // and we load that image + if (part.includes('https://') || part.includes('http://')) { + return false; + } + // We also should just filter out cases that don't have all the info we need + return VALID_IMAGE_REGEX.test(part); + }).map(function (part) { + var _a = part.split('|'), uri = _a[0], width = _a[1], height = _a[2]; + return { uri: uri, width: Number(width), height: Number(height) }; + }); + setTimeout(function () { + openComposer({ + text: text !== null && text !== void 0 ? text : undefined, + imageUris: IS_NATIVE ? imageUris : undefined, + logContext: 'Deeplink', + }); + }, 500); + }, [hasSession, closeAllActiveElements, openComposer]); +} +function useVerifyEmailIntent() { + var closeAllActiveElements = useCloseAllActiveElements(); + var _a = useIntentDialogs(), control = _a.verifyEmailDialogControl, setState = _a.setVerifyEmailState; + return React.useCallback(function (code) { + closeAllActiveElements(); + setState({ + code: code, + }); + setTimeout(function () { + control.open(); + }, 1000); + }, [closeAllActiveElements, control, setState]); +} diff --git a/src/lib/hooks/useIsBskyTeam.js b/src/lib/hooks/useIsBskyTeam.js new file mode 100644 index 0000000000..dcb5837bbf --- /dev/null +++ b/src/lib/hooks/useIsBskyTeam.js @@ -0,0 +1,6 @@ +import { useMemo } from 'react'; +import { useAnalytics } from '#/analytics'; +export function useIsBskyTeam() { + var ax = useAnalytics(); + return useMemo(function () { return ax.features.enabled(ax.features.IsBskyTeam); }, [ax.features]); +} diff --git a/src/lib/hooks/useIsKeyboardVisible.js b/src/lib/hooks/useIsKeyboardVisible.js new file mode 100644 index 0000000000..b143c19778 --- /dev/null +++ b/src/lib/hooks/useIsKeyboardVisible.js @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; +import { Keyboard } from 'react-native'; +import { IS_IOS } from '#/env'; +export function useIsKeyboardVisible(_a) { + var _b = _a === void 0 ? {} : _a, iosUseWillEvents = _b.iosUseWillEvents; + var _c = useState(false), isKeyboardVisible = _c[0], setKeyboardVisible = _c[1]; + // NOTE + // only iOS supports the "will" events + // -prf + var showEvent = IS_IOS && iosUseWillEvents ? 'keyboardWillShow' : 'keyboardDidShow'; + var hideEvent = IS_IOS && iosUseWillEvents ? 'keyboardWillHide' : 'keyboardDidHide'; + useEffect(function () { + var keyboardShowListener = Keyboard.addListener(showEvent, function () { + return setKeyboardVisible(true); + }); + var keyboardHideListener = Keyboard.addListener(hideEvent, function () { + return setKeyboardVisible(false); + }); + return function () { + keyboardHideListener.remove(); + keyboardShowListener.remove(); + }; + }, [showEvent, hideEvent]); + return [isKeyboardVisible]; +} diff --git a/src/lib/hooks/useMinimalShellTransform.js b/src/lib/hooks/useMinimalShellTransform.js new file mode 100644 index 0000000000..7fa8184ca7 --- /dev/null +++ b/src/lib/hooks/useMinimalShellTransform.js @@ -0,0 +1,55 @@ +import { interpolate, useAnimatedStyle } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useMinimalShellMode } from '#/state/shell/minimal-mode'; +import { useShellLayout } from '#/state/shell/shell-layout'; +import { IS_LIQUID_GLASS } from '#/env'; +// Keep these separated so that we only pay for useAnimatedStyle that gets used. +export function useMinimalShellHeaderTransform() { + var headerMode = useMinimalShellMode().headerMode; + var headerHeight = useShellLayout().headerHeight; + var topInset = useSafeAreaInsets().top; + var headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0; + var headerTransform = useAnimatedStyle(function () { + var headerModeValue = headerMode.get(); + return { + pointerEvents: headerModeValue === 0 ? 'auto' : 'none', + opacity: Math.pow(1 - headerModeValue, 2), + transform: [ + { + translateY: interpolate(headerModeValue, [0, 1], [0, headerPinnedHeight - headerHeight.get()]), + }, + ], + }; + }); + return headerTransform; +} +export function useMinimalShellFooterTransform() { + var footerMode = useMinimalShellMode().footerMode; + var footerHeight = useShellLayout().footerHeight; + var footerTransform = useAnimatedStyle(function () { + var footerModeValue = footerMode.get(); + return { + pointerEvents: footerModeValue === 0 ? 'auto' : 'none', + opacity: Math.pow(1 - footerModeValue, 2), + transform: [ + { + translateY: interpolate(footerModeValue, [0, 1], [0, footerHeight.get()]), + }, + ], + }; + }); + return footerTransform; +} +export function useMinimalShellFabTransform() { + var footerMode = useMinimalShellMode().footerMode; + var fabTransform = useAnimatedStyle(function () { + return { + transform: [ + { + translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]), + }, + ], + }; + }); + return fabTransform; +} diff --git a/src/lib/hooks/useNavigationDeduped.js b/src/lib/hooks/useNavigationDeduped.js new file mode 100644 index 0000000000..49a92c09f7 --- /dev/null +++ b/src/lib/hooks/useNavigationDeduped.js @@ -0,0 +1,70 @@ +import { useMemo } from 'react'; +import { useNavigation } from '@react-navigation/core'; +import { useDedupe } from '#/lib/hooks/useDedupe'; +export function useNavigationDeduped() { + var navigation = useNavigation(); + var dedupe = useDedupe(); + return useMemo(function () { return ({ + push: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + dedupe(function () { return navigation.push.apply(navigation, args); }); + }, + navigate: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + dedupe(function () { return navigation.navigate.apply(navigation, args); }); + }, + replace: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + dedupe(function () { return navigation.replace.apply(navigation, args); }); + }, + dispatch: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + dedupe(function () { return navigation.dispatch.apply(navigation, args); }); + }, + popToTop: function () { + dedupe(function () { return navigation.popToTop(); }); + }, + popTo: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + dedupe(function () { return navigation.popTo.apply(navigation, args); }); + }, + pop: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + dedupe(function () { return navigation.pop.apply(navigation, args); }); + }, + goBack: function () { + dedupe(function () { return navigation.goBack(); }); + }, + canGoBack: function () { + return navigation.canGoBack(); + }, + getState: function () { + return navigation.getState(); + }, + getParent: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return navigation.getParent.apply(navigation, args); + }, + }); }, [dedupe, navigation]); +} diff --git a/src/lib/hooks/useNavigationTabState.js b/src/lib/hooks/useNavigationTabState.js new file mode 100644 index 0000000000..3be2a40426 --- /dev/null +++ b/src/lib/hooks/useNavigationTabState.js @@ -0,0 +1,28 @@ +import { useNavigationState } from '@react-navigation/native'; +import { getTabState, TabState } from '#/lib/routes/helpers'; +export function useNavigationTabState() { + return useNavigationState(function (state) { + var res = { + isAtHome: getTabState(state, 'Home') !== TabState.Outside, + isAtSearch: getTabState(state, 'Search') !== TabState.Outside, + // FeedsTab no longer exists, but this check works for `Feeds` screen as well + isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside, + isAtBookmarks: getTabState(state, 'Bookmarks') !== TabState.Outside, + isAtNotifications: getTabState(state, 'Notifications') !== TabState.Outside, + isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside, + isAtMessages: getTabState(state, 'Messages') !== TabState.Outside, + }; + if (!res.isAtHome && + !res.isAtSearch && + !res.isAtFeeds && + !res.isAtNotifications && + !res.isAtMyProfile && + !res.isAtMessages) { + // HACK for some reason useNavigationState will give us pre-hydration results + // and not update after, so we force isAtHome if all came back false + // -prf + res.isAtHome = true; + } + return res; + }); +} diff --git a/src/lib/hooks/useNavigationTabState.web.js b/src/lib/hooks/useNavigationTabState.web.js new file mode 100644 index 0000000000..d61ba7d160 --- /dev/null +++ b/src/lib/hooks/useNavigationTabState.web.js @@ -0,0 +1,14 @@ +import { useNavigationState } from '@react-navigation/native'; +import { getCurrentRoute } from '#/lib/routes/helpers'; +export function useNavigationTabState() { + return useNavigationState(function (state) { + var currentRoute = state ? getCurrentRoute(state).name : 'Home'; + return { + isAtHome: currentRoute === 'Home', + isAtSearch: currentRoute === 'Search', + isAtNotifications: currentRoute === 'Notifications', + isAtMyProfile: currentRoute === 'MyProfile', + isAtMessages: currentRoute === 'Messages', + }; + }); +} diff --git a/src/lib/hooks/useNonReactiveCallback.js b/src/lib/hooks/useNonReactiveCallback.js new file mode 100644 index 0000000000..0acdd86908 --- /dev/null +++ b/src/lib/hooks/useNonReactiveCallback.js @@ -0,0 +1,23 @@ +import { useCallback, useInsertionEffect, useRef } from 'react'; +// This should be used sparingly. It erases reactivity, i.e. when the inputs +// change, the function itself will remain the same. This means that if you +// use this at a higher level of your tree, and then some state you read in it +// changes, there is no mechanism for anything below in the tree to "react" +// to this change (e.g. by knowing to call your function again). +// +// Also, you should avoid calling the returned function during rendering +// since the values captured by it are going to lag behind. +export function useNonReactiveCallback(fn) { + var ref = useRef(fn); + useInsertionEffect(function () { + ref.current = fn; + }, [fn]); + return useCallback(function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var latestFn = ref.current; + return latestFn.apply(void 0, args); + }, [ref]); +} diff --git a/src/lib/hooks/useNotificationHandler.js b/src/lib/hooks/useNotificationHandler.js new file mode 100644 index 0000000000..2a3b91ccbb --- /dev/null +++ b/src/lib/hooks/useNotificationHandler.js @@ -0,0 +1,398 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useEffect } from 'react'; +import * as Notifications from 'expo-notifications'; +import { AtUri } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { CommonActions, useNavigation } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAccountSwitcher } from '#/lib/hooks/useAccountSwitcher'; +import { logger as notyLogger } from '#/lib/notifications/util'; +import { useCurrentConvoId } from '#/state/messages/current-convo-id'; +import { RQKEY as RQKEY_NOTIFS } from '#/state/queries/notifications/feed'; +import { invalidateCachedUnreadPage } from '#/state/queries/notifications/unread'; +import { truncateAndInvalidate } from '#/state/queries/util'; +import { useSession } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { useCloseAllActiveElements } from '#/state/util'; +import { useAnalytics } from '#/analytics'; +import { IS_ANDROID, IS_IOS } from '#/env'; +import { resetToTab } from '#/Navigation'; +import { router } from '#/routes'; +var DEFAULT_HANDLER_OPTIONS = { + shouldShowBanner: false, + shouldShowList: false, + shouldPlaySound: false, + shouldSetBadge: true, +}; +/** + * Cached notification payload if we handled a notification while the user was + * using a different account. This is consumed after we finish switching + * accounts. + */ +var storedAccountSwitchPayload; +/** + * Used to ensure we don't handle the same notification twice + */ +var lastHandledNotificationDateDedupe = 0; +export function useNotificationsHandler() { + var _this = this; + var ax = useAnalytics(); + var logger = ax.logger.useChild(ax.logger.Context.Notifications); + var queryClient = useQueryClient(); + var _a = useSession(), currentAccount = _a.currentAccount, accounts = _a.accounts; + var onPressSwitchAccount = useAccountSwitcher().onPressSwitchAccount; + var navigation = useNavigation(); + var currentConvoId = useCurrentConvoId().currentConvoId; + var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut; + var closeAllActiveElements = useCloseAllActiveElements(); + var _ = useLingui()._; + // On Android, we cannot control which sound is used for a notification on Android + // 28 or higher. Instead, we have to configure a notification channel ahead of time + // which has the sounds we want in the configuration for that channel. These two + // channels allow for the mute/unmute functionality we want for the background + // handler. + useEffect(function () { + if (!IS_ANDROID) + return; + // assign both chat notifications to a group + // NOTE: I don't think that it will retroactively move them into the group + // if the channels already exist. no big deal imo -sfn + var CHAT_GROUP = 'chat'; + Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, { + name: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Chat"], ["Chat"])))), + description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You can choose whether chat notifications have sound in the chat settings within the app"], ["You can choose whether chat notifications have sound in the chat settings within the app"])))), + }); + Notifications.setNotificationChannelAsync('chat-messages', { + name: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Chat messages - sound"], ["Chat messages - sound"])))), + groupId: CHAT_GROUP, + importance: Notifications.AndroidImportance.MAX, + sound: 'dm.mp3', + showBadge: true, + vibrationPattern: [250], + lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, + }); + Notifications.setNotificationChannelAsync('chat-messages-muted', { + name: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Chat messages - silent"], ["Chat messages - silent"])))), + groupId: CHAT_GROUP, + importance: Notifications.AndroidImportance.MAX, + sound: null, + showBadge: true, + vibrationPattern: [250], + lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, + }); + Notifications.setNotificationChannelAsync('like', { + name: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Likes"], ["Likes"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('repost', { + name: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Reposts"], ["Reposts"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('reply', { + name: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Replies"], ["Replies"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('mention', { + name: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Mentions"], ["Mentions"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('quote', { + name: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Quotes"], ["Quotes"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('follow', { + name: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["New followers"], ["New followers"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('like-via-repost', { + name: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Likes of your reposts"], ["Likes of your reposts"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('repost-via-repost', { + name: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Reposts of your reposts"], ["Reposts of your reposts"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + Notifications.setNotificationChannelAsync('subscribed-post', { + name: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Activity from others"], ["Activity from others"])))), + importance: Notifications.AndroidImportance.HIGH, + }); + }, [_]); + useEffect(function () { + var handleNotification = function (payload) { + if (!payload) + return; + if (payload.reason === 'chat-message') { + logger.debug("useNotificationsHandler: handling chat message", { + payload: payload, + }); + if (payload.recipientDid !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) && + !storedAccountSwitchPayload) { + storePayloadForAccountSwitch(payload); + closeAllActiveElements(); + var account = accounts.find(function (a) { return a.did === payload.recipientDid; }); + if (account) { + onPressSwitchAccount(account, 'Notification'); + } + else { + setShowLoggedOut(true); + } + } + else { + navigation.dispatch(function (state) { + if (state.routes[0].name === 'Messages') { + if (state.routes[state.routes.length - 1].name === + 'MessagesConversation') { + return CommonActions.reset(__assign(__assign({}, state), { routes: __spreadArray(__spreadArray([], state.routes.slice(0, state.routes.length - 1), true), [ + { + name: 'MessagesConversation', + params: { + conversation: payload.convoId, + }, + }, + ], false) })); + } + else { + return CommonActions.navigate('MessagesConversation', { + conversation: payload.convoId, + }); + } + } + else { + return CommonActions.navigate('MessagesTab', { + screen: 'Messages', + params: { + pushToConversation: payload.convoId, + }, + }); + } + }); + } + } + else { + var url = notificationToURL(payload); + if (url === '/notifications') { + resetToTab('NotificationsTab'); + } + else if (url) { + var _a = router.matchPath(url), screen_1 = _a[0], params = _a[1]; + // @ts-expect-error router is not typed :/ -sfn + navigation.navigate('HomeTab', { screen: screen_1, params: params }); + logger.debug("useNotificationsHandler: navigate", { + screen: screen_1, + params: params, + }); + } + } + }; + Notifications.setNotificationHandler({ + handleNotification: function (e) { return __awaiter(_this, void 0, void 0, function () { + var payload, shouldAlert; + return __generator(this, function (_a) { + payload = getNotificationPayload(e); + if (!payload) + return [2 /*return*/, DEFAULT_HANDLER_OPTIONS]; + logger.debug('useNotificationsHandler: incoming', { e: e, payload: payload }); + if (payload.reason === 'chat-message' && + payload.recipientDid === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + shouldAlert = payload.convoId !== currentConvoId; + return [2 /*return*/, { + shouldShowList: shouldAlert, + shouldShowBanner: shouldAlert, + shouldPlaySound: false, + shouldSetBadge: false, + }]; + } + // Any notification other than a chat message should invalidate the unread page + invalidateCachedUnreadPage(); + return [2 /*return*/, DEFAULT_HANDLER_OPTIONS]; + }); + }); }, + }); + var responseReceivedListener = Notifications.addNotificationResponseReceivedListener(function (e) { + if (e.notification.date === lastHandledNotificationDateDedupe) + return; + lastHandledNotificationDateDedupe = e.notification.date; + logger.debug('useNotificationsHandler: response received', { + actionIdentifier: e.actionIdentifier, + }); + if (e.actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER) { + return; + } + var payload = getNotificationPayload(e.notification); + if (payload) { + logger.debug('User pressed a notification, opening notifications tab', {}); + ax.metric('notifications:openApp', { + reason: payload.reason, + causedBoot: false, + }); + invalidateCachedUnreadPage(); + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')); + if (payload.reason === 'mention' || + payload.reason === 'quote' || + payload.reason === 'reply') { + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')); + } + logger.debug('Notifications: handleNotification', { + content: e.notification.request.content, + payload: payload, + }); + handleNotification(payload); + Notifications.dismissAllNotificationsAsync(); + } + else { + logger.error('useNotificationsHandler: received no payload', { + identifier: e.notification.request.identifier, + }); + } + }); + // Whenever there's a stored payload, that means we had to switch accounts before handling the notification. + // Whenever currentAccount changes, we should try to handle it again. + if ((storedAccountSwitchPayload === null || storedAccountSwitchPayload === void 0 ? void 0 : storedAccountSwitchPayload.reason) === 'chat-message' && + (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === storedAccountSwitchPayload.recipientDid) { + handleNotification(storedAccountSwitchPayload); + storedAccountSwitchPayload = undefined; + } + return function () { + responseReceivedListener.remove(); + }; + }, [ + ax, + logger, + queryClient, + currentAccount, + currentConvoId, + accounts, + closeAllActiveElements, + currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + navigation, + onPressSwitchAccount, + setShowLoggedOut, + ]); +} +export function storePayloadForAccountSwitch(payload) { + storedAccountSwitchPayload = payload; +} +export function getNotificationPayload(e) { + if (e.request.trigger == null || + typeof e.request.trigger !== 'object' || + !('type' in e.request.trigger) || + e.request.trigger.type !== 'push') { + return null; + } + var payload = (IS_IOS ? e.request.trigger.payload : e.request.content.data); + if (payload && payload.reason) { + return payload; + } + else { + if (payload) { + notyLogger.debug('getNotificationPayload: received unknown payload', { + payload: payload, + identifier: e.request.identifier, + }); + } + return null; + } +} +export function notificationToURL(payload) { + switch (payload === null || payload === void 0 ? void 0 : payload.reason) { + case 'like': + case 'repost': + case 'like-via-repost': + case 'repost-via-repost': { + var urip = new AtUri(payload.subject); + if (urip.collection === 'app.bsky.feed.post') { + return "/profile/".concat(urip.host, "/post/").concat(urip.rkey); + } + else { + return '/notifications'; + } + } + case 'reply': + case 'quote': + case 'mention': + case 'subscribed-post': { + var urip = new AtUri(payload.uri); + if (urip.collection === 'app.bsky.feed.post') { + return "/profile/".concat(urip.host, "/post/").concat(urip.rkey); + } + else { + return '/notifications'; + } + } + case 'follow': + case 'starterpack-joined': { + var urip = new AtUri(payload.uri); + return "/profile/".concat(urip.host); + } + case 'chat-message': + // should be handled separately + return null; + case 'verified': + case 'unverified': + return '/notifications'; + default: + // do nothing if we don't know what to do with it + return null; + } +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13; diff --git a/src/lib/hooks/useOTAUpdates.js b/src/lib/hooks/useOTAUpdates.js new file mode 100644 index 0000000000..5c56f1e76d --- /dev/null +++ b/src/lib/hooks/useOTAUpdates.js @@ -0,0 +1,328 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { Alert, AppState } from 'react-native'; +import { nativeBuildVersion } from 'expo-application'; +import { checkForUpdateAsync, fetchUpdateAsync, isEnabled, reloadAsync, setExtraParamAsync, useUpdates, } from 'expo-updates'; +import { isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { IS_ANDROID, IS_IOS, IS_TESTFLIGHT } from '#/env'; +var MINIMUM_MINIMIZE_TIME = 15 * 60e3; +function setExtraParams() { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, setExtraParamAsync(IS_IOS ? 'ios-build-number' : 'android-build-number', + // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. + // This just ensures it gets passed as a string + "".concat(nativeBuildVersion))]; + case 1: + _a.sent(); + return [4 /*yield*/, setExtraParamAsync('channel', IS_TESTFLIGHT ? 'testflight' : 'production')]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +function setExtraParamsPullRequest(channel) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, setExtraParamAsync(IS_IOS ? 'ios-build-number' : 'android-build-number', + // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. + // This just ensures it gets passed as a string + "".concat(nativeBuildVersion))]; + case 1: + _a.sent(); + return [4 /*yield*/, setExtraParamAsync('channel', channel)]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +function updateTestflight() { + return __awaiter(this, void 0, void 0, function () { + var res; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, setExtraParams()]; + case 1: + _a.sent(); + return [4 /*yield*/, checkForUpdateAsync()]; + case 2: + res = _a.sent(); + if (!res.isAvailable) return [3 /*break*/, 4]; + return [4 /*yield*/, fetchUpdateAsync()]; + case 3: + _a.sent(); + Alert.alert('Update Available', 'A new version of the app is available. Relaunch now?', [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Relaunch', + style: 'default', + onPress: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, reloadAsync()]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }, + ]); + _a.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); +} +export function useApplyPullRequestOTAUpdate() { + var _this = this; + var currentlyRunning = useUpdates().currentlyRunning; + var _a = React.useState(false), pending = _a[0], setPending = _a[1]; + var currentChannel = currentlyRunning === null || currentlyRunning === void 0 ? void 0 : currentlyRunning.channel; + var isCurrentlyRunningPullRequestDeployment = currentChannel === null || currentChannel === void 0 ? void 0 : currentChannel.startsWith('pull-request'); + var tryApplyUpdate = function (channel) { return __awaiter(_this, void 0, void 0, function () { + var res; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setPending(true); + return [4 /*yield*/, setExtraParamsPullRequest(channel)]; + case 1: + _a.sent(); + return [4 /*yield*/, checkForUpdateAsync()]; + case 2: + res = _a.sent(); + if (res.isAvailable) { + Alert.alert('Deployment Available', "A deployment of ".concat(channel, " is availalble. Applying this deployment may result in a bricked installation, in which case you will need to reinstall the app and may lose local data. Are you sure you want to proceed?"), [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Relaunch', + style: 'default', + onPress: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, fetchUpdateAsync()]; + case 1: + _a.sent(); + return [4 /*yield*/, reloadAsync()]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }, + ]); + } + else { + Alert.alert('No Deployment Available', "No new deployments of ".concat(channel, " are currently available for your current native build.")); + } + setPending(false); + return [2 /*return*/]; + } + }); + }); }; + var revertToEmbedded = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, updateTestflight()]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + logger.error('Internal OTA Update Error', { error: "".concat(e_1) }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + return { + tryApplyUpdate: tryApplyUpdate, + revertToEmbedded: revertToEmbedded, + isCurrentlyRunningPullRequestDeployment: isCurrentlyRunningPullRequestDeployment, + currentChannel: currentChannel, + pending: pending, + }; +} +export function useOTAUpdates() { + var _this = this; + var shouldReceiveUpdates = isEnabled && !__DEV__; + var appState = React.useRef('active'); + var lastMinimize = React.useRef(0); + var ranInitialCheck = React.useRef(false); + var timeout = React.useRef(undefined); + var _a = useUpdates(), currentlyRunning = _a.currentlyRunning, isUpdatePending = _a.isUpdatePending; + var currentChannel = currentlyRunning === null || currentlyRunning === void 0 ? void 0 : currentlyRunning.channel; + var setCheckTimeout = React.useCallback(function () { + timeout.current = setTimeout(function () { return __awaiter(_this, void 0, void 0, function () { + var res, err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 6, , 7]); + return [4 /*yield*/, setExtraParams()]; + case 1: + _a.sent(); + logger.debug('Checking for update...'); + return [4 /*yield*/, checkForUpdateAsync()]; + case 2: + res = _a.sent(); + if (!res.isAvailable) return [3 /*break*/, 4]; + logger.debug('Attempting to fetch update...'); + return [4 /*yield*/, fetchUpdateAsync()]; + case 3: + _a.sent(); + return [3 /*break*/, 5]; + case 4: + logger.debug('No update available.'); + _a.label = 5; + case 5: return [3 /*break*/, 7]; + case 6: + err_1 = _a.sent(); + if (!isNetworkError(err_1)) { + logger.error('OTA Update Error', { safeMessage: err_1 }); + } + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); }, 10e3); + }, []); + var onIsTestFlight = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, updateTestflight()]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + err_2 = _a.sent(); + if (!isNetworkError(err_2)) { + logger.error('Internal OTA Update Error', { safeMessage: err_2 }); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }, []); + React.useEffect(function () { + // We don't need to check anything if the current update is a PR update + if (currentChannel === null || currentChannel === void 0 ? void 0 : currentChannel.startsWith('pull-request')) { + return; + } + // We use this setTimeout to allow analytics to initialize before we check for an update + // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This + // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update + // immediately. + if (IS_TESTFLIGHT) { + onIsTestFlight(); + return; + } + else if (!shouldReceiveUpdates || ranInitialCheck.current) { + return; + } + setCheckTimeout(); + ranInitialCheck.current = true; + }, [onIsTestFlight, currentChannel, setCheckTimeout, shouldReceiveUpdates]); + // After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available + // or B check for an update again. + React.useEffect(function () { + // We also don't start this timeout if the user is on a pull request update + if (!isEnabled || (currentChannel === null || currentChannel === void 0 ? void 0 : currentChannel.startsWith('pull-request'))) { + return; + } + // TEMP: disable wake-from-background OTA loading on Android. + // This is causing a crash when the thread view is open due to + // `maintainVisibleContentPosition`. See repro repo for more details: + // https://github.com/mozzius/ota-crash-repro + // Old Arch only - re-enable once we're on the New Archictecture! -sfn + if (IS_ANDROID) + return; + var subscription = AppState.addEventListener('change', function (nextAppState) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(appState.current.match(/inactive|background/) && + nextAppState === 'active')) return [3 /*break*/, 4]; + if (!(lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME)) return [3 /*break*/, 3]; + if (!isUpdatePending) return [3 /*break*/, 2]; + return [4 /*yield*/, reloadAsync()]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + setCheckTimeout(); + _a.label = 3; + case 3: return [3 /*break*/, 5]; + case 4: + lastMinimize.current = Date.now(); + _a.label = 5; + case 5: + appState.current = nextAppState; + return [2 /*return*/]; + } + }); + }); }); + return function () { + clearTimeout(timeout.current); + subscription.remove(); + }; + }, [isUpdatePending, currentChannel, setCheckTimeout]); +} diff --git a/src/lib/hooks/useOTAUpdates.web.js b/src/lib/hooks/useOTAUpdates.web.js new file mode 100644 index 0000000000..066d21a68b --- /dev/null +++ b/src/lib/hooks/useOTAUpdates.web.js @@ -0,0 +1,10 @@ +export function useOTAUpdates() { } +export function useApplyPullRequestOTAUpdate() { + return { + tryApplyUpdate: function () { }, + revertToEmbedded: function () { }, + isCurrentlyRunningPullRequestDeployment: false, + currentChannel: 'web-build', + pending: false, + }; +} diff --git a/src/lib/hooks/useOpenComposer.js b/src/lib/hooks/useOpenComposer.js new file mode 100644 index 0000000000..2961f2ea5e --- /dev/null +++ b/src/lib/hooks/useOpenComposer.js @@ -0,0 +1,18 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { Trans } from '@lingui/react/macro'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { useOpenComposer as useRootOpenComposer } from '#/state/shell/composer'; +export function useOpenComposer() { + var openComposer = useRootOpenComposer().openComposer; + var requireEmailVerification = useRequireEmailVerification(); + return useMemo(function () { + return { + openComposer: requireEmailVerification(openComposer, { + instructions: [ + _jsx(Trans, { children: "Before creating a post or replying, you must first verify your email." }, "pre-compose"), + ], + }), + }; + }, [openComposer, requireEmailVerification]); +} diff --git a/src/lib/hooks/useOpenLink.js b/src/lib/hooks/useOpenLink.js new file mode 100644 index 0000000000..7e338835c0 --- /dev/null +++ b/src/lib/hooks/useOpenLink.js @@ -0,0 +1,104 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import { Linking } from 'react-native'; +import * as WebBrowser from 'expo-web-browser'; +import { createBskyAppAbsoluteUrl, createProxiedUrl, isBskyAppUrl, isBskyRSSUrl, isRelativeUrl, toNiceDomain, } from '#/lib/strings/url-helpers'; +import { logger } from '#/logger'; +import { useInAppBrowser } from '#/state/preferences/in-app-browser'; +import { useTheme } from '#/alf'; +import { useDialogContext } from '#/components/Dialog'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +export function useOpenLink() { + var _this = this; + var ax = useAnalytics(); + var enabled = useInAppBrowser(); + var t = useTheme(); + var dialogContext = useDialogContext(); + var inAppBrowserConsentControl = useGlobalDialogsControlContext().inAppBrowserConsentControl; + var openLink = useCallback(function (url, override, shouldProxy) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (isBskyRSSUrl(url) && isRelativeUrl(url)) { + url = createBskyAppAbsoluteUrl(url); + } + if (!isBskyAppUrl(url)) { + ax.metric('link:clicked', { + domain: toNiceDomain(url), + url: url, + }); + if (shouldProxy) { + url = createProxiedUrl(url); + } + } + if (IS_NATIVE && !url.startsWith('mailto:')) { + if (override === undefined && enabled === undefined) { + // consent dialog is a global dialog, and while it's possible to nest dialogs, + // the actual components need to be nested. sibling dialogs on iOS are not supported. + // thus, check if we're in a dialog, and if so, close the existing dialog before opening the + // consent dialog -sfn + if (dialogContext.isWithinDialog) { + dialogContext.close(function () { + inAppBrowserConsentControl.open(url); + }); + } + else { + inAppBrowserConsentControl.open(url); + } + return [2 /*return*/]; + } + else if (override !== null && override !== void 0 ? override : enabled) { + WebBrowser.openBrowserAsync(url, { + presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, + toolbarColor: t.atoms.bg.backgroundColor, + controlsColor: t.palette.primary_500, + createTask: false, + }).catch(function (err) { + if (__DEV__) + logger.error('Could not open web browser', { message: err }); + Linking.openURL(url); + }); + return [2 /*return*/]; + } + } + Linking.openURL(url); + return [2 /*return*/]; + }); + }); }, [ax, enabled, inAppBrowserConsentControl, t, dialogContext]); + return openLink; +} diff --git a/src/lib/hooks/usePalette.js b/src/lib/hooks/usePalette.js new file mode 100644 index 0000000000..e562c3eb53 --- /dev/null +++ b/src/lib/hooks/usePalette.js @@ -0,0 +1,44 @@ +import { useMemo } from 'react'; +import { useTheme, } from '../ThemeContext'; +/** + * @deprecated use `useTheme` from `#/alf` + */ +export function usePalette(color) { + var theme = useTheme(); + return useMemo(function () { + var palette = theme.palette[color]; + return { + colors: palette, + view: { + backgroundColor: palette.background, + }, + viewLight: { + backgroundColor: palette.backgroundLight, + }, + btn: { + backgroundColor: palette.backgroundLight, + }, + border: { + borderColor: palette.border, + }, + borderDark: { + borderColor: palette.borderDark, + }, + text: { + color: palette.text, + }, + textLight: { + color: palette.textLight, + }, + textInverted: { + color: palette.textInverted, + }, + link: { + color: palette.link, + }, + icon: { + color: palette.icon, + }, + }; + }, [theme, color]); +} diff --git a/src/lib/hooks/usePermissions.js b/src/lib/hooks/usePermissions.js new file mode 100644 index 0000000000..a847a70c9c --- /dev/null +++ b/src/lib/hooks/usePermissions.js @@ -0,0 +1,142 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { Linking } from 'react-native'; +import { useCameraPermissions as useExpoCameraPermissions } from 'expo-camera'; +import * as MediaLibrary from 'expo-media-library'; +import { Alert } from '#/view/com/util/Alert'; +import { IS_WEB } from '#/env'; +var openPermissionAlert = function (perm) { + Alert.alert('Permission needed', "Bluesky does not have permission to access your ".concat(perm, "."), [ + { + text: 'Cancel', + style: 'cancel', + }, + { text: 'Open Settings', onPress: function () { return Linking.openSettings(); } }, + ]); +}; +export function usePhotoLibraryPermission() { + var _this = this; + var _a = MediaLibrary.usePermissions({ + granularPermissions: ['photo'], + }), res = _a[0], requestPermission = _a[1]; + var requestPhotoAccessIfNeeded = function () { return __awaiter(_this, void 0, void 0, function () { + var _a, canAskAgain, granted, status_1; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + // On the, we use to produce a filepicker + // This does not need any permission granting. + if (IS_WEB) { + return [2 /*return*/, true]; + } + if (!(res === null || res === void 0 ? void 0 : res.granted)) return [3 /*break*/, 1]; + return [2 /*return*/, true]; + case 1: + if (!(!res || res.status === 'undetermined' || (res === null || res === void 0 ? void 0 : res.canAskAgain))) return [3 /*break*/, 3]; + return [4 /*yield*/, requestPermission()]; + case 2: + _a = _b.sent(), canAskAgain = _a.canAskAgain, granted = _a.granted, status_1 = _a.status; + if (!canAskAgain && status_1 === 'undetermined') { + openPermissionAlert('photo library'); + } + return [2 /*return*/, granted]; + case 3: + openPermissionAlert('photo library'); + return [2 /*return*/, false]; + } + }); + }); }; + return { requestPhotoAccessIfNeeded: requestPhotoAccessIfNeeded }; +} +export function useVideoLibraryPermission() { + var _this = this; + var _a = MediaLibrary.usePermissions({ + granularPermissions: ['video'], + }), res = _a[0], requestPermission = _a[1]; + var requestVideoAccessIfNeeded = function () { return __awaiter(_this, void 0, void 0, function () { + var _a, canAskAgain, granted, status_2; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + // On the, we use to produce a filepicker + // This does not need any permission granting. + if (IS_WEB) { + return [2 /*return*/, true]; + } + if (!(res === null || res === void 0 ? void 0 : res.granted)) return [3 /*break*/, 1]; + return [2 /*return*/, true]; + case 1: + if (!(!res || res.status === 'undetermined' || (res === null || res === void 0 ? void 0 : res.canAskAgain))) return [3 /*break*/, 3]; + return [4 /*yield*/, requestPermission()]; + case 2: + _a = _b.sent(), canAskAgain = _a.canAskAgain, granted = _a.granted, status_2 = _a.status; + if (!canAskAgain && status_2 === 'undetermined') { + openPermissionAlert('video library'); + } + return [2 /*return*/, granted]; + case 3: + openPermissionAlert('video library'); + return [2 /*return*/, false]; + } + }); + }); }; + return { requestVideoAccessIfNeeded: requestVideoAccessIfNeeded }; +} +export function useCameraPermission() { + var _this = this; + var _a = useExpoCameraPermissions(), res = _a[0], requestPermission = _a[1]; + var requestCameraAccessIfNeeded = function () { return __awaiter(_this, void 0, void 0, function () { + var updatedRes; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(res === null || res === void 0 ? void 0 : res.granted)) return [3 /*break*/, 1]; + return [2 /*return*/, true]; + case 1: + if (!(!res || (res === null || res === void 0 ? void 0 : res.status) === 'undetermined' || (res === null || res === void 0 ? void 0 : res.canAskAgain))) return [3 /*break*/, 3]; + return [4 /*yield*/, requestPermission()]; + case 2: + updatedRes = _a.sent(); + return [2 /*return*/, updatedRes === null || updatedRes === void 0 ? void 0 : updatedRes.granted]; + case 3: + openPermissionAlert('camera'); + return [2 /*return*/, false]; + } + }); + }); }; + return { requestCameraAccessIfNeeded: requestCameraAccessIfNeeded }; +} diff --git a/src/lib/hooks/usePermissions.web.js b/src/lib/hooks/usePermissions.web.js new file mode 100644 index 0000000000..e3b46be586 --- /dev/null +++ b/src/lib/hooks/usePermissions.web.js @@ -0,0 +1,65 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +export function usePhotoLibraryPermission() { + var _this = this; + var requestPhotoAccessIfNeeded = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + // On the, we use to produce a filepicker + // This does not need any permission granting. + return [2 /*return*/, true]; + }); + }); }; + return { requestPhotoAccessIfNeeded: requestPhotoAccessIfNeeded }; +} +export function useCameraPermission() { + var _this = this; + var requestCameraAccessIfNeeded = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, false]; + }); + }); }; + return { requestCameraAccessIfNeeded: requestCameraAccessIfNeeded }; +} +export function useVideoLibraryPermission() { + var _this = this; + var requestVideoAccessIfNeeded = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, true]; + }); + }); }; + return { requestVideoAccessIfNeeded: requestVideoAccessIfNeeded }; +} diff --git a/src/lib/hooks/usePostViewTracking.js b/src/lib/hooks/usePostViewTracking.js new file mode 100644 index 0000000000..2c5bce7d5c --- /dev/null +++ b/src/lib/hooks/usePostViewTracking.js @@ -0,0 +1,24 @@ +import { useCallback, useRef } from 'react'; +import { useAnalytics } from '#/analytics'; +/** + * Hook that returns a callback to track post:view events. + * Handles deduplication so the same post URI is only tracked once per mount. + * + * @param logContext - The context where the post is being viewed + * @returns A callback that accepts a post and logs the view event + */ +export function usePostViewTracking(logContext) { + var ax = useAnalytics(); + var seenUrisRef = useRef(new Set()); + var trackPostView = useCallback(function (post) { + if (seenUrisRef.current.has(post.uri)) + return; + seenUrisRef.current.add(post.uri); + ax.metric('post:view', { + uri: post.uri, + authorDid: post.author.did, + logContext: logContext, + }); + }, [ax, logContext]); + return trackPostView; +} diff --git a/src/lib/hooks/useRequireEmailVerification.js b/src/lib/hooks/useRequireEmailVerification.js new file mode 100644 index 0000000000..3307e9647a --- /dev/null +++ b/src/lib/hooks/useRequireEmailVerification.js @@ -0,0 +1,51 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { useCallback } from 'react'; +import { Keyboard } from 'react-native'; +import { useEmail } from '#/state/email-verification'; +import { useRequireAuth, useSession } from '#/state/session'; +import { useCloseAllActiveElements } from '#/state/util'; +import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog'; +export function useRequireEmailVerification() { + var currentAccount = useSession().currentAccount; + var needsEmailVerification = useEmail().needsEmailVerification; + var requireAuth = useRequireAuth(); + var emailDialogControl = useEmailDialogControl(); + var closeAll = useCloseAllActiveElements(); + return useCallback(function (cb, config) { + if (config === void 0) { config = {}; } + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + if (!currentAccount) { + return requireAuth(function () { return cb.apply(void 0, args); }); + } + if (needsEmailVerification) { + Keyboard.dismiss(); + closeAll(); + emailDialogControl.open(__assign({ id: EmailDialogScreenID.Verify }, config)); + return undefined; + } + else { + return cb.apply(void 0, args); + } + }; + }, [ + needsEmailVerification, + currentAccount, + emailDialogControl, + closeAll, + requireAuth, + ]); +} diff --git a/src/lib/hooks/useSetTitle.js b/src/lib/hooks/useSetTitle.js new file mode 100644 index 0000000000..ffa307b75a --- /dev/null +++ b/src/lib/hooks/useSetTitle.js @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { bskyTitle } from '#/lib/strings/headings'; +import { useUnreadNotifications } from '#/state/queries/notifications/unread'; +export function useSetTitle(title) { + var navigation = useNavigation(); + var numUnread = useUnreadNotifications(); + useEffect(function () { + if (title) { + navigation.setOptions({ title: bskyTitle(title, numUnread) }); + } + }, [title, navigation, numUnread]); +} diff --git a/src/lib/hooks/useTLDs.js b/src/lib/hooks/useTLDs.js new file mode 100644 index 0000000000..527d979789 --- /dev/null +++ b/src/lib/hooks/useTLDs.js @@ -0,0 +1,11 @@ +import { useEffect, useState } from 'react'; +export function useTLDs() { + var _a = useState(), tlds = _a[0], setTlds = _a[1]; + useEffect(function () { + // @ts-expect-error - valid path + import('tldts/dist/index.cjs.min.js').then(function (tlds) { + setTlds(tlds); + }); + }, []); + return tlds; +} diff --git a/src/lib/hooks/useTabFocusEffect.js b/src/lib/hooks/useTabFocusEffect.js new file mode 100644 index 0000000000..b20461d62f --- /dev/null +++ b/src/lib/hooks/useTabFocusEffect.js @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { getTabState, TabState } from '#/lib/routes/helpers'; +export function useTabFocusEffect(tabName, cb) { + var _a = useState(false), isInside = _a[0], setIsInside = _a[1]; + // get root navigator state + var nav = useNavigation(); + while (nav.getParent()) { + nav = nav.getParent(); + } + var state = nav.getState(); + useEffect(function () { + // check if inside + var v = getTabState(state, tabName) !== TabState.Outside; + if (v !== isInside) { + // fire + setIsInside(v); + cb(v); + } + }, [state, isInside, setIsInside, tabName, cb]); +} diff --git a/src/lib/hooks/useTimeAgo.js b/src/lib/hooks/useTimeAgo.js new file mode 100644 index 0000000000..d44b447f32 --- /dev/null +++ b/src/lib/hooks/useTimeAgo.js @@ -0,0 +1,161 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { useCallback } from 'react'; +import { defineMessage, msg, plural } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { differenceInSeconds } from 'date-fns'; +var NOW = 5; +var MINUTE = 60; +var HOUR = MINUTE * 60; +var DAY = HOUR * 24; +var MONTH_30 = DAY * 30; +export function useGetTimeAgo(_a) { + var _b = _a === void 0 ? {} : _a, _c = _b.future, future = _c === void 0 ? false : _c; + var i18n = useLingui().i18n; + return useCallback(function (earlier, later, options) { + var diff = dateDiff(earlier, later, future ? 'up' : 'down'); + return formatDateDiff({ diff: diff, i18n: i18n, format: options === null || options === void 0 ? void 0 : options.format }); + }, [i18n, future]); +} +/** + * Returns the difference between `earlier` and `later` dates, based on + * opinionated rules. + * + * - All month are considered exactly 30 days. + * - Dates assume `earlier` <= `later`, and will otherwise return 'now'. + * - All values round down + */ +export function dateDiff(earlier, later, rounding) { + if (rounding === void 0) { rounding = 'down'; } + var diff = { + value: 0, + unit: 'now', + }; + var e = new Date(earlier); + var l = new Date(later); + var diffSeconds = differenceInSeconds(l, e); + if (diffSeconds < NOW) { + diff = { + value: 0, + unit: 'now', + }; + } + else if (diffSeconds < MINUTE) { + diff = { + value: diffSeconds, + unit: 'second', + }; + } + else if (diffSeconds < HOUR) { + var value = rounding === 'up' + ? Math.ceil(diffSeconds / MINUTE) + : Math.floor(diffSeconds / MINUTE); + diff = { + value: value, + unit: 'minute', + }; + } + else if (diffSeconds < DAY) { + var value = rounding === 'up' + ? Math.ceil(diffSeconds / HOUR) + : Math.floor(diffSeconds / HOUR); + diff = { + value: value, + unit: 'hour', + }; + } + else if (diffSeconds < MONTH_30) { + var value = rounding === 'up' + ? Math.ceil(diffSeconds / DAY) + : Math.floor(diffSeconds / DAY); + diff = { + value: value, + unit: 'day', + }; + } + else { + var value = rounding === 'up' + ? Math.ceil(diffSeconds / MONTH_30) + : Math.floor(diffSeconds / MONTH_30); + diff = { + value: value, + unit: 'month', + }; + } + return __assign(__assign({}, diff), { earlier: e, later: l }); +} +/** + * Accepts a `DateDiff` and teturns the difference between `earlier` and + * `later` dates, formatted as a natural language string. + * + * - All month are considered exactly 30 days. + * - Dates assume `earlier` <= `later`, and will otherwise return 'now'. + * - Differences >= 360 days are returned as the "M/D/YYYY" string + * - All values round down + */ +export function formatDateDiff(_a) { + var diff = _a.diff, _b = _a.format, format = _b === void 0 ? 'short' : _b, i18n = _a.i18n; + var long = format === 'long'; + switch (diff.unit) { + case 'now': { + return i18n._(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["now"], ["now"])))); + } + case 'second': { + return long + ? i18n._(plural(diff.value, { one: '# second', other: '# seconds' })) + : i18n._(defineMessage({ + message: "".concat(diff.value, "s"), + comment: "How many seconds have passed, displayed in a narrow form", + })); + } + case 'minute': { + return long + ? i18n._(plural(diff.value, { one: '# minute', other: '# minutes' })) + : i18n._(defineMessage({ + message: "".concat(diff.value, "m"), + comment: "How many minutes have passed, displayed in a narrow form", + })); + } + case 'hour': { + return long + ? i18n._(plural(diff.value, { one: '# hour', other: '# hours' })) + : i18n._(defineMessage({ + message: "".concat(diff.value, "h"), + comment: "How many hours have passed, displayed in a narrow form", + })); + } + case 'day': { + return long + ? i18n._(plural(diff.value, { one: '# day', other: '# days' })) + : i18n._(defineMessage({ + message: "".concat(diff.value, "d"), + comment: "How many days have passed, displayed in a narrow form", + })); + } + case 'month': { + if (diff.value < 12) { + return long + ? i18n._(plural(diff.value, { one: '# month', other: '# months' })) + : i18n._(defineMessage({ + message: plural(diff.value, { one: '#mo', other: '#mo' }), + comment: "How many months have passed, displayed in a narrow form", + })); + } + return i18n.date(new Date(diff.earlier)); + } + } +} +var templateObject_1; diff --git a/src/lib/hooks/useTimer.js b/src/lib/hooks/useTimer.js new file mode 100644 index 0000000000..88c84c30db --- /dev/null +++ b/src/lib/hooks/useTimer.js @@ -0,0 +1,27 @@ +import * as React from 'react'; +/** + * Helper hook to run persistent timers on views + */ +export function useTimer(time, handler) { + var timer = React.useRef(undefined); + // function to restart the timer + var reset = React.useCallback(function () { + if (timer.current) { + clearTimeout(timer.current); + } + timer.current = setTimeout(handler, time); + }, [time, timer, handler]); + // function to cancel the timer + var cancel = React.useCallback(function () { + if (timer.current) { + clearTimeout(timer.current); + timer.current = undefined; + } + }, [timer]); + // start the timer immediately + React.useEffect(function () { + reset(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return [reset, cancel]; +} diff --git a/src/lib/hooks/useToggleMutationQueue.js b/src/lib/hooks/useToggleMutationQueue.js new file mode 100644 index 0000000000..faddea79c5 --- /dev/null +++ b/src/lib/hooks/useToggleMutationQueue.js @@ -0,0 +1,123 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback, useEffect, useRef, useState } from 'react'; +function AbortError() { + var e = new Error(); + e.name = 'AbortError'; + return e; +} +export function useToggleMutationQueue(_a) { + var initialState = _a.initialState, runMutation = _a.runMutation, onSuccess = _a.onSuccess; + // We use the queue as a mutable object. + // This is safe becuase it is not used for rendering. + var queue = useState({ + activeTask: null, + queuedTask: null, + })[0]; + function processQueue() { + return __awaiter(this, void 0, void 0, function () { + var confirmedState, prevTask, nextTask, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (queue.activeTask) { + // There is another active processQueue call iterating over tasks. + // It will handle any newly added tasks, so we should exit early. + return [2 /*return*/]; + } + confirmedState = initialState; + _a.label = 1; + case 1: + _a.trys.push([1, , 8, 9]); + _a.label = 2; + case 2: + if (!queue.queuedTask) return [3 /*break*/, 7]; + prevTask = queue.activeTask; + nextTask = queue.queuedTask; + queue.activeTask = nextTask; + queue.queuedTask = null; + if ((prevTask === null || prevTask === void 0 ? void 0 : prevTask.isOn) === nextTask.isOn) { + // Skip multiple requests to update to the same value in a row. + prevTask.reject(new AbortError()); + return [3 /*break*/, 2]; + } + _a.label = 3; + case 3: + _a.trys.push([3, 5, , 6]); + return [4 /*yield*/, runMutation(confirmedState, nextTask.isOn)]; + case 4: + // The state received from the server feeds into the next task. + // This lets us queue deletions of not-yet-created resources. + confirmedState = _a.sent(); + nextTask.resolve(confirmedState); + return [3 /*break*/, 6]; + case 5: + e_1 = _a.sent(); + nextTask.reject(e_1); + return [3 /*break*/, 6]; + case 6: return [3 /*break*/, 2]; + case 7: return [3 /*break*/, 9]; + case 8: + onSuccess(confirmedState); + queue.activeTask = null; + queue.queuedTask = null; + return [7 /*endfinally*/]; + case 9: return [2 /*return*/]; + } + }); + }); + } + function queueToggle(isOn) { + return new Promise(function (resolve, reject) { + // This is a toggle, so the next queued value can safely replace the queued one. + if (queue.queuedTask) { + queue.queuedTask.reject(new AbortError()); + } + queue.queuedTask = { isOn: isOn, resolve: resolve, reject: reject }; + processQueue(); + }); + } + var queueToggleRef = useRef(queueToggle); + useEffect(function () { + queueToggleRef.current = queueToggle; + }); + var queueToggleStable = useCallback(function (isOn) { + var queueToggleLatest = queueToggleRef.current; + return queueToggleLatest(isOn); + }, []); + return queueToggleStable; +} diff --git a/src/lib/hooks/useTranslate.js b/src/lib/hooks/useTranslate.js new file mode 100644 index 0000000000..cef95dc4bf --- /dev/null +++ b/src/lib/hooks/useTranslate.js @@ -0,0 +1,104 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import * as IntentLauncher from 'expo-intent-launcher'; +import { useOpenLink } from '#/lib/hooks/useOpenLink'; +import { getTranslatorLink } from '#/locale/helpers'; +import { IS_ANDROID } from '#/env'; +/** + * Will always link out to Google Translate. If inline translation is desired, + * use `useTranslateOnDevice` + */ +export function useTranslate() { + var _this = this; + var openLink = useOpenLink(); + return useCallback(function (text, targetLangCode, sourceLanguage) { return __awaiter(_this, void 0, void 0, function () { + var translateUrl, err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + translateUrl = getTranslatorLink(text, targetLangCode, sourceLanguage); + if (!IS_ANDROID) return [3 /*break*/, 7]; + _a.label = 1; + case 1: + _a.trys.push([1, 4, , 6]); + return [4 /*yield*/, IntentLauncher.getApplicationIconAsync('com.google.android.apps.translate')]; + case 2: + // use `getApplicationIconAsync` to determine if the translate app is installed + if (!(_a.sent())) { + throw new Error('Translate app not installed'); + } + // TODO: this should only be called one at a time, use something like + // RQ's `scope` - otherwise can trigger the browser to open unexpectedly when the call throws -sfn + return [4 /*yield*/, IntentLauncher.startActivityAsync('android.intent.action.PROCESS_TEXT', { + type: 'text/plain', + extra: { + 'android.intent.extra.PROCESS_TEXT': text, + 'android.intent.extra.PROCESS_TEXT_READONLY': true, + }, + // note: to skip the intermediate app select, we need to specify a + // `className`. however, this isn't safe to hardcode, we'd need to query the + // package manager for the correct activity. this requires native code, so + // skip for now -sfn + // packageName: 'com.google.android.apps.translate', + // className: 'com.google.android.apps.translate.TranslateActivity', + })]; + case 3: + // TODO: this should only be called one at a time, use something like + // RQ's `scope` - otherwise can trigger the browser to open unexpectedly when the call throws -sfn + _a.sent(); + return [3 /*break*/, 6]; + case 4: + err_1 = _a.sent(); + if (__DEV__) + console.error(err_1); + // most likely means they don't have the translate app + return [4 /*yield*/, openLink(translateUrl)]; + case 5: + // most likely means they don't have the translate app + _a.sent(); + return [3 /*break*/, 6]; + case 6: return [3 /*break*/, 9]; + case 7: return [4 /*yield*/, openLink(translateUrl)]; + case 8: + _a.sent(); + _a.label = 9; + case 9: return [2 /*return*/]; + } + }); + }); }, [openLink]); +} diff --git a/src/lib/hooks/useWebMediaQueries.js b/src/lib/hooks/useWebMediaQueries.js new file mode 100644 index 0000000000..5c5e5000aa --- /dev/null +++ b/src/lib/hooks/useWebMediaQueries.js @@ -0,0 +1,22 @@ +import { useMediaQuery } from 'react-responsive'; +import { IS_NATIVE } from '#/env'; +/** + * @deprecated use `useBreakpoints` from `#/alf` instead + */ +export function useWebMediaQueries() { + var isDesktop = useMediaQuery({ minWidth: 1300 }); + var isTablet = useMediaQuery({ minWidth: 800, maxWidth: 1300 - 1 }); + var isMobile = useMediaQuery({ maxWidth: 800 - 1 }); + var isTabletOrMobile = isMobile || isTablet; + var isTabletOrDesktop = isDesktop || isTablet; + if (IS_NATIVE) { + return { + isMobile: true, + isTablet: false, + isTabletOrMobile: true, + isTabletOrDesktop: false, + isDesktop: false, + }; + } + return { isMobile: isMobile, isTablet: isTablet, isTabletOrMobile: isTabletOrMobile, isTabletOrDesktop: isTabletOrDesktop, isDesktop: isDesktop }; +} diff --git a/src/lib/hooks/useWebScrollRestoration.js b/src/lib/hooks/useWebScrollRestoration.js new file mode 100644 index 0000000000..4bb162a829 --- /dev/null +++ b/src/lib/hooks/useWebScrollRestoration.js @@ -0,0 +1,45 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useNavigation } from '@react-navigation/core'; +if ('scrollRestoration' in history) { + // Tell the brower not to mess with the scroll. + // We're doing that manually below. + history.scrollRestoration = 'manual'; +} +function createInitialScrollState() { + return { + scrollYs: new Map(), + focusedKey: null, + }; +} +export function useWebScrollRestoration() { + var state = useState(createInitialScrollState)[0]; + var navigation = useNavigation(); + useEffect(function () { + function onDispatch() { + if (state.focusedKey) { + // Remember where we were for later. + state.scrollYs.set(state.focusedKey, window.scrollY); + // TODO: Strictly speaking, this is a leak. We never clean up. + // This is because I'm not sure when it's appropriate to clean it up. + // It doesn't seem like popstate is enough because it can still Forward-Back again. + // Maybe we should use sessionStorage. Or check what Next.js is doing? + } + } + // We want to intercept any push/pop/replace *before* the re-render. + // There is no official way to do this yet, but this works okay for now. + // https://twitter.com/satya164/status/1737301243519725803 + navigation.addListener('__unsafe_action__', onDispatch); + return function () { + navigation.removeListener('__unsafe_action__', onDispatch); + }; + }, [state, navigation]); + var screenListeners = useMemo(function () { return ({ + focus: function (e) { + var _a, _b; + var scrollY = (_a = state.scrollYs.get(e.target)) !== null && _a !== void 0 ? _a : 0; + window.scrollTo(0, scrollY); + state.focusedKey = (_b = e.target) !== null && _b !== void 0 ? _b : null; + }, + }); }, [state]); + return screenListeners; +} diff --git a/src/lib/hooks/useWebScrollRestoration.native.js b/src/lib/hooks/useWebScrollRestoration.native.js new file mode 100644 index 0000000000..ae4526047e --- /dev/null +++ b/src/lib/hooks/useWebScrollRestoration.native.js @@ -0,0 +1,3 @@ +export function useWebScrollRestoration() { + return undefined; +} diff --git a/src/lib/icons.js b/src/lib/icons.js new file mode 100644 index 0000000000..7ec2c4883f --- /dev/null +++ b/src/lib/icons.js @@ -0,0 +1,63 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import Svg, { Ellipse, Line, Path, Rect } from 'react-native-svg'; +// Copyright (c) 2020 Refactoring UI Inc. +// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE +export function MagnifyingGlassIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 2 : _b, _c = _a.color, color = _c === void 0 ? 'currentColor' : _c; + return (_jsx(Svg, { fill: "none", viewBox: "0 0 24 24", strokeWidth: strokeWidth, stroke: color, width: size || 24, height: size || 24, style: style, children: _jsx(Path, { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" }) })); +} +export function MagnifyingGlassIcon2(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 2 : _b; + return (_jsxs(Svg, { fill: "none", viewBox: "0 0 24 24", strokeWidth: strokeWidth, stroke: "currentColor", width: size || 24, height: size || 24, style: style, children: [_jsx(Ellipse, { cx: "12", cy: "10.5", rx: "9", ry: "9" }), _jsx(Line, { x1: "18.5", y1: "17", x2: "22", y2: "20.5", strokeLinecap: "round" })] })); +} +export function CogIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.5 : _b; + return (_jsxs(Svg, { fill: "none", viewBox: "0 0 24 24", width: size || 32, height: size || 32, strokeWidth: strokeWidth, stroke: "currentColor", style: style, children: [_jsx(Path, { strokeLinecap: "round", strokeLinejoin: "round", d: "M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" }), _jsx(Path, { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" })] })); +} +// Copyright (c) 2020 Refactoring UI Inc. +// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE +export function UserGroupIcon(_a) { + var style = _a.style, size = _a.size; + return (_jsx(Svg, { fill: "none", viewBox: "0 0 24 24", width: size || 32, height: size || 32, strokeWidth: 2, stroke: "currentColor", style: style, children: _jsx(Path, { strokeLinecap: "round", strokeLinejoin: "round", d: "M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" }) })); +} +export function SquareIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.3 : _b; + return (_jsx(Svg, { fill: "none", viewBox: "0 0 24 24", strokeWidth: strokeWidth || 1, stroke: "currentColor", width: size || 24, height: size || 24, style: style, children: _jsx(Rect, { x: "6", y: "6", width: "12", height: "12", strokeLinejoin: "round" }) })); +} +export function RectWideIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.3 : _b; + return (_jsx(Svg, { fill: "none", viewBox: "0 0 24 24", strokeWidth: strokeWidth || 1, stroke: "currentColor", width: size || 24, height: size || 24, style: style, children: _jsx(Rect, { x: "4", y: "6", width: "16", height: "12", strokeLinejoin: "round" }) })); +} +export function RectTallIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.3 : _b; + return (_jsx(Svg, { fill: "none", viewBox: "0 0 24 24", strokeWidth: strokeWidth || 1, stroke: "currentColor", width: size || 24, height: size || 24, style: style, children: _jsx(Rect, { x: "6", y: "4", width: "12", height: "16", strokeLinejoin: "round" }) })); +} +export function ComposeIcon2(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.5 : _b; + return (_jsxs(Svg, { viewBox: "0 0 24 24", stroke: "currentColor", fill: "none", width: size || 24, height: size || 24, style: style, children: [_jsx(Path, { d: "M 20 9 L 20 16 C 20 18.209 18.209 20 16 20 L 8 20 C 5.791 20 4 18.209 4 16 L 4 8 C 4 5.791 5.791 4 8 4 L 15 4", strokeWidth: strokeWidth }), _jsx(Line, { strokeLinecap: "round", x1: "10", y1: "14", x2: "18.5", y2: "5.5", strokeWidth: strokeWidth * 1.5 }), _jsx(Line, { strokeLinecap: "round", x1: "20.5", y1: "3.5", x2: "21", y2: "3", strokeWidth: strokeWidth * 1.5 })] })); +} +export function InfoCircleIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.5 : _b; + return (_jsx(Svg, { fill: "none", viewBox: "0 0 24 24", strokeWidth: strokeWidth, stroke: "currentColor", width: size, height: size, style: style, children: _jsx(Path, { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" }) })); +} +export function HandIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.5 : _b; + return (_jsxs(Svg, { width: size, height: size, viewBox: "0 0 76 76", stroke: "currentColor", strokeWidth: strokeWidth, strokeLinecap: "round", fill: "none", style: style, children: [_jsx(Path, { d: "M33.5 37V11.5C33.5 8.46243 31.0376 6 28 6V6C24.9624 6 22.5 8.46243 22.5 11.5V48V48C22.5 48.5802 21.8139 48.8874 21.3811 48.501L13.2252 41.2189C10.72 38.9821 6.81945 39.4562 4.92296 42.228L4.77978 42.4372C3.17708 44.7796 3.50863 47.9385 5.56275 49.897L16.0965 59.9409C20.9825 64.5996 26.7533 68.231 33.0675 70.6201V70.6201C38.8234 72.798 45.1766 72.798 50.9325 70.6201L51.9256 70.2444C57.4044 68.1713 61.8038 63.9579 64.1113 58.5735V58.5735C65.6874 54.8962 66.5 50.937 66.5 46.9362V22.5C66.5 19.4624 64.0376 17 61 17V17C57.9624 17 55.5 19.4624 55.5 22.5V36.5" }), _jsx(Path, { d: "M55.5 37V11.5C55.5 8.46243 53.0376 6 50 6V6C46.9624 6 44.5 8.46243 44.5 11.5V37" }), _jsx(Path, { d: "M44.5 37V8.5C44.5 5.46243 42.0376 3 39 3V3C35.9624 3 33.5 5.46243 33.5 8.5V37" })] })); +} +export function HashtagIcon(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.5 : _b; + return (_jsxs(Svg, { fill: "none", stroke: "currentColor", viewBox: "0 0 30 30", strokeWidth: strokeWidth, width: size, height: size, style: style, children: [_jsx(Path, { d: "M2 10H28", strokeLinecap: "round" }), _jsx(Path, { d: "M2 20H28", strokeLinecap: "round" }), _jsx(Path, { d: "M11 3L9 27", strokeLinecap: "round" }), _jsx(Path, { d: "M21 3L19 27", strokeLinecap: "round" })] })); +} +// Copyright (c) 2020 Refactoring UI Inc. +// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE +export function ShieldExclamation(_a) { + var style = _a.style, size = _a.size, _b = _a.strokeWidth, strokeWidth = _b === void 0 ? 1.5 : _b; + var color = 'currentColor'; + if (style && + typeof style === 'object' && + 'color' in style && + typeof style.color === 'string') { + color = style.color; + } + return (_jsx(Svg, { width: size, height: size, fill: "none", viewBox: "0 0 24 24", strokeWidth: strokeWidth || 1.5, stroke: color, style: style, children: _jsx(Path, { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z" }) })); +} diff --git a/src/lib/interests.js b/src/lib/interests.js new file mode 100644 index 0000000000..e23fe5ec60 --- /dev/null +++ b/src/lib/interests.js @@ -0,0 +1,78 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { useMemo } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +export var interests = [ + 'animals', + 'art', + 'books', + 'comedy', + 'comics', + 'culture', + 'dev', + 'education', + 'finance', + 'food', + 'gaming', + 'journalism', + 'movies', + 'music', + 'nature', + 'news', + 'pets', + 'photography', + 'politics', + 'science', + 'sports', + 'tech', + 'tv', + 'writers', +]; +// most popular selected interests +export var popularInterests = [ + 'art', + 'gaming', + 'sports', + 'comics', + 'music', + 'politics', + 'photography', + 'science', + 'news', +]; +export function useInterestsDisplayNames() { + var _ = useLingui()._; + return useMemo(function () { + return { + // Keep this alphabetized + animals: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Animals"], ["Animals"])))), + art: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Art"], ["Art"])))), + books: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Books"], ["Books"])))), + comedy: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Comedy"], ["Comedy"])))), + comics: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Comics"], ["Comics"])))), + culture: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Culture"], ["Culture"])))), + dev: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Software Dev"], ["Software Dev"])))), + education: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Education"], ["Education"])))), + finance: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Finance"], ["Finance"])))), + food: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Food"], ["Food"])))), + gaming: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Video Games"], ["Video Games"])))), + journalism: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Journalism"], ["Journalism"])))), + movies: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Movies"], ["Movies"])))), + music: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Music"], ["Music"])))), + nature: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Nature"], ["Nature"])))), + news: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["News"], ["News"])))), + pets: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Pets"], ["Pets"])))), + photography: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Photography"], ["Photography"])))), + politics: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Politics"], ["Politics"])))), + science: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Science"], ["Science"])))), + sports: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Sports"], ["Sports"])))), + tech: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Tech"], ["Tech"])))), + tv: _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["TV"], ["TV"])))), + writers: _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Writers"], ["Writers"])))), + }; + }, [_]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24; diff --git a/src/lib/international-telephone-codes.js b/src/lib/international-telephone-codes.js new file mode 100644 index 0000000000..20fddc2fab --- /dev/null +++ b/src/lib/international-telephone-codes.js @@ -0,0 +1,1276 @@ +/** + * Note: data is from Wikipedia, but some have been removed to match `libphonenumber-js` + * Mostly tiny British overseas territories + Antarctica, all of which + * share codes with a larger country. If you've one of the 10 people from these + * places, you probably know what to do. + */ +export var INTERNATIONAL_TELEPHONE_CODES = { + AD: { + code: '+376', + unicodeFlag: '🇦🇩', + svgFlag: require('../../assets/icons/flags/AD.svg'), + }, + AF: { + code: '+93', + unicodeFlag: '🇦🇫', + svgFlag: require('../../assets/icons/flags/AF.svg'), + }, + AG: { + code: '+1268', + unicodeFlag: '🇦🇬', + svgFlag: require('../../assets/icons/flags/AG.svg'), + }, + AI: { + code: '+1264', + unicodeFlag: '🇦🇮', + svgFlag: require('../../assets/icons/flags/AI.svg'), + }, + AL: { + code: '+355', + unicodeFlag: '🇦🇱', + svgFlag: require('../../assets/icons/flags/AL.svg'), + }, + AM: { + code: '+374', + unicodeFlag: '🇦🇲', + svgFlag: require('../../assets/icons/flags/AM.svg'), + }, + AO: { + code: '+244', + unicodeFlag: '🇦🇴', + svgFlag: require('../../assets/icons/flags/AO.svg'), + }, + // sorry penguins :( + // same as Norfolk Island + // AQ: { + // code: '+672', + // unicodeFlag: '🇦🇶', + // svgFlag: require('../../assets/icons/flags/AQ.svg'), + // }, + AR: { + code: '+54', + unicodeFlag: '🇦🇷', + svgFlag: require('../../assets/icons/flags/AR.svg'), + }, + AS: { + code: '+1684', + unicodeFlag: '🇦🇸', + svgFlag: require('../../assets/icons/flags/AS.svg'), + }, + AT: { + code: '+43', + unicodeFlag: '🇦🇹', + svgFlag: require('../../assets/icons/flags/AT.svg'), + }, + AU: { + code: '+61', + unicodeFlag: '🇦🇺', + svgFlag: require('../../assets/icons/flags/AU.svg'), + }, + AW: { + code: '+297', + unicodeFlag: '🇦🇼', + svgFlag: require('../../assets/icons/flags/AW.svg'), + }, + AX: { + code: '+358', + unicodeFlag: '🇦🇽', + svgFlag: require('../../assets/icons/flags/AX.svg'), + }, + AZ: { + code: '+994', + unicodeFlag: '🇦🇿', + svgFlag: require('../../assets/icons/flags/AZ.svg'), + }, + BA: { + code: '+387', + unicodeFlag: '🇧🇦', + svgFlag: require('../../assets/icons/flags/BA.svg'), + }, + BB: { + code: '+1246', + unicodeFlag: '🇧🇧', + svgFlag: require('../../assets/icons/flags/BB.svg'), + }, + BD: { + code: '+880', + unicodeFlag: '🇧🇩', + svgFlag: require('../../assets/icons/flags/BD.svg'), + }, + BE: { + code: '+32', + unicodeFlag: '🇧🇪', + svgFlag: require('../../assets/icons/flags/BE.svg'), + }, + BF: { + code: '+226', + unicodeFlag: '🇧🇫', + svgFlag: require('../../assets/icons/flags/BF.svg'), + }, + BG: { + code: '+359', + unicodeFlag: '🇧🇬', + svgFlag: require('../../assets/icons/flags/BG.svg'), + }, + BH: { + code: '+973', + unicodeFlag: '🇧🇭', + svgFlag: require('../../assets/icons/flags/BH.svg'), + }, + BI: { + code: '+257', + unicodeFlag: '🇧🇮', + svgFlag: require('../../assets/icons/flags/BI.svg'), + }, + BJ: { + code: '+229', + unicodeFlag: '🇧🇯', + svgFlag: require('../../assets/icons/flags/BJ.svg'), + }, + BL: { + code: '+590', + unicodeFlag: '🇧🇱', + svgFlag: require('../../assets/icons/flags/BL.svg'), + }, + BM: { + code: '+1441', + unicodeFlag: '🇧🇲', + svgFlag: require('../../assets/icons/flags/BM.svg'), + }, + BN: { + code: '+673', + unicodeFlag: '🇧🇳', + svgFlag: require('../../assets/icons/flags/BN.svg'), + }, + BO: { + code: '+591', + unicodeFlag: '🇧🇴', + svgFlag: require('../../assets/icons/flags/BO.svg'), + }, + BQ: { + code: '+5997', + unicodeFlag: '🇧🇶', + svgFlag: require('../../assets/icons/flags/BQ.svg'), + }, + BR: { + code: '+55', + unicodeFlag: '🇧🇷', + svgFlag: require('../../assets/icons/flags/BR.svg'), + }, + BT: { + code: '+975', + unicodeFlag: '🇧🇹', + svgFlag: require('../../assets/icons/flags/BT.svg'), + }, + // same as Norway + // BV: { + // code: '+47', + // unicodeFlag: '🇧🇻', + // svgFlag: require('../../assets/icons/flags/BV.svg'), + // }, + BW: { + code: '+267', + unicodeFlag: '🇧🇼', + svgFlag: require('../../assets/icons/flags/BW.svg'), + }, + BY: { + code: '+375', + unicodeFlag: '🇧🇾', + svgFlag: require('../../assets/icons/flags/BY.svg'), + }, + BZ: { + code: '+501', + unicodeFlag: '🇧🇿', + svgFlag: require('../../assets/icons/flags/BZ.svg'), + }, + CA: { + code: '+1', + unicodeFlag: '🇨🇦', + svgFlag: require('../../assets/icons/flags/CA.svg'), + }, + CH: { + code: '+41', + unicodeFlag: '🇨🇭', + svgFlag: require('../../assets/icons/flags/CH.svg'), + }, + CI: { + code: '+225', + unicodeFlag: '🇨🇮', + svgFlag: require('../../assets/icons/flags/CI.svg'), + }, + CL: { + code: '+56', + unicodeFlag: '🇨🇱', + svgFlag: require('../../assets/icons/flags/CL.svg'), + }, + CM: { + code: '+237', + unicodeFlag: '🇨🇲', + svgFlag: require('../../assets/icons/flags/CM.svg'), + }, + CN: { + code: '+86', + unicodeFlag: '🇨🇳', + svgFlag: require('../../assets/icons/flags/CN.svg'), + }, + CO: { + code: '+57', + unicodeFlag: '🇨🇴', + svgFlag: require('../../assets/icons/flags/CO.svg'), + }, + CR: { + code: '+506', + unicodeFlag: '🇨🇷', + svgFlag: require('../../assets/icons/flags/CR.svg'), + }, + CU: { + code: '+53', + unicodeFlag: '🇨🇺', + svgFlag: require('../../assets/icons/flags/CU.svg'), + }, + CV: { + code: '+238', + unicodeFlag: '🇨🇻', + svgFlag: require('../../assets/icons/flags/CV.svg'), + }, + CW: { + code: '+599', + unicodeFlag: '🇨🇼', + svgFlag: require('../../assets/icons/flags/CW.svg'), + }, + CX: { + code: '+61', + unicodeFlag: '🇨🇽', + svgFlag: require('../../assets/icons/flags/CX.svg'), + }, + CY: { + code: '+357', + unicodeFlag: '🇨🇾', + svgFlag: require('../../assets/icons/flags/CY.svg'), + }, + DE: { + code: '+49', + unicodeFlag: '🇩🇪', + svgFlag: require('../../assets/icons/flags/DE.svg'), + }, + DJ: { + code: '+253', + unicodeFlag: '🇩🇯', + svgFlag: require('../../assets/icons/flags/DJ.svg'), + }, + DK: { + code: '+45', + unicodeFlag: '🇩🇰', + svgFlag: require('../../assets/icons/flags/DK.svg'), + }, + DM: { + code: '+767', + unicodeFlag: '🇩🇲', + svgFlag: require('../../assets/icons/flags/DM.svg'), + }, + DZ: { + code: '+213', + unicodeFlag: '🇩🇿', + svgFlag: require('../../assets/icons/flags/DZ.svg'), + }, + EC: { + code: '+593', + unicodeFlag: '🇪🇨', + svgFlag: require('../../assets/icons/flags/EC.svg'), + }, + EE: { + code: '+372', + unicodeFlag: '🇪🇪', + svgFlag: require('../../assets/icons/flags/EE.svg'), + }, + EG: { + code: '+20', + unicodeFlag: '🇪🇬', + svgFlag: require('../../assets/icons/flags/EG.svg'), + }, + EH: { + code: '+212', + unicodeFlag: '🇪🇭', + svgFlag: require('../../assets/icons/flags/EH.svg'), + }, + ER: { + code: '+291', + unicodeFlag: '🇪🇷', + svgFlag: require('../../assets/icons/flags/ER.svg'), + }, + ES: { + code: '+34', + unicodeFlag: '🇪🇸', + svgFlag: require('../../assets/icons/flags/ES.svg'), + }, + ET: { + code: '+251', + unicodeFlag: '🇪🇹', + svgFlag: require('../../assets/icons/flags/ET.svg'), + }, + FI: { + code: '+358', + unicodeFlag: '🇫🇮', + svgFlag: require('../../assets/icons/flags/FI.svg'), + }, + FJ: { + code: '+679', + unicodeFlag: '🇫🇯', + svgFlag: require('../../assets/icons/flags/FJ.svg'), + }, + FM: { + code: '+691', + unicodeFlag: '🇫🇲', + svgFlag: require('../../assets/icons/flags/FM.svg'), + }, + FR: { + code: '+33', + unicodeFlag: '🇫🇷', + svgFlag: require('../../assets/icons/flags/FR.svg'), + }, + GA: { + code: '+241', + unicodeFlag: '🇬🇦', + svgFlag: require('../../assets/icons/flags/GA.svg'), + }, + GD: { + code: '+1473', + unicodeFlag: '🇬🇩', + svgFlag: require('../../assets/icons/flags/GD.svg'), + }, + GE: { + code: '+995', + unicodeFlag: '🇬🇪', + svgFlag: require('../../assets/icons/flags/GE.svg'), + }, + GF: { + code: '+594', + unicodeFlag: '🇬🇫', + svgFlag: require('../../assets/icons/flags/GF.svg'), + }, + GG: { + code: '+44', + unicodeFlag: '🇬🇬', + svgFlag: require('../../assets/icons/flags/GG.svg'), + }, + GH: { + code: '+233', + unicodeFlag: '🇬🇭', + svgFlag: require('../../assets/icons/flags/GH.svg'), + }, + GI: { + code: '+350', + unicodeFlag: '🇬🇮', + svgFlag: require('../../assets/icons/flags/GI.svg'), + }, + GL: { + code: '+299', + unicodeFlag: '🇬🇱', + svgFlag: require('../../assets/icons/flags/GL.svg'), + }, + GN: { + code: '+224', + unicodeFlag: '🇬🇳', + svgFlag: require('../../assets/icons/flags/GN.svg'), + }, + GP: { + code: '+590', + unicodeFlag: '🇬🇵', + svgFlag: require('../../assets/icons/flags/GP.svg'), + }, + GQ: { + code: '+240', + unicodeFlag: '🇬🇶', + svgFlag: require('../../assets/icons/flags/GQ.svg'), + }, + GR: { + code: '+30', + unicodeFlag: '🇬🇷', + svgFlag: require('../../assets/icons/flags/GR.svg'), + }, + // same as Falkland Islands + // GS: { + // code: '+500', + // unicodeFlag: '🇬🇸', + // svgFlag: require('../../assets/icons/flags/GS.svg'), + // }, + GT: { + code: '+502', + unicodeFlag: '🇬🇹', + svgFlag: require('../../assets/icons/flags/GT.svg'), + }, + GU: { + code: '+1', + unicodeFlag: '🇬🇺', + svgFlag: require('../../assets/icons/flags/GU.svg'), + }, + GW: { + code: '+245', + unicodeFlag: '🇬🇼', + svgFlag: require('../../assets/icons/flags/GW.svg'), + }, + GY: { + code: '+592', + unicodeFlag: '🇬🇾', + svgFlag: require('../../assets/icons/flags/GY.svg'), + }, + HK: { + code: '+852', + unicodeFlag: '🇭🇰', + svgFlag: require('../../assets/icons/flags/HK.svg'), + }, + HN: { + code: '+504', + unicodeFlag: '🇭🇳', + svgFlag: require('../../assets/icons/flags/HN.svg'), + }, + HR: { + code: '+385', + unicodeFlag: '🇭🇷', + svgFlag: require('../../assets/icons/flags/HR.svg'), + }, + HT: { + code: '+509', + unicodeFlag: '🇭🇹', + svgFlag: require('../../assets/icons/flags/HT.svg'), + }, + HU: { + code: '+36', + unicodeFlag: '🇭🇺', + svgFlag: require('../../assets/icons/flags/HU.svg'), + }, + ID: { + code: '+62', + unicodeFlag: '🇮🇩', + svgFlag: require('../../assets/icons/flags/ID.svg'), + }, + IE: { + code: '+353', + unicodeFlag: '🇮🇪', + svgFlag: require('../../assets/icons/flags/IE.svg'), + }, + IL: { + code: '+972', + unicodeFlag: '🇮🇱', + svgFlag: require('../../assets/icons/flags/IL.svg'), + }, + IM: { + code: '+44', + unicodeFlag: '🇮🇲', + svgFlag: require('../../assets/icons/flags/IM.svg'), + }, + IN: { + code: '+91', + unicodeFlag: '🇮🇳', + svgFlag: require('../../assets/icons/flags/IN.svg'), + }, + IO: { + code: '+246', + unicodeFlag: '🇮🇴', + svgFlag: require('../../assets/icons/flags/IO.svg'), + }, + IQ: { + code: '+964', + unicodeFlag: '🇮🇶', + svgFlag: require('../../assets/icons/flags/IQ.svg'), + }, + IR: { + code: '+98', + unicodeFlag: '🇮🇷', + svgFlag: require('../../assets/icons/flags/IR.svg'), + }, + IS: { + code: '+354', + unicodeFlag: '🇮🇸', + svgFlag: require('../../assets/icons/flags/IS.svg'), + }, + IT: { + code: '+39', + unicodeFlag: '🇮🇹', + svgFlag: require('../../assets/icons/flags/IT.svg'), + }, + JE: { + code: '+44', + unicodeFlag: '🇯🇪', + svgFlag: require('../../assets/icons/flags/JE.svg'), + }, + JM: { + code: '+876', + unicodeFlag: '🇯🇲', + svgFlag: require('../../assets/icons/flags/JM.svg'), + }, + JO: { + code: '+962', + unicodeFlag: '🇯🇴', + svgFlag: require('../../assets/icons/flags/JO.svg'), + }, + JP: { + code: '+81', + unicodeFlag: '🇯🇵', + svgFlag: require('../../assets/icons/flags/JP.svg'), + }, + KE: { + code: '+254', + unicodeFlag: '🇰🇪', + svgFlag: require('../../assets/icons/flags/KE.svg'), + }, + KG: { + code: '+996', + unicodeFlag: '🇰🇬', + svgFlag: require('../../assets/icons/flags/KG.svg'), + }, + KH: { + code: '+855', + unicodeFlag: '🇰🇭', + svgFlag: require('../../assets/icons/flags/KH.svg'), + }, + KP: { + code: '+850', + unicodeFlag: '🇰🇵', + svgFlag: require('../../assets/icons/flags/KP.svg'), + }, + KR: { + code: '+82', + unicodeFlag: '🇰🇷', + svgFlag: require('../../assets/icons/flags/KR.svg'), + }, + KI: { + code: '+686', + unicodeFlag: '🇰🇮', + svgFlag: require('../../assets/icons/flags/KI.svg'), + }, + KN: { + code: '+1869', + unicodeFlag: '🇰🇳', + svgFlag: require('../../assets/icons/flags/KN.svg'), + }, + KW: { + code: '+965', + unicodeFlag: '🇰🇼', + svgFlag: require('../../assets/icons/flags/KW.svg'), + }, + KZ: { + code: '+7', + unicodeFlag: '🇰🇿', + svgFlag: require('../../assets/icons/flags/KZ.svg'), + }, + LB: { + code: '+961', + unicodeFlag: '🇱🇧', + svgFlag: require('../../assets/icons/flags/LB.svg'), + }, + LC: { + code: '+1758', + unicodeFlag: '🇱🇨', + svgFlag: require('../../assets/icons/flags/LC.svg'), + }, + LI: { + code: '+423', + unicodeFlag: '🇱🇮', + svgFlag: require('../../assets/icons/flags/LI.svg'), + }, + LK: { + code: '+94', + unicodeFlag: '🇱🇰', + svgFlag: require('../../assets/icons/flags/LK.svg'), + }, + LR: { + code: '+231', + unicodeFlag: '🇱🇷', + svgFlag: require('../../assets/icons/flags/LR.svg'), + }, + LS: { + code: '+266', + unicodeFlag: '🇱🇸', + svgFlag: require('../../assets/icons/flags/LS.svg'), + }, + LT: { + code: '+370', + unicodeFlag: '🇱🇹', + svgFlag: require('../../assets/icons/flags/LT.svg'), + }, + LU: { + code: '+352', + unicodeFlag: '🇱🇺', + svgFlag: require('../../assets/icons/flags/LU.svg'), + }, + LV: { + code: '+371', + unicodeFlag: '🇱🇻', + svgFlag: require('../../assets/icons/flags/LV.svg'), + }, + LY: { + code: '+218', + unicodeFlag: '🇱🇾', + svgFlag: require('../../assets/icons/flags/LY.svg'), + }, + MA: { + code: '+212', + unicodeFlag: '🇲🇦', + svgFlag: require('../../assets/icons/flags/MA.svg'), + }, + MC: { + code: '+377', + unicodeFlag: '🇲🇨', + svgFlag: require('../../assets/icons/flags/MC.svg'), + }, + ME: { + code: '+382', + unicodeFlag: '🇲🇪', + svgFlag: require('../../assets/icons/flags/ME.svg'), + }, + MF: { + code: '+590', + unicodeFlag: '🇲🇫', + svgFlag: require('../../assets/icons/flags/MF.svg'), + }, + MG: { + code: '+261', + unicodeFlag: '🇲🇬', + svgFlag: require('../../assets/icons/flags/MG.svg'), + }, + ML: { + code: '+223', + unicodeFlag: '🇲🇱', + svgFlag: require('../../assets/icons/flags/ML.svg'), + }, + MM: { + code: '+95', + unicodeFlag: '🇲🇲', + svgFlag: require('../../assets/icons/flags/MM.svg'), + }, + MN: { + code: '+976', + unicodeFlag: '🇲🇳', + svgFlag: require('../../assets/icons/flags/MN.svg'), + }, + MO: { + code: '+853', + unicodeFlag: '🇲🇴', + svgFlag: require('../../assets/icons/flags/MO.svg'), + }, + MQ: { + code: '+596', + unicodeFlag: '🇲🇶', + svgFlag: require('../../assets/icons/flags/MQ.svg'), + }, + MR: { + code: '+222', + unicodeFlag: '🇲🇷', + svgFlag: require('../../assets/icons/flags/MR.svg'), + }, + MS: { + code: '+1664', + unicodeFlag: '🇲🇸', + svgFlag: require('../../assets/icons/flags/MS.svg'), + }, + MT: { + code: '+356', + unicodeFlag: '🇲🇹', + svgFlag: require('../../assets/icons/flags/MT.svg'), + }, + MU: { + code: '+230', + unicodeFlag: '🇲🇺', + svgFlag: require('../../assets/icons/flags/MU.svg'), + }, + MV: { + code: '+960', + unicodeFlag: '🇲🇻', + svgFlag: require('../../assets/icons/flags/MV.svg'), + }, + MW: { + code: '+265', + unicodeFlag: '🇲🇼', + svgFlag: require('../../assets/icons/flags/MW.svg'), + }, + MX: { + code: '+52', + unicodeFlag: '🇲🇽', + svgFlag: require('../../assets/icons/flags/MX.svg'), + }, + MY: { + code: '+60', + unicodeFlag: '🇲🇾', + svgFlag: require('../../assets/icons/flags/MY.svg'), + }, + MZ: { + code: '+258', + unicodeFlag: '🇲🇿', + svgFlag: require('../../assets/icons/flags/MZ.svg'), + }, + NA: { + code: '+264', + unicodeFlag: '🇳🇦', + svgFlag: require('../../assets/icons/flags/NA.svg'), + }, + NC: { + code: '+687', + unicodeFlag: '🇳🇨', + svgFlag: require('../../assets/icons/flags/NC.svg'), + }, + NF: { + code: '+672', + unicodeFlag: '🇳🇫', + svgFlag: require('../../assets/icons/flags/NF.svg'), + }, + NG: { + code: '+234', + unicodeFlag: '🇳🇬', + svgFlag: require('../../assets/icons/flags/NG.svg'), + }, + NI: { + code: '+505', + unicodeFlag: '🇳🇮', + svgFlag: require('../../assets/icons/flags/NI.svg'), + }, + NO: { + code: '+47', + unicodeFlag: '🇳🇴', + svgFlag: require('../../assets/icons/flags/NO.svg'), + }, + NP: { + code: '+977', + unicodeFlag: '🇳🇵', + svgFlag: require('../../assets/icons/flags/NP.svg'), + }, + NR: { + code: '+674', + unicodeFlag: '🇳🇷', + svgFlag: require('../../assets/icons/flags/NR.svg'), + }, + NU: { + code: '+683', + unicodeFlag: '🇳🇺', + svgFlag: require('../../assets/icons/flags/NU.svg'), + }, + NZ: { + code: '+64', + unicodeFlag: '🇳🇿', + svgFlag: require('../../assets/icons/flags/NZ.svg'), + }, + OM: { + code: '+968', + unicodeFlag: '🇴🇲', + svgFlag: require('../../assets/icons/flags/OM.svg'), + }, + PA: { + code: '+507', + unicodeFlag: '🇵🇦', + svgFlag: require('../../assets/icons/flags/PA.svg'), + }, + PE: { + code: '+51', + unicodeFlag: '🇵🇪', + svgFlag: require('../../assets/icons/flags/PE.svg'), + }, + PF: { + code: '+689', + unicodeFlag: '🇵🇫', + svgFlag: require('../../assets/icons/flags/PF.svg'), + }, + PG: { + code: '+675', + unicodeFlag: '🇵🇬', + svgFlag: require('../../assets/icons/flags/PG.svg'), + }, + PK: { + code: '+92', + unicodeFlag: '🇵🇰', + svgFlag: require('../../assets/icons/flags/PK.svg'), + }, + PL: { + code: '+48', + unicodeFlag: '🇵🇱', + svgFlag: require('../../assets/icons/flags/PL.svg'), + }, + PM: { + code: '+508', + unicodeFlag: '🇵🇲', + svgFlag: require('../../assets/icons/flags/PM.svg'), + }, + // same as New Zealand + // PN: { + // code: '+64', + // unicodeFlag: '🇵🇳', + // svgFlag: require('../../assets/icons/flags/PN.svg'), + // }, + PR: { + code: '+1', + unicodeFlag: '🇵🇷', + svgFlag: require('../../assets/icons/flags/PR.svg'), + }, + PS: { + code: '+970', + unicodeFlag: '🇵🇸', + svgFlag: require('../../assets/icons/flags/PS.svg'), + }, + PT: { + code: '+351', + unicodeFlag: '🇵🇹', + svgFlag: require('../../assets/icons/flags/PT.svg'), + }, + PW: { + code: '+680', + unicodeFlag: '🇵🇼', + svgFlag: require('../../assets/icons/flags/PW.svg'), + }, + PY: { + code: '+595', + unicodeFlag: '🇵🇾', + svgFlag: require('../../assets/icons/flags/PY.svg'), + }, + QA: { + code: '+974', + unicodeFlag: '🇶🇦', + svgFlag: require('../../assets/icons/flags/QA.svg'), + }, + RE: { + code: '+262', + unicodeFlag: '🇷🇪', + svgFlag: require('../../assets/icons/flags/RE.svg'), + }, + RO: { + code: '+40', + unicodeFlag: '🇷🇴', + svgFlag: require('../../assets/icons/flags/RO.svg'), + }, + RS: { + code: '+381', + unicodeFlag: '🇷🇸', + svgFlag: require('../../assets/icons/flags/RS.svg'), + }, + RU: { + code: '+7', + unicodeFlag: '🇷🇺', + svgFlag: require('../../assets/icons/flags/RU.svg'), + }, + RW: { + code: '+250', + unicodeFlag: '🇷🇼', + svgFlag: require('../../assets/icons/flags/RW.svg'), + }, + SA: { + code: '+966', + unicodeFlag: '🇸🇦', + svgFlag: require('../../assets/icons/flags/SA.svg'), + }, + SB: { + code: '+677', + unicodeFlag: '🇸🇧', + svgFlag: require('../../assets/icons/flags/SB.svg'), + }, + SC: { + code: '+248', + unicodeFlag: '🇸🇨', + svgFlag: require('../../assets/icons/flags/SC.svg'), + }, + SE: { + code: '+46', + unicodeFlag: '🇸🇪', + svgFlag: require('../../assets/icons/flags/SE.svg'), + }, + SG: { + code: '+65', + unicodeFlag: '🇸🇬', + svgFlag: require('../../assets/icons/flags/SG.svg'), + }, + SH: { + code: '+290', + unicodeFlag: '🇸🇭', + svgFlag: require('../../assets/icons/flags/SH.svg'), + }, + SI: { + code: '+386', + unicodeFlag: '🇸🇮', + svgFlag: require('../../assets/icons/flags/SI.svg'), + }, + SJ: { + code: '+4779', + unicodeFlag: '🇸🇯', + svgFlag: require('../../assets/icons/flags/SJ.svg'), + }, + SK: { + code: '+421', + unicodeFlag: '🇸🇰', + svgFlag: require('../../assets/icons/flags/SK.svg'), + }, + SL: { + code: '+232', + unicodeFlag: '🇸🇱', + svgFlag: require('../../assets/icons/flags/SL.svg'), + }, + SM: { + code: '+378', + unicodeFlag: '🇸🇲', + svgFlag: require('../../assets/icons/flags/SM.svg'), + }, + SN: { + code: '+221', + unicodeFlag: '🇸🇳', + svgFlag: require('../../assets/icons/flags/SN.svg'), + }, + SO: { + code: '+252', + unicodeFlag: '🇸🇴', + svgFlag: require('../../assets/icons/flags/SO.svg'), + }, + SR: { + code: '+597', + unicodeFlag: '🇸🇷', + svgFlag: require('../../assets/icons/flags/SR.svg'), + }, + SS: { + code: '+211', + unicodeFlag: '🇸🇸', + svgFlag: require('../../assets/icons/flags/SS.svg'), + }, + ST: { + code: '+239', + unicodeFlag: '🇸🇹', + svgFlag: require('../../assets/icons/flags/ST.svg'), + }, + SV: { + code: '+503', + unicodeFlag: '🇸🇻', + svgFlag: require('../../assets/icons/flags/SV.svg'), + }, + SX: { + code: '+1721', + unicodeFlag: '🇸🇽', + svgFlag: require('../../assets/icons/flags/SX.svg'), + }, + SY: { + code: '+963', + unicodeFlag: '🇸🇾', + svgFlag: require('../../assets/icons/flags/SY.svg'), + }, + TD: { + code: '+235', + unicodeFlag: '🇹🇩', + svgFlag: require('../../assets/icons/flags/TD.svg'), + }, + TG: { + code: '+228', + unicodeFlag: '🇹🇬', + svgFlag: require('../../assets/icons/flags/TG.svg'), + }, + TH: { + code: '+66', + unicodeFlag: '🇹🇭', + svgFlag: require('../../assets/icons/flags/TH.svg'), + }, + TJ: { + code: '+992', + unicodeFlag: '🇹🇯', + svgFlag: require('../../assets/icons/flags/TJ.svg'), + }, + TK: { + code: '+690', + unicodeFlag: '🇹🇰', + svgFlag: require('../../assets/icons/flags/TK.svg'), + }, + TL: { + code: '+670', + unicodeFlag: '🇹🇱', + svgFlag: require('../../assets/icons/flags/TL.svg'), + }, + TM: { + code: '+993', + unicodeFlag: '🇹🇲', + svgFlag: require('../../assets/icons/flags/TM.svg'), + }, + TN: { + code: '+216', + unicodeFlag: '🇹🇳', + svgFlag: require('../../assets/icons/flags/TN.svg'), + }, + TO: { + code: '+676', + unicodeFlag: '🇹🇴', + svgFlag: require('../../assets/icons/flags/TO.svg'), + }, + TR: { + code: '+90', + unicodeFlag: '🇹🇷', + svgFlag: require('../../assets/icons/flags/TR.svg'), + }, + TT: { + code: '+868', + unicodeFlag: '🇹🇹', + svgFlag: require('../../assets/icons/flags/TT.svg'), + }, + TV: { + code: '+688', + unicodeFlag: '🇹🇻', + svgFlag: require('../../assets/icons/flags/TV.svg'), + }, + TZ: { + code: '+255', + unicodeFlag: '🇹🇿', + svgFlag: require('../../assets/icons/flags/TZ.svg'), + }, + UA: { + code: '+380', + unicodeFlag: '🇺🇦', + svgFlag: require('../../assets/icons/flags/UA.svg'), + }, + UG: { + code: '+256', + unicodeFlag: '🇺🇬', + svgFlag: require('../../assets/icons/flags/UG.svg'), + }, + US: { + code: '+1', + unicodeFlag: '🇺🇸', + svgFlag: require('../../assets/icons/flags/US.svg'), + }, + UY: { + code: '+598', + unicodeFlag: '🇺🇾', + svgFlag: require('../../assets/icons/flags/UY.svg'), + }, + UZ: { + code: '+998', + unicodeFlag: '🇺🇿', + svgFlag: require('../../assets/icons/flags/UZ.svg'), + }, + VC: { + code: '+1784', + unicodeFlag: '🇻🇨', + svgFlag: require('../../assets/icons/flags/VC.svg'), + }, + VE: { + code: '+58', + unicodeFlag: '🇻🇪', + svgFlag: require('../../assets/icons/flags/VE.svg'), + }, + VG: { + code: '+1284', + unicodeFlag: '🇻🇬', + svgFlag: require('../../assets/icons/flags/VG.svg'), + }, + VI: { + code: '+1340', + unicodeFlag: '🇻🇮', + svgFlag: require('../../assets/icons/flags/VI.svg'), + }, + VN: { + code: '+84', + unicodeFlag: '🇻🇳', + svgFlag: require('../../assets/icons/flags/VN.svg'), + }, + VU: { + code: '+678', + unicodeFlag: '🇻🇺', + svgFlag: require('../../assets/icons/flags/VU.svg'), + }, + WF: { + code: '+681', + unicodeFlag: '🇼🇫', + svgFlag: require('../../assets/icons/flags/WF.svg'), + }, + WS: { + code: '+685', + unicodeFlag: '🇼🇸', + svgFlag: require('../../assets/icons/flags/WS.svg'), + }, + YE: { + code: '+967', + unicodeFlag: '🇾🇪', + svgFlag: require('../../assets/icons/flags/YE.svg'), + }, + YT: { + code: '+262', + unicodeFlag: '🇾🇹', + svgFlag: require('../../assets/icons/flags/YT.svg'), + }, + ZA: { + code: '+27', + unicodeFlag: '🇿🇦', + svgFlag: require('../../assets/icons/flags/ZA.svg'), + }, + ZM: { + code: '+260', + unicodeFlag: '🇿🇲', + svgFlag: require('../../assets/icons/flags/ZM.svg'), + }, + ZW: { + code: '+263', + unicodeFlag: '🇿🇼', + svgFlag: require('../../assets/icons/flags/ZW.svg'), + }, + SZ: { + code: '+268', + unicodeFlag: '🇸🇿', + svgFlag: require('../../assets/icons/flags/SZ.svg'), + }, + MK: { + code: '+389', + unicodeFlag: '🇲🇰', + svgFlag: require('../../assets/icons/flags/MK.svg'), + }, + PH: { + code: '+63', + unicodeFlag: '🇵🇭', + svgFlag: require('../../assets/icons/flags/PH.svg'), + }, + NL: { + code: '+31', + unicodeFlag: '🇳🇱', + svgFlag: require('../../assets/icons/flags/NL.svg'), + }, + AE: { + code: '+971', + unicodeFlag: '🇦🇪', + svgFlag: require('../../assets/icons/flags/AE.svg'), + }, + MD: { + code: '+373', + unicodeFlag: '🇲🇩', + svgFlag: require('../../assets/icons/flags/MD.svg'), + }, + GM: { + code: '+220', + unicodeFlag: '🇬🇲', + svgFlag: require('../../assets/icons/flags/GM.svg'), + }, + DO: { + code: '+1', + unicodeFlag: '🇩🇴', + svgFlag: require('../../assets/icons/flags/DO.svg'), + }, + SD: { + code: '+249', + unicodeFlag: '🇸🇩', + svgFlag: require('../../assets/icons/flags/SD.svg'), + }, + LA: { + code: '+856', + unicodeFlag: '🇱🇦', + svgFlag: require('../../assets/icons/flags/LA.svg'), + }, + TW: { + code: '+886', + unicodeFlag: '🇹🇼', + svgFlag: require('../../assets/icons/flags/TW.svg'), + }, + CG: { + code: '+242', + unicodeFlag: '🇨🇬', + svgFlag: require('../../assets/icons/flags/CG.svg'), + }, + CZ: { + code: '+420', + unicodeFlag: '🇨🇿', + svgFlag: require('../../assets/icons/flags/CZ.svg'), + }, + GB: { + code: '+44', + unicodeFlag: '🇬🇧', + svgFlag: require('../../assets/icons/flags/GB.svg'), + }, + NE: { + code: '+227', + unicodeFlag: '🇳🇪', + svgFlag: require('../../assets/icons/flags/NE.svg'), + }, + CD: { + code: '+243', + unicodeFlag: '🇨🇩', + svgFlag: require('../../assets/icons/flags/CD.svg'), + }, + BS: { + code: '+1 242', + unicodeFlag: '🇧🇸', + svgFlag: require('../../assets/icons/flags/BS.svg'), + }, + CC: { + code: '+61 891', + unicodeFlag: '🇨🇨', + svgFlag: require('../../assets/icons/flags/CC.svg'), + }, + CF: { + code: '+236', + unicodeFlag: '🇨🇫', + svgFlag: require('../../assets/icons/flags/CF.svg'), + }, + CK: { + code: '+682', + unicodeFlag: '🇨🇰', + svgFlag: require('../../assets/icons/flags/CK.svg'), + }, + FK: { + code: '+500', + unicodeFlag: '🇫🇰', + svgFlag: require('../../assets/icons/flags/FK.svg'), + }, + FO: { + code: '+298', + unicodeFlag: '🇫🇴', + svgFlag: require('../../assets/icons/flags/FO.svg'), + }, + // same as Norfolk Island + // HM: { + // code: '+672', + // unicodeFlag: '🇭🇲', + // svgFlag: require('../../assets/icons/flags/HM.svg'), + // }, + KM: { + code: '+269', + unicodeFlag: '🇰🇲', + svgFlag: require('../../assets/icons/flags/KM.svg'), + }, + KY: { + code: '+1 345', + unicodeFlag: '🇰🇾', + svgFlag: require('../../assets/icons/flags/KY.svg'), + }, + MH: { + code: '+692', + unicodeFlag: '🇲🇭', + svgFlag: require('../../assets/icons/flags/MH.svg'), + }, + MP: { + code: '+1 670', + unicodeFlag: '🇲🇵', + svgFlag: require('../../assets/icons/flags/MP.svg'), + }, + TC: { + code: '+1 649', + unicodeFlag: '🇹🇨', + svgFlag: require('../../assets/icons/flags/TC.svg'), + }, + // same as Norfolk Island + // TF: { + // code: '+672', + // unicodeFlag: '🇹🇫', + // svgFlag: require('../../assets/icons/flags/TF.svg'), + // }, + // same as US mainland + // UM: { + // code: '+1', + // unicodeFlag: '🇺🇲', + // svgFlag: require('../../assets/icons/flags/UM.svg'), + // }, + VA: { + code: '+39', + unicodeFlag: '🇻🇦', + svgFlag: require('../../assets/icons/flags/VA.svg'), + }, + XK: { + code: '+383', + unicodeFlag: '🇽🇰', + svgFlag: require('../../assets/icons/flags/XK.svg'), + }, +}; +var DEFAULT_PHONE_COUNTRY = 'US'; +export function getDefaultCountry(location) { + var _a; + var locationCountryCode = (_a = location === null || location === void 0 ? void 0 : location.countryCode) === null || _a === void 0 ? void 0 : _a.toUpperCase(); + if (locationCountryCode && + locationCountryCode in INTERNATIONAL_TELEPHONE_CODES) { + return locationCountryCode; + } + return DEFAULT_PHONE_COUNTRY; +} diff --git a/src/lib/jwt.js b/src/lib/jwt.js new file mode 100644 index 0000000000..e0ed419825 --- /dev/null +++ b/src/lib/jwt.js @@ -0,0 +1,27 @@ +import { jwtDecode } from 'jwt-decode'; +import { logger } from '#/logger'; +/** + * Simple check if a JWT token has expired. Does *not* validate the token or check for revocation status, + * just checks the expiration time. + * + * @param token The JWT token to check. + * @returns `true` if the token has expired, `false` otherwise. + */ +export function isJwtExpired(token) { + try { + var payload = jwtDecode(token); + if (!payload.exp) + return true; + var now = Math.floor(Date.now() / 1000); + return now >= payload.exp; + } + catch (_a) { + logger.error("session: could not decode jwt"); + return true; // invalid token or parse error + } +} +export function isAppPassword(token) { + var payload = jwtDecode(token); + // @ts-ignore + return payload.scope === 'com.atproto.appPass'; +} diff --git a/src/lib/link-meta/link-meta.js b/src/lib/link-meta/link-meta.js new file mode 100644 index 0000000000..cdad95ed76 --- /dev/null +++ b/src/lib/link-meta/link-meta.js @@ -0,0 +1,1336 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { LINK_META_PROXY } from '#/lib/constants'; +import { getGiphyMetaUri } from '#/lib/strings/embed-player'; +import { parseStarterPackUri } from '#/lib/strings/starter-pack'; +import { isBskyAppUrl } from '../strings/url-helpers'; +export var LikelyType; +(function (LikelyType) { + LikelyType[LikelyType["HTML"] = 0] = "HTML"; + LikelyType[LikelyType["Text"] = 1] = "Text"; + LikelyType[LikelyType["Image"] = 2] = "Image"; + LikelyType[LikelyType["Video"] = 3] = "Video"; + LikelyType[LikelyType["Audio"] = 4] = "Audio"; + LikelyType[LikelyType["AtpData"] = 5] = "AtpData"; + LikelyType[LikelyType["Other"] = 6] = "Other"; +})(LikelyType || (LikelyType = {})); +export function getLinkMeta(agent_1, url_1) { + return __awaiter(this, arguments, void 0, function (agent, url, timeout) { + var urlp, shouldFollowRedirect, giphyMetaUri, likelyType, meta, htmlExemptedHostnames, isExemptedFromHtmlCheck, controller, to, response, body, e_1; + if (timeout === void 0) { timeout = 15e3; } + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isBskyAppUrl(url) && !parseStarterPackUri(url)) { + return [2 /*return*/, { + likelyType: LikelyType.AtpData, + url: url, + }]; + } + shouldFollowRedirect = false; + try { + urlp = new URL(url); + giphyMetaUri = getGiphyMetaUri(urlp); + if (giphyMetaUri) { + url = giphyMetaUri; + urlp = new URL(url); + } + // follow redirects for soundcloud shortlinks + // QUESTION - do we want to follow redirects in other cases? -sfn + shouldFollowRedirect = urlp.hostname === 'on.soundcloud.com'; + } + catch (e) { + return [2 /*return*/, { + error: 'Invalid URL', + likelyType: LikelyType.Other, + url: url, + }]; + } + likelyType = getLikelyType(urlp); + meta = { + likelyType: likelyType, + url: url, + }; + htmlExemptedHostnames = ['storage.courtlistener.com']; + isExemptedFromHtmlCheck = htmlExemptedHostnames.includes(urlp.hostname); + // Skip early return only for hosts exempted from the HTML check + if (likelyType !== LikelyType.HTML && !isExemptedFromHtmlCheck) { + return [2 /*return*/, meta]; + } + controller = new AbortController(); + to = setTimeout(function () { return controller.abort(); }, timeout || 5e3); + _a.label = 1; + case 1: + _a.trys.push([1, 4, 5, 6]); + return [4 /*yield*/, fetch("".concat(LINK_META_PROXY(agent.serviceUrl.toString() || '')).concat(encodeURIComponent(url)), { signal: controller.signal })]; + case 2: + response = _a.sent(); + return [4 /*yield*/, response.json()]; + case 3: + body = _a.sent(); + if (body.error !== '') { + throw new Error(body.error); + } + meta.description = body.description; + meta.image = body.image; + meta.title = body.title; + if (shouldFollowRedirect) { + meta.url = body.url; + } + return [3 /*break*/, 6]; + case 4: + e_1 = _a.sent(); + // failed + console.error(e_1); + meta.error = e_1 instanceof Error ? e_1.toString() : 'Failed to fetch link'; + return [3 /*break*/, 6]; + case 5: + clearTimeout(to); + return [7 /*endfinally*/]; + case 6: return [2 /*return*/, meta]; + } + }); + }); +} +export function getLikelyType(url) { + if (typeof url === 'string') { + try { + url = new URL(url); + } + catch (e) { + return LikelyType.Other; + } + } + var ext = url.pathname.split('.').pop() || ''; + if (ext === 'html' || ext === 'htm' || ext === 'php') { + return LikelyType.HTML; + } + var mimeType = EXT_MIME_TYPES[ext]; + if (!mimeType) { + return LikelyType.HTML; + } + if (mimeType.startsWith('text/')) { + return LikelyType.Text; + } + if (mimeType.startsWith('image/')) { + return LikelyType.Image; + } + if (mimeType.startsWith('video/')) { + return LikelyType.Video; + } + if (mimeType.startsWith('audio/')) { + return LikelyType.Audio; + } + return LikelyType.Other; +} +var EXT_MIME_TYPES = { + '123': 'application/vnd.lotus-1-2-3', + '1km': 'application/vnd.1000minds.decision-model+xml', + '3dml': 'text/vnd.in3d.3dml', + '3ds': 'image/x-3ds', + '3g2': 'video/3gpp2', + '3gp': 'video/3gpp', + '3gpp': 'video/3gpp', + '3mf': 'model/3mf', + '7z': 'application/x-7z-compressed', + aab: 'application/x-authorware-bin', + aac: 'audio/x-aac', + aam: 'application/x-authorware-map', + aas: 'application/x-authorware-seg', + abw: 'application/x-abiword', + ac: 'application/vnd.nokia.n-gage.ac+xml', + acc: 'application/vnd.americandynamics.acc', + ace: 'application/x-ace-compressed', + acu: 'application/vnd.acucobol', + acutc: 'application/vnd.acucorp', + adp: 'audio/adpcm', + aep: 'application/vnd.audiograph', + afm: 'application/x-font-type1', + afp: 'application/vnd.ibm.modcap', + age: 'application/vnd.age', + ahead: 'application/vnd.ahead.space', + ai: 'application/postscript', + aif: 'audio/x-aiff', + aifc: 'audio/x-aiff', + aiff: 'audio/x-aiff', + air: 'application/vnd.adobe.air-application-installer-package+zip', + ait: 'application/vnd.dvb.ait', + ami: 'application/vnd.amiga.ami', + amr: 'audio/amr', + apk: 'application/vnd.android.package-archive', + apng: 'image/apng', + appcache: 'text/cache-manifest', + application: 'application/x-ms-application', + apr: 'application/vnd.lotus-approach', + arc: 'application/x-freearc', + arj: 'application/x-arj', + asc: 'application/pgp-signature', + asf: 'video/x-ms-asf', + asm: 'text/x-asm', + aso: 'application/vnd.accpac.simply.aso', + asx: 'video/x-ms-asf', + atc: 'application/vnd.acucorp', + atom: 'application/atom+xml', + atomcat: 'application/atomcat+xml', + atomdeleted: 'application/atomdeleted+xml', + atomsvc: 'application/atomsvc+xml', + atx: 'application/vnd.antix.game-component', + au: 'audio/basic', + avi: 'video/x-msvideo', + avif: 'image/avif', + aw: 'application/applixware', + azf: 'application/vnd.airzip.filesecure.azf', + azs: 'application/vnd.airzip.filesecure.azs', + azv: 'image/vnd.airzip.accelerator.azv', + azw: 'application/vnd.amazon.ebook', + b16: 'image/vnd.pco.b16', + bat: 'application/x-msdownload', + bcpio: 'application/x-bcpio', + bdf: 'application/x-font-bdf', + bdm: 'application/vnd.syncml.dm+wbxml', + bdoc: 'application/x-bdoc', + bed: 'application/vnd.realvnc.bed', + bh2: 'application/vnd.fujitsu.oasysprs', + bin: 'application/octet-stream', + blb: 'application/x-blorb', + blorb: 'application/x-blorb', + bmi: 'application/vnd.bmi', + bmml: 'application/vnd.balsamiq.bmml+xml', + bmp: 'image/x-ms-bmp', + book: 'application/vnd.framemaker', + box: 'application/vnd.previewsystems.box', + boz: 'application/x-bzip2', + bpk: 'application/octet-stream', + bsp: 'model/vnd.valve.source.compiled-map', + btif: 'image/prs.btif', + buffer: 'application/octet-stream', + bz: 'application/x-bzip', + bz2: 'application/x-bzip2', + c: 'text/x-c', + c11amc: 'application/vnd.cluetrust.cartomobile-config', + c11amz: 'application/vnd.cluetrust.cartomobile-config-pkg', + c4d: 'application/vnd.clonk.c4group', + c4f: 'application/vnd.clonk.c4group', + c4g: 'application/vnd.clonk.c4group', + c4p: 'application/vnd.clonk.c4group', + c4u: 'application/vnd.clonk.c4group', + cab: 'application/vnd.ms-cab-compressed', + caf: 'audio/x-caf', + cap: 'application/vnd.tcpdump.pcap', + car: 'application/vnd.curl.car', + cat: 'application/vnd.ms-pki.seccat', + cb7: 'application/x-cbr', + cba: 'application/x-cbr', + cbr: 'application/x-cbr', + cbt: 'application/x-cbr', + cbz: 'application/x-cbr', + cc: 'text/x-c', + cco: 'application/x-cocoa', + cct: 'application/x-director', + ccxml: 'application/ccxml+xml', + cdbcmsg: 'application/vnd.contact.cmsg', + cdf: 'application/x-netcdf', + cdfx: 'application/cdfx+xml', + cdkey: 'application/vnd.mediastation.cdkey', + cdmia: 'application/cdmi-capability', + cdmic: 'application/cdmi-container', + cdmid: 'application/cdmi-domain', + cdmio: 'application/cdmi-object', + cdmiq: 'application/cdmi-queue', + cdx: 'chemical/x-cdx', + cdxml: 'application/vnd.chemdraw+xml', + cdy: 'application/vnd.cinderella', + cer: 'application/pkix-cert', + cfs: 'application/x-cfs-compressed', + cgm: 'image/cgm', + chat: 'application/x-chat', + chm: 'application/vnd.ms-htmlhelp', + chrt: 'application/vnd.kde.kchart', + cif: 'chemical/x-cif', + cii: 'application/vnd.anser-web-certificate-issue-initiation', + cil: 'application/vnd.ms-artgalry', + cjs: 'application/node', + cla: 'application/vnd.claymore', + class: 'application/java-vm', + clkk: 'application/vnd.crick.clicker.keyboard', + clkp: 'application/vnd.crick.clicker.palette', + clkt: 'application/vnd.crick.clicker.template', + clkw: 'application/vnd.crick.clicker.wordbank', + clkx: 'application/vnd.crick.clicker', + clp: 'application/x-msclip', + cmc: 'application/vnd.cosmocaller', + cmdf: 'chemical/x-cmdf', + cml: 'chemical/x-cml', + cmp: 'application/vnd.yellowriver-custom-menu', + cmx: 'image/x-cmx', + cod: 'application/vnd.rim.cod', + coffee: 'text/coffeescript', + com: 'application/x-msdownload', + conf: 'text/plain', + cpio: 'application/x-cpio', + cpp: 'text/x-c', + cpt: 'application/mac-compactpro', + crd: 'application/x-mscardfile', + crl: 'application/pkix-crl', + crt: 'application/x-x509-ca-cert', + crx: 'application/x-chrome-extension', + cryptonote: 'application/vnd.rig.cryptonote', + csh: 'application/x-csh', + csl: 'application/vnd.citationstyles.style+xml', + csml: 'chemical/x-csml', + csp: 'application/vnd.commonspace', + css: 'text/css', + cst: 'application/x-director', + csv: 'text/csv', + cu: 'application/cu-seeme', + curl: 'text/vnd.curl', + cww: 'application/prs.cww', + cxt: 'application/x-director', + cxx: 'text/x-c', + dae: 'model/vnd.collada+xml', + daf: 'application/vnd.mobius.daf', + dart: 'application/vnd.dart', + dataless: 'application/vnd.fdsn.seed', + davmount: 'application/davmount+xml', + dbf: 'application/vnd.dbf', + dbk: 'application/docbook+xml', + dcr: 'application/x-director', + dcurl: 'text/vnd.curl.dcurl', + dd2: 'application/vnd.oma.dd2+xml', + ddd: 'application/vnd.fujixerox.ddd', + ddf: 'application/vnd.syncml.dmddf+xml', + dds: 'image/vnd.ms-dds', + deb: 'application/x-debian-package', + def: 'text/plain', + deploy: 'application/octet-stream', + der: 'application/x-x509-ca-cert', + dfac: 'application/vnd.dreamfactory', + dgc: 'application/x-dgc-compressed', + dic: 'text/x-c', + dir: 'application/x-director', + dis: 'application/vnd.mobius.dis', + 'disposition-notification': 'message/disposition-notification', + dist: 'application/octet-stream', + distz: 'application/octet-stream', + djv: 'image/vnd.djvu', + djvu: 'image/vnd.djvu', + dll: 'application/x-msdownload', + dmg: 'application/x-apple-diskimage', + dmp: 'application/vnd.tcpdump.pcap', + dms: 'application/octet-stream', + dna: 'application/vnd.dna', + doc: 'application/msword', + docm: 'application/vnd.ms-word.document.macroenabled.12', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + dot: 'application/msword', + dotm: 'application/vnd.ms-word.template.macroenabled.12', + dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + dp: 'application/vnd.osgi.dp', + dpg: 'application/vnd.dpgraph', + dra: 'audio/vnd.dra', + drle: 'image/dicom-rle', + dsc: 'text/prs.lines.tag', + dssc: 'application/dssc+der', + dtb: 'application/x-dtbook+xml', + dtd: 'application/xml-dtd', + dts: 'audio/vnd.dts', + dtshd: 'audio/vnd.dts.hd', + dump: 'application/octet-stream', + dvb: 'video/vnd.dvb.file', + dvi: 'application/x-dvi', + dwd: 'application/atsc-dwd+xml', + dwf: 'model/vnd.dwf', + dwg: 'image/vnd.dwg', + dxf: 'image/vnd.dxf', + dxp: 'application/vnd.spotfire.dxp', + dxr: 'application/x-director', + ear: 'application/java-archive', + ecelp4800: 'audio/vnd.nuera.ecelp4800', + ecelp7470: 'audio/vnd.nuera.ecelp7470', + ecelp9600: 'audio/vnd.nuera.ecelp9600', + ecma: 'application/ecmascript', + edm: 'application/vnd.novadigm.edm', + edx: 'application/vnd.novadigm.edx', + efif: 'application/vnd.picsel', + ei6: 'application/vnd.pg.osasli', + elc: 'application/octet-stream', + emf: 'image/emf', + eml: 'message/rfc822', + emma: 'application/emma+xml', + emotionml: 'application/emotionml+xml', + emz: 'application/x-msmetafile', + eol: 'audio/vnd.digital-winds', + eot: 'application/vnd.ms-fontobject', + eps: 'application/postscript', + epub: 'application/epub+zip', + es: 'application/ecmascript', + es3: 'application/vnd.eszigno3+xml', + esa: 'application/vnd.osgi.subsystem', + esf: 'application/vnd.epson.esf', + et3: 'application/vnd.eszigno3+xml', + etx: 'text/x-setext', + eva: 'application/x-eva', + evy: 'application/x-envoy', + exe: 'application/x-msdownload', + exi: 'application/exi', + exp: 'application/express', + exr: 'image/aces', + ext: 'application/vnd.novadigm.ext', + ez: 'application/andrew-inset', + ez2: 'application/vnd.ezpix-album', + ez3: 'application/vnd.ezpix-package', + f: 'text/x-fortran', + f4v: 'video/x-f4v', + f77: 'text/x-fortran', + f90: 'text/x-fortran', + fbs: 'image/vnd.fastbidsheet', + fcdt: 'application/vnd.adobe.formscentral.fcdt', + fcs: 'application/vnd.isac.fcs', + fdf: 'application/vnd.fdf', + fdt: 'application/fdt+xml', + fe_launch: 'application/vnd.denovo.fcselayout-link', + fg5: 'application/vnd.fujitsu.oasysgp', + fgd: 'application/x-director', + fh: 'image/x-freehand', + fh4: 'image/x-freehand', + fh5: 'image/x-freehand', + fh7: 'image/x-freehand', + fhc: 'image/x-freehand', + fig: 'application/x-xfig', + fits: 'image/fits', + flac: 'audio/x-flac', + fli: 'video/x-fli', + flo: 'application/vnd.micrografx.flo', + flv: 'video/x-flv', + flw: 'application/vnd.kde.kivio', + flx: 'text/vnd.fmi.flexstor', + fly: 'text/vnd.fly', + fm: 'application/vnd.framemaker', + fnc: 'application/vnd.frogans.fnc', + fo: 'application/vnd.software602.filler.form+xml', + for: 'text/x-fortran', + fpx: 'image/vnd.fpx', + frame: 'application/vnd.framemaker', + fsc: 'application/vnd.fsc.weblaunch', + fst: 'image/vnd.fst', + ftc: 'application/vnd.fluxtime.clip', + fti: 'application/vnd.anser-web-funds-transfer-initiation', + fvt: 'video/vnd.fvt', + fxp: 'application/vnd.adobe.fxp', + fxpl: 'application/vnd.adobe.fxp', + fzs: 'application/vnd.fuzzysheet', + g2w: 'application/vnd.geoplan', + g3: 'image/g3fax', + g3w: 'application/vnd.geospace', + gac: 'application/vnd.groove-account', + gam: 'application/x-tads', + gbr: 'application/rpki-ghostbusters', + gca: 'application/x-gca-compressed', + gdl: 'model/vnd.gdl', + gdoc: 'application/vnd.google-apps.document', + ged: 'text/vnd.familysearch.gedcom', + geo: 'application/vnd.dynageo', + geojson: 'application/geo+json', + gex: 'application/vnd.geometry-explorer', + ggb: 'application/vnd.geogebra.file', + ggt: 'application/vnd.geogebra.tool', + ghf: 'application/vnd.groove-help', + gif: 'image/gif', + gim: 'application/vnd.groove-identity-message', + glb: 'model/gltf-binary', + gltf: 'model/gltf+json', + gml: 'application/gml+xml', + gmx: 'application/vnd.gmx', + gnumeric: 'application/x-gnumeric', + gph: 'application/vnd.flographit', + gpx: 'application/gpx+xml', + gqf: 'application/vnd.grafeq', + gqs: 'application/vnd.grafeq', + gram: 'application/srgs', + gramps: 'application/x-gramps-xml', + gre: 'application/vnd.geometry-explorer', + grv: 'application/vnd.groove-injector', + grxml: 'application/srgs+xml', + gsf: 'application/x-font-ghostscript', + gsheet: 'application/vnd.google-apps.spreadsheet', + gslides: 'application/vnd.google-apps.presentation', + gtar: 'application/x-gtar', + gtm: 'application/vnd.groove-tool-message', + gtw: 'model/vnd.gtw', + gv: 'text/vnd.graphviz', + gxf: 'application/gxf', + gxt: 'application/vnd.geonext', + gz: 'application/gzip', + h: 'text/x-c', + h261: 'video/h261', + h263: 'video/h263', + h264: 'video/h264', + hal: 'application/vnd.hal+xml', + hbci: 'application/vnd.hbci', + hbs: 'text/x-handlebars-template', + hdd: 'application/x-virtualbox-hdd', + hdf: 'application/x-hdf', + heic: 'image/heic', + heics: 'image/heic-sequence', + heif: 'image/heif', + heifs: 'image/heif-sequence', + hej2: 'image/hej2k', + held: 'application/atsc-held+xml', + hh: 'text/x-c', + hjson: 'application/hjson', + hlp: 'application/winhlp', + hpgl: 'application/vnd.hp-hpgl', + hpid: 'application/vnd.hp-hpid', + hps: 'application/vnd.hp-hps', + hqx: 'application/mac-binhex40', + hsj2: 'image/hsj2', + htc: 'text/x-component', + htke: 'application/vnd.kenameaapp', + htm: 'text/html', + html: 'text/html', + hvd: 'application/vnd.yamaha.hv-dic', + hvp: 'application/vnd.yamaha.hv-voice', + hvs: 'application/vnd.yamaha.hv-script', + i2g: 'application/vnd.intergeo', + icc: 'application/vnd.iccprofile', + ice: 'x-conference/x-cooltalk', + icm: 'application/vnd.iccprofile', + ico: 'image/x-icon', + ics: 'text/calendar', + ief: 'image/ief', + ifb: 'text/calendar', + ifm: 'application/vnd.shana.informed.formdata', + iges: 'model/iges', + igl: 'application/vnd.igloader', + igm: 'application/vnd.insors.igm', + igs: 'model/iges', + igx: 'application/vnd.micrografx.igx', + iif: 'application/vnd.shana.informed.interchange', + img: 'application/octet-stream', + imp: 'application/vnd.accpac.simply.imp', + ims: 'application/vnd.ms-ims', + in: 'text/plain', + ini: 'text/plain', + ink: 'application/inkml+xml', + inkml: 'application/inkml+xml', + install: 'application/x-install-instructions', + iota: 'application/vnd.astraea-software.iota', + ipfix: 'application/ipfix', + ipk: 'application/vnd.shana.informed.package', + irm: 'application/vnd.ibm.rights-management', + irp: 'application/vnd.irepository.package+xml', + iso: 'application/x-iso9660-image', + itp: 'application/vnd.shana.informed.formtemplate', + its: 'application/its+xml', + ivp: 'application/vnd.immervision-ivp', + ivu: 'application/vnd.immervision-ivu', + jad: 'text/vnd.sun.j2me.app-descriptor', + jade: 'text/jade', + jam: 'application/vnd.jam', + jar: 'application/java-archive', + jardiff: 'application/x-java-archive-diff', + java: 'text/x-java-source', + jhc: 'image/jphc', + jisp: 'application/vnd.jisp', + jls: 'image/jls', + jlt: 'application/vnd.hp-jlyt', + jng: 'image/x-jng', + jnlp: 'application/x-java-jnlp-file', + joda: 'application/vnd.joost.joda-archive', + jp2: 'image/jp2', + jpe: 'image/jpeg', + jpeg: 'image/jpeg', + jpf: 'image/jpx', + jpg: 'image/jpeg', + jpg2: 'image/jp2', + jpgm: 'video/jpm', + jpgv: 'video/jpeg', + jph: 'image/jph', + jpm: 'video/jpm', + jpx: 'image/jpx', + js: 'application/javascript', + json: 'application/json', + json5: 'application/json5', + jsonld: 'application/ld+json', + jsonml: 'application/jsonml+json', + jsx: 'text/jsx', + jxr: 'image/jxr', + jxra: 'image/jxra', + jxrs: 'image/jxrs', + jxs: 'image/jxs', + jxsc: 'image/jxsc', + jxsi: 'image/jxsi', + jxss: 'image/jxss', + kar: 'audio/midi', + karbon: 'application/vnd.kde.karbon', + kdbx: 'application/x-keepass2', + key: 'application/x-iwork-keynote-sffkey', + kfo: 'application/vnd.kde.kformula', + kia: 'application/vnd.kidspiration', + kml: 'application/vnd.google-earth.kml+xml', + kmz: 'application/vnd.google-earth.kmz', + kne: 'application/vnd.kinar', + knp: 'application/vnd.kinar', + kon: 'application/vnd.kde.kontour', + kpr: 'application/vnd.kde.kpresenter', + kpt: 'application/vnd.kde.kpresenter', + kpxx: 'application/vnd.ds-keypoint', + ksp: 'application/vnd.kde.kspread', + ktr: 'application/vnd.kahootz', + ktx: 'image/ktx', + ktx2: 'image/ktx2', + ktz: 'application/vnd.kahootz', + kwd: 'application/vnd.kde.kword', + kwt: 'application/vnd.kde.kword', + lasxml: 'application/vnd.las.las+xml', + latex: 'application/x-latex', + lbd: 'application/vnd.llamagraphics.life-balance.desktop', + lbe: 'application/vnd.llamagraphics.life-balance.exchange+xml', + les: 'application/vnd.hhe.lesson-player', + less: 'text/less', + lgr: 'application/lgr+xml', + lha: 'application/x-lzh-compressed', + link66: 'application/vnd.route66.link66+xml', + list: 'text/plain', + list3820: 'application/vnd.ibm.modcap', + listafp: 'application/vnd.ibm.modcap', + litcoffee: 'text/coffeescript', + lnk: 'application/x-ms-shortcut', + log: 'text/plain', + lostxml: 'application/lost+xml', + lrf: 'application/octet-stream', + lrm: 'application/vnd.ms-lrm', + ltf: 'application/vnd.frogans.ltf', + lua: 'text/x-lua', + luac: 'application/x-lua-bytecode', + lvp: 'audio/vnd.lucent.voice', + lwp: 'application/vnd.lotus-wordpro', + lzh: 'application/x-lzh-compressed', + m13: 'application/x-msmediaview', + m14: 'application/x-msmediaview', + m1v: 'video/mpeg', + m21: 'application/mp21', + m2a: 'audio/mpeg', + m2v: 'video/mpeg', + m3a: 'audio/mpeg', + m3u: 'audio/x-mpegurl', + m3u8: 'application/vnd.apple.mpegurl', + m4a: 'audio/x-m4a', + m4p: 'application/mp4', + m4s: 'video/iso.segment', + m4u: 'video/vnd.mpegurl', + m4v: 'video/x-m4v', + ma: 'application/mathematica', + mads: 'application/mads+xml', + maei: 'application/mmt-aei+xml', + mag: 'application/vnd.ecowin.chart', + maker: 'application/vnd.framemaker', + man: 'text/troff', + manifest: 'text/cache-manifest', + map: 'application/json', + mar: 'application/octet-stream', + markdown: 'text/markdown', + mathml: 'application/mathml+xml', + mb: 'application/mathematica', + mbk: 'application/vnd.mobius.mbk', + mbox: 'application/mbox', + mc1: 'application/vnd.medcalcdata', + mcd: 'application/vnd.mcd', + mcurl: 'text/vnd.curl.mcurl', + md: 'text/markdown', + mdb: 'application/x-msaccess', + mdi: 'image/vnd.ms-modi', + mdx: 'text/mdx', + me: 'text/troff', + mesh: 'model/mesh', + meta4: 'application/metalink4+xml', + metalink: 'application/metalink+xml', + mets: 'application/mets+xml', + mfm: 'application/vnd.mfmp', + mft: 'application/rpki-manifest', + mgp: 'application/vnd.osgeo.mapguide.package', + mgz: 'application/vnd.proteus.magazine', + mid: 'audio/midi', + midi: 'audio/midi', + mie: 'application/x-mie', + mif: 'application/vnd.mif', + mime: 'message/rfc822', + mj2: 'video/mj2', + mjp2: 'video/mj2', + mjs: 'application/javascript', + mk3d: 'video/x-matroska', + mka: 'audio/x-matroska', + mkd: 'text/x-markdown', + mks: 'video/x-matroska', + mkv: 'video/x-matroska', + mlp: 'application/vnd.dolby.mlp', + mmd: 'application/vnd.chipnuts.karaoke-mmd', + mmf: 'application/vnd.smaf', + mml: 'text/mathml', + mmr: 'image/vnd.fujixerox.edmics-mmr', + mng: 'video/x-mng', + mny: 'application/x-msmoney', + mobi: 'application/x-mobipocket-ebook', + mods: 'application/mods+xml', + mov: 'video/quicktime', + movie: 'video/x-sgi-movie', + mp2: 'audio/mpeg', + mp21: 'application/mp21', + mp2a: 'audio/mpeg', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + mp4a: 'audio/mp4', + mp4s: 'application/mp4', + mp4v: 'video/mp4', + mpc: 'application/vnd.mophun.certificate', + mpd: 'application/dash+xml', + mpe: 'video/mpeg', + mpeg: 'video/mpeg', + mpg: 'video/mpeg', + mpg4: 'video/mp4', + mpga: 'audio/mpeg', + mpkg: 'application/vnd.apple.installer+xml', + mpm: 'application/vnd.blueice.multipass', + mpn: 'application/vnd.mophun.application', + mpp: 'application/vnd.ms-project', + mpt: 'application/vnd.ms-project', + mpy: 'application/vnd.ibm.minipay', + mqy: 'application/vnd.mobius.mqy', + mrc: 'application/marc', + mrcx: 'application/marcxml+xml', + ms: 'text/troff', + mscml: 'application/mediaservercontrol+xml', + mseed: 'application/vnd.fdsn.mseed', + mseq: 'application/vnd.mseq', + msf: 'application/vnd.epson.msf', + msg: 'application/vnd.ms-outlook', + msh: 'model/mesh', + msi: 'application/x-msdownload', + msl: 'application/vnd.mobius.msl', + msm: 'application/octet-stream', + msp: 'application/octet-stream', + msty: 'application/vnd.muvee.style', + mtl: 'model/mtl', + mts: 'model/vnd.mts', + mus: 'application/vnd.musician', + musd: 'application/mmt-usd+xml', + musicxml: 'application/vnd.recordare.musicxml+xml', + mvb: 'application/x-msmediaview', + mvt: 'application/vnd.mapbox-vector-tile', + mwf: 'application/vnd.mfer', + mxf: 'application/mxf', + mxl: 'application/vnd.recordare.musicxml', + mxmf: 'audio/mobile-xmf', + mxml: 'application/xv+xml', + mxs: 'application/vnd.triscape.mxs', + mxu: 'video/vnd.mpegurl', + 'n-gage': 'application/vnd.nokia.n-gage.symbian.install', + n3: 'text/n3', + nb: 'application/mathematica', + nbp: 'application/vnd.wolfram.player', + nc: 'application/x-netcdf', + ncx: 'application/x-dtbncx+xml', + nfo: 'text/x-nfo', + ngdat: 'application/vnd.nokia.n-gage.data', + nitf: 'application/vnd.nitf', + nlu: 'application/vnd.neurolanguage.nlu', + nml: 'application/vnd.enliven', + nnd: 'application/vnd.noblenet-directory', + nns: 'application/vnd.noblenet-sealer', + nnw: 'application/vnd.noblenet-web', + npx: 'image/vnd.net-fpx', + nq: 'application/n-quads', + nsc: 'application/x-conference', + nsf: 'application/vnd.lotus-notes', + nt: 'application/n-triples', + ntf: 'application/vnd.nitf', + numbers: 'application/x-iwork-numbers-sffnumbers', + nzb: 'application/x-nzb', + oa2: 'application/vnd.fujitsu.oasys2', + oa3: 'application/vnd.fujitsu.oasys3', + oas: 'application/vnd.fujitsu.oasys', + obd: 'application/x-msbinder', + obgx: 'application/vnd.openblox.game+xml', + obj: 'model/obj', + oda: 'application/oda', + odb: 'application/vnd.oasis.opendocument.database', + odc: 'application/vnd.oasis.opendocument.chart', + odf: 'application/vnd.oasis.opendocument.formula', + odft: 'application/vnd.oasis.opendocument.formula-template', + odg: 'application/vnd.oasis.opendocument.graphics', + odi: 'application/vnd.oasis.opendocument.image', + odm: 'application/vnd.oasis.opendocument.text-master', + odp: 'application/vnd.oasis.opendocument.presentation', + ods: 'application/vnd.oasis.opendocument.spreadsheet', + odt: 'application/vnd.oasis.opendocument.text', + oga: 'audio/ogg', + ogex: 'model/vnd.opengex', + ogg: 'audio/ogg', + ogv: 'video/ogg', + ogx: 'application/ogg', + omdoc: 'application/omdoc+xml', + onepkg: 'application/onenote', + onetmp: 'application/onenote', + onetoc: 'application/onenote', + onetoc2: 'application/onenote', + opf: 'application/oebps-package+xml', + opml: 'text/x-opml', + oprc: 'application/vnd.palm', + opus: 'audio/ogg', + org: 'text/x-org', + osf: 'application/vnd.yamaha.openscoreformat', + osfpvg: 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + osm: 'application/vnd.openstreetmap.data+xml', + otc: 'application/vnd.oasis.opendocument.chart-template', + otf: 'font/otf', + otg: 'application/vnd.oasis.opendocument.graphics-template', + oth: 'application/vnd.oasis.opendocument.text-web', + oti: 'application/vnd.oasis.opendocument.image-template', + otp: 'application/vnd.oasis.opendocument.presentation-template', + ots: 'application/vnd.oasis.opendocument.spreadsheet-template', + ott: 'application/vnd.oasis.opendocument.text-template', + ova: 'application/x-virtualbox-ova', + ovf: 'application/x-virtualbox-ovf', + owl: 'application/rdf+xml', + oxps: 'application/oxps', + oxt: 'application/vnd.openofficeorg.extension', + p: 'text/x-pascal', + p10: 'application/pkcs10', + p12: 'application/x-pkcs12', + p7b: 'application/x-pkcs7-certificates', + p7c: 'application/pkcs7-mime', + p7m: 'application/pkcs7-mime', + p7r: 'application/x-pkcs7-certreqresp', + p7s: 'application/pkcs7-signature', + p8: 'application/pkcs8', + pac: 'application/x-ns-proxy-autoconfig', + pages: 'application/x-iwork-pages-sffpages', + pas: 'text/x-pascal', + paw: 'application/vnd.pawaafile', + pbd: 'application/vnd.powerbuilder6', + pbm: 'image/x-portable-bitmap', + pcap: 'application/vnd.tcpdump.pcap', + pcf: 'application/x-font-pcf', + pcl: 'application/vnd.hp-pcl', + pclxl: 'application/vnd.hp-pclxl', + pct: 'image/x-pict', + pcurl: 'application/vnd.curl.pcurl', + pcx: 'image/x-pcx', + pdb: 'application/x-pilot', + pde: 'text/x-processing', + pdf: 'application/pdf', + pem: 'application/x-x509-ca-cert', + pfa: 'application/x-font-type1', + pfb: 'application/x-font-type1', + pfm: 'application/x-font-type1', + pfr: 'application/font-tdpfr', + pfx: 'application/x-pkcs12', + pgm: 'image/x-portable-graymap', + pgn: 'application/x-chess-pgn', + pgp: 'application/pgp-encrypted', + php: 'application/x-httpd-php', + pic: 'image/x-pict', + pkg: 'application/octet-stream', + pki: 'application/pkixcmp', + pkipath: 'application/pkix-pkipath', + pkpass: 'application/vnd.apple.pkpass', + pl: 'application/x-perl', + plb: 'application/vnd.3gpp.pic-bw-large', + plc: 'application/vnd.mobius.plc', + plf: 'application/vnd.pocketlearn', + pls: 'application/pls+xml', + pm: 'application/x-perl', + pml: 'application/vnd.ctc-posml', + png: 'image/png', + pnm: 'image/x-portable-anymap', + portpkg: 'application/vnd.macports.portpkg', + pot: 'application/vnd.ms-powerpoint', + potm: 'application/vnd.ms-powerpoint.template.macroenabled.12', + potx: 'application/vnd.openxmlformats-officedocument.presentationml.template', + ppam: 'application/vnd.ms-powerpoint.addin.macroenabled.12', + ppd: 'application/vnd.cups-ppd', + ppm: 'image/x-portable-pixmap', + pps: 'application/vnd.ms-powerpoint', + ppsm: 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + ppsx: 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + ppt: 'application/vnd.ms-powerpoint', + pptm: 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + pqa: 'application/vnd.palm', + prc: 'application/x-pilot', + pre: 'application/vnd.lotus-freelance', + prf: 'application/pics-rules', + provx: 'application/provenance+xml', + ps: 'application/postscript', + psb: 'application/vnd.3gpp.pic-bw-small', + psd: 'image/vnd.adobe.photoshop', + psf: 'application/x-font-linux-psf', + pskcxml: 'application/pskc+xml', + pti: 'image/prs.pti', + ptid: 'application/vnd.pvi.ptid1', + pub: 'application/x-mspublisher', + pvb: 'application/vnd.3gpp.pic-bw-var', + pwn: 'application/vnd.3m.post-it-notes', + pya: 'audio/vnd.ms-playready.media.pya', + pyv: 'video/vnd.ms-playready.media.pyv', + qam: 'application/vnd.epson.quickanime', + qbo: 'application/vnd.intu.qbo', + qfx: 'application/vnd.intu.qfx', + qps: 'application/vnd.publishare-delta-tree', + qt: 'video/quicktime', + qwd: 'application/vnd.quark.quarkxpress', + qwt: 'application/vnd.quark.quarkxpress', + qxb: 'application/vnd.quark.quarkxpress', + qxd: 'application/vnd.quark.quarkxpress', + qxl: 'application/vnd.quark.quarkxpress', + qxt: 'application/vnd.quark.quarkxpress', + ra: 'audio/x-realaudio', + ram: 'audio/x-pn-realaudio', + raml: 'application/raml+yaml', + rapd: 'application/route-apd+xml', + rar: 'application/x-rar-compressed', + ras: 'image/x-cmu-raster', + rcprofile: 'application/vnd.ipunplugged.rcprofile', + rdf: 'application/rdf+xml', + rdz: 'application/vnd.data-vision.rdz', + relo: 'application/p2p-overlay+xml', + rep: 'application/vnd.businessobjects', + res: 'application/x-dtbresource+xml', + rgb: 'image/x-rgb', + rif: 'application/reginfo+xml', + rip: 'audio/vnd.rip', + ris: 'application/x-research-info-systems', + rl: 'application/resource-lists+xml', + rlc: 'image/vnd.fujixerox.edmics-rlc', + rld: 'application/resource-lists-diff+xml', + rm: 'application/vnd.rn-realmedia', + rmi: 'audio/midi', + rmp: 'audio/x-pn-realaudio-plugin', + rms: 'application/vnd.jcp.javame.midlet-rms', + rmvb: 'application/vnd.rn-realmedia-vbr', + rnc: 'application/relax-ng-compact-syntax', + rng: 'application/xml', + roa: 'application/rpki-roa', + roff: 'text/troff', + rp9: 'application/vnd.cloanto.rp9', + rpm: 'application/x-redhat-package-manager', + rpss: 'application/vnd.nokia.radio-presets', + rpst: 'application/vnd.nokia.radio-preset', + rq: 'application/sparql-query', + rs: 'application/rls-services+xml', + rsat: 'application/atsc-rsat+xml', + rsd: 'application/rsd+xml', + rsheet: 'application/urc-ressheet+xml', + rss: 'application/rss+xml', + rtf: 'text/rtf', + rtx: 'text/richtext', + run: 'application/x-makeself', + rusd: 'application/route-usd+xml', + s: 'text/x-asm', + s3m: 'audio/s3m', + saf: 'application/vnd.yamaha.smaf-audio', + sass: 'text/x-sass', + sbml: 'application/sbml+xml', + sc: 'application/vnd.ibm.secure-container', + scd: 'application/x-msschedule', + scm: 'application/vnd.lotus-screencam', + scq: 'application/scvp-cv-request', + scs: 'application/scvp-cv-response', + scss: 'text/x-scss', + scurl: 'text/vnd.curl.scurl', + sda: 'application/vnd.stardivision.draw', + sdc: 'application/vnd.stardivision.calc', + sdd: 'application/vnd.stardivision.impress', + sdkd: 'application/vnd.solent.sdkm+xml', + sdkm: 'application/vnd.solent.sdkm+xml', + sdp: 'application/sdp', + sdw: 'application/vnd.stardivision.writer', + sea: 'application/x-sea', + see: 'application/vnd.seemail', + seed: 'application/vnd.fdsn.seed', + sema: 'application/vnd.sema', + semd: 'application/vnd.semd', + semf: 'application/vnd.semf', + senmlx: 'application/senml+xml', + sensmlx: 'application/sensml+xml', + ser: 'application/java-serialized-object', + setpay: 'application/set-payment-initiation', + setreg: 'application/set-registration-initiation', + 'sfd-hdstx': 'application/vnd.hydrostatix.sof-data', + sfs: 'application/vnd.spotfire.sfs', + sfv: 'text/x-sfv', + sgi: 'image/sgi', + sgl: 'application/vnd.stardivision.writer-global', + sgm: 'text/sgml', + sgml: 'text/sgml', + sh: 'application/x-sh', + shar: 'application/x-shar', + shex: 'text/shex', + shf: 'application/shf+xml', + shtml: 'text/html', + sid: 'image/x-mrsid-image', + sieve: 'application/sieve', + sig: 'application/pgp-signature', + sil: 'audio/silk', + silo: 'model/mesh', + sis: 'application/vnd.symbian.install', + sisx: 'application/vnd.symbian.install', + sit: 'application/x-stuffit', + sitx: 'application/x-stuffitx', + siv: 'application/sieve', + skd: 'application/vnd.koan', + skm: 'application/vnd.koan', + skp: 'application/vnd.koan', + skt: 'application/vnd.koan', + sldm: 'application/vnd.ms-powerpoint.slide.macroenabled.12', + sldx: 'application/vnd.openxmlformats-officedocument.presentationml.slide', + slim: 'text/slim', + slm: 'text/slim', + sls: 'application/route-s-tsid+xml', + slt: 'application/vnd.epson.salt', + sm: 'application/vnd.stepmania.stepchart', + smf: 'application/vnd.stardivision.math', + smi: 'application/smil+xml', + smil: 'application/smil+xml', + smv: 'video/x-smv', + smzip: 'application/vnd.stepmania.package', + snd: 'audio/basic', + snf: 'application/x-font-snf', + so: 'application/octet-stream', + spc: 'application/x-pkcs7-certificates', + spdx: 'text/spdx', + spf: 'application/vnd.yamaha.smaf-phrase', + spl: 'application/x-futuresplash', + spot: 'text/vnd.in3d.spot', + spp: 'application/scvp-vp-response', + spq: 'application/scvp-vp-request', + spx: 'audio/ogg', + sql: 'application/x-sql', + src: 'application/x-wais-source', + srt: 'application/x-subrip', + sru: 'application/sru+xml', + srx: 'application/sparql-results+xml', + ssdl: 'application/ssdl+xml', + sse: 'application/vnd.kodak-descriptor', + ssf: 'application/vnd.epson.ssf', + ssml: 'application/ssml+xml', + st: 'application/vnd.sailingtracker.track', + stc: 'application/vnd.sun.xml.calc.template', + std: 'application/vnd.sun.xml.draw.template', + stf: 'application/vnd.wt.stf', + sti: 'application/vnd.sun.xml.impress.template', + stk: 'application/hyperstudio', + stl: 'model/stl', + stpx: 'model/step+xml', + stpxz: 'model/step-xml+zip', + stpz: 'model/step+zip', + str: 'application/vnd.pg.format', + stw: 'application/vnd.sun.xml.writer.template', + styl: 'text/stylus', + stylus: 'text/stylus', + sub: 'text/vnd.dvb.subtitle', + sus: 'application/vnd.sus-calendar', + susp: 'application/vnd.sus-calendar', + sv4cpio: 'application/x-sv4cpio', + sv4crc: 'application/x-sv4crc', + svc: 'application/vnd.dvb.service', + svd: 'application/vnd.svd', + svg: 'image/svg+xml', + svgz: 'image/svg+xml', + swa: 'application/x-director', + swf: 'application/x-shockwave-flash', + swi: 'application/vnd.aristanetworks.swi', + swidtag: 'application/swid+xml', + sxc: 'application/vnd.sun.xml.calc', + sxd: 'application/vnd.sun.xml.draw', + sxg: 'application/vnd.sun.xml.writer.global', + sxi: 'application/vnd.sun.xml.impress', + sxm: 'application/vnd.sun.xml.math', + sxw: 'application/vnd.sun.xml.writer', + t: 'text/troff', + t3: 'application/x-t3vm-image', + t38: 'image/t38', + taglet: 'application/vnd.mynfc', + tao: 'application/vnd.tao.intent-module-archive', + tap: 'image/vnd.tencent.tap', + tar: 'application/x-tar', + tcap: 'application/vnd.3gpp2.tcap', + tcl: 'application/x-tcl', + td: 'application/urc-targetdesc+xml', + teacher: 'application/vnd.smart.teacher', + tei: 'application/tei+xml', + teicorpus: 'application/tei+xml', + tex: 'application/x-tex', + texi: 'application/x-texinfo', + texinfo: 'application/x-texinfo', + text: 'text/plain', + tfi: 'application/thraud+xml', + tfm: 'application/x-tex-tfm', + tfx: 'image/tiff-fx', + tga: 'image/x-tga', + thmx: 'application/vnd.ms-officetheme', + tif: 'image/tiff', + tiff: 'image/tiff', + tk: 'application/x-tcl', + tmo: 'application/vnd.tmobile-livetv', + toml: 'application/toml', + torrent: 'application/x-bittorrent', + tpl: 'application/vnd.groove-tool-template', + tpt: 'application/vnd.trid.tpt', + tr: 'text/troff', + tra: 'application/vnd.trueapp', + trig: 'application/trig', + trm: 'application/x-msterminal', + ts: 'video/mp2t', + tsd: 'application/timestamped-data', + tsv: 'text/tab-separated-values', + ttc: 'font/collection', + ttf: 'font/ttf', + ttl: 'text/turtle', + ttml: 'application/ttml+xml', + twd: 'application/vnd.simtech-mindmapper', + twds: 'application/vnd.simtech-mindmapper', + txd: 'application/vnd.genomatix.tuxedo', + txf: 'application/vnd.mobius.txf', + txt: 'text/plain', + u32: 'application/x-authorware-bin', + u8dsn: 'message/global-delivery-status', + u8hdr: 'message/global-headers', + u8mdn: 'message/global-disposition-notification', + u8msg: 'message/global', + ubj: 'application/ubjson', + udeb: 'application/x-debian-package', + ufd: 'application/vnd.ufdl', + ufdl: 'application/vnd.ufdl', + ulx: 'application/x-glulx', + umj: 'application/vnd.umajin', + unityweb: 'application/vnd.unity', + uoml: 'application/vnd.uoml+xml', + uri: 'text/uri-list', + uris: 'text/uri-list', + urls: 'text/uri-list', + usdz: 'model/vnd.usdz+zip', + ustar: 'application/x-ustar', + utz: 'application/vnd.uiq.theme', + uu: 'text/x-uuencode', + uva: 'audio/vnd.dece.audio', + uvd: 'application/vnd.dece.data', + uvf: 'application/vnd.dece.data', + uvg: 'image/vnd.dece.graphic', + uvh: 'video/vnd.dece.hd', + uvi: 'image/vnd.dece.graphic', + uvm: 'video/vnd.dece.mobile', + uvp: 'video/vnd.dece.pd', + uvs: 'video/vnd.dece.sd', + uvt: 'application/vnd.dece.ttml+xml', + uvu: 'video/vnd.uvvu.mp4', + uvv: 'video/vnd.dece.video', + uvva: 'audio/vnd.dece.audio', + uvvd: 'application/vnd.dece.data', + uvvf: 'application/vnd.dece.data', + uvvg: 'image/vnd.dece.graphic', + uvvh: 'video/vnd.dece.hd', + uvvi: 'image/vnd.dece.graphic', + uvvm: 'video/vnd.dece.mobile', + uvvp: 'video/vnd.dece.pd', + uvvs: 'video/vnd.dece.sd', + uvvt: 'application/vnd.dece.ttml+xml', + uvvu: 'video/vnd.uvvu.mp4', + uvvv: 'video/vnd.dece.video', + uvvx: 'application/vnd.dece.unspecified', + uvvz: 'application/vnd.dece.zip', + uvx: 'application/vnd.dece.unspecified', + uvz: 'application/vnd.dece.zip', + vbox: 'application/x-virtualbox-vbox', + 'vbox-extpack': 'application/x-virtualbox-vbox-extpack', + vcard: 'text/vcard', + vcd: 'application/x-cdlink', + vcf: 'text/x-vcard', + vcg: 'application/vnd.groove-vcard', + vcs: 'text/x-vcalendar', + vcx: 'application/vnd.vcx', + vdi: 'application/x-virtualbox-vdi', + vds: 'model/vnd.sap.vds', + vhd: 'application/x-virtualbox-vhd', + vis: 'application/vnd.visionary', + viv: 'video/vnd.vivo', + vmdk: 'application/x-virtualbox-vmdk', + vob: 'video/x-ms-vob', + vor: 'application/vnd.stardivision.writer', + vox: 'application/x-authorware-bin', + vrml: 'model/vrml', + vsd: 'application/vnd.visio', + vsf: 'application/vnd.vsf', + vss: 'application/vnd.visio', + vst: 'application/vnd.visio', + vsw: 'application/vnd.visio', + vtf: 'image/vnd.valve.source.texture', + vtt: 'text/vtt', + vtu: 'model/vnd.vtu', + vxml: 'application/voicexml+xml', + w3d: 'application/x-director', + wad: 'application/x-doom', + wadl: 'application/vnd.sun.wadl+xml', + war: 'application/java-archive', + wasm: 'application/wasm', + wav: 'audio/x-wav', + wax: 'audio/x-ms-wax', + wbmp: 'image/vnd.wap.wbmp', + wbs: 'application/vnd.criticaltools.wbs+xml', + wbxml: 'application/vnd.wap.wbxml', + wcm: 'application/vnd.ms-works', + wdb: 'application/vnd.ms-works', + wdp: 'image/vnd.ms-photo', + weba: 'audio/webm', + webapp: 'application/x-web-app-manifest+json', + webm: 'video/webm', + webmanifest: 'application/manifest+json', + webp: 'image/webp', + wg: 'application/vnd.pmi.widget', + wgt: 'application/widget', + wks: 'application/vnd.ms-works', + wm: 'video/x-ms-wm', + wma: 'audio/x-ms-wma', + wmd: 'application/x-ms-wmd', + wmf: 'image/wmf', + wml: 'text/vnd.wap.wml', + wmlc: 'application/vnd.wap.wmlc', + wmls: 'text/vnd.wap.wmlscript', + wmlsc: 'application/vnd.wap.wmlscriptc', + wmv: 'video/x-ms-wmv', + wmx: 'video/x-ms-wmx', + wmz: 'application/x-msmetafile', + woff: 'font/woff', + woff2: 'font/woff2', + wpd: 'application/vnd.wordperfect', + wpl: 'application/vnd.ms-wpl', + wps: 'application/vnd.ms-works', + wqd: 'application/vnd.wqd', + wri: 'application/x-mswrite', + wrl: 'model/vrml', + wsc: 'message/vnd.wfa.wsc', + wsdl: 'application/wsdl+xml', + wspolicy: 'application/wspolicy+xml', + wtb: 'application/vnd.webturbo', + wvx: 'video/x-ms-wvx', + x32: 'application/x-authorware-bin', + x3d: 'model/x3d+xml', + x3db: 'model/x3d+fastinfoset', + x3dbz: 'model/x3d+binary', + x3dv: 'model/x3d-vrml', + x3dvz: 'model/x3d+vrml', + x3dz: 'model/x3d+xml', + x_b: 'model/vnd.parasolid.transmit.binary', + x_t: 'model/vnd.parasolid.transmit.text', + xaml: 'application/xaml+xml', + xap: 'application/x-silverlight-app', + xar: 'application/vnd.xara', + xav: 'application/xcap-att+xml', + xbap: 'application/x-ms-xbap', + xbd: 'application/vnd.fujixerox.docuworks.binder', + xbm: 'image/x-xbitmap', + xca: 'application/xcap-caps+xml', + xcs: 'application/calendar+xml', + xdf: 'application/xcap-diff+xml', + xdm: 'application/vnd.syncml.dm+xml', + xdp: 'application/vnd.adobe.xdp+xml', + xdssc: 'application/dssc+xml', + xdw: 'application/vnd.fujixerox.docuworks', + xel: 'application/xcap-el+xml', + xenc: 'application/xenc+xml', + xer: 'application/patch-ops-error+xml', + xfdf: 'application/vnd.adobe.xfdf', + xfdl: 'application/vnd.xfdl', + xht: 'application/xhtml+xml', + xhtml: 'application/xhtml+xml', + xhvml: 'application/xv+xml', + xif: 'image/vnd.xiff', + xla: 'application/vnd.ms-excel', + xlam: 'application/vnd.ms-excel.addin.macroenabled.12', + xlc: 'application/vnd.ms-excel', + xlf: 'application/xliff+xml', + xlm: 'application/vnd.ms-excel', + xls: 'application/vnd.ms-excel', + xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + xlsm: 'application/vnd.ms-excel.sheet.macroenabled.12', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + xlt: 'application/vnd.ms-excel', + xltm: 'application/vnd.ms-excel.template.macroenabled.12', + xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + xlw: 'application/vnd.ms-excel', + xm: 'audio/xm', + xml: 'text/xml', + xns: 'application/xcap-ns+xml', + xo: 'application/vnd.olpc-sugar', + xop: 'application/xop+xml', + xpi: 'application/x-xpinstall', + xpl: 'application/xproc+xml', + xpm: 'image/x-xpixmap', + xpr: 'application/vnd.is-xpr', + xps: 'application/vnd.ms-xpsdocument', + xpw: 'application/vnd.intercon.formnet', + xpx: 'application/vnd.intercon.formnet', + xsd: 'application/xml', + xsl: 'application/xslt+xml', + xslt: 'application/xslt+xml', + xsm: 'application/vnd.syncml+xml', + xspf: 'application/xspf+xml', + xul: 'application/vnd.mozilla.xul+xml', + xvm: 'application/xv+xml', + xvml: 'application/xv+xml', + xwd: 'image/x-xwindowdump', + xyz: 'chemical/x-xyz', + xz: 'application/x-xz', + yaml: 'text/yaml', + yang: 'application/yang', + yin: 'application/yin+xml', + yml: 'text/yaml', + ymp: 'text/x-suse-ymp', + z1: 'application/x-zmachine', + z2: 'application/x-zmachine', + z3: 'application/x-zmachine', + z4: 'application/x-zmachine', + z5: 'application/x-zmachine', + z6: 'application/x-zmachine', + z7: 'application/x-zmachine', + z8: 'application/x-zmachine', + zaz: 'application/vnd.zzazz.deck+xml', + zip: 'application/zip', + zir: 'application/vnd.zul', + zirz: 'application/vnd.zul', + zmm: 'application/vnd.handheld-entertainment+xml', +}; diff --git a/src/lib/link-meta/resolve-short-link.js b/src/lib/link-meta/resolve-short-link.js new file mode 100644 index 0000000000..40c51f692d --- /dev/null +++ b/src/lib/link-meta/resolve-short-link.js @@ -0,0 +1,77 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { logger } from '#/logger'; +export function resolveShortLink(shortLink) { + return __awaiter(this, void 0, void 0, function () { + var controller, to, res, json, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + controller = new AbortController(); + to = setTimeout(function () { return controller.abort(); }, 2e3); + _a.label = 1; + case 1: + _a.trys.push([1, 4, 5, 6]); + return [4 /*yield*/, fetch(shortLink, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + signal: controller.signal, + })]; + case 2: + res = _a.sent(); + if (res.status !== 200) { + logger.error('Failed to resolve short link', { status: res.status }); + return [2 /*return*/, shortLink]; + } + return [4 /*yield*/, res.json()]; + case 3: + json = (_a.sent()); + return [2 /*return*/, json.url]; + case 4: + e_1 = _a.sent(); + logger.error('Failed to resolve short link', { safeMessage: e_1 }); + return [2 /*return*/, shortLink]; + case 5: + clearTimeout(to); + return [7 /*endfinally*/]; + case 6: return [2 /*return*/]; + } + }); + }); +} diff --git a/src/lib/media/avatar-generator.js b/src/lib/media/avatar-generator.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/lib/media/manip.js b/src/lib/media/manip.js new file mode 100644 index 0000000000..c6312393a6 --- /dev/null +++ b/src/lib/media/manip.js @@ -0,0 +1,515 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { Image as RNImage } from 'react-native'; +import uuid from 'react-native-uuid'; +import { cacheDirectory, copyAsync, createDownloadResumable, deleteAsync, EncodingType, getInfoAsync, makeDirectoryAsync, StorageAccessFramework, writeAsStringAsync, } from 'expo-file-system/legacy'; +import { manipulateAsync, SaveFormat } from 'expo-image-manipulator'; +import * as MediaLibrary from 'expo-media-library'; +import * as Sharing from 'expo-sharing'; +import { Buffer } from 'buffer'; +import { POST_IMG_MAX } from '#/lib/constants'; +import { logger } from '#/logger'; +import { IS_ANDROID, IS_IOS } from '#/env'; +export function compressIfNeeded(img_1) { + return __awaiter(this, arguments, void 0, function (img, maxSize) { + var resizedImage, finalImageMovedPath, finalImg; + if (maxSize === void 0) { maxSize = POST_IMG_MAX.size; } + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (img.size < maxSize) { + return [2 /*return*/, img]; + } + return [4 /*yield*/, doResize(normalizePath(img.path), { + width: img.width, + height: img.height, + mode: 'stretch', + maxSize: maxSize, + })]; + case 1: + resizedImage = _a.sent(); + return [4 /*yield*/, moveToPermanentPath(resizedImage.path, '.jpg')]; + case 2: + finalImageMovedPath = _a.sent(); + finalImg = __assign(__assign({}, resizedImage), { path: finalImageMovedPath }); + return [2 /*return*/, finalImg]; + } + }); + }); +} +export function downloadAndResize(opts) { + return __awaiter(this, void 0, void 0, function () { + var appendExt, urip, ext, path; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + appendExt = 'jpeg'; + try { + urip = new URL(opts.uri); + ext = urip.pathname.split('.').pop(); + if (ext === 'png') { + appendExt = 'png'; + } + } + catch (e) { + console.error('Invalid URI', opts.uri, e); + return [2 /*return*/]; + } + path = createPath(appendExt); + _a.label = 1; + case 1: + _a.trys.push([1, , 4, 5]); + return [4 /*yield*/, downloadImage(opts.uri, path, opts.timeout)]; + case 2: + _a.sent(); + return [4 /*yield*/, doResize(path, opts)]; + case 3: return [2 /*return*/, _a.sent()]; + case 4: + safeDeleteAsync(path); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function shareImageModal(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var imageUri, imagePath; + var uri = _b.uri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, Sharing.isAvailableAsync()]; + case 1: + if (!(_c.sent())) { + // TODO might need to give an error to the user in this case -prf + return [2 /*return*/]; + } + return [4 /*yield*/, downloadImage(uri, createPath('jpg'), 15e3)]; + case 2: + imageUri = _c.sent(); + return [4 /*yield*/, moveToPermanentPath(imageUri, '.jpg')]; + case 3: + imagePath = _c.sent(); + safeDeleteAsync(imageUri); + return [4 /*yield*/, Sharing.shareAsync(imagePath, { + mimeType: 'image/jpeg', + UTI: 'image/jpeg', + })]; + case 4: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +var ALBUM_NAME = 'Bluesky'; +export function saveImageToMediaLibrary(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var imageUri, imagePath, album, err_1, err_2, err2_1, err_3; + var uri = _b.uri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, downloadImage(uri, createPath('jpg'), 15e3)]; + case 1: + imageUri = _c.sent(); + return [4 /*yield*/, moveToPermanentPath(imageUri, '.jpg') + // save + ]; + case 2: + imagePath = _c.sent(); + _c.label = 3; + case 3: + _c.trys.push([3, 25, 26, 27]); + if (!IS_ANDROID) return [3 /*break*/, 22]; + return [4 /*yield*/, MediaLibrary.getAlbumAsync(ALBUM_NAME)]; + case 4: + album = _c.sent(); + if (!album) return [3 /*break*/, 19]; + _c.label = 5; + case 5: + _c.trys.push([5, 9, , 10]); + return [4 /*yield*/, MediaLibrary.albumNeedsMigrationAsync(album)]; + case 6: + if (!_c.sent()) return [3 /*break*/, 8]; + return [4 /*yield*/, MediaLibrary.migrateAlbumIfNeededAsync(album)]; + case 7: + _c.sent(); + _c.label = 8; + case 8: return [3 /*break*/, 10]; + case 9: + err_1 = _c.sent(); + logger.info('Attempted and failed to migrate album', { + safeMessage: err_1, + }); + return [3 /*break*/, 10]; + case 10: + _c.trys.push([10, 12, , 18]); + // if album exists, put the image straight in there + return [4 /*yield*/, MediaLibrary.createAssetAsync(imagePath, album)]; + case 11: + // if album exists, put the image straight in there + _c.sent(); + return [3 /*break*/, 18]; + case 12: + err_2 = _c.sent(); + logger.info('Failed to create asset', { safeMessage: err_2 }); + _c.label = 13; + case 13: + _c.trys.push([13, 15, , 17]); + return [4 /*yield*/, MediaLibrary.createAlbumAsync(ALBUM_NAME, undefined, undefined, imagePath)]; + case 14: + _c.sent(); + return [3 /*break*/, 17]; + case 15: + err2_1 = _c.sent(); + logger.info('Failed to create asset in a fresh album', { + safeMessage: err2_1, + }); + // ... and if all else fails, just put it in DCIM + return [4 /*yield*/, MediaLibrary.createAssetAsync(imagePath)]; + case 16: + // ... and if all else fails, just put it in DCIM + _c.sent(); + return [3 /*break*/, 17]; + case 17: return [3 /*break*/, 18]; + case 18: return [3 /*break*/, 21]; + case 19: + // otherwise, create album with asset (albums must always have at least one asset) + return [4 /*yield*/, MediaLibrary.createAlbumAsync(ALBUM_NAME, undefined, undefined, imagePath)]; + case 20: + // otherwise, create album with asset (albums must always have at least one asset) + _c.sent(); + _c.label = 21; + case 21: return [3 /*break*/, 24]; + case 22: return [4 /*yield*/, MediaLibrary.saveToLibraryAsync(imagePath)]; + case 23: + _c.sent(); + _c.label = 24; + case 24: return [3 /*break*/, 27]; + case 25: + err_3 = _c.sent(); + logger.error(err_3 instanceof Error ? err_3 : String(err_3), { + message: 'Failed to save image to media library', + }); + throw err_3; + case 26: + safeDeleteAsync(imagePath); + return [7 /*endfinally*/]; + case 27: return [2 /*return*/]; + } + }); + }); +} +export function getImageDim(path) { + return new Promise(function (resolve, reject) { + RNImage.getSize(path, function (width, height) { + resolve({ width: width, height: height }); + }, reject); + }); +} +function doResize(localUri, opts) { + return __awaiter(this, void 0, void 0, function () { + var imageRes, newDimensions, minQualityPercentage, maxQualityPercentage, newDataUri, intermediateUris, qualityPercentage, resizeRes, fileInfo, _i, intermediateUris_1, intermediateUri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, manipulateAsync(localUri, [], {})]; + case 1: + imageRes = _a.sent(); + newDimensions = getResizedDimensions({ + width: imageRes.width, + height: imageRes.height, + }); + minQualityPercentage = 0; + maxQualityPercentage = 101 // exclusive + ; + intermediateUris = []; + _a.label = 2; + case 2: + if (!(maxQualityPercentage - minQualityPercentage > 1)) return [3 /*break*/, 5]; + qualityPercentage = Math.round((maxQualityPercentage + minQualityPercentage) / 2); + return [4 /*yield*/, manipulateAsync(localUri, [{ resize: newDimensions }], { + format: SaveFormat.JPEG, + compress: qualityPercentage / 100, + })]; + case 3: + resizeRes = _a.sent(); + intermediateUris.push(resizeRes.uri); + return [4 /*yield*/, getInfoAsync(resizeRes.uri)]; + case 4: + fileInfo = _a.sent(); + if (!fileInfo.exists) { + throw new Error('The image manipulation library failed to create a new image.'); + } + if (fileInfo.size < opts.maxSize) { + minQualityPercentage = qualityPercentage; + newDataUri = { + path: normalizePath(resizeRes.uri), + mime: 'image/jpeg', + size: fileInfo.size, + width: resizeRes.width, + height: resizeRes.height, + }; + } + else { + maxQualityPercentage = qualityPercentage; + } + return [3 /*break*/, 2]; + case 5: + for (_i = 0, intermediateUris_1 = intermediateUris; _i < intermediateUris_1.length; _i++) { + intermediateUri = intermediateUris_1[_i]; + if ((newDataUri === null || newDataUri === void 0 ? void 0 : newDataUri.path) !== normalizePath(intermediateUri)) { + safeDeleteAsync(intermediateUri); + } + } + if (newDataUri) { + safeDeleteAsync(imageRes.uri); + return [2 /*return*/, newDataUri]; + } + throw new Error("This image is too big! We couldn't compress it down to ".concat(opts.maxSize, " bytes")); + } + }); + }); +} +function moveToPermanentPath(path, ext) { + return __awaiter(this, void 0, void 0, function () { + var filename, destinationPath; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + filename = uuid.v4(); + destinationPath = joinPath(cacheDirectory, filename + ext); + return [4 /*yield*/, copyAsync({ + from: normalizePath(path), + to: normalizePath(destinationPath), + })]; + case 1: + _a.sent(); + safeDeleteAsync(path); + return [2 /*return*/, normalizePath(destinationPath)]; + } + }); + }); +} +export function safeDeleteAsync(path) { + return __awaiter(this, void 0, void 0, function () { + var normalizedPath, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + normalizedPath = normalizePath(path); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, deleteAsync(normalizedPath, { idempotent: true })]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + console.error('Failed to delete file', e_1); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); +} +function joinPath(a, b) { + if (a.endsWith('/')) { + if (b.startsWith('/')) { + return a.slice(0, -1) + b; + } + return a + b; + } + else if (b.startsWith('/')) { + return a + b; + } + return a + '/' + b; +} +function normalizePath(str, allPlatforms) { + if (allPlatforms === void 0) { allPlatforms = false; } + if (IS_ANDROID || allPlatforms) { + if (!str.startsWith('file://')) { + return "file://".concat(str); + } + } + return str; +} +export function saveBytesToDisk(filename, bytes, type) { + return __awaiter(this, void 0, void 0, function () { + var encoded; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + encoded = Buffer.from(bytes).toString('base64'); + return [4 /*yield*/, saveToDevice(filename, encoded, type)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); +} +export function saveToDevice(filename, encoded, type) { + return __awaiter(this, void 0, void 0, function () { + var permissions, fileUrl, e_2; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 7, , 8]); + if (!IS_IOS) return [3 /*break*/, 2]; + return [4 /*yield*/, withTempFile(filename, encoded, function (tmpFileUrl) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Sharing.shareAsync(tmpFileUrl, { UTI: type })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); })]; + case 1: + _a.sent(); + return [2 /*return*/, true]; + case 2: return [4 /*yield*/, StorageAccessFramework.requestDirectoryPermissionsAsync()]; + case 3: + permissions = _a.sent(); + if (!permissions.granted) { + return [2 /*return*/, false]; + } + return [4 /*yield*/, StorageAccessFramework.createFileAsync(permissions.directoryUri, filename, type)]; + case 4: + fileUrl = _a.sent(); + return [4 /*yield*/, writeAsStringAsync(fileUrl, encoded, { + encoding: EncodingType.Base64, + })]; + case 5: + _a.sent(); + return [2 /*return*/, true]; + case 6: return [3 /*break*/, 8]; + case 7: + e_2 = _a.sent(); + logger.error('Error occurred while saving file', { message: e_2 }); + return [2 /*return*/, false]; + case 8: return [2 /*return*/]; + } + }); + }); +} +function withTempFile(filename, encoded, cb) { + return __awaiter(this, void 0, void 0, function () { + var tmpDirUri, tmpFileUrl; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + tmpDirUri = joinPath(cacheDirectory, String(uuid.v4())); + return [4 /*yield*/, makeDirectoryAsync(tmpDirUri, { intermediates: true })]; + case 1: + _a.sent(); + _a.label = 2; + case 2: + _a.trys.push([2, , 5, 6]); + tmpFileUrl = joinPath(tmpDirUri, filename); + return [4 /*yield*/, writeAsStringAsync(tmpFileUrl, encoded, { + encoding: EncodingType.Base64, + })]; + case 3: + _a.sent(); + return [4 /*yield*/, cb(tmpFileUrl)]; + case 4: return [2 /*return*/, _a.sent()]; + case 5: + safeDeleteAsync(tmpDirUri); + return [7 /*endfinally*/]; + case 6: return [2 /*return*/]; + } + }); + }); +} +export function getResizedDimensions(originalDims) { + if (originalDims.width <= POST_IMG_MAX.width && + originalDims.height <= POST_IMG_MAX.height) { + return originalDims; + } + var ratio = Math.min(POST_IMG_MAX.width / originalDims.width, POST_IMG_MAX.height / originalDims.height); + return { + width: Math.round(originalDims.width * ratio), + height: Math.round(originalDims.height * ratio), + }; +} +function createPath(ext) { + // cacheDirectory will never be null on native, so the null check here is not necessary except for typescript. + // we use a web-only function for downloadAndResize on web + return "".concat(cacheDirectory !== null && cacheDirectory !== void 0 ? cacheDirectory : '', "/").concat(uuid.v4(), ".").concat(ext); +} +function downloadImage(uri, path, timeout) { + return __awaiter(this, void 0, void 0, function () { + var dlResumable, timedOut, to1, dlRes; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + dlResumable = createDownloadResumable(uri, path, { cache: true }); + timedOut = false; + to1 = setTimeout(function () { + timedOut = true; + dlResumable.cancelAsync(); + }, timeout); + return [4 /*yield*/, dlResumable.downloadAsync()]; + case 1: + dlRes = _a.sent(); + clearTimeout(to1); + if (!(dlRes === null || dlRes === void 0 ? void 0 : dlRes.uri)) { + if (timedOut) { + throw new Error('Failed to download image - timed out'); + } + else { + throw new Error('Failed to download image - dlRes is undefined'); + } + } + return [2 /*return*/, normalizePath(dlRes.uri)]; + } + }); + }); +} diff --git a/src/lib/media/manip.web.js b/src/lib/media/manip.web.js new file mode 100644 index 0000000000..a80ed4ca78 --- /dev/null +++ b/src/lib/media/manip.web.js @@ -0,0 +1,230 @@ +/// +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { blobToDataUri, getDataUriSize } from './util'; +export function compressIfNeeded(img, maxSize) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (img.size < maxSize) { + return [2 /*return*/, img]; + } + return [4 /*yield*/, doResize(img.path, { + width: img.width, + height: img.height, + mode: 'stretch', + maxSize: maxSize, + })]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); +} +export function downloadAndResize(opts) { + return __awaiter(this, void 0, void 0, function () { + var controller, to, res, resBody, dataUri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + controller = new AbortController(); + to = setTimeout(function () { return controller.abort(); }, opts.timeout || 5e3); + return [4 /*yield*/, fetch(opts.uri)]; + case 1: + res = _a.sent(); + return [4 /*yield*/, res.blob()]; + case 2: + resBody = _a.sent(); + clearTimeout(to); + return [4 /*yield*/, blobToDataUri(resBody)]; + case 3: + dataUri = _a.sent(); + return [4 /*yield*/, doResize(dataUri, opts)]; + case 4: return [2 /*return*/, _a.sent()]; + } + }); + }); +} +export function shareImageModal(_opts) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + // TODO + throw new Error('TODO'); + }); + }); +} +export function saveImageToMediaLibrary(_opts) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + // TODO + throw new Error('TODO'); + }); + }); +} +export function getImageDim(path) { + return __awaiter(this, void 0, void 0, function () { + var img, promise; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + img = document.createElement('img'); + promise = new Promise(function (resolve, reject) { + img.onload = resolve; + img.onerror = reject; + }); + img.src = path; + return [4 /*yield*/, promise]; + case 1: + _a.sent(); + return [2 /*return*/, { width: img.width, height: img.height }]; + } + }); + }); +} +function doResize(dataUri, opts) { + return __awaiter(this, void 0, void 0, function () { + var newDataUri, minQualityPercentage, maxQualityPercentage, qualityPercentage, tempDataUri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + minQualityPercentage = 0; + maxQualityPercentage = 101 //exclusive + ; + _a.label = 1; + case 1: + if (!(maxQualityPercentage - minQualityPercentage > 1)) return [3 /*break*/, 3]; + qualityPercentage = Math.round((maxQualityPercentage + minQualityPercentage) / 2); + return [4 /*yield*/, createResizedImage(dataUri, { + width: opts.width, + height: opts.height, + quality: qualityPercentage / 100, + mode: opts.mode, + })]; + case 2: + tempDataUri = _a.sent(); + if (getDataUriSize(tempDataUri) < opts.maxSize) { + minQualityPercentage = qualityPercentage; + newDataUri = tempDataUri; + } + else { + maxQualityPercentage = qualityPercentage; + } + return [3 /*break*/, 1]; + case 3: + if (!newDataUri) { + throw new Error('Failed to compress image'); + } + return [2 /*return*/, { + path: newDataUri, + mime: 'image/jpeg', + size: getDataUriSize(newDataUri), + width: opts.width, + height: opts.height, + }]; + } + }); + }); +} +function createResizedImage(dataUri, _a) { + var width = _a.width, height = _a.height, quality = _a.quality, mode = _a.mode; + return new Promise(function (resolve, reject) { + var img = document.createElement('img'); + img.addEventListener('load', function () { + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + if (!ctx) { + return reject(new Error('Failed to resize image')); + } + var scale = 1; + if (mode === 'cover') { + scale = img.width < img.height ? width / img.width : height / img.height; + } + else if (mode === 'contain') { + scale = img.width > img.height ? width / img.width : height / img.height; + } + var w = img.width * scale; + var h = img.height * scale; + canvas.width = w; + canvas.height = h; + ctx.drawImage(img, 0, 0, w, h); + resolve(canvas.toDataURL('image/jpeg', quality)); + }); + img.addEventListener('error', function (ev) { + reject(ev.error); + }); + img.src = dataUri; + }); +} +export function saveBytesToDisk(filename, bytes, type) { + return __awaiter(this, void 0, void 0, function () { + var blob, url; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + blob = new Blob([bytes], { type: type }); + url = URL.createObjectURL(blob); + return [4 /*yield*/, downloadUrl(url, filename) + // Firefox requires a small delay + ]; + case 1: + _a.sent(); + // Firefox requires a small delay + setTimeout(function () { return URL.revokeObjectURL(url); }, 100); + return [2 /*return*/, true]; + } + }); + }); +} +function downloadUrl(href, filename) { + return __awaiter(this, void 0, void 0, function () { + var a; + return __generator(this, function (_a) { + a = document.createElement('a'); + a.href = href; + a.download = filename; + a.click(); + return [2 /*return*/]; + }); + }); +} +export function safeDeleteAsync() { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/]; + }); + }); +} diff --git a/src/lib/media/picker.e2e.js b/src/lib/media/picker.e2e.js new file mode 100644 index 0000000000..79e5c1a2d7 --- /dev/null +++ b/src/lib/media/picker.e2e.js @@ -0,0 +1,141 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { documentDirectory, getInfoAsync, readDirectoryAsync, } from 'expo-file-system/legacy'; +import ExpoImageCropTool from '@bsky.app/expo-image-crop-tool'; +import { compressIfNeeded } from './manip'; +function getFile() { + return __awaiter(this, void 0, void 0, function () { + var imagesDir, files, file, fileInfo; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + imagesDir = documentDirectory + .split('/') + .slice(0, -6) + .concat(['Media', 'DCIM', '100APPLE']) + .join('/'); + return [4 /*yield*/, readDirectoryAsync(imagesDir)]; + case 1: + files = _a.sent(); + files = files.filter(function (file) { return file.endsWith('.JPG'); }); + file = "".concat(imagesDir, "/").concat(files[0]); + return [4 /*yield*/, getInfoAsync(file)]; + case 2: + fileInfo = _a.sent(); + if (!fileInfo.exists) { + throw new Error('Failed to get file info'); + } + return [4 /*yield*/, compressIfNeeded({ + path: file, + mime: 'image/jpeg', + size: fileInfo.size, + width: 4288, + height: 2848, + })]; + case 3: return [2 /*return*/, _a.sent()]; + } + }); + }); +} +export function openPicker() { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getFile()]; + case 1: return [2 /*return*/, [_a.sent()]]; + } + }); + }); +} +export function openUnifiedPicker() { + return __awaiter(this, void 0, void 0, function () { + var file; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getFile()]; + case 1: + file = _a.sent(); + return [2 /*return*/, { + assets: [ + __assign({ type: 'image', uri: file.path, mimeType: file.mime }, file), + ], + canceled: false, + }]; + } + }); + }); +} +export function openCamera() { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getFile()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); +} +export function openCropper(opts) { + return __awaiter(this, void 0, void 0, function () { + var item; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, ExpoImageCropTool.openCropperAsync(__assign(__assign({}, opts), { format: 'jpeg' }))]; + case 1: + item = _a.sent(); + return [2 /*return*/, { + path: item.path, + mime: item.mimeType, + size: item.size, + width: item.width, + height: item.height, + }]; + } + }); + }); +} diff --git a/src/lib/media/picker.js b/src/lib/media/picker.js new file mode 100644 index 0000000000..b511eec432 --- /dev/null +++ b/src/lib/media/picker.js @@ -0,0 +1,101 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { launchCameraAsync } from 'expo-image-picker'; +import ExpoImageCropTool from '@bsky.app/expo-image-crop-tool'; +import { t } from '@lingui/core/macro'; +export { openPicker, openUnifiedPicker, } from './picker.shared'; +export function openCamera(customOpts) { + return __awaiter(this, void 0, void 0, function () { + var opts, res, asset; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + opts = __assign({ mediaTypes: 'images' }, customOpts); + return [4 /*yield*/, launchCameraAsync(opts)]; + case 1: + res = _c.sent(); + if (!res || !res.assets) { + throw new Error('Camera was closed before taking a photo'); + } + asset = res === null || res === void 0 ? void 0 : res.assets[0]; + return [2 /*return*/, { + path: asset.uri, + mime: (_a = asset.mimeType) !== null && _a !== void 0 ? _a : 'image/jpeg', + size: (_b = asset.fileSize) !== null && _b !== void 0 ? _b : 0, + width: asset.width, + height: asset.height, + }]; + } + }); + }); +} +export function openCropper(opts) { + return __awaiter(this, void 0, void 0, function () { + var item; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, ExpoImageCropTool.openCropperAsync(__assign(__assign({ doneButtonText: t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Done"], ["Done"]))), cancelButtonText: t(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Cancel"], ["Cancel"]))) }, opts), { format: 'jpeg' }))]; + case 1: + item = _a.sent(); + return [2 /*return*/, { + path: item.path, + mime: item.mimeType, + size: item.size, + width: item.width, + height: item.height, + }]; + } + }); + }); +} +var templateObject_1, templateObject_2; diff --git a/src/lib/media/picker.shared.js b/src/lib/media/picker.shared.js new file mode 100644 index 0000000000..fbf5a998a0 --- /dev/null +++ b/src/lib/media/picker.shared.js @@ -0,0 +1,107 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { launchImageLibraryAsync, UIImagePickerPreferredAssetRepresentationMode, } from 'expo-image-picker'; +import { t } from '@lingui/core/macro'; +import * as Toast from '#/view/com/util/Toast'; +import { IS_IOS, IS_WEB } from '#/env'; +import { VIDEO_MAX_DURATION_MS } from '../constants'; +import { getDataUriSize } from './util'; +export function openPicker(opts) { + return __awaiter(this, void 0, void 0, function () { + var response; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, launchImageLibraryAsync(__assign(__assign({ exif: false, mediaTypes: ['images'], quality: 1, selectionLimit: 1 }, opts), { legacy: true, preferredAssetRepresentationMode: UIImagePickerPreferredAssetRepresentationMode.Automatic }))]; + case 1: + response = _b.sent(); + return [2 /*return*/, ((_a = response.assets) !== null && _a !== void 0 ? _a : []) + .filter(function (asset) { + var _a; + if ((_a = asset.mimeType) === null || _a === void 0 ? void 0 : _a.startsWith('image/')) + return true; + Toast.show(t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Only image files are supported"], ["Only image files are supported"]))), 'exclamation-circle'); + return false; + }) + .map(function (image) { return ({ + mime: image.mimeType || 'image/jpeg', + height: image.height, + width: image.width, + path: image.uri, + size: getDataUriSize(image.uri), + }); })]; + } + }); + }); +} +export function openUnifiedPicker(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var selectionCountRemaining = _b.selectionCountRemaining; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, launchImageLibraryAsync({ + exif: false, + mediaTypes: ['images', 'videos'], + quality: 1, + allowsMultipleSelection: true, + legacy: true, + base64: IS_WEB, + selectionLimit: IS_IOS ? selectionCountRemaining : undefined, + preferredAssetRepresentationMode: UIImagePickerPreferredAssetRepresentationMode.Automatic, + videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000, + })]; + case 1: return [2 /*return*/, _c.sent()]; + } + }); + }); +} +var templateObject_1; diff --git a/src/lib/media/picker.web.js b/src/lib/media/picker.web.js new file mode 100644 index 0000000000..d7c04f1490 --- /dev/null +++ b/src/lib/media/picker.web.js @@ -0,0 +1,51 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +export { openPicker, openUnifiedPicker } from './picker.shared'; +export function openCamera(_opts) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + throw new Error('openCamera is not supported on web'); + }); + }); +} +export function openCropper(_opts) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + throw new Error('openCropper is not supported on web. Use EditImageDialog instead.'); + }); + }); +} diff --git a/src/lib/media/save-image.ios.js b/src/lib/media/save-image.ios.js new file mode 100644 index 0000000000..bd4c0447fa --- /dev/null +++ b/src/lib/media/save-image.ios.js @@ -0,0 +1,81 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import * as Toast from '#/components/Toast'; +import { IS_NATIVE } from '#/env'; +import { saveImageToMediaLibrary } from './manip'; +/** + * Same as `saveImageToMediaLibrary`, but also handles permissions and toasts + * + * iOS doesn't not require permissions to save images to the media library, + * so this file is platform-split as it's much simpler than the Android version. + */ +export function useSaveImageToMediaLibrary() { + var _this = this; + var _ = useLingui()._; + return useCallback(function (uri) { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!IS_NATIVE) { + throw new Error('useSaveImageToMediaLibrary is native only'); + } + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, saveImageToMediaLibrary({ uri: uri })]; + case 2: + _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Image saved"], ["Image saved"]))))); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to save image: ", ""], ["Failed to save image: ", ""])), String(e_1))), { type: 'error' }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [_]); +} +var templateObject_1, templateObject_2; diff --git a/src/lib/media/save-image.js b/src/lib/media/save-image.js new file mode 100644 index 0000000000..9e0c2feead --- /dev/null +++ b/src/lib/media/save-image.js @@ -0,0 +1,125 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import * as MediaLibrary from 'expo-media-library'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import * as Toast from '#/components/Toast'; +import { IS_NATIVE } from '#/env'; +import { saveImageToMediaLibrary } from './manip'; +/** + * Same as `saveImageToMediaLibrary`, but also handles permissions and toasts + */ +export function useSaveImageToMediaLibrary() { + var _this = this; + var _ = useLingui()._; + var _a = MediaLibrary.usePermissions({ + granularPermissions: ['photo'], + }), permissionResponse = _a[0], requestPermission = _a[1], getPermission = _a[2]; + return useCallback(function (uri) { return __awaiter(_this, void 0, void 0, function () { + function save() { + return __awaiter(this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, saveImageToMediaLibrary({ uri: uri })]; + case 1: + _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Image saved"], ["Image saved"]))))); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to save image: ", ""], ["Failed to save image: ", ""])), String(e_1))), { + type: 'error', + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); + } + var permission, _a, askAgain; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!IS_NATIVE) { + throw new Error('useSaveImageToMediaLibrary is native only'); + } + if (!(permissionResponse !== null && permissionResponse !== void 0)) return [3 /*break*/, 1]; + _a = permissionResponse; + return [3 /*break*/, 3]; + case 1: return [4 /*yield*/, getPermission()]; + case 2: + _a = (_b.sent()); + _b.label = 3; + case 3: + permission = _a; + if (!permission.granted) return [3 /*break*/, 5]; + return [4 /*yield*/, save()]; + case 4: + _b.sent(); + return [3 /*break*/, 11]; + case 5: + if (!permission.canAskAgain) return [3 /*break*/, 10]; + return [4 /*yield*/, requestPermission()]; + case 6: + askAgain = _b.sent(); + if (!askAgain.granted) return [3 /*break*/, 8]; + return [4 /*yield*/, save()]; + case 7: + _b.sent(); + return [3 /*break*/, 9]; + case 8: + // since we've been explicitly denied, show a toast. + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Images cannot be saved unless permission is granted to access your photo library."], ["Images cannot be saved unless permission is granted to access your photo library."])))), { type: 'error' }); + _b.label = 9; + case 9: return [3 /*break*/, 11]; + case 10: + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Permission to access your photo library was denied. Please enable it in your system settings."], ["Permission to access your photo library was denied. Please enable it in your system settings."])))), { type: 'error' }); + _b.label = 11; + case 11: return [2 /*return*/]; + } + }); + }); }, [permissionResponse, requestPermission, getPermission, _]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/lib/media/types.js b/src/lib/media/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/lib/media/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/lib/media/util.js b/src/lib/media/util.js new file mode 100644 index 0000000000..7c81a13c3c --- /dev/null +++ b/src/lib/media/util.js @@ -0,0 +1,26 @@ +export function extractDataUriMime(uri) { + return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';')); +} +// Fairly accurate estimate that is more performant +// than decoding and checking length of URI +export function getDataUriSize(uri) { + return Math.round((uri.length * 3) / 4); +} +export function isUriImage(uri) { + return /\.(jpg|jpeg|png|webp).*$/.test(uri); +} +export function blobToDataUri(blob) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onloadend = function () { + if (typeof reader.result === 'string') { + resolve(reader.result); + } + else { + reject(new Error('Failed to read blob')); + } + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} diff --git a/src/lib/media/video/compress.js b/src/lib/media/video/compress.js new file mode 100644 index 0000000000..3a73f72bf7 --- /dev/null +++ b/src/lib/media/video/compress.js @@ -0,0 +1,82 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { getVideoMetaData, Video } from 'react-native-compressor'; +import { SUPPORTED_MIME_TYPES } from '#/lib/constants'; +import { extToMime } from './util'; +var MIN_SIZE_FOR_COMPRESSION = 25; // 25mb +export function compressVideo(file, opts) { + return __awaiter(this, void 0, void 0, function () { + var _a, onProgress, signal, isAcceptableFormat, minimumFileSizeForCompress, compressed, info; + var _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + _a = opts || {}, onProgress = _a.onProgress, signal = _a.signal; + isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(file.mimeType); + if (file.mimeType === 'image/gif') { + // let's hope they're small enough that they don't need compression! + // this compression library doesn't support gifs + // worst case - server rejects them. I think that's fine -sfn + return [2 /*return*/, { uri: file.uri, size: (_b = file.fileSize) !== null && _b !== void 0 ? _b : -1, mimeType: 'image/gif' }]; + } + minimumFileSizeForCompress = isAcceptableFormat + ? MIN_SIZE_FOR_COMPRESSION + : 0; + return [4 /*yield*/, Video.compress(file.uri, { + compressionMethod: 'manual', + bitrate: 3000000, // 3mbps + maxSize: 1920, + // WARNING: this ONE SPECIFIC ARG is in MB -sfn + minimumFileSizeForCompress: minimumFileSizeForCompress, + getCancellationId: function (id) { + if (signal) { + signal.addEventListener('abort', function () { + Video.cancelCompression(id); + }); + } + }, + }, onProgress)]; + case 1: + compressed = _c.sent(); + return [4 /*yield*/, getVideoMetaData(compressed)]; + case 2: + info = _c.sent(); + return [2 /*return*/, { uri: compressed, size: info.size, mimeType: extToMime(info.extension) }]; + } + }); + }); +} diff --git a/src/lib/media/video/compress.web.js b/src/lib/media/video/compress.web.js new file mode 100644 index 0000000000..62b1dbfdac --- /dev/null +++ b/src/lib/media/video/compress.web.js @@ -0,0 +1,85 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { VIDEO_MAX_SIZE } from '#/lib/constants'; +import { VideoTooLargeError } from '#/lib/media/video/errors'; +// doesn't actually compress, converts to ArrayBuffer +export function compressVideo(asset, _opts) { + return __awaiter(this, void 0, void 0, function () { + var _a, mimeType, base64, blob, uri; + var _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + _a = parseDataUrl(asset.uri), mimeType = _a.mimeType, base64 = _a.base64; + blob = base64ToBlob(base64, mimeType); + uri = URL.createObjectURL(blob); + if (blob.size > VIDEO_MAX_SIZE) { + throw new VideoTooLargeError(); + } + _b = { + size: blob.size, + uri: uri + }; + return [4 /*yield*/, blob.arrayBuffer()]; + case 1: return [2 /*return*/, (_b.bytes = _c.sent(), + _b.mimeType = mimeType, + _b)]; + } + }); + }); +} +function parseDataUrl(dataUrl) { + var _a = dataUrl.slice('data:'.length).split(';base64,'), mimeType = _a[0], base64 = _a[1]; + if (!mimeType || !base64) { + throw new Error('Invalid data URL'); + } + return { mimeType: mimeType, base64: base64 }; +} +function base64ToBlob(base64, mimeType) { + var byteCharacters = atob(base64); + var byteArrays = []; + for (var offset = 0; offset < byteCharacters.length; offset += 512) { + var slice = byteCharacters.slice(offset, offset + 512); + var byteNumbers = new Array(slice.length); + for (var i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + var byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + return new Blob(byteArrays, { type: mimeType }); +} diff --git a/src/lib/media/video/errors.js b/src/lib/media/video/errors.js new file mode 100644 index 0000000000..dd6168a5ba --- /dev/null +++ b/src/lib/media/video/errors.js @@ -0,0 +1,45 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var VideoTooLargeError = /** @class */ (function (_super) { + __extends(VideoTooLargeError, _super); + function VideoTooLargeError() { + var _this = _super.call(this, 'Videos cannot be larger than 100 MB') || this; + _this.name = 'VideoTooLargeError'; + return _this; + } + return VideoTooLargeError; +}(Error)); +export { VideoTooLargeError }; +var ServerError = /** @class */ (function (_super) { + __extends(ServerError, _super); + function ServerError(message) { + var _this = _super.call(this, message) || this; + _this.name = 'ServerError'; + return _this; + } + return ServerError; +}(Error)); +export { ServerError }; +var UploadLimitError = /** @class */ (function (_super) { + __extends(UploadLimitError, _super); + function UploadLimitError(message) { + var _this = _super.call(this, message) || this; + _this.name = 'UploadLimitError'; + return _this; + } + return UploadLimitError; +}(Error)); +export { UploadLimitError }; diff --git a/src/lib/media/video/types.js b/src/lib/media/video/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/lib/media/video/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/lib/media/video/upload.js b/src/lib/media/video/upload.js new file mode 100644 index 0000000000..8066c3365b --- /dev/null +++ b/src/lib/media/video/upload.js @@ -0,0 +1,104 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { createUploadTask, FileSystemUploadType } from 'expo-file-system/legacy'; +import { msg } from '@lingui/core/macro'; +import { nanoid } from 'nanoid/non-secure'; +import { AbortError } from '#/lib/async/cancelable'; +import { ServerError } from '#/lib/media/video/errors'; +import { getServiceAuthToken, getVideoUploadLimits } from './upload.shared'; +import { createVideoEndpointUrl, mimeToExt } from './util'; +export function uploadVideo(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var uri, token, uploadTask, res, responseBody; + var video = _b.video, agent = _b.agent, did = _b.did, setProgress = _b.setProgress, signal = _b.signal, _ = _b._; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (signal.aborted) { + throw new AbortError(); + } + return [4 /*yield*/, getVideoUploadLimits(agent, _)]; + case 1: + _c.sent(); + uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { + did: did, + name: "".concat(nanoid(12), ".").concat(mimeToExt(video.mimeType)), + }); + if (signal.aborted) { + throw new AbortError(); + } + return [4 /*yield*/, getServiceAuthToken({ + agent: agent, + lxm: 'com.atproto.repo.uploadBlob', + exp: Date.now() / 1000 + 60 * 30, // 30 minutes + })]; + case 2: + token = _c.sent(); + uploadTask = createUploadTask(uri, video.uri, { + headers: { + 'content-type': video.mimeType, + Authorization: "Bearer ".concat(token), + }, + httpMethod: 'POST', + uploadType: FileSystemUploadType.BINARY_CONTENT, + }, function (p) { return setProgress(p.totalBytesSent / p.totalBytesExpectedToSend); }); + if (signal.aborted) { + throw new AbortError(); + } + return [4 /*yield*/, uploadTask.uploadAsync()]; + case 3: + res = _c.sent(); + if (!(res === null || res === void 0 ? void 0 : res.body)) { + throw new Error('No response'); + } + responseBody = JSON.parse(res.body); + if (!responseBody.jobId) { + throw new ServerError(responseBody.error || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to upload video"], ["Failed to upload video"]))))); + } + if (signal.aborted) { + throw new AbortError(); + } + return [2 /*return*/, responseBody]; + } + }); + }); +} +var templateObject_1; diff --git a/src/lib/media/video/upload.shared.js b/src/lib/media/video/upload.shared.js new file mode 100644 index 0000000000..869988e213 --- /dev/null +++ b/src/lib/media/video/upload.shared.js @@ -0,0 +1,107 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { msg } from '@lingui/core/macro'; +import { VIDEO_SERVICE_DID } from '#/lib/constants'; +import { UploadLimitError } from '#/lib/media/video/errors'; +import { getServiceAuthAudFromUrl } from '#/lib/strings/url-helpers'; +import { createVideoAgent } from './util'; +export function getServiceAuthToken(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var pdsAud, serviceAuth; + var agent = _b.agent, aud = _b.aud, lxm = _b.lxm, exp = _b.exp; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + pdsAud = getServiceAuthAudFromUrl(agent.dispatchUrl); + if (!pdsAud) { + throw new Error('Agent does not have a PDS URL'); + } + return [4 /*yield*/, agent.com.atproto.server.getServiceAuth({ + aud: aud !== null && aud !== void 0 ? aud : pdsAud, + lxm: lxm, + exp: exp, + })]; + case 1: + serviceAuth = (_c.sent()).data; + return [2 /*return*/, serviceAuth.token]; + } + }); + }); +} +export function getVideoUploadLimits(agent, _) { + return __awaiter(this, void 0, void 0, function () { + var token, videoAgent, limits; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getServiceAuthToken({ + agent: agent, + lxm: 'app.bsky.video.getUploadLimits', + aud: VIDEO_SERVICE_DID, + })]; + case 1: + token = _a.sent(); + videoAgent = createVideoAgent(); + return [4 /*yield*/, videoAgent.app.bsky.video + .getUploadLimits({}, { headers: { Authorization: "Bearer ".concat(token) } }) + .catch(function (err) { + if (err instanceof Error) { + throw new UploadLimitError(err.message); + } + else { + throw err; + } + })]; + case 2: + limits = (_a.sent()).data; + if (!limits.canUpload) { + if (limits.message) { + throw new UploadLimitError(limits.message); + } + else { + throw new UploadLimitError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You have temporarily reached the limit for video uploads. Please try again later."], ["You have temporarily reached the limit for video uploads. Please try again later."]))))); + } + } + return [2 /*return*/]; + } + }); + }); +} +var templateObject_1; diff --git a/src/lib/media/video/upload.web.js b/src/lib/media/video/upload.web.js new file mode 100644 index 0000000000..04fd037f10 --- /dev/null +++ b/src/lib/media/video/upload.web.js @@ -0,0 +1,126 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { msg } from '@lingui/core/macro'; +import { nanoid } from 'nanoid/non-secure'; +import { AbortError } from '#/lib/async/cancelable'; +import { ServerError } from '#/lib/media/video/errors'; +import { getServiceAuthToken, getVideoUploadLimits } from './upload.shared'; +import { createVideoEndpointUrl, mimeToExt } from './util'; +export function uploadVideo(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var uri, bytes, token, xhr, res; + var video = _b.video, agent = _b.agent, did = _b.did, setProgress = _b.setProgress, signal = _b.signal, _ = _b._; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (signal.aborted) { + throw new AbortError(); + } + return [4 /*yield*/, getVideoUploadLimits(agent, _)]; + case 1: + _c.sent(); + uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { + did: did, + name: "".concat(nanoid(12), ".").concat(mimeToExt(video.mimeType)), + }); + bytes = video.bytes; + if (!!bytes) return [3 /*break*/, 3]; + if (signal.aborted) { + throw new AbortError(); + } + return [4 /*yield*/, fetch(video.uri).then(function (res) { return res.arrayBuffer(); })]; + case 2: + bytes = _c.sent(); + _c.label = 3; + case 3: + if (signal.aborted) { + throw new AbortError(); + } + return [4 /*yield*/, getServiceAuthToken({ + agent: agent, + lxm: 'com.atproto.repo.uploadBlob', + exp: Date.now() / 1000 + 60 * 30, // 30 minutes + })]; + case 4: + token = _c.sent(); + if (signal.aborted) { + throw new AbortError(); + } + xhr = new XMLHttpRequest(); + return [4 /*yield*/, new Promise(function (resolve, reject) { + xhr.upload.addEventListener('progress', function (e) { + var progress = e.loaded / e.total; + setProgress(progress); + }); + xhr.onloadend = function () { + if (signal.aborted) { + reject(new AbortError()); + } + else if (xhr.readyState === 4) { + var uploadRes = JSON.parse(xhr.responseText); + resolve(uploadRes); + } + else { + reject(new ServerError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to upload video"], ["Failed to upload video"])))))); + } + }; + xhr.onerror = function () { + reject(new ServerError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to upload video"], ["Failed to upload video"])))))); + }; + xhr.open('POST', uri); + xhr.setRequestHeader('Content-Type', video.mimeType); + xhr.setRequestHeader('Authorization', "Bearer ".concat(token)); + xhr.send(bytes); + })]; + case 5: + res = _c.sent(); + if (!res.jobId) { + throw new ServerError(res.error || _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to upload video"], ["Failed to upload video"]))))); + } + if (signal.aborted) { + throw new AbortError(); + } + return [2 /*return*/, res]; + } + }); + }); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/lib/media/video/util.js b/src/lib/media/video/util.js new file mode 100644 index 0000000000..f8d40991f5 --- /dev/null +++ b/src/lib/media/video/util.js @@ -0,0 +1,49 @@ +import { AtpAgent } from '@atproto/api'; +import { VIDEO_SERVICE } from '#/lib/constants'; +export var createVideoEndpointUrl = function (route, params) { + var url = new URL(VIDEO_SERVICE); + url.pathname = route; + if (params) { + for (var key in params) { + url.searchParams.set(key, params[key]); + } + } + return url.href; +}; +export function createVideoAgent() { + return new AtpAgent({ + service: VIDEO_SERVICE, + }); +} +export function mimeToExt(mimeType) { + switch (mimeType) { + case 'video/mp4': + return 'mp4'; + case 'video/webm': + return 'webm'; + case 'video/mpeg': + return 'mpeg'; + case 'video/quicktime': + return 'mov'; + case 'image/gif': + return 'gif'; + default: + throw new Error("Unsupported mime type: ".concat(mimeType)); + } +} +export function extToMime(ext) { + switch (ext.toLowerCase()) { + case 'mp4': + return 'video/mp4'; + case 'webm': + return 'video/webm'; + case 'mpeg': + return 'video/mpeg'; + case 'mov': + return 'video/quicktime'; + case 'gif': + return 'image/gif'; + default: + throw new Error("Unsupported file extension: ".concat(ext)); + } +} diff --git a/src/lib/merge-refs.js b/src/lib/merge-refs.js new file mode 100644 index 0000000000..6166fe06b6 --- /dev/null +++ b/src/lib/merge-refs.js @@ -0,0 +1,27 @@ +/** + * This TypeScript function merges multiple React refs into a single ref callback. + * When developing low level UI components, it is common to have to use a local ref + * but also support an external one using React.forwardRef. + * Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility. + * Today a ref can be a function or an object, tomorrow it could be another thing, who knows. + * This utility handles compatibility for you. + * This function is inspired by https://github.com/gregberge/react-merge-refs + * @param refs - An array of React refs, which can be either `React.MutableRefObject` or + * `React.LegacyRef`. These refs are used to store references to DOM elements or React components. + * The `mergeRefs` function takes in an array of these refs and returns a callback function that + * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and + * returns a ref callback function that can be used to merge multiple refs into a single ref. + */ +export function mergeRefs(refs) { + return function (value) { + refs.forEach(function (ref) { + if (typeof ref === 'function') { + ref(value); + } + else if (ref != null) { + ; + ref.current = value; + } + }); + }; +} diff --git a/src/lib/moderation.js b/src/lib/moderation.js new file mode 100644 index 0000000000..7df14ff9f7 --- /dev/null +++ b/src/lib/moderation.js @@ -0,0 +1,93 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import React from 'react'; +import { BskyAgent, LABELS, } from '@atproto/api'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +export var ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']; +export var OTHER_SELF_LABELS = ['graphic-media']; +export var SELF_LABELS = __spreadArray(__spreadArray([], ADULT_CONTENT_LABELS, true), OTHER_SELF_LABELS, true); +export function getModerationCauseKey(cause) { + var source = cause.source.type === 'labeler' + ? cause.source.did + : cause.source.type === 'list' + ? cause.source.list.uri + : 'user'; + if (cause.type === 'label') { + return "label:".concat(cause.label.val, ":").concat(source); + } + return "".concat(cause.type, ":").concat(source); +} +export function isJustAMute(modui) { + return modui.filters.length === 1 && modui.filters[0].type === 'muted'; +} +export function moduiContainsHideableOffense(modui) { + var label = modui.filters.at(0); + if (label && label.type === 'label') { + return labelIsHideableOffense(label.label); + } + return false; +} +export function labelIsHideableOffense(label) { + return ['!hide', '!takedown'].includes(label.val); +} +export function getLabelingServiceTitle(_a) { + var displayName = _a.displayName, handle = _a.handle; + return displayName + ? sanitizeDisplayName(displayName) + : sanitizeHandle(handle, '@'); +} +export function lookupLabelValueDefinition(labelValue, customDefs) { + var def; + if (!labelValue.startsWith('!') && customDefs) { + def = customDefs.find(function (d) { return d.identifier === labelValue; }); + } + if (!def) { + def = LABELS[labelValue]; + } + return def; +} +export function isAppLabeler(labeler) { + if (typeof labeler === 'string') { + return BskyAgent.appLabelers.includes(labeler); + } + return BskyAgent.appLabelers.includes(labeler.creator.did); +} +export function isLabelerSubscribed(labeler, modOpts) { + labeler = typeof labeler === 'string' ? labeler : labeler.creator.did; + if (isAppLabeler(labeler)) { + return true; + } + return modOpts.prefs.labelers.find(function (l) { return l.did === labeler; }); +} +export function useLabelSubject(_a) { + var label = _a.label; + return React.useMemo(function () { + var cid = label.cid, uri = label.uri; + if (cid) { + return { + subject: { + uri: uri, + cid: cid, + }, + }; + } + else { + return { + subject: { + did: uri, + }, + }; + } + }, [label]); +} +export function unique(value, index, array) { + return (array.findIndex(function (item) { return getModerationCauseKey(item) === getModerationCauseKey(value); }) === index); +} diff --git a/src/lib/moderation/blocked-and-muted.js b/src/lib/moderation/blocked-and-muted.js new file mode 100644 index 0000000000..624a6ec882 --- /dev/null +++ b/src/lib/moderation/blocked-and-muted.js @@ -0,0 +1,8 @@ +export function isBlockedOrBlocking(profile) { + var _a, _b; + return ((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.blockedBy) || ((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.blocking); +} +export function isMuted(profile) { + var _a, _b; + return ((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.muted) || ((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.mutedByList); +} diff --git a/src/lib/moderation/create-sanitized-display-name.js b/src/lib/moderation/create-sanitized-display-name.js new file mode 100644 index 0000000000..d4fa901680 --- /dev/null +++ b/src/lib/moderation/create-sanitized-display-name.js @@ -0,0 +1,11 @@ +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +export function createSanitizedDisplayName(profile, noAt, moderation) { + if (noAt === void 0) { noAt = false; } + if (profile.displayName != null && profile.displayName !== '') { + return sanitizeDisplayName(profile.displayName, moderation); + } + else { + return sanitizeHandle(profile.handle, noAt ? '' : '@'); + } +} diff --git a/src/lib/moderation/useGlobalLabelStrings.js b/src/lib/moderation/useGlobalLabelStrings.js new file mode 100644 index 0000000000..887120e944 --- /dev/null +++ b/src/lib/moderation/useGlobalLabelStrings.js @@ -0,0 +1,45 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { useMemo } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +export function useGlobalLabelStrings() { + var _ = useLingui()._; + return useMemo(function () { return ({ + '!hide': { + name: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Content Blocked"], ["Content Blocked"])))), + description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["This content has been hidden by the moderators."], ["This content has been hidden by the moderators."])))), + }, + '!warn': { + name: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Content Warning"], ["Content Warning"])))), + description: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["This content has received a general warning from moderators."], ["This content has received a general warning from moderators."])))), + }, + '!no-unauthenticated': { + name: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Sign-in Required"], ["Sign-in Required"])))), + description: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["This user has requested that their content only be shown to signed-in users."], ["This user has requested that their content only be shown to signed-in users."])))), + }, + porn: { + name: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Adult Content"], ["Adult Content"])))), + description: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Explicit sexual images."], ["Explicit sexual images."])))), + }, + sexual: { + name: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Sexually Suggestive"], ["Sexually Suggestive"])))), + description: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Does not include nudity."], ["Does not include nudity."])))), + }, + nudity: { + name: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Non-sexual Nudity"], ["Non-sexual Nudity"])))), + description: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["E.g. artistic nudes."], ["E.g. artistic nudes."])))), + }, + 'graphic-media': { + name: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Graphic Media"], ["Graphic Media"])))), + description: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Explicit or potentially disturbing media."], ["Explicit or potentially disturbing media."])))), + }, + gore: { + name: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Graphic Media"], ["Graphic Media"])))), + description: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Explicit or potentially disturbing media."], ["Explicit or potentially disturbing media."])))), + }, + }); }, [_]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16; diff --git a/src/lib/moderation/useLabelBehaviorDescription.js b/src/lib/moderation/useLabelBehaviorDescription.js new file mode 100644 index 0000000000..df6f948b6c --- /dev/null +++ b/src/lib/moderation/useLabelBehaviorDescription.js @@ -0,0 +1,73 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +export function useLabelBehaviorDescription(labelValueDef, pref) { + var _ = useLingui()._; + if (pref === 'ignore') { + return _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Off"], ["Off"])))); + } + if (labelValueDef.blurs === 'content' || labelValueDef.blurs === 'media') { + if (pref === 'hide') { + return _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Hide"], ["Hide"])))); + } + return _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Warn"], ["Warn"])))); + } + else if (labelValueDef.severity === 'alert') { + if (pref === 'hide') { + return _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Hide"], ["Hide"])))); + } + return _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Warn"], ["Warn"])))); + } + else if (labelValueDef.severity === 'inform') { + if (pref === 'hide') { + return _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Hide"], ["Hide"])))); + } + return _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Show badge"], ["Show badge"])))); + } + else { + if (pref === 'hide') { + return _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Hide"], ["Hide"])))); + } + return _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Disabled"], ["Disabled"])))); + } +} +export function useLabelLongBehaviorDescription(labelValueDef, pref) { + var _ = useLingui()._; + if (pref === 'ignore') { + return _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Disabled"], ["Disabled"])))); + } + if (labelValueDef.blurs === 'content') { + if (pref === 'hide') { + return _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Warn content and filter from feeds"], ["Warn content and filter from feeds"])))); + } + return _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Warn content"], ["Warn content"])))); + } + else if (labelValueDef.blurs === 'media') { + if (pref === 'hide') { + return _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Blur images and filter from feeds"], ["Blur images and filter from feeds"])))); + } + return _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Blur images"], ["Blur images"])))); + } + else if (labelValueDef.severity === 'alert') { + if (pref === 'hide') { + return _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Show warning and filter from feeds"], ["Show warning and filter from feeds"])))); + } + return _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Show warning"], ["Show warning"])))); + } + else if (labelValueDef.severity === 'inform') { + if (pref === 'hide') { + return _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Show badge and filter from feeds"], ["Show badge and filter from feeds"])))); + } + return _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Show badge"], ["Show badge"])))); + } + else { + if (pref === 'hide') { + return _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Filter from feeds"], ["Filter from feeds"])))); + } + return _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Disabled"], ["Disabled"])))); + } +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20; diff --git a/src/lib/moderation/useLabelInfo.js b/src/lib/moderation/useLabelInfo.js new file mode 100644 index 0000000000..d82e8871f9 --- /dev/null +++ b/src/lib/moderation/useLabelInfo.js @@ -0,0 +1,63 @@ +import { interpretLabelValueDefinition, LABELS, } from '@atproto/api'; +import { useLingui } from '@lingui/react'; +import * as bcp47Match from 'bcp-47-match'; +import { useGlobalLabelStrings, } from '#/lib/moderation/useGlobalLabelStrings'; +import { useLabelDefinitions } from '#/state/preferences'; +export function useLabelInfo(label) { + var i18n = useLingui().i18n; + var _a = useLabelDefinitions(), labelDefs = _a.labelDefs, labelers = _a.labelers; + var globalLabelStrings = useGlobalLabelStrings(); + var def = getDefinition(labelDefs, label); + return { + label: label, + def: def, + strings: getLabelStrings(i18n.locale, globalLabelStrings, def), + labeler: labelers.find(function (labeler) { return label.src === labeler.creator.did; }), + }; +} +export function getDefinition(labelDefs, label) { + var _a; + // check local definitions + var customDef = !label.val.startsWith('!') && + ((_a = labelDefs[label.src]) === null || _a === void 0 ? void 0 : _a.find(function (def) { return def.identifier === label.val && def.definedBy === label.src; })); + if (customDef) { + return customDef; + } + // check global definitions + var globalDef = LABELS[label.val]; + if (globalDef) { + return globalDef; + } + // fallback to a noop definition + return interpretLabelValueDefinition({ + identifier: label.val, + severity: 'none', + blurs: 'none', + defaultSetting: 'ignore', + locales: [], + }, label.src); +} +export function getLabelStrings(locale, globalLabelStrings, def) { + if (!def.definedBy) { + // global definition, look up strings + if (def.identifier in globalLabelStrings) { + return globalLabelStrings[def.identifier]; + } + } + else { + // try to find locale match in the definition's strings + var localeMatch = def.locales.find(function (strings) { return bcp47Match.basicFilter(locale, strings.lang).length > 0; }); + if (localeMatch) { + return localeMatch; + } + // fall back to the zero item if no match + if (def.locales[0]) { + return def.locales[0]; + } + } + return { + lang: locale, + name: def.identifier, + description: "Labeled \"".concat(def.identifier, "\""), + }; +} diff --git a/src/lib/moderation/useModerationCauseDescription.js b/src/lib/moderation/useModerationCauseDescription.js new file mode 100644 index 0000000000..495e4f8518 --- /dev/null +++ b/src/lib/moderation/useModerationCauseDescription.js @@ -0,0 +1,155 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import React from 'react'; +import { BSKY_LABELER_DID, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useLabelDefinitions } from '#/state/preferences'; +import { useSession } from '#/state/session'; +import { CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign } from '#/components/icons/CircleBanSign'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { EyeSlash_Stroke2_Corner0_Rounded as EyeSlash } from '#/components/icons/EyeSlash'; +import { Warning_Stroke2_Corner0_Rounded as Warning } from '#/components/icons/Warning'; +import { useGlobalLabelStrings } from './useGlobalLabelStrings'; +import { getDefinition, getLabelStrings } from './useLabelInfo'; +export function useModerationCauseDescription(cause) { + var currentAccount = useSession().currentAccount; + var _a = useLingui(), _ = _a._, i18n = _a.i18n; + var _b = useLabelDefinitions(), labelDefs = _b.labelDefs, labelers = _b.labelers; + var globalLabelStrings = useGlobalLabelStrings(); + return React.useMemo(function () { + if (!cause) { + return { + icon: Warning, + name: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Content Warning"], ["Content Warning"])))), + description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Moderator has chosen to set a general warning on the content."], ["Moderator has chosen to set a general warning on the content."])))), + }; + } + if (cause.type === 'blocking') { + if (cause.source.type === 'list') { + return { + icon: CircleBanSign, + name: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["User Blocked by \"", "\""], ["User Blocked by \"", "\""])), cause.source.list.name)), + description: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["You have blocked this user. You cannot view their content."], ["You have blocked this user. You cannot view their content."])))), + }; + } + else { + return { + icon: CircleBanSign, + name: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["User Blocked"], ["User Blocked"])))), + description: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["You have blocked this user. You cannot view their content."], ["You have blocked this user. You cannot view their content."])))), + }; + } + } + if (cause.type === 'blocked-by') { + return { + icon: CircleBanSign, + name: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["User Blocking You"], ["User Blocking You"])))), + description: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["This user has blocked you. You cannot view their content."], ["This user has blocked you. You cannot view their content."])))), + }; + } + if (cause.type === 'block-other') { + return { + icon: CircleBanSign, + name: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Content Not Available"], ["Content Not Available"])))), + description: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["This content is not available because one of the users involved has blocked the other."], ["This content is not available because one of the users involved has blocked the other."])))), + }; + } + if (cause.type === 'muted') { + if (cause.source.type === 'list') { + return { + icon: EyeSlash, + name: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Muted by \"", "\""], ["Muted by \"", "\""])), cause.source.list.name)), + description: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["You have muted this user"], ["You have muted this user"])))), + }; + } + else { + return { + icon: EyeSlash, + name: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Account Muted"], ["Account Muted"])))), + description: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["You have muted this account."], ["You have muted this account."])))), + }; + } + } + if (cause.type === 'mute-word') { + return { + icon: EyeSlash, + name: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Post Hidden by Muted Word"], ["Post Hidden by Muted Word"])))), + description: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["You've chosen to hide a word or tag within this post."], ["You've chosen to hide a word or tag within this post."])))), + }; + } + if (cause.type === 'hidden') { + return { + icon: EyeSlash, + name: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Post Hidden by You"], ["Post Hidden by You"])))), + description: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["You have hidden this post"], ["You have hidden this post"])))), + }; + } + if (cause.type === 'reply-hidden') { + var isMe = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === cause.source.did; + return { + icon: EyeSlash, + name: isMe + ? _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Reply Hidden by You"], ["Reply Hidden by You"])))) + : _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Reply Hidden by Thread Author"], ["Reply Hidden by Thread Author"])))), + description: isMe + ? _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["You hid this reply."], ["You hid this reply."])))) + : _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["The author of this thread has hidden this reply."], ["The author of this thread has hidden this reply."])))), + }; + } + if (cause.type === 'label') { + var def = cause.labelDef || getDefinition(labelDefs, cause.label); + var strings = getLabelStrings(i18n.locale, globalLabelStrings, def); + var labeler = labelers.find(function (l) { return l.creator.did === cause.label.src; }); + var source = labeler + ? sanitizeHandle(labeler.creator.handle, '@') + : undefined; + var sourceDisplayName = labeler === null || labeler === void 0 ? void 0 : labeler.creator.displayName; + if (!source) { + if (cause.label.src === BSKY_LABELER_DID) { + source = 'moderation.bsky.app'; + sourceDisplayName = 'Bluesky Moderation Service'; + } + else { + source = _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["an unknown labeler"], ["an unknown labeler"])))); + } + } + if (def.identifier === 'porn' || def.identifier === 'sexual') { + strings.name = _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Adult Content"], ["Adult Content"])))); + } + return { + icon: def.identifier === '!no-unauthenticated' + ? EyeSlash + : def.severity === 'alert' + ? Warning + : CircleInfo, + name: strings.name, + description: strings.description, + source: source, + sourceDisplayName: sourceDisplayName, + sourceType: cause.source.type, + sourceAvi: labeler === null || labeler === void 0 ? void 0 : labeler.creator.avatar, + sourceDid: cause.label.src, + isSubjectAccount: cause.label.uri.startsWith('did:'), + }; + } + // should never happen + return { + icon: CircleInfo, + name: '', + description: "", + }; + }, [ + labelDefs, + labelers, + globalLabelStrings, + cause, + _, + i18n.locale, + currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + ]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24; diff --git a/src/lib/notifications/notifications.e2e.js b/src/lib/notifications/notifications.e2e.js new file mode 100644 index 0000000000..51fe49ecad --- /dev/null +++ b/src/lib/notifications/notifications.e2e.js @@ -0,0 +1,78 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useCallback } from 'react'; +export function useNotificationsRegistration() { } +export function useRequestNotificationsPermission() { + var _this = this; + return function (_context) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); }; +} +export function useGetAndRegisterPushToken() { + var _this = this; + return useCallback(function () { + var args_1 = []; + for (var _i = 0; _i < arguments.length; _i++) { + args_1[_i] = arguments[_i]; + } + return __awaiter(_this, __spreadArray([], args_1, true), void 0, function (_a) { + _a = {}; + return __generator(this, function (_b) { + return [2 /*return*/]; + }); + }); + }, []); +} +export function decrementBadgeCount(_by) { + return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); +} +export function resetBadgeCount() { + return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); +} diff --git a/src/lib/notifications/notifications.js b/src/lib/notifications/notifications.js new file mode 100644 index 0000000000..08a5cc9194 --- /dev/null +++ b/src/lib/notifications/notifications.js @@ -0,0 +1,419 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useCallback, useEffect } from 'react'; +import { Platform } from 'react-native'; +import * as Notifications from 'expo-notifications'; +import { getBadgeCountAsync, setBadgeCountAsync } from 'expo-notifications'; +import debounce from 'lodash.debounce'; +import { BLUESKY_NOTIF_SERVICE_HEADERS, PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID, } from '#/lib/constants'; +import { logger as notyLogger } from '#/lib/notifications/util'; +import { isNetworkError } from '#/lib/strings/errors'; +import { useAgent, useSession } from '#/state/session'; +import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +import { IS_DEV, IS_NATIVE } from '#/env'; +/** + * @private + * Registers the device's push notification token with the Bluesky server. + */ +function _registerPushToken(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var payload, error_1; + var _c, _d; + var agent = _b.agent, currentAccount = _b.currentAccount, token = _b.token, _e = _b.extra, extra = _e === void 0 ? {} : _e; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + _f.trys.push([0, 2, , 3]); + payload = { + serviceDid: ((_c = currentAccount.service) === null || _c === void 0 ? void 0 : _c.includes('staging')) + ? PUBLIC_STAGING_APPVIEW_DID + : PUBLIC_APPVIEW_DID, + platform: Platform.OS, + token: token.data, + appId: 'xyz.blueskyweb.app', + ageRestricted: (_d = extra.ageRestricted) !== null && _d !== void 0 ? _d : false, + }; + notyLogger.debug("registerPushToken: registering", __assign({}, payload)); + return [4 /*yield*/, agent.app.bsky.notification.registerPush(payload, { + headers: BLUESKY_NOTIF_SERVICE_HEADERS, + })]; + case 1: + _f.sent(); + notyLogger.debug("registerPushToken: success"); + return [3 /*break*/, 3]; + case 2: + error_1 = _f.sent(); + if (!isNetworkError(error_1)) { + notyLogger.error("registerPushToken: failed", { safeMessage: error_1 }); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); +} +/** + * @private + * Debounced version of `_registerPushToken` to prevent multiple calls. + */ +var _registerPushTokenDebounced = debounce(_registerPushToken, 100); +/** + * Hook to register the device's push notification token with the Bluesky. If + * the user is not logged in, this will do nothing. + * + * Use this instead of using `_registerPushToken` or + * `_registerPushTokenDebounced` directly. + */ +export function useRegisterPushToken() { + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + return useCallback(function (_a) { + var token = _a.token, isAgeRestricted = _a.isAgeRestricted; + if (!currentAccount) + return; + return _registerPushTokenDebounced({ + agent: agent, + currentAccount: currentAccount, + token: token, + extra: { + ageRestricted: isAgeRestricted, + }, + }); + }, [agent, currentAccount]); +} +/** + * Retreive the device's push notification token, if permissions are granted. + */ +function getPushToken() { + return __awaiter(this, void 0, void 0, function () { + var granted; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Notifications.getPermissionsAsync()]; + case 1: + granted = (_a.sent()).granted; + notyLogger.debug("getPushToken", { granted: granted }); + if (granted) { + return [2 /*return*/, Notifications.getDevicePushTokenAsync()]; + } + return [2 /*return*/]; + } + }); + }); +} +/** + * Hook to get the device push token and register it with the Bluesky server. + * Should only be called after a user has logged-in, since registration is an + * authed endpoint. + * + * N.B. A previous regression in `expo-notifications` caused + * `addPushTokenListener` to not fire on Android after calling + * `getPushToken()`. Therefore, as insurance, we also call + * `registerPushToken` here. + * + * Because `registerPushToken` is debounced, even if the the listener _does_ + * fire, it's OK to also call `registerPushToken` below since only a single + * call will be made to the server (ideally). This does race the listener (if + * it fires), so there's a possibility that multiple calls will be made, but + * that is acceptable. + * + * @see https://github.com/expo/expo/issues/28656 + * @see https://github.com/expo/expo/issues/29909 + * @see https://github.com/bluesky-social/social-app/pull/4467 + */ +export function useGetAndRegisterPushToken() { + var _this = this; + var aa = useAgeAssurance(); + var registerPushToken = useRegisterPushToken(); + return useCallback(function () { + var args_1 = []; + for (var _i = 0; _i < arguments.length; _i++) { + args_1[_i] = arguments[_i]; + } + return __awaiter(_this, __spreadArray([], args_1, true), void 0, function (_a) { + var token; + var _b = _a === void 0 ? {} : _a, isAgeRestrictedOverride = _b.isAgeRestricted; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!IS_NATIVE || IS_DEV) + return [2 /*return*/]; + return [4 /*yield*/, getPushToken()]; + case 1: + token = _c.sent(); + notyLogger.debug("useGetAndRegisterPushToken", { + token: token !== null && token !== void 0 ? token : 'undefined', + }); + if (token) { + /** + * The listener should have registered the token already, but just in + * case, call the debounced function again. + */ + registerPushToken({ + token: token, + isAgeRestricted: isAgeRestrictedOverride !== null && isAgeRestrictedOverride !== void 0 ? isAgeRestrictedOverride : aa.state.access !== aa.Access.Full, + }); + } + return [2 /*return*/, token]; + } + }); + }); + }, [registerPushToken, aa]); +} +/** + * Hook to register the device's push notification token with the Bluesky + * server, as well as listen for push token updates, should they occurr. + * + * Registered via the shell, which wraps the navigation stack, meaning if we + * have a current account, this handling will be registered and ready to go. + */ +export function useNotificationsRegistration() { + var _this = this; + var currentAccount = useSession().currentAccount; + var registerPushToken = useRegisterPushToken(); + var getAndRegisterPushToken = useGetAndRegisterPushToken(); + var aa = useAgeAssurance(); + useEffect(function () { + /** + * We want this to init right away _after_ we have a logged in user, and + * _after_ we've loaded their age assurance state. + */ + if (!currentAccount) + return; + notyLogger.debug("useNotificationsRegistration"); + /** + * Init push token, if permissions are granted already. If they weren't, + * they'll be requested by the `useRequestNotificationsPermission` hook + * below. + */ + getAndRegisterPushToken(); + /** + * Register the push token with the Bluesky server, whenever it changes. + * This is also fired any time `getDevicePushTokenAsync` is called. + * + * Since this is registered immediately after `getAndRegisterPushToken`, it + * should also detect that getter and be fired almost immediately after this. + * + * According to the Expo docs, there is a chance that the token will change + * while the app is open in some rare cases. This will fire + * `registerPushToken` whenever that happens. + * + * @see https://docs.expo.dev/versions/latest/sdk/notifications/#addpushtokenlistenerlistener + */ + var subscription = Notifications.addPushTokenListener(function (token) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + registerPushToken({ + token: token, + isAgeRestricted: aa.state.access !== aa.Access.Full, + }); + notyLogger.debug("addPushTokenListener callback", { token: token }); + return [2 /*return*/]; + }); + }); }); + return function () { + subscription.remove(); + }; + }, [currentAccount, getAndRegisterPushToken, registerPushToken, aa]); +} +export function useRequestNotificationsPermission() { + var _this = this; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var getAndRegisterPushToken = useGetAndRegisterPushToken(); + return function (context) { return __awaiter(_this, void 0, void 0, function () { + var permissions, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Notifications.getPermissionsAsync()]; + case 1: + permissions = _a.sent(); + if (!IS_NATIVE || + (permissions === null || permissions === void 0 ? void 0 : permissions.status) === 'granted' || + ((permissions === null || permissions === void 0 ? void 0 : permissions.status) === 'denied' && !permissions.canAskAgain)) { + return [2 /*return*/]; + } + if (context === 'AfterOnboarding') { + return [2 /*return*/]; + } + if (context === 'Home' && !currentAccount) { + return [2 /*return*/]; + } + return [4 /*yield*/, Notifications.requestPermissionsAsync()]; + case 2: + res = _a.sent(); + ax.metric("notifications:request", { + context: context, + status: res.status, + }); + if (res.granted) { + if (currentAccount) { + /** + * If we have an account in scope, we can safely call + * `getAndRegisterPushToken`. + */ + getAndRegisterPushToken(); + } + else { + /** + * Right after login, `currentAccount` in this scope will be undefined, + * but calling `getPushToken` will result in `addPushTokenListener` + * listeners being called, which will handle the registration with the + * Bluesky server. + */ + getPushToken(); + } + } + return [2 /*return*/]; + } + }); + }); }; +} +export function decrementBadgeCount(by) { + return __awaiter(this, void 0, void 0, function () { + var count; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!IS_NATIVE) + return [2 /*return*/]; + return [4 /*yield*/, getBadgeCountAsync()]; + case 1: + count = _a.sent(); + count -= by; + if (count < 0) { + count = 0; + } + return [4 /*yield*/, BackgroundNotificationHandler.setBadgeCountAsync(count)]; + case 2: + _a.sent(); + return [4 /*yield*/, setBadgeCountAsync(count)]; + case 3: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function resetBadgeCount() { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, BackgroundNotificationHandler.setBadgeCountAsync(0)]; + case 1: + _a.sent(); + return [4 /*yield*/, setBadgeCountAsync(0)]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function unregisterPushToken(agents) { + return __awaiter(this, void 0, void 0, function () { + var token, _i, agents_1, agent, error_2; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!IS_NATIVE) + return [2 /*return*/]; + _b.label = 1; + case 1: + _b.trys.push([1, 9, , 10]); + return [4 /*yield*/, getPushToken()]; + case 2: + token = _b.sent(); + if (!token) return [3 /*break*/, 7]; + _i = 0, agents_1 = agents; + _b.label = 3; + case 3: + if (!(_i < agents_1.length)) return [3 /*break*/, 6]; + agent = agents_1[_i]; + return [4 /*yield*/, agent.app.bsky.notification.unregisterPush({ + serviceDid: agent.serviceUrl.hostname.includes('staging') + ? PUBLIC_STAGING_APPVIEW_DID + : PUBLIC_APPVIEW_DID, + platform: Platform.OS, + token: token.data, + appId: 'xyz.blueskyweb.app', + }, { + headers: BLUESKY_NOTIF_SERVICE_HEADERS, + })]; + case 4: + _b.sent(); + notyLogger.debug("Push token unregistered for ".concat((_a = agent.session) === null || _a === void 0 ? void 0 : _a.handle)); + _b.label = 5; + case 5: + _i++; + return [3 /*break*/, 3]; + case 6: return [3 /*break*/, 8]; + case 7: + notyLogger.debug('Tried to unregister push token, but could not find one'); + _b.label = 8; + case 8: return [3 /*break*/, 10]; + case 9: + error_2 = _b.sent(); + notyLogger.debug('Failed to unregister push token', { message: error_2 }); + return [3 /*break*/, 10]; + case 10: return [2 /*return*/]; + } + }); + }); +} diff --git a/src/lib/notifications/util.js b/src/lib/notifications/util.js new file mode 100644 index 0000000000..2e87301578 --- /dev/null +++ b/src/lib/notifications/util.js @@ -0,0 +1,2 @@ +import { Logger } from '#/logger'; +export var logger = Logger.create(Logger.Context.Notifications); diff --git a/src/lib/numbers.js b/src/lib/numbers.js new file mode 100644 index 0000000000..d52aaa15ba --- /dev/null +++ b/src/lib/numbers.js @@ -0,0 +1,3 @@ +export function clamp(v, min, max) { + return Math.min(max, Math.max(min, v)); +} diff --git a/src/lib/once.js b/src/lib/once.js new file mode 100644 index 0000000000..7b52f9aa9a --- /dev/null +++ b/src/lib/once.js @@ -0,0 +1,22 @@ +import { useCallback, useRef } from 'react'; +export function callOnce() { + var ran = false; + return function runCallbackOnce(cb) { + if (ran) + return; + ran = true; + cb(); + }; +} +export function useCallOnce(cb) { + var ran = useRef(false); + return useCallback(function (icb) { + if (ran.current) + return; + ran.current = true; + if (icb) + icb(); + else if (cb) + cb(); + }, [cb]); +} diff --git a/src/lib/parseLinkingUrl.js b/src/lib/parseLinkingUrl.js new file mode 100644 index 0000000000..550c15cb13 --- /dev/null +++ b/src/lib/parseLinkingUrl.js @@ -0,0 +1,10 @@ +export function parseLinkingUrl(url) { + /* + * Hack: add a third slash to bluesky:// urls so that `URL.host` is empty and + * `URL.pathname` has the full path. + */ + if (url.startsWith('bluesky://') && !url.startsWith('bluesky:///')) { + url = url.replace('bluesky://', 'bluesky:///'); + } + return new URL(url); +} diff --git a/src/lib/persisted-query-storage.js b/src/lib/persisted-query-storage.js new file mode 100644 index 0000000000..e15f0b8dc0 --- /dev/null +++ b/src/lib/persisted-query-storage.js @@ -0,0 +1,97 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { create as createArchiveDB } from '#/storage/archive/db'; +function createId(id) { + return "react-query-cache-".concat(id); +} +/** + * Creates an MMKV-based storage adapter for persisting react-query cache on native platforms. + * Each storage instance uses a separate MMKV store identified by the provided id. + * MMKV provides synchronous access but we wrap it in Promises for API compatibility. + * + * @param id - Unique identifier for this storage instance (used as MMKV store id) + */ +export function createPersistedQueryStorage(id) { + var _this = this; + var store = createArchiveDB({ id: createId(id) }); + return { + getItem: function (key) { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, store.get(key)]; + case 1: return [2 /*return*/, (_a = (_b.sent())) !== null && _a !== void 0 ? _a : null]; + } + }); + }); }, + setItem: function (key, value) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, store.set(key, value)]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + removeItem: function (key) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, store.delete(key)]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }; +} +export function clearPersistedQueryStorage(id) { + return __awaiter(this, void 0, void 0, function () { + var store; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + store = createArchiveDB({ id: createId(id) }); + return [4 /*yield*/, store.clear()]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} diff --git a/src/lib/react-query.js b/src/lib/react-query.js new file mode 100644 index 0000000000..e414135ce2 --- /dev/null +++ b/src/lib/react-query.js @@ -0,0 +1,206 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useEffect, useRef, useState } from 'react'; +import { AppState } from 'react-native'; +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; +import { focusManager, onlineManager, QueryClient } from '@tanstack/react-query'; +import { PersistQueryClientProvider, } from '@tanstack/react-query-persist-client'; +import { createPersistedQueryStorage } from '#/lib/persisted-query-storage'; +import { listenNetworkConfirmed, listenNetworkLost } from '#/state/events'; +import { PERSISTED_QUERY_ROOT } from '#/state/queries'; +import * as env from '#/env'; +import { IS_NATIVE, IS_WEB } from '#/env'; +function checkIsOnline() { + return __awaiter(this, void 0, void 0, function () { + var controller_1, res, json, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 3, , 4]); + controller_1 = new AbortController(); + setTimeout(function () { + controller_1.abort(); + }, 15e3); + return [4 /*yield*/, fetch('https://public.api.bsky.app/xrpc/_health', { + cache: 'no-store', + signal: controller_1.signal, + })]; + case 1: + res = _a.sent(); + return [4 /*yield*/, res.json()]; + case 2: + json = _a.sent(); + if (json.version) { + return [2 /*return*/, true]; + } + else { + return [2 /*return*/, false]; + } + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + return [2 /*return*/, false]; + case 4: return [2 /*return*/]; + } + }); + }); +} +var receivedNetworkLost = false; +var receivedNetworkConfirmed = false; +var isNetworkStateUnclear = false; +listenNetworkLost(function () { + receivedNetworkLost = true; + onlineManager.setOnline(false); +}); +listenNetworkConfirmed(function () { + receivedNetworkConfirmed = true; + onlineManager.setOnline(true); +}); +var checkPromise; +function checkIsOnlineIfNeeded() { + if (checkPromise) { + return; + } + receivedNetworkLost = false; + receivedNetworkConfirmed = false; + checkPromise = checkIsOnline().then(function (nextIsOnline) { + checkPromise = undefined; + if (nextIsOnline && receivedNetworkLost) { + isNetworkStateUnclear = true; + } + if (!nextIsOnline && receivedNetworkConfirmed) { + isNetworkStateUnclear = true; + } + if (!isNetworkStateUnclear) { + onlineManager.setOnline(nextIsOnline); + } + }); +} +setInterval(function () { + if (AppState.currentState === 'active') { + if (!onlineManager.isOnline() || isNetworkStateUnclear) { + checkIsOnlineIfNeeded(); + } + } +}, 2000); +focusManager.setEventListener(function (onFocus) { + if (IS_NATIVE) { + var subscription_1 = AppState.addEventListener('change', function (status) { + focusManager.setFocused(status === 'active'); + }); + return function () { return subscription_1.remove(); }; + } + else if (typeof window !== 'undefined' && window.addEventListener) { + // these handlers are a bit redundant but focus catches when the browser window + // is blurred/focused while visibilitychange seems to only handle when the + // window minimizes (both of them catch tab changes) + // there's no harm to redundant fires because refetchOnWindowFocus is only + // used with queries that employ stale data times + var handler_1 = function () { return onFocus(); }; + window.addEventListener('focus', handler_1, false); + window.addEventListener('visibilitychange', handler_1, false); + return function () { + window.removeEventListener('visibilitychange', handler_1); + window.removeEventListener('focus', handler_1); + }; + } +}); +var createQueryClient = function () { + return new QueryClient({ + defaultOptions: { + queries: { + // NOTE + // refetchOnWindowFocus breaks some UIs (like feeds) + // so we only selectively want to enable this + // -prf + refetchOnWindowFocus: false, + // Structural sharing between responses makes it impossible to rely on + // "first seen" timestamps on objects to determine if they're fresh. + // Disable this optimization so that we can rely on "first seen" timestamps. + structuralSharing: false, + // We don't want to retry queries by default, because in most cases we + // want to fail early and show a response to the user. There are + // exceptions, and those can be made on a per-query basis. For others, we + // should give users controls to retry. + retry: false, + }, + }, + }); +}; +var dehydrateOptions = { + shouldDehydrateMutation: function (_) { return false; }, + shouldDehydrateQuery: function (query) { + var root = String(query.queryKey[0]); + return root === PERSISTED_QUERY_ROOT; + }, +}; +export function QueryProvider(_a) { + var children = _a.children, currentDid = _a.currentDid; + return (_jsx(QueryProviderInner + // Enforce we never reuse cache between users. + // These two props MUST stay in sync. + , { currentDid: currentDid, children: children }, currentDid)); +} +function QueryProviderInner(_a) { + var children = _a.children, currentDid = _a.currentDid; + var initialDid = useRef(currentDid); + if (currentDid !== initialDid.current) { + throw Error('Something is very wrong. Expected did to be stable due to key above.'); + } + // We create the query client here so that it's scoped to a specific DID. + // Do not move the query client creation outside of this component. + var _b = useState(function () { return createQueryClient(); }), queryClient = _b[0], _setQueryClient = _b[1]; + var _c = useState(function () { + var storage = createPersistedQueryStorage(currentDid !== null && currentDid !== void 0 ? currentDid : 'logged-out'); + var asyncPersister = createAsyncStoragePersister({ + storage: storage, + key: 'queryClient-' + (currentDid !== null && currentDid !== void 0 ? currentDid : 'logged-out'), + }); + return { + persister: asyncPersister, + dehydrateOptions: dehydrateOptions, + buster: env.APP_VERSION, + }; + }), persistOptions = _c[0], _setPersistOptions = _c[1]; + useEffect(function () { + if (IS_WEB) { + window.__TANSTACK_QUERY_CLIENT__ = queryClient; + } + }, [queryClient]); + return (_jsx(PersistQueryClientProvider, { client: queryClient, persistOptions: persistOptions, children: children })); +} diff --git a/src/lib/routes/helpers.js b/src/lib/routes/helpers.js new file mode 100644 index 0000000000..1e00e8fcba --- /dev/null +++ b/src/lib/routes/helpers.js @@ -0,0 +1,87 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +export function getRootNavigation(nav) { + while (nav.getParent()) { + nav = nav.getParent(); + } + return nav; +} +export function getCurrentRoute(state) { + var _a, _b, _c, _d; + if (!state) { + return { name: 'Home' }; + } + var node = state.routes[state.index || 0]; + while (((_a = node.state) === null || _a === void 0 ? void 0 : _a.routes) && typeof ((_b = node.state) === null || _b === void 0 ? void 0 : _b.index) === 'number') { + node = (_c = node.state) === null || _c === void 0 ? void 0 : _c.routes[(_d = node.state) === null || _d === void 0 ? void 0 : _d.index]; + } + return node; +} +export function isStateAtTabRoot(state) { + if (!state) { + // NOTE + // if state is not defined it's because init is occurring + // and therefore we can safely assume we're at root + // -prf + return true; + } + var currentRoute = getCurrentRoute(state); + return (isTab(currentRoute.name, 'Home') || + isTab(currentRoute.name, 'Search') || + isTab(currentRoute.name, 'Messages') || + isTab(currentRoute.name, 'Notifications') || + isTab(currentRoute.name, 'MyProfile')); +} +export function isTab(current, route) { + // NOTE + // our tab routes can be variously referenced by 3 different names + // this helper deals with that weirdness + // -prf + return (current === route || + current === "".concat(route, "Tab") || + current === "".concat(route, "Inner")); +} +export var TabState; +(function (TabState) { + TabState[TabState["InsideAtRoot"] = 0] = "InsideAtRoot"; + TabState[TabState["Inside"] = 1] = "Inside"; + TabState[TabState["Outside"] = 2] = "Outside"; +})(TabState || (TabState = {})); +export function getTabState(state, tab) { + if (!state) { + return TabState.Outside; + } + var currentRoute = getCurrentRoute(state); + if (isTab(currentRoute.name, tab)) { + return TabState.InsideAtRoot; + } + else if (isTab(state.routes[state.index || 0].name, tab)) { + return TabState.Inside; + } + return TabState.Outside; +} +export function buildStateObject(stack, route, params, state) { + if (state === void 0) { state = []; } + if (stack === 'Flat') { + return { + routes: [{ name: route, params: params }], + }; + } + return { + routes: [ + { + name: stack, + state: { + routes: __spreadArray(__spreadArray([], state, true), [{ name: route, params: params }], false), + }, + }, + ], + }; +} diff --git a/src/lib/routes/links.js b/src/lib/routes/links.js new file mode 100644 index 0000000000..0c9aca4420 --- /dev/null +++ b/src/lib/routes/links.js @@ -0,0 +1,48 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { AtUri } from '@atproto/api'; +import { isInvalidHandle } from '#/lib/strings/handles'; +export function makeProfileLink(info) { + var segments = []; + for (var _i = 1; _i < arguments.length; _i++) { + segments[_i - 1] = arguments[_i]; + } + var handleSegment = info.did; + if (info.handle && !isInvalidHandle(info.handle)) { + handleSegment = info.handle; + } + return __spreadArray(["/profile", handleSegment], segments, true).join('/'); +} +export function makeCustomFeedLink(did, rkey, segment, feedCacheKey) { + return (__spreadArray(["/profile", did, 'feed', rkey], (segment ? [segment] : []), true).join('/') + + (feedCacheKey ? "?feedCacheKey=".concat(encodeURIComponent(feedCacheKey)) : '')); +} +export function makeListLink(did, rkey) { + var segments = []; + for (var _i = 2; _i < arguments.length; _i++) { + segments[_i - 2] = arguments[_i]; + } + return __spreadArray(["/profile", did, 'lists', rkey], segments, true).join('/'); +} +export function makeTagLink(did) { + return "/search?q=".concat(encodeURIComponent(did)); +} +export function makeSearchLink(props) { + return "/search?q=".concat(encodeURIComponent(props.query + (props.from ? " from:".concat(props.from) : ''))); +} +export function makeStarterPackLink(starterPackOrName, rkey) { + if (typeof starterPackOrName === 'string') { + return "https://bsky.app/start/".concat(starterPackOrName, "/").concat(rkey); + } + else { + var uriRkey = new AtUri(starterPackOrName.uri).rkey; + return "https://bsky.app/start/".concat(starterPackOrName.creator.handle, "/").concat(uriRkey); + } +} diff --git a/src/lib/routes/router.js b/src/lib/routes/router.js new file mode 100644 index 0000000000..4dfbaa96b5 --- /dev/null +++ b/src/lib/routes/router.js @@ -0,0 +1,77 @@ +var Router = /** @class */ (function () { + function Router(description) { + var _this = this; + this.routes = []; + var _loop_1 = function (screen_1, pattern) { + if (typeof pattern === 'string') { + this_1.routes.push([screen_1, createRoute(pattern)]); + } + else { + pattern.forEach(function (subPattern) { + _this.routes.push([screen_1, createRoute(subPattern)]); + }); + } + }; + var this_1 = this; + for (var _i = 0, _a = Object.entries(description); _i < _a.length; _i++) { + var _b = _a[_i], screen_1 = _b[0], pattern = _b[1]; + _loop_1(screen_1, pattern); + } + } + Router.prototype.matchName = function (name) { + for (var _i = 0, _a = this.routes; _i < _a.length; _i++) { + var _b = _a[_i], screenName = _b[0], route = _b[1]; + if (screenName === name) { + return route; + } + } + }; + Router.prototype.matchPath = function (path) { + var name = 'NotFound'; + var params = {}; + for (var _i = 0, _a = this.routes; _i < _a.length; _i++) { + var _b = _a[_i], screenName = _b[0], route = _b[1]; + var res = route.match(path); + if (res) { + name = screenName; + params = res.params; + break; + } + } + return [name, params]; + }; + return Router; +}()); +export { Router }; +function createRoute(pattern) { + var pathParamNames = new Set(); + var matcherReInternal = pattern.replace(/:([\w]+)/g, function (_m, name) { + pathParamNames.add(name); + return "(?<".concat(name, ">[^/]+)"); + }); + var matcherRe = new RegExp("^".concat(matcherReInternal, "([?]|$)"), 'i'); + return { + match: function (path) { + var _a = new URL(path, 'http://throwaway.com'), pathname = _a.pathname, searchParams = _a.searchParams; + var addedParams = Object.fromEntries(searchParams.entries()); + var res = matcherRe.exec(pathname); + if (res) { + return { params: Object.assign(addedParams, res.groups || {}) }; + } + return undefined; + }, + build: function (params) { + if (params === void 0) { params = {}; } + var str = pattern.replace(/:([\w]+)/g, function (_m, name) { return params[encodeURIComponent(name)] || 'undefined'; }); + var hasQp = false; + var qp = new URLSearchParams(); + for (var paramName in params) { + if (!pathParamNames.has(paramName)) { + qp.set(paramName, params[paramName]); + hasQp = true; + } + } + return str + (hasQp ? "?".concat(qp.toString()) : ''); + }, + }; +} diff --git a/src/lib/routes/types.js b/src/lib/routes/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/lib/routes/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/lib/sharing.js b/src/lib/sharing.js new file mode 100644 index 0000000000..6595e43a2e --- /dev/null +++ b/src/lib/sharing.js @@ -0,0 +1,107 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { Share } from 'react-native'; +// import * as Sharing from 'expo-sharing' +import { setStringAsync } from 'expo-clipboard'; +import { t } from '@lingui/core/macro'; +import * as Toast from '#/view/com/util/Toast'; +import { IS_ANDROID, IS_IOS } from '#/env'; +/** + * This function shares a URL using the native Share API if available, or copies it to the clipboard + * and displays a toast message if not (mostly on web) + * @param {string} url - A string representing the URL that needs to be shared or copied to the + * clipboard. + */ +export function shareUrl(url) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!IS_ANDROID) return [3 /*break*/, 2]; + return [4 /*yield*/, Share.share({ message: url })]; + case 1: + _a.sent(); + return [3 /*break*/, 5]; + case 2: + if (!IS_IOS) return [3 /*break*/, 4]; + return [4 /*yield*/, Share.share({ url: url })]; + case 3: + _a.sent(); + return [3 /*break*/, 5]; + case 4: + // React Native Share is not supported by web. Web Share API + // has increasing but not full support, so default to clipboard + setStringAsync(url); + Toast.show(t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Copied to clipboard"], ["Copied to clipboard"]))), 'clipboard-check'); + _a.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); +} +/** + * This function shares a text using the native Share API if available, or copies it to the clipboard + * and displays a toast message if not (mostly on web) + * + * @param {string} text - A string representing the text that needs to be shared or copied to the + * clipboard. + */ +export function shareText(text) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(IS_ANDROID || IS_IOS)) return [3 /*break*/, 2]; + return [4 /*yield*/, Share.share({ message: text })]; + case 1: + _a.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, setStringAsync(text)]; + case 3: + _a.sent(); + Toast.show(t(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Copied to clipboard"], ["Copied to clipboard"]))), 'clipboard-check'); + _a.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); +} +var templateObject_1, templateObject_2; diff --git a/src/lib/strings/bidi.js b/src/lib/strings/bidi.js new file mode 100644 index 0000000000..0010576176 --- /dev/null +++ b/src/lib/strings/bidi.js @@ -0,0 +1,9 @@ +var LEFT_TO_RIGHT_EMBEDDING = '\u202A'; +var POP_DIRECTIONAL_FORMATTING = '\u202C'; +/* + * Force LTR directionality in a string. + * https://www.unicode.org/reports/tr9/#Directional_Formatting_Characters + */ +export function forceLTR(str) { + return LEFT_TO_RIGHT_EMBEDDING + str + POP_DIRECTIONAL_FORMATTING; +} diff --git a/src/lib/strings/capitalize.js b/src/lib/strings/capitalize.js new file mode 100644 index 0000000000..5eaf8b7bab --- /dev/null +++ b/src/lib/strings/capitalize.js @@ -0,0 +1,3 @@ +export function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/src/lib/strings/constants.js b/src/lib/strings/constants.js new file mode 100644 index 0000000000..c4bae74460 --- /dev/null +++ b/src/lib/strings/constants.js @@ -0,0 +1 @@ +export var NON_BREAKING_SPACE = '\u00A0'; diff --git a/src/lib/strings/display-names.js b/src/lib/strings/display-names.js new file mode 100644 index 0000000000..4977141f1e --- /dev/null +++ b/src/lib/strings/display-names.js @@ -0,0 +1,29 @@ +// \u2705 = ✅ +// \u2713 = ✓ +// \u2714 = ✔ +// \u2611 = ☑ +var CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu; +var CONTROL_CHARS_RE = /[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g; +var MULTIPLE_SPACES_RE = /[\s][\s\u200B]+/g; +export function sanitizeDisplayName(str, moderation) { + if (moderation === null || moderation === void 0 ? void 0 : moderation.blur) { + return ''; + } + if (typeof str === 'string') { + return str + .replace(CHECK_MARKS_RE, '') + .replace(CONTROL_CHARS_RE, '') + .replace(MULTIPLE_SPACES_RE, ' ') + .trim(); + } + return ''; +} +export function combinedDisplayName(_a) { + var handle = _a.handle, displayName = _a.displayName; + if (!handle) { + return ''; + } + return displayName + ? "".concat(sanitizeDisplayName(displayName), " (@").concat(handle, ")") + : "@".concat(handle); +} diff --git a/src/lib/strings/email.js b/src/lib/strings/email.js new file mode 100644 index 0000000000..5421b45cf5 --- /dev/null +++ b/src/lib/strings/email.js @@ -0,0 +1,5 @@ +var COMMON_ERROR_PATTERN = /([a-zA-Z0-9._%+-]+)@(gnail\.(co|com)|gmaill\.(co|com)|gmai\.(co|com)|gmail\.co|gmal\.(co|com)|iclod\.(co|com)|icloud\.co|outllok\.(co|com)|outlok\.(co|com)|outlook\.co|yaoo\.(co|com)|yaho\.(co|com)|yahoo\.co|yahooo\.(co|com))$/; +export function isEmailMaybeInvalid(email, dynamicTldts) { + var isIcann = dynamicTldts.parse(email).isIcann; + return !isIcann || COMMON_ERROR_PATTERN.test(email); +} diff --git a/src/lib/strings/embed-player.js b/src/lib/strings/embed-player.js new file mode 100644 index 0000000000..186b08d873 --- /dev/null +++ b/src/lib/strings/embed-player.js @@ -0,0 +1,510 @@ +import { Dimensions } from 'react-native'; +import { IS_WEB, IS_WEB_SAFARI } from '#/env'; +var SCREEN_HEIGHT = Dimensions.get('window').height; +var IFRAME_HOST = IS_WEB + ? // @ts-ignore only for web + window.location.host === 'localhost:8100' + ? 'http://localhost:8100' + : 'https://bsky.app' + : __DEV__ && !process.env.JEST_WORKER_ID + ? 'http://localhost:8100' + : 'https://bsky.app'; +export var embedPlayerSources = [ + 'youtube', + 'youtubeShorts', + 'twitch', + 'spotify', + 'soundcloud', + 'appleMusic', + 'vimeo', + 'giphy', + 'tenor', + 'flickr', + 'bandcamp', +]; +export var externalEmbedLabels = { + youtube: 'YouTube', + youtubeShorts: 'YouTube Shorts', + vimeo: 'Vimeo', + twitch: 'Twitch', + giphy: 'GIPHY', + tenor: 'Tenor', + spotify: 'Spotify', + appleMusic: 'Apple Music', + soundcloud: 'SoundCloud', + flickr: 'Flickr', + bandcamp: 'Bandcamp', +}; +var giphyRegex = /media(?:[0-4]\.giphy\.com|\.giphy\.com)/i; +var gifFilenameRegex = /^(\S+)\.(webp|gif|mp4)$/i; +export function parseEmbedPlayerFromUrl(url) { + var _a, _b; + var urlp; + try { + urlp = new URL(url); + } + catch (e) { + return undefined; + } + // youtube + if (urlp.hostname === 'youtu.be') { + var videoId = urlp.pathname.split('/')[1]; + var t = (_a = urlp.searchParams.get('t')) !== null && _a !== void 0 ? _a : '0'; + var seek = encodeURIComponent(t.replace(/s$/, '')); + if (videoId) { + return { + type: 'youtube_video', + source: 'youtube', + playerUri: "".concat(IFRAME_HOST, "/iframe/youtube.html?videoId=").concat(videoId, "&start=").concat(seek), + }; + } + } + if (urlp.hostname === 'www.youtube.com' || + urlp.hostname === 'youtube.com' || + urlp.hostname === 'm.youtube.com' || + urlp.hostname === 'music.youtube.com') { + var _c = urlp.pathname.split('/'), __ = _c[0], page = _c[1], shortOrLiveVideoId = _c[2]; + var isShorts = page === 'shorts'; + var isLive = page === 'live'; + var videoId = isShorts || isLive + ? shortOrLiveVideoId + : urlp.searchParams.get('v'); + var t = (_b = urlp.searchParams.get('t')) !== null && _b !== void 0 ? _b : '0'; + var seek = encodeURIComponent(t.replace(/s$/, '')); + if (videoId) { + return { + type: isShorts ? 'youtube_short' : 'youtube_video', + source: isShorts ? 'youtubeShorts' : 'youtube', + hideDetails: isShorts ? true : undefined, + playerUri: "".concat(IFRAME_HOST, "/iframe/youtube.html?videoId=").concat(videoId, "&start=").concat(seek), + }; + } + } + // twitch + if (urlp.hostname === 'twitch.tv' || + urlp.hostname === 'www.twitch.tv' || + urlp.hostname === 'm.twitch.tv') { + var parent_1 = IS_WEB + ? // @ts-ignore only for web + window.location.hostname + : 'localhost'; + var _d = urlp.pathname.split('/'), __ = _d[0], channelOrVideo = _d[1], clipOrId = _d[2], id = _d[3]; + if (channelOrVideo === 'videos') { + return { + type: 'twitch_video', + source: 'twitch', + playerUri: "https://player.twitch.tv/?volume=0.5&!muted&autoplay&video=".concat(clipOrId, "&parent=").concat(parent_1), + }; + } + else if (clipOrId === 'clip') { + return { + type: 'twitch_video', + source: 'twitch', + playerUri: "https://clips.twitch.tv/embed?volume=0.5&autoplay=true&clip=".concat(id, "&parent=").concat(parent_1), + }; + } + else if (channelOrVideo) { + return { + type: 'twitch_video', + source: 'twitch', + playerUri: "https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=".concat(channelOrVideo, "&parent=").concat(parent_1), + }; + } + } + // spotify + if (urlp.hostname === 'open.spotify.com') { + var _e = urlp.pathname.split('/'), __ = _e[0], typeOrLocale = _e[1], idOrType = _e[2], id = _e[3]; + if (idOrType) { + if (typeOrLocale === 'playlist' || idOrType === 'playlist') { + return { + type: 'spotify_playlist', + source: 'spotify', + playerUri: "https://open.spotify.com/embed/playlist/".concat(id !== null && id !== void 0 ? id : idOrType), + }; + } + if (typeOrLocale === 'album' || idOrType === 'album') { + return { + type: 'spotify_album', + source: 'spotify', + playerUri: "https://open.spotify.com/embed/album/".concat(id !== null && id !== void 0 ? id : idOrType), + }; + } + if (typeOrLocale === 'track' || idOrType === 'track') { + return { + type: 'spotify_song', + source: 'spotify', + playerUri: "https://open.spotify.com/embed/track/".concat(id !== null && id !== void 0 ? id : idOrType), + }; + } + if (typeOrLocale === 'episode' || idOrType === 'episode') { + return { + type: 'spotify_song', + source: 'spotify', + playerUri: "https://open.spotify.com/embed/episode/".concat(id !== null && id !== void 0 ? id : idOrType), + }; + } + if (typeOrLocale === 'show' || idOrType === 'show') { + return { + type: 'spotify_song', + source: 'spotify', + playerUri: "https://open.spotify.com/embed/show/".concat(id !== null && id !== void 0 ? id : idOrType), + }; + } + } + } + // soundcloud + if (urlp.hostname === 'soundcloud.com' || + urlp.hostname === 'www.soundcloud.com') { + var _f = urlp.pathname.split('/'), __ = _f[0], user = _f[1], trackOrSets = _f[2], set = _f[3]; + if (user && trackOrSets) { + if (trackOrSets === 'sets' && set) { + return { + type: 'soundcloud_set', + source: 'soundcloud', + playerUri: "https://w.soundcloud.com/player/?url=".concat(url, "&auto_play=true&visual=false&hide_related=true"), + }; + } + return { + type: 'soundcloud_track', + source: 'soundcloud', + playerUri: "https://w.soundcloud.com/player/?url=".concat(url, "&auto_play=true&visual=false&hide_related=true"), + }; + } + } + if (urlp.hostname === 'music.apple.com' || + urlp.hostname === 'music.apple.com') { + // This should always have: locale, type (playlist or album), name, and id. We won't use spread since we want + // to check if the length is correct + var pathParams = urlp.pathname.split('/'); + var type = pathParams[2]; + var songId = urlp.searchParams.get('i'); + if (pathParams.length === 5 && + (type === 'playlist' || type === 'album' || type === 'song')) { + // We want to append the songId to the end of the url if it exists + var embedUri = "https://embed.music.apple.com".concat(urlp.pathname).concat(songId ? "?i=".concat(songId) : ''); + if (type === 'playlist') { + return { + type: 'apple_music_playlist', + source: 'appleMusic', + playerUri: embedUri, + }; + } + else if (type === 'album') { + if (songId) { + return { + type: 'apple_music_song', + source: 'appleMusic', + playerUri: embedUri, + }; + } + else { + return { + type: 'apple_music_album', + source: 'appleMusic', + playerUri: embedUri, + }; + } + } + else if (type === 'song') { + return { + type: 'apple_music_song', + source: 'appleMusic', + playerUri: embedUri, + }; + } + } + } + if (urlp.hostname === 'vimeo.com' || urlp.hostname === 'www.vimeo.com') { + var _g = urlp.pathname.split('/'), __ = _g[0], videoId = _g[1]; + if (videoId) { + return { + type: 'vimeo_video', + source: 'vimeo', + playerUri: "https://player.vimeo.com/video/".concat(videoId, "?autoplay=1"), + }; + } + } + if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') { + var _h = urlp.pathname.split('/'), __ = _h[0], gifs = _h[1], nameAndId = _h[2]; + /* + * nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name) + * We want to get the id of the gif, then direct to media.giphy.com/media/{id}/giphy.webp so we can + * use it in an component + */ + if (gifs === 'gifs' && nameAndId) { + var gifId = nameAndId.split('-').pop(); + if (gifId) { + return { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: "https://giphy.com/gifs/".concat(gifId), + playerUri: "https://i.giphy.com/media/".concat(gifId, "/200.webp"), + }; + } + } + } + // There are five possible hostnames that also can be giphy urls: media.giphy.com and media0-4.giphy.com + // These can include (presumably) a tracking id in the path name, so we have to check for that as well + if (giphyRegex.test(urlp.hostname)) { + // We can link directly to the gif, if its a proper link + var _j = urlp.pathname.split('/'), __ = _j[0], media = _j[1], trackingOrId = _j[2], idOrFilename = _j[3], filename = _j[4]; + if (media === 'media') { + if (idOrFilename && gifFilenameRegex.test(idOrFilename)) { + return { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: "https://giphy.com/gifs/".concat(trackingOrId), + playerUri: "https://i.giphy.com/media/".concat(trackingOrId, "/200.webp"), + }; + } + else if (filename && gifFilenameRegex.test(filename)) { + return { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: "https://giphy.com/gifs/".concat(idOrFilename), + playerUri: "https://i.giphy.com/media/".concat(idOrFilename, "/200.webp"), + }; + } + } + } + // Finally, we should see if it is a link to i.giphy.com. These links don't necessarily end in .gif but can also + // be .webp + if (urlp.hostname === 'i.giphy.com' || urlp.hostname === 'www.i.giphy.com') { + var _k = urlp.pathname.split('/'), __ = _k[0], mediaOrFilename = _k[1], filename = _k[2]; + if (mediaOrFilename === 'media' && filename) { + var gifId = filename.split('.')[0]; + return { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: "https://giphy.com/gifs/".concat(gifId), + playerUri: "https://i.giphy.com/media/".concat(gifId, "/200.webp"), + }; + } + else if (mediaOrFilename) { + var gifId = mediaOrFilename.split('.')[0]; + return { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: "https://giphy.com/gifs/".concat(gifId), + playerUri: "https://i.giphy.com/media/".concat(mediaOrFilename.split('.')[0], "/200.webp"), + }; + } + } + var tenorGif = parseTenorGif(urlp); + if (tenorGif.success) { + var playerUri = tenorGif.playerUri, dimensions = tenorGif.dimensions; + return { + type: 'tenor_gif', + source: 'tenor', + isGif: true, + hideDetails: true, + playerUri: playerUri, + dimensions: dimensions, + }; + } + // this is a standard flickr path! we can use the embedder for albums and groups, so validate the path + if (urlp.hostname === 'www.flickr.com' || urlp.hostname === 'flickr.com') { + var i = urlp.pathname.length - 1; + while (i > 0 && urlp.pathname.charAt(i) === '/') { + --i; + } + var path_components = urlp.pathname.slice(1, i + 1).split('/'); + if (path_components.length === 4) { + // discard username - it's not relevant + var photos = path_components[0], __ = path_components[1], albums = path_components[2], id = path_components[3]; + if (photos === 'photos' && albums === 'albums') { + // this at least has the shape of a valid photo-album URL! + return { + type: 'flickr_album', + source: 'flickr', + playerUri: "https://embedr.flickr.com/photosets/".concat(id), + }; + } + } + if (path_components.length === 3) { + var groups = path_components[0], id = path_components[1], pool = path_components[2]; + if (groups === 'groups' && pool === 'pool') { + return { + type: 'flickr_album', + source: 'flickr', + playerUri: "https://embedr.flickr.com/groups/".concat(id), + }; + } + } + // not an album or a group pool, don't know what to do with this! + return undefined; + } + // link shortened flickr path + if (urlp.hostname === 'flic.kr') { + var b58alph = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'; + var _l = urlp.pathname.split('/'), __ = _l[0], type = _l[1], idBase58Enc = _l[2]; + var id = 0n; + for (var _i = 0, idBase58Enc_1 = idBase58Enc; _i < idBase58Enc_1.length; _i++) { + var char = idBase58Enc_1[_i]; + var nextIdx = b58alph.indexOf(char); + if (nextIdx >= 0) { + id = id * 58n + BigInt(nextIdx); + } + else { + // not b58 encoded, ergo not a valid link to embed + return undefined; + } + } + switch (type) { + case 'go': + var formattedGroupId = "".concat(id); + return { + type: 'flickr_album', + source: 'flickr', + playerUri: "https://embedr.flickr.com/groups/".concat(formattedGroupId.slice(0, -2), "@N").concat(formattedGroupId.slice(-2)), + }; + case 's': + return { + type: 'flickr_album', + source: 'flickr', + playerUri: "https://embedr.flickr.com/photosets/".concat(id), + }; + default: + // we don't know what this is so we can't embed it + return undefined; + } + } + var bandcampRegex = /^[a-z\d][a-z\d-]{2,}[a-z\d]\.bandcamp\.com$/i; + if (bandcampRegex.test(urlp.hostname)) { + var pathComponents = urlp.pathname.split('/'); + switch (pathComponents[1]) { + case 'album': + return { + type: 'bandcamp_album', + source: 'bandcamp', + playerUri: "https://bandcamp.com/EmbeddedPlayer/url=".concat(encodeURIComponent(urlp.href), "/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/"), + }; + case 'track': + return { + type: 'bandcamp_track', + source: 'bandcamp', + playerUri: "https://bandcamp.com/EmbeddedPlayer/url=".concat(encodeURIComponent(urlp.href), "/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/"), + }; + default: + return undefined; + } + } +} +export function getPlayerAspect(_a) { + var type = _a.type, hasThumb = _a.hasThumb, width = _a.width; + if (!hasThumb) + return { aspectRatio: 16 / 9 }; + switch (type) { + case 'youtube_video': + case 'twitch_video': + case 'vimeo_video': + return { aspectRatio: 16 / 9 }; + case 'youtube_short': + if (SCREEN_HEIGHT < 600) { + return { aspectRatio: (9 / 16) * 1.75 }; + } + else { + return { aspectRatio: (9 / 16) * 1.5 }; + } + case 'spotify_album': + case 'apple_music_album': + case 'apple_music_playlist': + case 'spotify_playlist': + case 'soundcloud_set': + return { height: 380 }; + case 'spotify_song': + if (width <= 300) { + return { height: 155 }; + } + return { height: 232 }; + case 'soundcloud_track': + return { height: 165 }; + case 'apple_music_song': + return { height: 150 }; + case 'bandcamp_album': + case 'bandcamp_track': + return { aspectRatio: 1 }; + default: + return { aspectRatio: 16 / 9 }; + } +} +export function getGifDims(originalHeight, originalWidth, viewWidth) { + var scaledHeight = (originalHeight / originalWidth) * viewWidth; + return { + height: scaledHeight > 250 ? 250 : scaledHeight, + width: (250 / scaledHeight) * viewWidth, + }; +} +export function getGiphyMetaUri(url) { + if (giphyRegex.test(url.hostname) || url.hostname === 'i.giphy.com') { + var params = parseEmbedPlayerFromUrl(url.toString()); + if (params && params.type === 'giphy_gif') { + return params.metaUri; + } + } +} +export function parseTenorGif(urlp) { + if (urlp.hostname !== 'media.tenor.com') { + return { success: false }; + } + var _a = urlp.pathname.split('/'), __ = _a[0], id = _a[1], filename = _a[2]; + if (!id || !filename) { + return { success: false }; + } + if (!id.includes('AAAAC')) { + return { success: false }; + } + var h = urlp.searchParams.get('hh'); + var w = urlp.searchParams.get('ww'); + if (!h || !w) { + return { success: false }; + } + var dimensions = { + height: Number(h), + width: Number(w), + }; + // Validate dimensions are valid positive numbers + if (isNaN(dimensions.height) || + isNaN(dimensions.width) || + dimensions.height <= 0 || + dimensions.width <= 0) { + return { success: false }; + } + if (IS_WEB) { + if (IS_WEB_SAFARI) { + id = id.replace('AAAAC', 'AAAP1'); + filename = filename.replace('.gif', '.mp4'); + } + else { + id = id.replace('AAAAC', 'AAAP3'); + filename = filename.replace('.gif', '.webm'); + } + } + else { + id = id.replace('AAAAC', 'AAAAM'); + } + return { + success: true, + playerUri: "https://t.gifs.bsky.app/".concat(id, "/").concat(filename), + dimensions: dimensions, + }; +} +export function isTenorGifUri(url) { + try { + return parseTenorGif(typeof url === 'string' ? new URL(url) : url).success; + } + catch (_a) { + // Invalid URL + return false; + } +} diff --git a/src/lib/strings/errors.js b/src/lib/strings/errors.js new file mode 100644 index 0000000000..0d17266a2c --- /dev/null +++ b/src/lib/strings/errors.js @@ -0,0 +1,85 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { XRPCError } from '@atproto/xrpc'; +import { t } from '@lingui/core/macro'; +export function cleanError(str) { + if (!str) { + return ''; + } + if (typeof str !== 'string') { + str = str.toString(); + } + if (isNetworkError(str)) { + return t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Unable to connect. Please check your internet connection and try again."], ["Unable to connect. Please check your internet connection and try again."]))); + } + if (str.includes('Upstream Failure') || + str.includes('NotEnoughResources') || + str.includes('pipethrough network error')) { + return t(templateObject_2 || (templateObject_2 = __makeTemplateObject(["The server appears to be experiencing issues. Please try again in a few moments."], ["The server appears to be experiencing issues. Please try again in a few moments."]))); + } + /** + * @see https://github.com/bluesky-social/atproto/blob/255cfcebb54332a7129af768a93004e22c6858e3/packages/pds/src/actor-store/preference/transactor.ts#L24 + */ + if (str.includes('Do not have authorization to set preferences') && + str.includes('app.bsky.actor.defs#personalDetailsPref')) { + return t(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You cannot update your birthdate while using an app password. Please sign in with your main password to update your birthdate."], ["You cannot update your birthdate while using an app password. Please sign in with your main password to update your birthdate."]))); + } + if (str.includes('Bad token scope') || str.includes('Bad token method')) { + return t(templateObject_4 || (templateObject_4 = __makeTemplateObject(["This feature is not available while using an App Password. Please sign in with your main password."], ["This feature is not available while using an App Password. Please sign in with your main password."]))); + } + if (str.includes('Account has been suspended')) { + return t(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Account has been suspended"], ["Account has been suspended"]))); + } + if (str.includes('Account is deactivated')) { + return t(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Account is deactivated"], ["Account is deactivated"]))); + } + if (str.includes('Profile not found')) { + return t(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Profile not found"], ["Profile not found"]))); + } + if (str.includes('Unable to resolve handle')) { + return t(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Unable to resolve handle"], ["Unable to resolve handle"]))); + } + if (str.startsWith('Error: ')) { + return str.slice('Error: '.length); + } + return str; +} +var NETWORK_ERRORS = [ + 'Abort', + 'Network request failed', + 'Failed to fetch', + 'Load failed', + 'Upstream service unreachable', + 'NetworkError when attempting to fetch resource', +]; +export function isNetworkError(e) { + var str = String(e); + for (var _i = 0, NETWORK_ERRORS_1 = NETWORK_ERRORS; _i < NETWORK_ERRORS_1.length; _i++) { + var err = NETWORK_ERRORS_1[_i]; + if (str.includes(err)) { + return true; + } + } + return false; +} +export function isErrorMaybeAppPasswordPermissions(e) { + if (e instanceof XRPCError && e.error === 'TokenInvalid') { + return true; + } + var str = String(e); + return str.includes('Bad token scope') || str.includes('Bad token method'); +} +/** + * Intended to capture "User cancelled" or "Crop cancelled" errors + * that we often get from expo modules such @bsky.app/expo-image-crop-tool + * + * The exact name has changed in the past so let's just see if the string + * contains "cancel" + */ +export function isCancelledError(e) { + var str = String(e).toLowerCase(); + return str.includes('cancel'); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/lib/strings/handles.js b/src/lib/strings/handles.js new file mode 100644 index 0000000000..843ad2b6ff --- /dev/null +++ b/src/lib/strings/handles.js @@ -0,0 +1,53 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +// Regex from the go implementation +// https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10 +import { forceLTR } from '#/lib/strings/bidi'; +var VALIDATE_REGEX = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; +export var MAX_SERVICE_HANDLE_LENGTH = 18; +export function makeValidHandle(str) { + if (str.length > 20) { + str = str.slice(0, 20); + } + str = str.toLowerCase(); + return str.replace(/^[^a-z0-9]+/g, '').replace(/[^a-z0-9-]/g, ''); +} +export function createFullHandle(name, domain) { + name = (name || '').replace(/[.]+$/, ''); + domain = (domain || '').replace(/^[.]+/, ''); + return "".concat(name, ".").concat(domain); +} +export function isInvalidHandle(handle) { + return handle === 'handle.invalid'; +} +export function sanitizeHandle(handle, prefix, forceLeftToRight) { + if (prefix === void 0) { prefix = ''; } + if (forceLeftToRight === void 0) { forceLeftToRight = true; } + var lowercasedWithPrefix = "".concat(prefix).concat(handle.toLocaleLowerCase()); + return isInvalidHandle(handle) + ? '⚠Invalid Handle' + : forceLeftToRight + ? forceLTR(lowercasedWithPrefix) + : lowercasedWithPrefix; +} +// More checks from https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/index.ts#L72 +export function validateServiceHandle(str, userDomain) { + var fullHandle = createFullHandle(str, userDomain); + var results = { + handleChars: !str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')), + hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'), + frontLengthNotTooShort: str.length >= 3, + frontLengthNotTooLong: str.length <= MAX_SERVICE_HANDLE_LENGTH, + totalLength: fullHandle.length <= 253, + }; + return __assign(__assign({}, results), { overall: !Object.values(results).includes(false) }); +} diff --git a/src/lib/strings/headings.js b/src/lib/strings/headings.js new file mode 100644 index 0000000000..21d255ec4c --- /dev/null +++ b/src/lib/strings/headings.js @@ -0,0 +1,4 @@ +export function bskyTitle(page, unreadCountLabel) { + var unreadPrefix = unreadCountLabel ? "(".concat(unreadCountLabel, ") ") : ''; + return "".concat(unreadPrefix).concat(page, " \u2014 Bluesky"); +} diff --git a/src/lib/strings/helpers.js b/src/lib/strings/helpers.js new file mode 100644 index 0000000000..3c534464a0 --- /dev/null +++ b/src/lib/strings/helpers.js @@ -0,0 +1,64 @@ +import { countGraphemes } from 'unicode-segmenter/grapheme'; +import { shortenLinks } from './rich-text-manip'; +export function enforceLen(str, len, ellipsis, mode) { + if (ellipsis === void 0) { ellipsis = false; } + if (mode === void 0) { mode = 'end'; } + str = str || ''; + if (str.length > len) { + if (ellipsis) { + if (mode === 'end') { + return str.slice(0, len) + '…'; + } + else if (mode === 'middle') { + var half = Math.floor(len / 2); + return str.slice(0, half) + '…' + str.slice(-half); + } + else { + // fallback + return str.slice(0, len); + } + } + else { + return str.slice(0, len); + } + } + return str; +} +export function isOverMaxGraphemeCount(_a) { + var text = _a.text, maxCount = _a.maxCount; + if (typeof text === 'string') { + return countGraphemes(text) > maxCount; + } + else { + return shortenLinks(text).graphemeLength > maxCount; + } +} +export function countLines(str) { + var _a, _b; + if (!str) + return 0; + return (_b = (_a = str.match(/\n/g)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; +} +// Augments search query with additional syntax like `from:me` +export function augmentSearchQuery(query, _a) { + var did = _a.did; + // Don't do anything if there's no DID + if (!did) { + return query; + } + // replace “smart quotes” with normal ones + // iOS keyboard will add fancy unicode quotes, but only normal ones work + query = query.replaceAll(/[“”]/g, '"'); + // We don't want to replace substrings that are being "quoted" because those + // are exact string matches, so what we'll do here is to split them apart + // Even-indexed strings are unquoted, odd-indexed strings are quoted + var splits = query.split(/("(?:[^"\\]|\\.)*")/g); + return splits + .map(function (str, idx) { + if (idx % 2 === 0) { + return str.replaceAll(/(^|\s)from:me(\s|$)/g, "$1".concat(did, "$2")); + } + return str; + }) + .join(''); +} diff --git a/src/lib/strings/mention-manip.js b/src/lib/strings/mention-manip.js new file mode 100644 index 0000000000..721db94882 --- /dev/null +++ b/src/lib/strings/mention-manip.js @@ -0,0 +1,20 @@ +export function getMentionAt(text, cursorPos) { + var re = /(^|\s)@([a-z0-9.-]*)/gi; + var match; + while ((match = re.exec(text))) { + var spaceOffset = match[1].length; + var index = match.index + spaceOffset; + if (cursorPos >= index && + cursorPos <= index + match[0].length - spaceOffset) { + return { value: match[2], index: index }; + } + } + return undefined; +} +export function insertMentionAt(text, cursorPos, mention) { + var target = getMentionAt(text, cursorPos); + if (target) { + return "".concat(text.slice(0, target.index), "@").concat(mention, " ").concat(text.slice(target.index + target.value.length + 1)); + } + return text; +} diff --git a/src/lib/strings/password.js b/src/lib/strings/password.js new file mode 100644 index 0000000000..0403964d96 --- /dev/null +++ b/src/lib/strings/password.js @@ -0,0 +1,15 @@ +// Regex for base32 string for testing reset code +var RESET_CODE_REGEX = /^[A-Z2-7]{5}-[A-Z2-7]{5}$/; +export function checkAndFormatResetCode(code) { + // Trim the reset code + var fixed = code.trim().toUpperCase(); + // Add a dash if needed + if (fixed.length === 10) { + fixed = "".concat(fixed.slice(0, 5), "-").concat(fixed.slice(5, 10)); + } + // Check that it is a valid format + if (!RESET_CODE_REGEX.test(fixed)) { + return false; + } + return fixed; +} diff --git a/src/lib/strings/rich-text-detection.js b/src/lib/strings/rich-text-detection.js new file mode 100644 index 0000000000..bd6c068f0a --- /dev/null +++ b/src/lib/strings/rich-text-detection.js @@ -0,0 +1,39 @@ +import { isValidDomain } from './url-helpers'; +export function detectLinkables(text) { + var _a, _b; + var re = /((^|\s|\()@[a-z0-9.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi; + var segments = []; + var match; + var start = 0; + while ((match = re.exec(text))) { + var matchIndex = match.index; + var matchValue = match[0]; + if (((_a = match.groups) === null || _a === void 0 ? void 0 : _a.domain) && !isValidDomain((_b = match.groups) === null || _b === void 0 ? void 0 : _b.domain)) { + continue; + } + if (/\s|\(/.test(matchValue)) { + // HACK + // skip the starting space + // we have to do this because RN doesnt support negative lookaheads + // -prf + matchIndex++; + matchValue = matchValue.slice(1); + } + // strip ending punctuation + if (/[.,;!?]$/.test(matchValue)) { + matchValue = matchValue.slice(0, -1); + } + if (/[)]$/.test(matchValue) && !matchValue.includes('(')) { + matchValue = matchValue.slice(0, -1); + } + if (start !== matchIndex) { + segments.push(text.slice(start, matchIndex)); + } + segments.push({ link: matchValue }); + start = matchIndex + matchValue.length; + } + if (start < text.length) { + segments.push(text.slice(start)); + } + return segments; +} diff --git a/src/lib/strings/rich-text-helpers.js b/src/lib/strings/rich-text-helpers.js new file mode 100644 index 0000000000..9c4fd05dc7 --- /dev/null +++ b/src/lib/strings/rich-text-helpers.js @@ -0,0 +1,23 @@ +import { AppBskyRichtextFacet } from '@atproto/api'; +import { linkRequiresWarning } from './url-helpers'; +export function richTextToString(rt, loose) { + var text = rt.text, facets = rt.facets; + if (!(facets === null || facets === void 0 ? void 0 : facets.length)) { + return text; + } + var result = ''; + for (var _i = 0, _a = rt.segments(); _i < _a.length; _i++) { + var segment = _a[_i]; + var link = segment.link; + if (link && AppBskyRichtextFacet.validateLink(link).success) { + var href = link.uri; + var text_1 = segment.text; + var requiresWarning = linkRequiresWarning(href, text_1); + result += !requiresWarning ? href : loose ? "[".concat(text_1, "](").concat(href, ")") : text_1; + } + else { + result += segment.text; + } + } + return result; +} diff --git a/src/lib/strings/rich-text-manip.js b/src/lib/strings/rich-text-manip.js new file mode 100644 index 0000000000..caacc9f1ce --- /dev/null +++ b/src/lib/strings/rich-text-manip.js @@ -0,0 +1,49 @@ +import { AppBskyRichtextFacet, UnicodeString } from '@atproto/api'; +import { toShortUrl } from './url-helpers'; +export function shortenLinks(rt) { + var _a; + if (!((_a = rt.facets) === null || _a === void 0 ? void 0 : _a.length)) { + return rt; + } + rt = rt.clone(); + // enumerate the link facets + if (rt.facets) { + for (var _i = 0, _b = rt.facets; _i < _b.length; _i++) { + var facet = _b[_i]; + var isLink = !!facet.features.find(AppBskyRichtextFacet.isLink); + if (!isLink) { + continue; + } + // extract and shorten the URL + var _c = facet.index, byteStart = _c.byteStart, byteEnd = _c.byteEnd; + var url = rt.unicodeText.slice(byteStart, byteEnd); + var shortened = new UnicodeString(toShortUrl(url)); + // insert the shorten URL + rt.insert(byteStart, shortened.utf16); + // update the facet to cover the new shortened URL + facet.index.byteStart = byteStart; + facet.index.byteEnd = byteStart + shortened.length; + // remove the old URL + rt.delete(byteStart + shortened.length, byteEnd + shortened.length); + } + } + return rt; +} +// filter out any mention facets that didn't map to a user +export function stripInvalidMentions(rt) { + var _a, _b; + if (!((_a = rt.facets) === null || _a === void 0 ? void 0 : _a.length)) { + return rt; + } + rt = rt.clone(); + if (rt.facets) { + rt.facets = (_b = rt.facets) === null || _b === void 0 ? void 0 : _b.filter(function (facet) { + var mention = facet.features.find(AppBskyRichtextFacet.isMention); + if (mention && !mention.did) { + return false; + } + return true; + }); + } + return rt; +} diff --git a/src/lib/strings/starter-pack.js b/src/lib/strings/starter-pack.js new file mode 100644 index 0000000000..e0142c3cc1 --- /dev/null +++ b/src/lib/strings/starter-pack.js @@ -0,0 +1,90 @@ +import { AtUri } from '@atproto/api'; +export function createStarterPackLinkFromAndroidReferrer(referrerQueryString) { + try { + // The referrer string is just some URL parameters, so lets add them to a fake URL + var url = new URL('http://throwaway.com/?' + referrerQueryString); + var utmContent = url.searchParams.get('utm_content'); + var utmSource = url.searchParams.get('utm_source'); + if (!utmContent) + return null; + if (utmSource !== 'bluesky') + return null; + // This should be a string like `starterpack_haileyok.com_rkey` + var contentParts = utmContent.split('_'); + if (contentParts[0] !== 'starterpack') + return null; + if (contentParts.length !== 3) + return null; + return "at://".concat(contentParts[1], "/app.bsky.graph.starterpack/").concat(contentParts[2]); + } + catch (e) { + return null; + } +} +export function parseStarterPackUri(uri) { + if (!uri) + return null; + try { + if (uri.startsWith('at://')) { + var atUri = new AtUri(uri); + if (atUri.collection !== 'app.bsky.graph.starterpack') + return null; + if (atUri.rkey) { + return { + name: atUri.hostname, + rkey: atUri.rkey, + }; + } + return null; + } + else { + var url = new URL(uri); + var parts = url.pathname.split('/'); + var __ = parts[0], path = parts[1], name_1 = parts[2], rkey = parts[3]; + if (parts.length !== 4) + return null; + if (path !== 'starter-pack' && path !== 'start') + return null; + if (!name_1 || !rkey) + return null; + return { + name: name_1, + rkey: rkey, + }; + } + } + catch (e) { + return null; + } +} +export function createStarterPackGooglePlayUri(name, rkey) { + if (!name || !rkey) + return null; + return "https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_".concat(name, "_").concat(rkey); +} +export function httpStarterPackUriToAtUri(httpUri) { + if (!httpUri) + return null; + var parsed = parseStarterPackUri(httpUri); + if (!parsed) + return null; + if (httpUri.startsWith('at://')) + return httpUri; + return "at://".concat(parsed.name, "/app.bsky.graph.starterpack/").concat(parsed.rkey); +} +export function getStarterPackOgCard(didOrStarterPack, rkey) { + if (typeof didOrStarterPack === 'string') { + return "https://ogcard.cdn.bsky.app/start/".concat(didOrStarterPack, "/").concat(rkey); + } + else { + var rkey_1 = new AtUri(didOrStarterPack.uri).rkey; + return "https://ogcard.cdn.bsky.app/start/".concat(didOrStarterPack.creator.did, "/").concat(rkey_1); + } +} +export function createStarterPackUri(_a) { + var did = _a.did, rkey = _a.rkey; + return new AtUri("at://".concat(did, "/app.bsky.graph.starterpack/").concat(rkey)).toString(); +} +export function startUriToStarterPackUri(uri) { + return uri.replace('/start/', '/starter-pack/'); +} diff --git a/src/lib/strings/time.js b/src/lib/strings/time.js new file mode 100644 index 0000000000..1aa5aa2d3b --- /dev/null +++ b/src/lib/strings/time.js @@ -0,0 +1,42 @@ +import { msg } from '@lingui/core/macro'; +export function niceDate(i18n, date, dateStyle) { + if (dateStyle === void 0) { dateStyle = 'long'; } + var d = new Date(date); + if (dateStyle === 'dot separated') { + return i18n._(msg({ + context: 'date and time formatted like this: [time] · [date]', + message: "".concat(i18n.date(d, { timeStyle: 'short' }), " \u00B7 ").concat(i18n.date(d, { dateStyle: 'medium' })), + })); + } + return i18n.date(d, { + dateStyle: dateStyle, + timeStyle: 'short', + }); +} +export function getAge(birthDate) { + var today = new Date(); + var age = today.getFullYear() - birthDate.getFullYear(); + var m = today.getMonth() - birthDate.getMonth(); + if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + return age; +} +/** + * Get a Date object that is N years ago from now + * @param years number of years + * @returns Date object + */ +export function getDateAgo(years) { + var date = new Date(); + date.setFullYear(date.getFullYear() - years); + return date; +} +/** + * Compares two dates by year, month, and day only + */ +export function simpleAreDatesEqual(a, b) { + return (a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate()); +} diff --git a/src/lib/strings/url-helpers.js b/src/lib/strings/url-helpers.js new file mode 100644 index 0000000000..7f0a2d745e --- /dev/null +++ b/src/lib/strings/url-helpers.js @@ -0,0 +1,369 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { AtUri } from '@atproto/api'; +import psl from 'psl'; +import TLDs from 'tlds'; +import { BSKY_SERVICE } from '#/lib/constants'; +import { isInvalidHandle } from '#/lib/strings/handles'; +import { startUriToStarterPackUri } from '#/lib/strings/starter-pack'; +import { logger } from '#/logger'; +export var BSKY_APP_HOST = 'https://bsky.app'; +var BSKY_TRUSTED_HOSTS = __spreadArray([ + 'bsky\\.app', + 'bsky\\.social', + 'blueskyweb\\.xyz', + 'blueskyweb\\.zendesk\\.com' +], (__DEV__ ? ['localhost:19006', 'localhost:8100'] : []), true); +/* + * This will allow any BSKY_TRUSTED_HOSTS value by itself or with a subdomain. + * It will also allow relative paths like /profile as well as #. + */ +var TRUSTED_REGEX = new RegExp("^(http(s)?://(([\\w-]+\\.)?".concat(BSKY_TRUSTED_HOSTS.join('|([\\w-]+\\.)?'), ")|/|#)")); +export function isValidDomain(str) { + return !!TLDs.find(function (tld) { + var i = str.lastIndexOf(tld); + if (i === -1) { + return false; + } + return str.charAt(i - 1) === '.' && i === str.length - tld.length; + }); +} +export function makeRecordUri(didOrName, collection, rkey) { + var urip = new AtUri('at://placeholder.placeholder/'); + // @ts-expect-error TODO new-sdk-migration + urip.host = didOrName; + urip.collection = collection; + urip.rkey = rkey; + return urip.toString(); +} +export function toNiceDomain(url) { + try { + var urlp = new URL(url); + if ("https://".concat(urlp.host) === BSKY_SERVICE) { + return 'Bluesky Social'; + } + return urlp.host ? urlp.host : url; + } + catch (e) { + return url; + } +} +export function toShortUrl(url) { + try { + var urlp = new URL(url); + if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') { + return url; + } + var path = (urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash; + if (path.length > 15) { + return urlp.host + path.slice(0, 13) + '...'; + } + return urlp.host + path; + } + catch (e) { + return url; + } +} +export function toShareUrl(url) { + if (!url.startsWith('https')) { + var urlp = new URL('https://bsky.app'); + urlp.pathname = url; + url = urlp.toString(); + } + return url; +} +export function toBskyAppUrl(url) { + return new URL(url, BSKY_APP_HOST).toString(); +} +export function isBskyAppUrl(url) { + return url.startsWith('https://bsky.app/'); +} +export function isRelativeUrl(url) { + return /^\/[^/]/.test(url); +} +export function isBskyRSSUrl(url) { + return ((url.startsWith('https://bsky.app/') || isRelativeUrl(url)) && + /\/rss\/?$/.test(url)); +} +export function isExternalUrl(url) { + var external = !isBskyAppUrl(url) && url.startsWith('http'); + var rss = isBskyRSSUrl(url); + return external || rss; +} +export function isTrustedUrl(url) { + return TRUSTED_REGEX.test(url); +} +export function isBskyPostUrl(url) { + if (isBskyAppUrl(url)) { + try { + var urlp = new URL(url); + return /profile\/(?[^/]+)\/post\/(?[^/]+)/i.test(urlp.pathname); + } + catch (_a) { } + } + return false; +} +export function isBskyCustomFeedUrl(url) { + if (isBskyAppUrl(url)) { + try { + var urlp = new URL(url); + return /profile\/(?[^/]+)\/feed\/(?[^/]+)/i.test(urlp.pathname); + } + catch (_a) { } + } + return false; +} +export function isBskyListUrl(url) { + if (isBskyAppUrl(url)) { + try { + var urlp = new URL(url); + return /profile\/(?[^/]+)\/lists\/(?[^/]+)/i.test(urlp.pathname); + } + catch (_a) { + console.error('Unexpected error in isBskyListUrl()', url); + } + } + return false; +} +export function isBskyStartUrl(url) { + if (isBskyAppUrl(url)) { + try { + var urlp = new URL(url); + return /start\/(?[^/]+)\/(?[^/]+)/i.test(urlp.pathname); + } + catch (_a) { + console.error('Unexpected error in isBskyStartUrl()', url); + } + } + return false; +} +export function isBskyStarterPackUrl(url) { + if (isBskyAppUrl(url)) { + try { + var urlp = new URL(url); + return /starter-pack\/(?[^/]+)\/(?[^/]+)/i.test(urlp.pathname); + } + catch (_a) { + console.error('Unexpected error in isBskyStartUrl()', url); + } + } + return false; +} +export function isBskyDownloadUrl(url) { + if (isExternalUrl(url)) { + return false; + } + return url === '/download' || url.startsWith('/download?'); +} +export function convertBskyAppUrlIfNeeded(url) { + if (isBskyAppUrl(url)) { + try { + var urlp = new URL(url); + if (isBskyStartUrl(url)) { + return startUriToStarterPackUri(urlp.pathname); + } + // special-case search links + if (urlp.pathname === '/search') { + return "/search?q=".concat(urlp.searchParams.get('q')); + } + return urlp.pathname; + } + catch (e) { + console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e); + } + } + else if (isShortLink(url)) { + // We only want to do this on native, web handles the 301 for us + return shortLinkToHref(url); + } + return url; +} +export function listUriToHref(url) { + try { + var _a = new AtUri(url), hostname = _a.hostname, rkey = _a.rkey; + return "/profile/".concat(hostname, "/lists/").concat(rkey); + } + catch (_b) { + return ''; + } +} +export function feedUriToHref(url) { + try { + var _a = new AtUri(url), hostname = _a.hostname, rkey = _a.rkey; + return "/profile/".concat(hostname, "/feed/").concat(rkey); + } + catch (_b) { + return ''; + } +} +export function postUriToRelativePath(uri, options) { + try { + var _a = new AtUri(uri), hostname = _a.hostname, rkey = _a.rkey; + var handleOrDid = (options === null || options === void 0 ? void 0 : options.handle) && !isInvalidHandle(options.handle) + ? options.handle + : hostname; + return "/profile/".concat(handleOrDid, "/post/").concat(rkey); + } + catch (_b) { + return undefined; + } +} +/** + * Checks if the label in the post text matches the host of the link facet. + * + * Hosts are case-insensitive, so should be lowercase for comparison. + * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 + */ +export function linkRequiresWarning(uri, label) { + var labelDomain = labelToDomain(label); + // We should trust any relative URL or a # since we know it links to internal content + if (isRelativeUrl(uri) || uri === '#') { + return false; + } + var urip; + try { + urip = new URL(uri); + } + catch (_a) { + return true; + } + var host = urip.hostname.toLowerCase(); + if (isTrustedUrl(uri)) { + // if this is a link to internal content, warn if it represents itself as a URL to another app + return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain); + } + else { + // if this is a link to external content, warn if the label doesnt match the target + if (!labelDomain) { + return true; + } + return labelDomain !== host; + } +} +/** + * Returns a lowercase domain hostname if the label is a valid URL. + * + * Hosts are case-insensitive, so should be lowercase for comparison. + * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 + */ +export function labelToDomain(label) { + // any spaces just immediately consider the label a non-url + if (/\s/.test(label)) { + return undefined; + } + try { + return new URL(label).hostname.toLowerCase(); + } + catch (_a) { } + try { + return new URL('https://' + label).hostname.toLowerCase(); + } + catch (_b) { } + return undefined; +} +export function isPossiblyAUrl(str) { + str = str.trim(); + if (str.startsWith('http://')) { + return true; + } + if (str.startsWith('https://')) { + return true; + } + var firstWord = str.split(/[\s\/]/)[0]; + return isValidDomain(firstWord); +} +export function splitApexDomain(hostname) { + var hostnamep = psl.parse(hostname); + if (hostnamep.error || !hostnamep.listed || !hostnamep.domain) { + return ['', hostname]; + } + return [ + hostnamep.subdomain ? "".concat(hostnamep.subdomain, ".") : '', + hostnamep.domain, + ]; +} +export function createBskyAppAbsoluteUrl(path) { + var sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, ''); + return "".concat(BSKY_APP_HOST.replace(/\/$/, ''), "/").concat(sanitizedPath); +} +export function createProxiedUrl(url) { + var u; + try { + u = new URL(url); + } + catch (_a) { + return url; + } + if ((u === null || u === void 0 ? void 0 : u.protocol) !== 'http:' && (u === null || u === void 0 ? void 0 : u.protocol) !== 'https:') { + return url; + } + return "https://go.bsky.app/redirect?u=".concat(encodeURIComponent(url)); +} +export function isShortLink(url) { + return url.startsWith('https://go.bsky.app/'); +} +export function shortLinkToHref(url) { + try { + var urlp = new URL(url); + // For now we only support starter packs, but in the future we should add additional paths to this check + var parts = urlp.pathname.split('/').filter(Boolean); + if (parts.length === 1) { + return "/starter-pack-short/".concat(parts[0]); + } + return url; + } + catch (e) { + logger.error('Failed to parse possible short link', { safeMessage: e }); + return url; + } +} +export function getHostnameFromUrl(url) { + var urlp; + try { + urlp = new URL(url); + } + catch (e) { + return null; + } + return urlp.hostname; +} +export function getServiceAuthAudFromUrl(url) { + var hostname = getHostnameFromUrl(url); + if (!hostname) { + return null; + } + return "did:web:".concat(hostname); +} +// passes URL.parse, and has a TLD etc +export function definitelyUrl(maybeUrl) { + try { + if (maybeUrl.endsWith('.')) + return null; + // Prepend 'https://' if the input doesn't start with a protocol + if (!maybeUrl.startsWith('https://') && !maybeUrl.startsWith('http://')) { + maybeUrl = 'https://' + maybeUrl; + } + var url = new URL(maybeUrl); + // Extract the hostname and split it into labels + var hostname = url.hostname; + var labels = hostname.split('.'); + // Ensure there are at least two labels (e.g., 'example' and 'com') + if (labels.length < 2) + return null; + var tld = labels[labels.length - 1]; + // Check that the TLD is at least two characters long and contains only letters + if (!/^[a-z]{2,}$/i.test(tld)) + return null; + return url.toString(); + } + catch (_a) { + return null; + } +} diff --git a/src/lib/styles.js b/src/lib/styles.js new file mode 100644 index 0000000000..d829b45f24 --- /dev/null +++ b/src/lib/styles.js @@ -0,0 +1,219 @@ +var _a; +import { Dimensions, StyleSheet, } from 'react-native'; +import { IS_WEB } from '#/env'; +// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest +/** + * @deprecated use ALF colors instead + */ +export var colors = { + white: '#ffffff', + black: '#000000', + gray1: '#F3F3F8', + gray2: '#E2E2E4', + gray3: '#B9B9C1', + gray4: '#8D8E96', + gray5: '#545664', + gray6: '#373942', + gray7: '#26272D', + gray8: '#141417', + blue0: '#bfe1ff', + blue1: '#8bc7fd', + blue2: '#52acfe', + blue3: '#0085ff', + blue4: '#0062bd', + blue5: '#034581', + blue6: '#012561', + blue7: '#001040', + red1: '#ffe6eb', + red2: '#fba2b2', + red3: '#ec4868', + red4: '#d11043', + red5: '#970721', + red6: '#690419', + red7: '#4F0314', + pink1: '#f8ccff', + pink2: '#e966ff', + pink3: '#db00ff', + pink4: '#a601c1', + pink5: '#570066', + purple1: '#ebdbff', + purple2: '#ba85ff', + purple3: '#9747ff', + purple4: '#6d00fa', + purple5: '#380080', + green1: '#c1ffb8', + green2: '#27f406', + green3: '#20bc07', + green4: '#148203', + green5: '#082b03', + unreadNotifBg: '#ebf6ff', + brandBlue: '#0066FF', + like: '#ec4899', +}; +export var gradients = { + blueLight: { start: '#5A71FA', end: colors.blue3 }, // buttons + blue: { start: '#5E55FB', end: colors.blue3 }, // fab + blueDark: { start: '#5F45E0', end: colors.blue3 }, // avis, banner +}; +/** + * @deprecated use atoms from `#/alf` + */ +export var s = StyleSheet.create((_a = { + // helpers + footerSpacer: { height: 100 }, + contentContainer: { paddingBottom: 200 }, + contentContainerExtra: { paddingBottom: 300 }, + border0: { borderWidth: 0 }, + border1: { borderWidth: 1 }, + borderTop1: { borderTopWidth: 1 }, + borderRight1: { borderRightWidth: 1 }, + borderBottom1: { borderBottomWidth: 1 }, + borderLeft1: { borderLeftWidth: 1 }, + hidden: { display: 'none' }, + dimmed: { opacity: 0.5 }, + // font weights + fw600: { fontWeight: '600' }, + bold: { fontWeight: '600' }, + fw500: { fontWeight: '600' }, + semiBold: { fontWeight: '600' }, + fw400: { fontWeight: '400' }, + normal: { fontWeight: '400' }, + fw300: { fontWeight: '400' }, + light: { fontWeight: '400' }, + // text decoration + underline: { textDecorationLine: 'underline' }, + // font variants + tabularNum: { fontVariant: ['tabular-nums'] }, + // font sizes + f9: { fontSize: 9 }, + f10: { fontSize: 10 }, + f11: { fontSize: 11 }, + f12: { fontSize: 12 }, + f13: { fontSize: 13 }, + f14: { fontSize: 14 }, + f15: { fontSize: 15 }, + f16: { fontSize: 16 }, + f17: { fontSize: 17 }, + f18: { fontSize: 18 } + }, + // line heights + _a['lh13-1'] = { lineHeight: 13 }, + _a['lh13-1.3'] = { lineHeight: 16.9 }, // 1.3 of 13px + _a['lh14-1'] = { lineHeight: 14 }, + _a['lh14-1.3'] = { lineHeight: 18.2 }, // 1.3 of 14px + _a['lh15-1'] = { lineHeight: 15 }, + _a['lh15-1.3'] = { lineHeight: 19.5 }, // 1.3 of 15px + _a['lh16-1'] = { lineHeight: 16 }, + _a['lh16-1.3'] = { lineHeight: 20.8 }, // 1.3 of 16px + _a['lh17-1'] = { lineHeight: 17 }, + _a['lh17-1.3'] = { lineHeight: 22.1 }, // 1.3 of 17px + _a['lh18-1'] = { lineHeight: 18 }, + _a['lh18-1.3'] = { lineHeight: 23.4 }, // 1.3 of 18px + // margins + _a.mr2 = { marginRight: 2 }, + _a.mr5 = { marginRight: 5 }, + _a.mr10 = { marginRight: 10 }, + _a.mr20 = { marginRight: 20 }, + _a.ml2 = { marginLeft: 2 }, + _a.ml5 = { marginLeft: 5 }, + _a.ml10 = { marginLeft: 10 }, + _a.ml20 = { marginLeft: 20 }, + _a.mt2 = { marginTop: 2 }, + _a.mt5 = { marginTop: 5 }, + _a.mt10 = { marginTop: 10 }, + _a.mt20 = { marginTop: 20 }, + _a.mb2 = { marginBottom: 2 }, + _a.mb5 = { marginBottom: 5 }, + _a.mb10 = { marginBottom: 10 }, + _a.mb20 = { marginBottom: 20 }, + // paddings + _a.p2 = { padding: 2 }, + _a.p5 = { padding: 5 }, + _a.p10 = { padding: 10 }, + _a.p20 = { padding: 20 }, + _a.pr2 = { paddingRight: 2 }, + _a.pr5 = { paddingRight: 5 }, + _a.pr10 = { paddingRight: 10 }, + _a.pr20 = { paddingRight: 20 }, + _a.pl2 = { paddingLeft: 2 }, + _a.pl5 = { paddingLeft: 5 }, + _a.pl10 = { paddingLeft: 10 }, + _a.pl20 = { paddingLeft: 20 }, + _a.pt2 = { paddingTop: 2 }, + _a.pt5 = { paddingTop: 5 }, + _a.pt10 = { paddingTop: 10 }, + _a.pt20 = { paddingTop: 20 }, + _a.pb2 = { paddingBottom: 2 }, + _a.pb5 = { paddingBottom: 5 }, + _a.pb10 = { paddingBottom: 10 }, + _a.pb20 = { paddingBottom: 20 }, + _a.px5 = { paddingHorizontal: 5 }, + // flex + _a.flexRow = { flexDirection: 'row' }, + _a.flexCol = { flexDirection: 'column' }, + _a.flex1 = { flex: 1 }, + _a.flexGrow1 = { flexGrow: 1 }, + _a.alignCenter = { alignItems: 'center' }, + _a.alignBaseline = { alignItems: 'baseline' }, + _a.justifyCenter = { justifyContent: 'center' }, + // position + _a.absolute = { position: 'absolute' }, + // dimensions + _a.w100pct = { width: '100%' }, + _a.h100pct = { height: '100%' }, + _a.hContentRegion = IS_WEB ? { minHeight: '100%' } : { height: '100%' }, + _a.window = { + width: Dimensions.get('window').width, + height: Dimensions.get('window').height, + }, + // text align + _a.textLeft = { textAlign: 'left' }, + _a.textCenter = { textAlign: 'center' }, + _a.textRight = { textAlign: 'right' }, + // colors + _a.white = { color: colors.white }, + _a.black = { color: colors.black }, + _a.gray1 = { color: colors.gray1 }, + _a.gray2 = { color: colors.gray2 }, + _a.gray3 = { color: colors.gray3 }, + _a.gray4 = { color: colors.gray4 }, + _a.gray5 = { color: colors.gray5 }, + _a.blue1 = { color: colors.blue1 }, + _a.blue2 = { color: colors.blue2 }, + _a.blue3 = { color: colors.blue3 }, + _a.blue4 = { color: colors.blue4 }, + _a.blue5 = { color: colors.blue5 }, + _a.red1 = { color: colors.red1 }, + _a.red2 = { color: colors.red2 }, + _a.red3 = { color: colors.red3 }, + _a.red4 = { color: colors.red4 }, + _a.red5 = { color: colors.red5 }, + _a.pink1 = { color: colors.pink1 }, + _a.pink2 = { color: colors.pink2 }, + _a.pink3 = { color: colors.pink3 }, + _a.pink4 = { color: colors.pink4 }, + _a.pink5 = { color: colors.pink5 }, + _a.purple1 = { color: colors.purple1 }, + _a.purple2 = { color: colors.purple2 }, + _a.purple3 = { color: colors.purple3 }, + _a.purple4 = { color: colors.purple4 }, + _a.purple5 = { color: colors.purple5 }, + _a.green1 = { color: colors.green1 }, + _a.green2 = { color: colors.green2 }, + _a.green3 = { color: colors.green3 }, + _a.green4 = { color: colors.green4 }, + _a.green5 = { color: colors.green5 }, + _a.brandBlue = { color: colors.brandBlue }, + _a.likeColor = { color: colors.like }, + _a)); +export function lh(theme, type, height) { + return { + lineHeight: Math.round((theme.typography[type].fontSize || 16) * height), + }; +} +export function addStyle(base, addedStyle) { + if (Array.isArray(base)) { + return base.concat([addedStyle]); + } + return [base, addedStyle]; +} diff --git a/src/lib/themes.js b/src/lib/themes.js new file mode 100644 index 0000000000..6bee6068d8 --- /dev/null +++ b/src/lib/themes.js @@ -0,0 +1,331 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { Platform } from 'react-native'; +import { tokens } from '#/alf'; +import { darkPalette, dimPalette, lightPalette } from '#/alf/themes'; +import { fontWeight } from '#/alf/tokens'; +import { colors } from './styles'; +export var defaultTheme = { + colorScheme: 'light', + palette: { + default: { + background: lightPalette.white, + backgroundLight: lightPalette.contrast_25, + text: lightPalette.black, + textLight: lightPalette.contrast_700, + textInverted: lightPalette.white, + link: lightPalette.primary_500, + border: lightPalette.contrast_100, + borderDark: lightPalette.contrast_200, + icon: lightPalette.contrast_500, + // non-standard + textVeryLight: lightPalette.contrast_400, + replyLine: lightPalette.contrast_100, + replyLineDot: lightPalette.contrast_200, + unreadNotifBg: lightPalette.primary_25, + unreadNotifBorder: lightPalette.primary_100, + postCtrl: lightPalette.contrast_500, + brandText: lightPalette.primary_500, + emptyStateIcon: lightPalette.contrast_300, + borderLinkHover: lightPalette.contrast_300, + }, + primary: { + background: colors.blue3, + backgroundLight: colors.blue2, + text: colors.white, + textLight: colors.blue0, + textInverted: colors.blue3, + link: colors.blue0, + border: colors.blue4, + borderDark: colors.blue5, + icon: colors.blue4, + }, + secondary: { + background: colors.green3, + backgroundLight: colors.green2, + text: colors.white, + textLight: colors.green1, + textInverted: colors.green4, + link: colors.green1, + border: colors.green4, + borderDark: colors.green5, + icon: colors.green4, + }, + inverted: { + background: darkPalette.black, + backgroundLight: darkPalette.contrast_50, + text: darkPalette.white, + textLight: darkPalette.contrast_700, + textInverted: darkPalette.black, + link: darkPalette.primary_500, + border: darkPalette.contrast_100, + borderDark: darkPalette.contrast_200, + icon: darkPalette.contrast_500, + }, + error: { + background: colors.red3, + backgroundLight: colors.red2, + text: colors.white, + textLight: colors.red1, + textInverted: colors.red3, + link: colors.red1, + border: colors.red4, + borderDark: colors.red5, + icon: colors.red4, + }, + }, + shapes: { + button: { + // TODO + }, + bigButton: { + // TODO + }, + smallButton: { + // TODO + }, + }, + typography: { + '2xl-thin': { + fontSize: 18, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + '2xl': { + fontSize: 18, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + '2xl-medium': { + fontSize: 18, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + '2xl-bold': { + fontSize: 18, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + '2xl-heavy': { + fontSize: 18, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.bold, + }, + 'xl-thin': { + fontSize: 17, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + xl: { + fontSize: 17, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + 'xl-medium': { + fontSize: 17, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'xl-bold': { + fontSize: 17, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'xl-heavy': { + fontSize: 17, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.bold, + }, + 'lg-thin': { + fontSize: 16, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + lg: { + fontSize: 16, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + 'lg-medium': { + fontSize: 16, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'lg-bold': { + fontSize: 16, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'lg-heavy': { + fontSize: 16, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.bold, + }, + 'md-thin': { + fontSize: 15, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + md: { + fontSize: 15, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + 'md-medium': { + fontSize: 15, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'md-bold': { + fontSize: 15, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'md-heavy': { + fontSize: 15, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.bold, + }, + 'sm-thin': { + fontSize: 14, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + sm: { + fontSize: 14, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + 'sm-medium': { + fontSize: 14, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'sm-bold': { + fontSize: 14, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'sm-heavy': { + fontSize: 14, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.bold, + }, + 'xs-thin': { + fontSize: 13, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + xs: { + fontSize: 13, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + 'xs-medium': { + fontSize: 13, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'xs-bold': { + fontSize: 13, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'xs-heavy': { + fontSize: 13, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.bold, + }, + 'title-2xl': { + fontSize: 34, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'title-xl': { + fontSize: 28, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.semiBold, + }, + 'title-lg': { + fontSize: 22, + fontWeight: fontWeight.semiBold, + }, + title: { + fontWeight: fontWeight.semiBold, + fontSize: 20, + letterSpacing: tokens.TRACKING, + }, + 'title-sm': { + fontWeight: fontWeight.semiBold, + fontSize: 17, + letterSpacing: tokens.TRACKING, + }, + 'post-text': { + fontSize: 16, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + 'post-text-lg': { + fontSize: 20, + letterSpacing: tokens.TRACKING, + fontWeight: fontWeight.normal, + }, + 'button-lg': { + fontWeight: fontWeight.semiBold, + fontSize: 18, + letterSpacing: tokens.TRACKING, + }, + button: { + fontWeight: fontWeight.semiBold, + fontSize: 14, + letterSpacing: tokens.TRACKING, + }, + mono: { + fontSize: 14, + fontFamily: Platform.OS === 'android' ? 'monospace' : 'Courier New', + }, + }, +}; +export var darkTheme = __assign(__assign({}, defaultTheme), { colorScheme: 'dark', palette: __assign(__assign({}, defaultTheme.palette), { default: { + background: darkPalette.contrast_0, + backgroundLight: darkPalette.contrast_25, + text: darkPalette.white, + textLight: darkPalette.contrast_600, + textInverted: darkPalette.black, + link: darkPalette.primary_500, + border: darkPalette.contrast_100, + borderDark: darkPalette.contrast_200, + icon: darkPalette.contrast_500, + // non-standard + textVeryLight: darkPalette.contrast_400, + replyLine: darkPalette.contrast_200, + replyLineDot: darkPalette.contrast_200, + unreadNotifBg: darkPalette.primary_25, + unreadNotifBorder: darkPalette.primary_100, + postCtrl: darkPalette.contrast_500, + brandText: darkPalette.primary_500, + emptyStateIcon: darkPalette.contrast_300, + borderLinkHover: darkPalette.contrast_300, + }, primary: __assign(__assign({}, defaultTheme.palette.primary), { textInverted: colors.blue2 }), secondary: __assign(__assign({}, defaultTheme.palette.secondary), { textInverted: colors.green2 }), inverted: { + background: darkPalette.white, + backgroundLight: lightPalette.contrast_50, + text: lightPalette.black, + textLight: lightPalette.contrast_700, + textInverted: darkPalette.white, + link: lightPalette.primary_500, + border: lightPalette.contrast_100, + borderDark: lightPalette.contrast_200, + icon: lightPalette.contrast_500, + } }) }); +export var dimTheme = __assign(__assign({}, darkTheme), { palette: __assign(__assign({}, darkTheme.palette), { default: __assign(__assign({}, darkTheme.palette.default), { background: dimPalette.contrast_0, backgroundLight: dimPalette.contrast_25, text: dimPalette.white, textLight: dimPalette.contrast_700, textInverted: dimPalette.black, link: dimPalette.primary_500, border: dimPalette.contrast_100, borderDark: dimPalette.contrast_200, icon: dimPalette.contrast_500, + // non-standard + textVeryLight: dimPalette.contrast_400, replyLine: dimPalette.contrast_200, replyLineDot: dimPalette.contrast_200, unreadNotifBg: dimPalette.primary_25, unreadNotifBorder: dimPalette.primary_100, postCtrl: dimPalette.contrast_500, brandText: dimPalette.primary_500, emptyStateIcon: dimPalette.contrast_300, borderLinkHover: dimPalette.contrast_300 }) }) }); diff --git a/src/lib/type-guards.js b/src/lib/type-guards.js new file mode 100644 index 0000000000..e925c9c8c5 --- /dev/null +++ b/src/lib/type-guards.js @@ -0,0 +1,9 @@ +export function isObj(v) { + return !!v && typeof v === 'object'; +} +export function hasProp(data, prop) { + return prop in data; +} +export function isStrArray(v) { + return Array.isArray(v) && v.every(function (item) { return typeof item === 'string'; }); +} diff --git a/src/locale/__tests__/helpers.test.js b/src/locale/__tests__/helpers.test.js new file mode 100644 index 0000000000..04ddc0e1be --- /dev/null +++ b/src/locale/__tests__/helpers.test.js @@ -0,0 +1,15 @@ +import { expect, test } from '@jest/globals'; +import { sanitizeAppLanguageSetting } from '#/locale/helpers'; +import { AppLanguage } from '#/locale/languages'; +test('sanitizeAppLanguageSetting', function () { + expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en); + expect(sanitizeAppLanguageSetting('el')).toBe(AppLanguage.el); + expect(sanitizeAppLanguageSetting('pt-BR')).toBe(AppLanguage.pt_BR); + expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi); + expect(sanitizeAppLanguageSetting('id')).toBe(AppLanguage.id); + expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en); + expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en); + expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en); + expect(sanitizeAppLanguageSetting('vi')).toBe(AppLanguage.vi); + expect(sanitizeAppLanguageSetting('ne')).toBe(AppLanguage.ne); +}); diff --git a/src/locale/deviceLocales.js b/src/locale/deviceLocales.js new file mode 100644 index 0000000000..1e276e3f48 --- /dev/null +++ b/src/locale/deviceLocales.js @@ -0,0 +1,60 @@ +import { getLocales as defaultGetLocales } from 'expo-localization'; +import { dedupArray } from '#/lib/functions'; +/** + * Normalized locales + * + * Handles legacy migration for Java devices. + * + * {@link https://github.com/bluesky-social/social-app/pull/4461} + * {@link https://xml.coverpages.org/iso639a.html} + * + * Convert Chinese language tags for Native. + * + * {@link https://datatracker.ietf.org/doc/html/rfc5646#appendix-A} + * {@link https://developer.apple.com/documentation/packagedescription/languagetag} + * {@link https://gist.github.com/amake/0ac7724681ac1c178c6f95a5b09f03ce#new-locales-vs-old-locales-chinese} + */ +export function getLocales() { + var _a; + var locales = (_a = defaultGetLocales === null || defaultGetLocales === void 0 ? void 0 : defaultGetLocales()) !== null && _a !== void 0 ? _a : []; + var output = []; + for (var _i = 0, locales_1 = locales; _i < locales_1.length; _i++) { + var locale = locales_1[_i]; + if (typeof locale.languageCode === 'string') { + if (locale.languageCode === 'in') { + // indonesian + locale.languageCode = 'id'; + } + if (locale.languageCode === 'iw') { + // hebrew + locale.languageCode = 'he'; + } + if (locale.languageCode === 'ji') { + // yiddish + locale.languageCode = 'yi'; + } + } + if (typeof locale.languageTag === 'string') { + if (locale.languageTag.startsWith('zh-Hans') || + locale.languageTag === 'zh-CN') { + // Simplified Chinese to zh-Hans-CN + locale.languageTag = 'zh-Hans-CN'; + } + if (locale.languageTag.startsWith('zh-Hant') || + locale.languageTag === 'zh-TW') { + // Traditional Chinese to zh-Hant-TW + locale.languageTag = 'zh-Hant-TW'; + } + } + // @ts-ignore checked above + output.push(locale); + } + return output; +} +export var deviceLocales = getLocales(); +/** + * BCP-47 language tag without region e.g. array of 2-char lang codes + * + * {@link https://docs.expo.dev/versions/latest/sdk/localization/#locale} + */ +export var deviceLanguageCodes = dedupArray(deviceLocales.map(function (l) { return l.languageCode; })); diff --git a/src/locale/helpers.js b/src/locale/helpers.js new file mode 100644 index 0000000000..9061e0965e --- /dev/null +++ b/src/locale/helpers.js @@ -0,0 +1,279 @@ +import { AppBskyFeedPost } from '@atproto/api'; +import * as bcp47Match from 'bcp-47-match'; +import lande from 'lande'; +import { hasProp } from '#/lib/type-guards'; +import { AppLanguage, LANGUAGES_MAP_CODE2, LANGUAGES_MAP_CODE3, } from './languages'; +export function code2ToCode3(lang) { + var _a; + if (lang.length === 2) { + return ((_a = LANGUAGES_MAP_CODE2[lang]) === null || _a === void 0 ? void 0 : _a.code3) || lang; + } + return lang; +} +export function code3ToCode2(lang) { + var _a; + if (lang.length === 3) { + return ((_a = LANGUAGES_MAP_CODE3[lang]) === null || _a === void 0 ? void 0 : _a.code2) || lang; + } + return lang; +} +export function code3ToCode2Strict(lang) { + var _a; + if (lang.length === 3) { + return (_a = LANGUAGES_MAP_CODE3[lang]) === null || _a === void 0 ? void 0 : _a.code2; + } + return undefined; +} +function getLocalizedLanguage(langCode, appLang) { + try { + var allNames = new Intl.DisplayNames([appLang], { + type: 'language', + fallback: 'none', + languageDisplay: 'standard', + }); + var translatedName = allNames.of(langCode); + if (translatedName) { + return translatedName; + } + } + catch (e) { + // ignore RangeError from Intl.DisplayNames APIs + if (!(e instanceof RangeError)) { + throw e; + } + } +} +export function languageName(language, appLang) { + // if Intl.DisplayNames is unavailable on the target, display the English name + if (!Intl.DisplayNames) { + return language.name; + } + return getLocalizedLanguage(language.code2, appLang) || language.name; +} +export function codeToLanguageName(lang2or3, appLang) { + var code2 = code3ToCode2(lang2or3); + var knownLanguage = LANGUAGES_MAP_CODE2[code2]; + return knownLanguage ? languageName(knownLanguage, appLang) : code2; +} +export function getPostLanguage(post) { + var candidates = []; + var postText = ''; + if (hasProp(post.record, 'text') && typeof post.record.text === 'string') { + postText = post.record.text; + } + if (AppBskyFeedPost.isRecord(post.record) && + hasProp(post.record, 'langs') && + Array.isArray(post.record.langs)) { + candidates = post.record.langs; + } + // if there's only one declared language, use that + if ((candidates === null || candidates === void 0 ? void 0 : candidates.length) === 1) { + return candidates[0]; + } + // no text? can't determine + if (postText.trim().length === 0) { + return undefined; + } + // run the language model + var langsProbabilityMap = lande(postText); + // filter down using declared languages + if (candidates === null || candidates === void 0 ? void 0 : candidates.length) { + langsProbabilityMap = langsProbabilityMap.filter(function (_a) { + var lang = _a[0], _probability = _a[1]; + return candidates.includes(code3ToCode2(lang)); + }); + } + if (langsProbabilityMap[0]) { + return code3ToCode2(langsProbabilityMap[0][0]); + } +} +export function isPostInLanguage(post, targetLangs) { + var lang = getPostLanguage(post); + if (!lang) { + // the post has no text, so we just say "yes" for now + return true; + } + return bcp47Match.basicFilter(lang, targetLangs).length > 0; +} +export function getTranslatorLink(text, targetLangCode, sourceLanguage) { + return "https://translate.google.com/?sl=".concat(sourceLanguage !== null && sourceLanguage !== void 0 ? sourceLanguage : 'auto', "&tl=").concat(targetLangCode, "&text=").concat(encodeURIComponent(text)); +} +/** + * Returns a valid `appLanguage` value from an arbitrary string. + * + * Context: post-refactor, we populated some user's `appLanguage` setting with + * `postLanguage`, which can be a comma-separated list of values. This breaks + * `appLanguage` handling in the app, so we introduced this util to parse out a + * valid `appLanguage` from the pre-populated `postLanguage` values. + * + * The `appLanguage` will continue to be incorrect until the user returns to + * language settings and selects a new option, at which point we'll re-save + * their choice, which should then be a valid option. Since we don't know when + * this will happen, we should leave this here until we feel it's safe to + * remove, or we re-migrate their storage. + */ +export function sanitizeAppLanguageSetting(appLanguage) { + var langs = appLanguage.split(',').filter(Boolean); + for (var _i = 0, langs_1 = langs; _i < langs_1.length; _i++) { + var lang = langs_1[_i]; + switch (fixLegacyLanguageCode(lang)) { + case 'en': + return AppLanguage.en; + case 'an': + return AppLanguage.an; + case 'ast': + return AppLanguage.ast; + case 'ca': + return AppLanguage.ca; + case 'cy': + return AppLanguage.cy; + case 'da': + return AppLanguage.da; + case 'de': + return AppLanguage.de; + case 'el': + return AppLanguage.el; + case 'en-GB': + return AppLanguage.en_GB; + case 'eo': + return AppLanguage.eo; + case 'es': + return AppLanguage.es; + case 'eu': + return AppLanguage.eu; + case 'fi': + return AppLanguage.fi; + case 'fr': + return AppLanguage.fr; + case 'fy': + return AppLanguage.fy; + case 'ga': + return AppLanguage.ga; + case 'gd': + return AppLanguage.gd; + case 'gl': + return AppLanguage.gl; + case 'hi': + return AppLanguage.hi; + case 'hu': + return AppLanguage.hu; + case 'ia': + return AppLanguage.ia; + case 'id': + return AppLanguage.id; + case 'it': + return AppLanguage.it; + case 'ja': + return AppLanguage.ja; + case 'km': + return AppLanguage.km; + case 'ko': + return AppLanguage.ko; + case 'ne': + return AppLanguage.ne; + case 'nl': + return AppLanguage.nl; + case 'pl': + return AppLanguage.pl; + case 'pt-BR': + return AppLanguage.pt_BR; + case 'pt-PT': + return AppLanguage.pt_PT; + case 'ro': + return AppLanguage.ro; + case 'ru': + return AppLanguage.ru; + case 'sv': + return AppLanguage.sv; + case 'th': + return AppLanguage.th; + case 'tr': + return AppLanguage.tr; + case 'uk': + return AppLanguage.uk; + case 'vi': + return AppLanguage.vi; + case 'zh-Hans-CN': + return AppLanguage.zh_CN; + case 'zh-Hant-HK': + return AppLanguage.zh_HK; + case 'zh-Hant-TW': + return AppLanguage.zh_TW; + default: + continue; + } + } + return AppLanguage.en; +} +/** + * Handles legacy migration for Java devices. + * + * {@link https://github.com/bluesky-social/social-app/pull/4461} + * {@link https://xml.coverpages.org/iso639a.html} + */ +export function fixLegacyLanguageCode(code) { + if (code === 'in') { + // indonesian + return 'id'; + } + if (code === 'iw') { + // hebrew + return 'he'; + } + if (code === 'ji') { + // yiddish + return 'yi'; + } + return code; +} +/** + * Find the first language supported by our translation infra. Values should be + * in order of preference, and match the values of {@link AppLanguage}. + * + * If no match, returns `en`. + */ +export function findSupportedAppLanguage(languageTags) { + var supported = new Set(Object.values(AppLanguage)); + for (var _i = 0, languageTags_1 = languageTags; _i < languageTags_1.length; _i++) { + var tag = languageTags_1[_i]; + if (!tag) + continue; + if (supported.has(tag)) { + return tag; + } + } + return AppLanguage.en; +} +/** + * Gets region name for a given country code and language. + * + * Falls back to English if unavailable/error, and if that fails, returns the country code. + * + * Intl.DisplayNames is widely available + has been polyfilled on native + */ +export function regionName(countryCode, appLang) { + var translatedName = getLocalizedRegionName(countryCode, appLang); + if (translatedName) { + return translatedName; + } + // Fallback: get English name. Needed for i.e. Esperanto + var englishName = getLocalizedRegionName(countryCode, 'en'); + if (englishName) { + return englishName; + } + // Final fallback: return country code + return countryCode; +} +function getLocalizedRegionName(countryCode, appLang) { + try { + var allNames = new Intl.DisplayNames([appLang], { + type: 'region', + fallback: 'none', + }); + return allNames.of(countryCode); + } + catch (err) { + console.warn('Error getting localized region name:', err); + return undefined; + } +} diff --git a/src/locale/i18n.js b/src/locale/i18n.js new file mode 100644 index 0000000000..6b05c4f182 --- /dev/null +++ b/src/locale/i18n.js @@ -0,0 +1,604 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +// Don't remove -force from these because detection is VERY slow on low-end Android. +// https://github.com/formatjs/formatjs/issues/4463#issuecomment-2176070577 +import '@formatjs/intl-locale/polyfill-force'; +import '@formatjs/intl-pluralrules/polyfill-force'; +import '@formatjs/intl-numberformat/polyfill-force'; +import '@formatjs/intl-displaynames/polyfill-force'; +import '@formatjs/intl-pluralrules/locale-data/en'; +import '@formatjs/intl-numberformat/locale-data/en'; +import '@formatjs/intl-displaynames/locale-data/en'; +import { useEffect, useState } from 'react'; +import { i18n } from '@lingui/core'; +import defaultLocale from 'date-fns/locale/en-US'; +import { sanitizeAppLanguageSetting } from '#/locale/helpers'; +import { AppLanguage } from '#/locale/languages'; +import { messages as messagesAn } from '#/locale/locales/an/messages'; +import { messages as messagesAst } from '#/locale/locales/ast/messages'; +import { messages as messagesCa } from '#/locale/locales/ca/messages'; +import { messages as messagesCy } from '#/locale/locales/cy/messages'; +import { messages as messagesDa } from '#/locale/locales/da/messages'; +import { messages as messagesDe } from '#/locale/locales/de/messages'; +import { messages as messagesEl } from '#/locale/locales/el/messages'; +import { messages as messagesEn } from '#/locale/locales/en/messages'; +import { messages as messagesEn_GB } from '#/locale/locales/en-GB/messages'; +import { messages as messagesEo } from '#/locale/locales/eo/messages'; +import { messages as messagesEs } from '#/locale/locales/es/messages'; +import { messages as messagesEu } from '#/locale/locales/eu/messages'; +import { messages as messagesFi } from '#/locale/locales/fi/messages'; +import { messages as messagesFr } from '#/locale/locales/fr/messages'; +import { messages as messagesFy } from '#/locale/locales/fy/messages'; +import { messages as messagesGa } from '#/locale/locales/ga/messages'; +import { messages as messagesGd } from '#/locale/locales/gd/messages'; +import { messages as messagesGl } from '#/locale/locales/gl/messages'; +import { messages as messagesHi } from '#/locale/locales/hi/messages'; +import { messages as messagesHu } from '#/locale/locales/hu/messages'; +import { messages as messagesIa } from '#/locale/locales/ia/messages'; +import { messages as messagesId } from '#/locale/locales/id/messages'; +import { messages as messagesIt } from '#/locale/locales/it/messages'; +import { messages as messagesJa } from '#/locale/locales/ja/messages'; +import { messages as messagesKm } from '#/locale/locales/km/messages'; +import { messages as messagesKo } from '#/locale/locales/ko/messages'; +import { messages as messagesNe } from '#/locale/locales/ne/messages'; +import { messages as messagesNl } from '#/locale/locales/nl/messages'; +import { messages as messagesPl } from '#/locale/locales/pl/messages'; +import { messages as messagesPt_BR } from '#/locale/locales/pt-BR/messages'; +import { messages as messagesPt_PT } from '#/locale/locales/pt-PT/messages'; +import { messages as messagesRo } from '#/locale/locales/ro/messages'; +import { messages as messagesRu } from '#/locale/locales/ru/messages'; +import { messages as messagesSv } from '#/locale/locales/sv/messages'; +import { messages as messagesTh } from '#/locale/locales/th/messages'; +import { messages as messagesTr } from '#/locale/locales/tr/messages'; +import { messages as messagesUk } from '#/locale/locales/uk/messages'; +import { messages as messagesVi } from '#/locale/locales/vi/messages'; +import { messages as messagesZh_CN } from '#/locale/locales/zh-CN/messages'; +import { messages as messagesZh_HK } from '#/locale/locales/zh-HK/messages'; +import { messages as messagesZh_TW } from '#/locale/locales/zh-TW/messages'; +import { useLanguagePrefs } from '#/state/preferences'; +/** + * We do a dynamic import of just the catalog that we need + */ +export function dynamicActivate(locale) { + return __awaiter(this, void 0, void 0, function () { + var _a, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale, dateLocale; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = locale; + switch (_a) { + case AppLanguage.an: return [3 /*break*/, 1]; + case AppLanguage.ast: return [3 /*break*/, 3]; + case AppLanguage.ca: return [3 /*break*/, 5]; + case AppLanguage.cy: return [3 /*break*/, 7]; + case AppLanguage.da: return [3 /*break*/, 9]; + case AppLanguage.de: return [3 /*break*/, 11]; + case AppLanguage.el: return [3 /*break*/, 13]; + case AppLanguage.en_GB: return [3 /*break*/, 15]; + case AppLanguage.eo: return [3 /*break*/, 17]; + case AppLanguage.es: return [3 /*break*/, 19]; + case AppLanguage.eu: return [3 /*break*/, 21]; + case AppLanguage.fi: return [3 /*break*/, 23]; + case AppLanguage.fr: return [3 /*break*/, 25]; + case AppLanguage.fy: return [3 /*break*/, 27]; + case AppLanguage.ga: return [3 /*break*/, 29]; + case AppLanguage.gd: return [3 /*break*/, 31]; + case AppLanguage.gl: return [3 /*break*/, 33]; + case AppLanguage.hi: return [3 /*break*/, 35]; + case AppLanguage.hu: return [3 /*break*/, 37]; + case AppLanguage.ia: return [3 /*break*/, 39]; + case AppLanguage.id: return [3 /*break*/, 41]; + case AppLanguage.it: return [3 /*break*/, 43]; + case AppLanguage.ja: return [3 /*break*/, 45]; + case AppLanguage.km: return [3 /*break*/, 47]; + case AppLanguage.ko: return [3 /*break*/, 49]; + case AppLanguage.ne: return [3 /*break*/, 51]; + case AppLanguage.nl: return [3 /*break*/, 53]; + case AppLanguage.pl: return [3 /*break*/, 55]; + case AppLanguage.pt_BR: return [3 /*break*/, 57]; + case AppLanguage.pt_PT: return [3 /*break*/, 59]; + case AppLanguage.ro: return [3 /*break*/, 61]; + case AppLanguage.ru: return [3 /*break*/, 63]; + case AppLanguage.sv: return [3 /*break*/, 65]; + case AppLanguage.th: return [3 /*break*/, 67]; + case AppLanguage.tr: return [3 /*break*/, 69]; + case AppLanguage.uk: return [3 /*break*/, 71]; + case AppLanguage.vi: return [3 /*break*/, 73]; + case AppLanguage.zh_CN: return [3 /*break*/, 75]; + case AppLanguage.zh_HK: return [3 /*break*/, 77]; + case AppLanguage.zh_TW: return [3 /*break*/, 79]; + } + return [3 /*break*/, 81]; + case 1: + i18n.loadAndActivate({ locale: locale, messages: messagesAn }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/es'), + import('@formatjs/intl-pluralrules/locale-data/an'), + import('@formatjs/intl-numberformat/locale-data/es'), + import('@formatjs/intl-displaynames/locale-data/es'), + ])]; + case 2: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 3: + i18n.loadAndActivate({ locale: locale, messages: messagesAst }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/es'), + import('@formatjs/intl-pluralrules/locale-data/ast'), + import('@formatjs/intl-numberformat/locale-data/ast'), + import('@formatjs/intl-displaynames/locale-data/ast'), + ])]; + case 4: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 5: + i18n.loadAndActivate({ locale: locale, messages: messagesCa }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/ca'), + import('@formatjs/intl-pluralrules/locale-data/ca'), + import('@formatjs/intl-numberformat/locale-data/ca'), + import('@formatjs/intl-displaynames/locale-data/ca'), + ])]; + case 6: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 7: + i18n.loadAndActivate({ locale: locale, messages: messagesCy }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/cy'), + import('@formatjs/intl-pluralrules/locale-data/cy'), + import('@formatjs/intl-numberformat/locale-data/cy'), + import('@formatjs/intl-displaynames/locale-data/cy'), + ])]; + case 8: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 9: + i18n.loadAndActivate({ locale: locale, messages: messagesDa }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/da'), + import('@formatjs/intl-pluralrules/locale-data/da'), + import('@formatjs/intl-numberformat/locale-data/da'), + import('@formatjs/intl-displaynames/locale-data/da'), + ])]; + case 10: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 11: + i18n.loadAndActivate({ locale: locale, messages: messagesDe }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/de'), + import('@formatjs/intl-pluralrules/locale-data/de'), + import('@formatjs/intl-numberformat/locale-data/de'), + import('@formatjs/intl-displaynames/locale-data/de'), + ])]; + case 12: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 13: + i18n.loadAndActivate({ locale: locale, messages: messagesEl }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/el'), + import('@formatjs/intl-pluralrules/locale-data/el'), + import('@formatjs/intl-numberformat/locale-data/el'), + import('@formatjs/intl-displaynames/locale-data/el'), + ])]; + case 14: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 15: + i18n.loadAndActivate({ locale: locale, messages: messagesEn_GB }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/en-GB'), + import('@formatjs/intl-pluralrules/locale-data/en'), + import('@formatjs/intl-numberformat/locale-data/en-GB'), + import('@formatjs/intl-displaynames/locale-data/en-GB'), + ])]; + case 16: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 17: + i18n.loadAndActivate({ locale: locale, messages: messagesEo }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/eo'), + import('@formatjs/intl-pluralrules/locale-data/eo'), + import('@formatjs/intl-numberformat/locale-data/eo'), + // borked, see https://github.com/bluesky-social/social-app/pull/9574 + // import('@formatjs/intl-displaynames/locale-data/eo'), + ])]; + case 18: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 19: + i18n.loadAndActivate({ locale: locale, messages: messagesEs }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/es'), + import('@formatjs/intl-pluralrules/locale-data/es'), + import('@formatjs/intl-numberformat/locale-data/es'), + import('@formatjs/intl-displaynames/locale-data/es'), + ])]; + case 20: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 21: + i18n.loadAndActivate({ locale: locale, messages: messagesEu }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/eu'), + import('@formatjs/intl-pluralrules/locale-data/eu'), + import('@formatjs/intl-numberformat/locale-data/eu'), + import('@formatjs/intl-displaynames/locale-data/eu'), + ])]; + case 22: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 23: + i18n.loadAndActivate({ locale: locale, messages: messagesFi }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/fi'), + import('@formatjs/intl-pluralrules/locale-data/fi'), + import('@formatjs/intl-numberformat/locale-data/fi'), + import('@formatjs/intl-displaynames/locale-data/fi'), + ])]; + case 24: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 25: + i18n.loadAndActivate({ locale: locale, messages: messagesFr }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/fr'), + import('@formatjs/intl-pluralrules/locale-data/fr'), + import('@formatjs/intl-numberformat/locale-data/fr'), + import('@formatjs/intl-displaynames/locale-data/fr'), + ])]; + case 26: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 27: + i18n.loadAndActivate({ locale: locale, messages: messagesFy }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/fy'), + import('@formatjs/intl-pluralrules/locale-data/fy'), + import('@formatjs/intl-numberformat/locale-data/fy'), + import('@formatjs/intl-displaynames/locale-data/fy'), + ])]; + case 28: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 29: + i18n.loadAndActivate({ locale: locale, messages: messagesGa }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ga'), + import('@formatjs/intl-numberformat/locale-data/ga'), + import('@formatjs/intl-displaynames/locale-data/ga'), + ])]; + case 30: + _b.sent(); + return [2 /*return*/, undefined]; + case 31: + i18n.loadAndActivate({ locale: locale, messages: messagesGd }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/gd'), + import('@formatjs/intl-pluralrules/locale-data/gd'), + import('@formatjs/intl-numberformat/locale-data/gd'), + import('@formatjs/intl-displaynames/locale-data/gd'), + ])]; + case 32: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 33: + i18n.loadAndActivate({ locale: locale, messages: messagesGl }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/gl'), + import('@formatjs/intl-pluralrules/locale-data/gl'), + import('@formatjs/intl-numberformat/locale-data/gl'), + import('@formatjs/intl-displaynames/locale-data/gl'), + ])]; + case 34: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 35: + i18n.loadAndActivate({ locale: locale, messages: messagesHi }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/hi'), + import('@formatjs/intl-pluralrules/locale-data/hi'), + import('@formatjs/intl-numberformat/locale-data/hi'), + import('@formatjs/intl-displaynames/locale-data/hi'), + ])]; + case 36: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 37: + i18n.loadAndActivate({ locale: locale, messages: messagesHu }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/hu'), + import('@formatjs/intl-pluralrules/locale-data/hu'), + import('@formatjs/intl-numberformat/locale-data/hu'), + import('@formatjs/intl-displaynames/locale-data/hu'), + ])]; + case 38: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 39: + i18n.loadAndActivate({ locale: locale, messages: messagesIa }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ia'), + import('@formatjs/intl-numberformat/locale-data/ia'), + import('@formatjs/intl-displaynames/locale-data/ia'), + ])]; + case 40: + _b.sent(); + return [2 /*return*/, undefined]; + case 41: + i18n.loadAndActivate({ locale: locale, messages: messagesId }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/id'), + import('@formatjs/intl-pluralrules/locale-data/id'), + import('@formatjs/intl-numberformat/locale-data/id'), + import('@formatjs/intl-displaynames/locale-data/id'), + ])]; + case 42: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 43: + i18n.loadAndActivate({ locale: locale, messages: messagesIt }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/it'), + import('@formatjs/intl-pluralrules/locale-data/it'), + import('@formatjs/intl-numberformat/locale-data/it'), + import('@formatjs/intl-displaynames/locale-data/it'), + ])]; + case 44: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 45: + i18n.loadAndActivate({ locale: locale, messages: messagesJa }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/ja'), + import('@formatjs/intl-pluralrules/locale-data/ja'), + import('@formatjs/intl-numberformat/locale-data/ja'), + import('@formatjs/intl-displaynames/locale-data/ja'), + ])]; + case 46: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 47: + i18n.loadAndActivate({ locale: locale, messages: messagesKm }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/km'), + import('@formatjs/intl-pluralrules/locale-data/km'), + import('@formatjs/intl-numberformat/locale-data/km'), + import('@formatjs/intl-displaynames/locale-data/km'), + ])]; + case 48: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 49: + i18n.loadAndActivate({ locale: locale, messages: messagesKo }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/ko'), + import('@formatjs/intl-pluralrules/locale-data/ko'), + import('@formatjs/intl-numberformat/locale-data/ko'), + import('@formatjs/intl-displaynames/locale-data/ko'), + ])]; + case 50: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 51: + i18n.loadAndActivate({ locale: locale, messages: messagesNe }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ne'), + import('@formatjs/intl-numberformat/locale-data/ne'), + import('@formatjs/intl-displaynames/locale-data/ne'), + ])]; + case 52: + _b.sent(); + return [2 /*return*/, undefined]; + case 53: + i18n.loadAndActivate({ locale: locale, messages: messagesNl }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/nl'), + import('@formatjs/intl-pluralrules/locale-data/nl'), + import('@formatjs/intl-numberformat/locale-data/nl'), + import('@formatjs/intl-displaynames/locale-data/nl'), + ])]; + case 54: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 55: + i18n.loadAndActivate({ locale: locale, messages: messagesPl }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/pl'), + import('@formatjs/intl-pluralrules/locale-data/pl'), + import('@formatjs/intl-numberformat/locale-data/pl'), + import('@formatjs/intl-displaynames/locale-data/pl'), + ])]; + case 56: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 57: + i18n.loadAndActivate({ locale: locale, messages: messagesPt_BR }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/pt-BR'), + import('@formatjs/intl-pluralrules/locale-data/pt'), + import('@formatjs/intl-numberformat/locale-data/pt'), + import('@formatjs/intl-displaynames/locale-data/pt'), + ])]; + case 58: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 59: + i18n.loadAndActivate({ locale: locale, messages: messagesPt_PT }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/pt'), + import('@formatjs/intl-pluralrules/locale-data/pt-PT'), + import('@formatjs/intl-numberformat/locale-data/pt-PT'), + import('@formatjs/intl-displaynames/locale-data/pt-PT'), + ])]; + case 60: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 61: + i18n.loadAndActivate({ locale: locale, messages: messagesRo }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/ro'), + import('@formatjs/intl-pluralrules/locale-data/ro'), + import('@formatjs/intl-numberformat/locale-data/ro'), + import('@formatjs/intl-displaynames/locale-data/ro'), + ])]; + case 62: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 63: + i18n.loadAndActivate({ locale: locale, messages: messagesRu }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/ru'), + import('@formatjs/intl-pluralrules/locale-data/ru'), + import('@formatjs/intl-numberformat/locale-data/ru'), + import('@formatjs/intl-displaynames/locale-data/ru'), + ])]; + case 64: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 65: + i18n.loadAndActivate({ locale: locale, messages: messagesSv }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/sv'), + import('@formatjs/intl-pluralrules/locale-data/sv'), + import('@formatjs/intl-numberformat/locale-data/sv'), + import('@formatjs/intl-displaynames/locale-data/sv'), + ])]; + case 66: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 67: + i18n.loadAndActivate({ locale: locale, messages: messagesTh }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/th'), + import('@formatjs/intl-pluralrules/locale-data/th'), + import('@formatjs/intl-numberformat/locale-data/th'), + import('@formatjs/intl-displaynames/locale-data/th'), + ])]; + case 68: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 69: + i18n.loadAndActivate({ locale: locale, messages: messagesTr }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/tr'), + import('@formatjs/intl-pluralrules/locale-data/tr'), + import('@formatjs/intl-numberformat/locale-data/tr'), + import('@formatjs/intl-displaynames/locale-data/tr'), + ])]; + case 70: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 71: + i18n.loadAndActivate({ locale: locale, messages: messagesUk }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/uk'), + import('@formatjs/intl-pluralrules/locale-data/uk'), + import('@formatjs/intl-numberformat/locale-data/uk'), + import('@formatjs/intl-displaynames/locale-data/uk'), + ])]; + case 72: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 73: + i18n.loadAndActivate({ locale: locale, messages: messagesVi }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/vi'), + import('@formatjs/intl-pluralrules/locale-data/vi'), + import('@formatjs/intl-numberformat/locale-data/vi'), + import('@formatjs/intl-displaynames/locale-data/vi'), + ])]; + case 74: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 75: + i18n.loadAndActivate({ locale: locale, messages: messagesZh_CN }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/zh-CN'), + import('@formatjs/intl-pluralrules/locale-data/zh'), + import('@formatjs/intl-numberformat/locale-data/zh'), + import('@formatjs/intl-displaynames/locale-data/zh'), + ])]; + case 76: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 77: + i18n.loadAndActivate({ locale: locale, messages: messagesZh_HK }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/zh-HK'), + import('@formatjs/intl-pluralrules/locale-data/zh'), + import('@formatjs/intl-numberformat/locale-data/zh'), + import('@formatjs/intl-displaynames/locale-data/zh'), + ])]; + case 78: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 79: + i18n.loadAndActivate({ locale: locale, messages: messagesZh_TW }); + return [4 /*yield*/, Promise.all([ + import('date-fns/locale/zh-TW'), + import('@formatjs/intl-pluralrules/locale-data/zh'), + import('@formatjs/intl-numberformat/locale-data/zh'), + import('@formatjs/intl-displaynames/locale-data/zh'), + ])]; + case 80: + dateLocale = (_b.sent())[0].default; + return [2 /*return*/, dateLocale]; + case 81: + { + i18n.loadAndActivate({ locale: locale, messages: messagesEn }); + return [2 /*return*/, defaultLocale]; + } + _b.label = 82; + case 82: return [2 /*return*/]; + } + }); + }); +} +export function useLocaleLanguage() { + var appLanguage = useLanguagePrefs().appLanguage; + var _a = useState(defaultLocale), dateLocale = _a[0], setDateLocale = _a[1]; + useEffect(function () { + dynamicActivate(sanitizeAppLanguageSetting(appLanguage)).then(function (locale) { + setDateLocale(locale !== null && locale !== void 0 ? locale : defaultLocale); + }); + }, [appLanguage]); + return dateLocale; +} diff --git a/src/locale/i18n.web.js b/src/locale/i18n.web.js new file mode 100644 index 0000000000..946e17585e --- /dev/null +++ b/src/locale/i18n.web.js @@ -0,0 +1,466 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useEffect, useState } from 'react'; +import { i18n } from '@lingui/core'; +import defaultLocale from 'date-fns/locale/en-US'; +import { sanitizeAppLanguageSetting } from '#/locale/helpers'; +import { AppLanguage } from '#/locale/languages'; +import { useLanguagePrefs } from '#/state/preferences'; +/** + * We do a dynamic import of just the catalog that we need + */ +export function dynamicActivate(locale) { + return __awaiter(this, void 0, void 0, function () { + var mod, dateLocale, _a; + var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13; + return __generator(this, function (_14) { + switch (_14.label) { + case 0: + dateLocale = defaultLocale; + _a = locale; + switch (_a) { + case AppLanguage.an: return [3 /*break*/, 1]; + case AppLanguage.ast: return [3 /*break*/, 3]; + case AppLanguage.ca: return [3 /*break*/, 5]; + case AppLanguage.cy: return [3 /*break*/, 7]; + case AppLanguage.da: return [3 /*break*/, 9]; + case AppLanguage.de: return [3 /*break*/, 11]; + case AppLanguage.el: return [3 /*break*/, 13]; + case AppLanguage.en_GB: return [3 /*break*/, 15]; + case AppLanguage.eo: return [3 /*break*/, 17]; + case AppLanguage.es: return [3 /*break*/, 19]; + case AppLanguage.eu: return [3 /*break*/, 21]; + case AppLanguage.fi: return [3 /*break*/, 23]; + case AppLanguage.fr: return [3 /*break*/, 25]; + case AppLanguage.fy: return [3 /*break*/, 27]; + case AppLanguage.ga: return [3 /*break*/, 29]; + case AppLanguage.gd: return [3 /*break*/, 31]; + case AppLanguage.gl: return [3 /*break*/, 33]; + case AppLanguage.hi: return [3 /*break*/, 35]; + case AppLanguage.hu: return [3 /*break*/, 37]; + case AppLanguage.ia: return [3 /*break*/, 39]; + case AppLanguage.id: return [3 /*break*/, 41]; + case AppLanguage.it: return [3 /*break*/, 43]; + case AppLanguage.ja: return [3 /*break*/, 45]; + case AppLanguage.km: return [3 /*break*/, 47]; + case AppLanguage.ko: return [3 /*break*/, 49]; + case AppLanguage.ne: return [3 /*break*/, 51]; + case AppLanguage.nl: return [3 /*break*/, 53]; + case AppLanguage.pl: return [3 /*break*/, 55]; + case AppLanguage.pt_BR: return [3 /*break*/, 57]; + case AppLanguage.pt_PT: return [3 /*break*/, 59]; + case AppLanguage.ro: return [3 /*break*/, 61]; + case AppLanguage.ru: return [3 /*break*/, 63]; + case AppLanguage.sv: return [3 /*break*/, 65]; + case AppLanguage.th: return [3 /*break*/, 67]; + case AppLanguage.tr: return [3 /*break*/, 69]; + case AppLanguage.uk: return [3 /*break*/, 71]; + case AppLanguage.vi: return [3 /*break*/, 73]; + case AppLanguage.zh_CN: return [3 /*break*/, 75]; + case AppLanguage.zh_HK: return [3 /*break*/, 77]; + case AppLanguage.zh_TW: return [3 /*break*/, 79]; + } + return [3 /*break*/, 81]; + case 1: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/an/messages"), + import('date-fns/locale/es'), + ])]; + case 2: + _b = _14.sent(), mod = _b[0], dateLocale = _b[1].default; + return [3 /*break*/, 83]; + case 3: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/ast/messages"), + import('date-fns/locale/es'), + ])]; + case 4: + _c = _14.sent(), mod = _c[0], dateLocale = _c[1].default; + return [3 /*break*/, 83]; + case 5: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/ca/messages"), + import('date-fns/locale/ca'), + ])]; + case 6: + _d = _14.sent(), mod = _d[0], dateLocale = _d[1].default; + return [3 /*break*/, 83]; + case 7: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/cy/messages"), + import('date-fns/locale/cy'), + ])]; + case 8: + _e = _14.sent(), mod = _e[0], dateLocale = _e[1].default; + return [3 /*break*/, 83]; + case 9: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/da/messages"), + import('date-fns/locale/da'), + ])]; + case 10: + _f = _14.sent(), mod = _f[0], dateLocale = _f[1].default; + return [3 /*break*/, 83]; + case 11: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/de/messages"), + import('date-fns/locale/de'), + ])]; + case 12: + _g = _14.sent(), mod = _g[0], dateLocale = _g[1].default; + return [3 /*break*/, 83]; + case 13: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/el/messages"), + import('date-fns/locale/el'), + ])]; + case 14: + _h = _14.sent(), mod = _h[0], dateLocale = _h[1].default; + return [3 /*break*/, 83]; + case 15: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/en-GB/messages"), + import('date-fns/locale/en-GB'), + ])]; + case 16: + _j = _14.sent(), mod = _j[0], dateLocale = _j[1].default; + return [3 /*break*/, 83]; + case 17: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/eo/messages"), + import('date-fns/locale/eo'), + ])]; + case 18: + _k = _14.sent(), mod = _k[0], dateLocale = _k[1].default; + return [3 /*break*/, 83]; + case 19: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/es/messages"), + import('date-fns/locale/es'), + ])]; + case 20: + _l = _14.sent(), mod = _l[0], dateLocale = _l[1].default; + return [3 /*break*/, 83]; + case 21: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/eu/messages"), + import('date-fns/locale/eu'), + ])]; + case 22: + _m = _14.sent(), mod = _m[0], dateLocale = _m[1].default; + return [3 /*break*/, 83]; + case 23: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/fi/messages"), + import('date-fns/locale/fi'), + ])]; + case 24: + _o = _14.sent(), mod = _o[0], dateLocale = _o[1].default; + return [3 /*break*/, 83]; + case 25: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/fr/messages"), + import('date-fns/locale/fr'), + ])]; + case 26: + _p = _14.sent(), mod = _p[0], dateLocale = _p[1].default; + return [3 /*break*/, 83]; + case 27: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/fy/messages"), + import('date-fns/locale/fy'), + ])]; + case 28: + _q = _14.sent(), mod = _q[0], dateLocale = _q[1].default; + return [3 /*break*/, 83]; + case 29: return [4 /*yield*/, import("./locales/ga/messages")]; + case 30: + mod = _14.sent(); + return [3 /*break*/, 83]; + case 31: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/gd/messages"), + import('date-fns/locale/gd'), + ])]; + case 32: + _r = _14.sent(), mod = _r[0], dateLocale = _r[1].default; + return [3 /*break*/, 83]; + case 33: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/gl/messages"), + import('date-fns/locale/gl'), + ])]; + case 34: + _s = _14.sent(), mod = _s[0], dateLocale = _s[1].default; + return [3 /*break*/, 83]; + case 35: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/hi/messages"), + import('date-fns/locale/hi'), + ])]; + case 36: + _t = _14.sent(), mod = _t[0], dateLocale = _t[1].default; + return [3 /*break*/, 83]; + case 37: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/hu/messages"), + import('date-fns/locale/hu'), + ])]; + case 38: + _u = _14.sent(), mod = _u[0], dateLocale = _u[1].default; + return [3 /*break*/, 83]; + case 39: return [4 /*yield*/, import("./locales/ia/messages")]; + case 40: + mod = _14.sent(); + return [3 /*break*/, 83]; + case 41: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/id/messages"), + import('date-fns/locale/id'), + ])]; + case 42: + _v = _14.sent(), mod = _v[0], dateLocale = _v[1].default; + return [3 /*break*/, 83]; + case 43: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/it/messages"), + import('date-fns/locale/it'), + ])]; + case 44: + _w = _14.sent(), mod = _w[0], dateLocale = _w[1].default; + return [3 /*break*/, 83]; + case 45: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/ja/messages"), + import('date-fns/locale/ja'), + ])]; + case 46: + _x = _14.sent(), mod = _x[0], dateLocale = _x[1].default; + return [3 /*break*/, 83]; + case 47: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/km/messages"), + import('date-fns/locale/km'), + ])]; + case 48: + _y = _14.sent(), mod = _y[0], dateLocale = _y[1].default; + return [3 /*break*/, 83]; + case 49: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/ko/messages"), + import('date-fns/locale/ko'), + ])]; + case 50: + _z = _14.sent(), mod = _z[0], dateLocale = _z[1].default; + return [3 /*break*/, 83]; + case 51: return [4 /*yield*/, import("./locales/ne/messages")]; + case 52: + mod = _14.sent(); + return [3 /*break*/, 83]; + case 53: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/nl/messages"), + import('date-fns/locale/nl'), + ])]; + case 54: + _0 = _14.sent(), mod = _0[0], dateLocale = _0[1].default; + return [3 /*break*/, 83]; + case 55: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/pl/messages"), + import('date-fns/locale/pl'), + ])]; + case 56: + _1 = _14.sent(), mod = _1[0], dateLocale = _1[1].default; + return [3 /*break*/, 83]; + case 57: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/pt-BR/messages"), + import('date-fns/locale/pt-BR'), + ])]; + case 58: + _2 = _14.sent(), mod = _2[0], dateLocale = _2[1].default; + return [3 /*break*/, 83]; + case 59: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/pt-PT/messages"), + import('date-fns/locale/pt'), + ])]; + case 60: + _3 = _14.sent(), mod = _3[0], dateLocale = _3[1].default; + return [3 /*break*/, 83]; + case 61: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/ro/messages"), + import('date-fns/locale/ro'), + ])]; + case 62: + _4 = _14.sent(), mod = _4[0], dateLocale = _4[1].default; + return [3 /*break*/, 83]; + case 63: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/ru/messages"), + import('date-fns/locale/ru'), + ])]; + case 64: + _5 = _14.sent(), mod = _5[0], dateLocale = _5[1].default; + return [3 /*break*/, 83]; + case 65: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/sv/messages"), + import('date-fns/locale/sv'), + ])]; + case 66: + _6 = _14.sent(), mod = _6[0], dateLocale = _6[1].default; + return [3 /*break*/, 83]; + case 67: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/th/messages"), + import('date-fns/locale/th'), + ])]; + case 68: + _7 = _14.sent(), mod = _7[0], dateLocale = _7[1].default; + return [3 /*break*/, 83]; + case 69: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/tr/messages"), + import('date-fns/locale/tr'), + ])]; + case 70: + _8 = _14.sent(), mod = _8[0], dateLocale = _8[1].default; + return [3 /*break*/, 83]; + case 71: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/uk/messages"), + import('date-fns/locale/uk'), + ])]; + case 72: + _9 = _14.sent(), mod = _9[0], dateLocale = _9[1].default; + return [3 /*break*/, 83]; + case 73: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/vi/messages"), + import('date-fns/locale/vi'), + ])]; + case 74: + _10 = _14.sent(), mod = _10[0], dateLocale = _10[1].default; + return [3 /*break*/, 83]; + case 75: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/zh-CN/messages"), + import('date-fns/locale/zh-CN'), + ])]; + case 76: + _11 = _14.sent(), mod = _11[0], dateLocale = _11[1].default; + return [3 /*break*/, 83]; + case 77: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/zh-HK/messages"), + import('date-fns/locale/zh-HK'), + ])]; + case 78: + _12 = _14.sent(), mod = _12[0], dateLocale = _12[1].default; + return [3 /*break*/, 83]; + case 79: + ; + return [4 /*yield*/, Promise.all([ + import("./locales/zh-TW/messages"), + import('date-fns/locale/zh-TW'), + ])]; + case 80: + _13 = _14.sent(), mod = _13[0], dateLocale = _13[1].default; + return [3 /*break*/, 83]; + case 81: return [4 /*yield*/, import("./locales/en/messages")]; + case 82: + mod = _14.sent(); + return [3 /*break*/, 83]; + case 83: + i18n.load(locale, mod.messages); + i18n.activate(locale); + return [2 /*return*/, dateLocale]; + } + }); + }); +} +export function useLocaleLanguage() { + var appLanguage = useLanguagePrefs().appLanguage; + var _a = useState(defaultLocale), dateLocale = _a[0], setDateLocale = _a[1]; + useEffect(function () { + var sanitizedLanguage = sanitizeAppLanguageSetting(appLanguage); + document.documentElement.lang = sanitizedLanguage; + dynamicActivate(sanitizedLanguage).then(function (locale) { + setDateLocale(locale); + }); + }, [appLanguage]); + return dateLocale; +} diff --git a/src/locale/i18nProvider.js b/src/locale/i18nProvider.js new file mode 100644 index 0000000000..1ce2f38319 --- /dev/null +++ b/src/locale/i18nProvider.js @@ -0,0 +1,22 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext } from 'react'; +import { i18n } from '@lingui/core'; +import { I18nProvider as DefaultI18nProvider } from '@lingui/react'; +import { useLocaleLanguage } from './i18n'; +var DateLocaleContext = createContext(undefined); +DateLocaleContext.displayName = 'DateLocaleContext'; +export default function I18nProvider(_a) { + var children = _a.children; + var dateLocale = useLocaleLanguage(); + return (_jsx(DateLocaleContext, { value: dateLocale, children: _jsx(DefaultI18nProvider, { i18n: i18n, children: children }) })); +} +/** + * Returns a `date-fns` locale corresponding to the current app language + */ +export function useDateLocale() { + var ctx = useContext(DateLocaleContext); + if (!ctx) { + throw new Error('useDateLocale must be used within an I18nProvider'); + } + return ctx; +} diff --git a/src/locale/languages.js b/src/locale/languages.js new file mode 100644 index 0000000000..426a0b2f0e --- /dev/null +++ b/src/locale/languages.js @@ -0,0 +1,4141 @@ +export var AppLanguage; +(function (AppLanguage) { + AppLanguage["en"] = "en"; + AppLanguage["an"] = "an"; + AppLanguage["ast"] = "ast"; + AppLanguage["ca"] = "ca"; + AppLanguage["cy"] = "cy"; + AppLanguage["da"] = "da"; + AppLanguage["de"] = "de"; + AppLanguage["el"] = "el"; + AppLanguage["en_GB"] = "en-GB"; + AppLanguage["eo"] = "eo"; + AppLanguage["es"] = "es"; + AppLanguage["eu"] = "eu"; + AppLanguage["fi"] = "fi"; + AppLanguage["fr"] = "fr"; + AppLanguage["fy"] = "fy"; + AppLanguage["ga"] = "ga"; + AppLanguage["gd"] = "gd"; + AppLanguage["gl"] = "gl"; + AppLanguage["hi"] = "hi"; + AppLanguage["hu"] = "hu"; + AppLanguage["ia"] = "ia"; + AppLanguage["id"] = "id"; + AppLanguage["it"] = "it"; + AppLanguage["ja"] = "ja"; + AppLanguage["km"] = "km"; + AppLanguage["ko"] = "ko"; + AppLanguage["ne"] = "ne"; + AppLanguage["nl"] = "nl"; + AppLanguage["pl"] = "pl"; + AppLanguage["pt_BR"] = "pt-BR"; + AppLanguage["pt_PT"] = "pt-PT"; + AppLanguage["ro"] = "ro"; + AppLanguage["ru"] = "ru"; + AppLanguage["sv"] = "sv"; + AppLanguage["th"] = "th"; + AppLanguage["tr"] = "tr"; + AppLanguage["uk"] = "uk"; + AppLanguage["vi"] = "vi"; + AppLanguage["zh_CN"] = "zh-Hans-CN"; + AppLanguage["zh_HK"] = "zh-Hant-HK"; + AppLanguage["zh_TW"] = "zh-Hant-TW"; +})(AppLanguage || (AppLanguage = {})); +export var APP_LANGUAGES = [ + { code2: AppLanguage.en, name: 'English' }, + { code2: AppLanguage.an, name: 'aragonés – Aragonese' }, + { code2: AppLanguage.ast, name: 'asturianu – Asturian' }, + { code2: AppLanguage.ca, name: 'català – Catalan' }, + { code2: AppLanguage.cy, name: 'Cymraeg – Welsh' }, + { code2: AppLanguage.da, name: 'dansk – Danish' }, + { code2: AppLanguage.de, name: 'Deutsch – German' }, + { code2: AppLanguage.el, name: 'Ελληνικά – Greek' }, + { code2: AppLanguage.en_GB, name: 'British English' }, + { code2: AppLanguage.eo, name: 'Esperanto' }, + { code2: AppLanguage.es, name: 'español – Spanish' }, + { code2: AppLanguage.eu, name: 'euskara – Basque' }, + { code2: AppLanguage.fi, name: 'suomi – Finnish' }, + { code2: AppLanguage.fr, name: 'français – French' }, + { code2: AppLanguage.fy, name: 'Frysk – Western Frisian' }, + { code2: AppLanguage.ga, name: 'Gaeilge – Irish' }, + { code2: AppLanguage.gd, name: 'Gàidhlig – Scottish Gaelic' }, + { code2: AppLanguage.gl, name: 'galego – Galician' }, + { code2: AppLanguage.hi, name: 'हिंदी – Hindi' }, + { code2: AppLanguage.hu, name: 'magyar – Hungarian' }, + { code2: AppLanguage.ia, name: 'Interlingua' }, + { code2: AppLanguage.id, name: 'Bahasa Indonesia – Indonesian' }, + { code2: AppLanguage.it, name: 'italiano – Italian' }, + { code2: AppLanguage.ja, name: '日本語 – Japanese' }, + { code2: AppLanguage.km, name: 'ភាសាខ្មែរ – Khmer' }, + { code2: AppLanguage.ko, name: '한국어 – Korean' }, + { code2: AppLanguage.ne, name: 'नेपाली – Nepali' }, + { code2: AppLanguage.nl, name: 'Nederlands – Dutch' }, + { code2: AppLanguage.pl, name: 'polski – Polish' }, + { + code2: AppLanguage.pt_BR, + name: 'português do Brasil – Brazilian Portuguese', + }, + { code2: AppLanguage.pt_PT, name: 'português europeu – European Portuguese' }, + { code2: AppLanguage.ro, name: 'română – Romanian' }, + { code2: AppLanguage.ru, name: 'русский – Russian' }, + { code2: AppLanguage.sv, name: 'svenska – Swedish' }, + { code2: AppLanguage.th, name: 'ภาษาไทย – Thai' }, + { code2: AppLanguage.tr, name: 'Türkçe – Turkish' }, + { code2: AppLanguage.uk, name: 'українська – Ukrainian' }, + { code2: AppLanguage.vi, name: 'Tiếng Việt – Vietnamese' }, + { code2: AppLanguage.zh_CN, name: '简体中文 – Simplified Chinese' }, + { code2: AppLanguage.zh_TW, name: '繁體中文 – Traditional Chinese' }, + { code2: AppLanguage.zh_HK, name: '粵文 – Cantonese' }, +]; +// Pre-generated list using Intl.DisplayNames to localize the language name. +// https://developers.google.com/android/reference/com/google/mlkit/nl/translate/TranslateLanguage +// https://developer.apple.com/documentation/foundation/nslocale/isolanguagecodes +export var LANGUAGES = [ + { + code3: 'aar', + code2: 'aa', + name: 'Afar', + en: 'Afar', + android: false, + ios: false, + }, + { + code3: 'abk', + code2: 'ab', + name: 'Abkhazian', + en: 'Abkhazian', + android: false, + ios: false, + }, + { + code3: 'ace', + code2: '', + name: 'Achinese', + en: 'Achinese', + android: false, + ios: false, + }, + { + code3: 'ach', + code2: '', + name: 'Acoli', + en: 'Acoli', + android: false, + ios: false, + }, + { + code3: 'ada', + code2: '', + name: 'Adangme', + en: 'Adangme', + android: false, + ios: false, + }, + { + code3: 'ady', + code2: '', + name: 'Adyghe; Adygei', + en: 'Adyghe; Adygei', + android: false, + ios: false, + }, + { + code3: 'afa', + code2: '', + name: 'Afro-Asiatic languages', + en: 'Afro-Asiatic languages', + android: false, + ios: false, + }, + { + code3: 'afh', + code2: '', + name: 'Afrihili', + en: 'Afrihili', + android: false, + ios: false, + }, + { + code3: 'afr', + code2: 'af', + name: 'Afrikaans', + en: 'Afrikaans', + android: false, + ios: false, + }, + { + code3: 'ain', + code2: '', + name: 'Ainu', + en: 'Ainu', + android: false, + ios: false, + }, + { + code3: 'aka', + code2: 'ak', + name: 'Akan', + en: 'Akan', + android: false, + ios: false, + }, + { + code3: 'akk', + code2: '', + name: 'Akkadian', + en: 'Akkadian', + android: false, + ios: false, + }, + { + code3: 'alb', + code2: 'sq', + name: 'Albanian', + en: 'Albanian', + android: true, + ios: false, + }, + { + code3: 'ale', + code2: '', + name: 'Aleut', + en: 'Aleut', + android: false, + ios: false, + }, + { + code3: 'alg', + code2: '', + name: 'Algonquian languages', + en: 'Algonquian languages', + android: false, + ios: false, + }, + { + code3: 'alt', + code2: '', + name: 'Southern Altai', + en: 'Southern Altai', + android: false, + ios: false, + }, + { + code3: 'amh', + code2: 'am', + name: 'አማርኛ', + en: 'Amharic', + android: false, + ios: false, + }, + { + code3: 'ang', + code2: '', + name: 'English, Old (ca.450-1100)', + en: 'English, Old (ca.450-1100)', + android: false, + ios: false, + }, + { + code3: 'anp', + code2: '', + name: 'Angika', + en: 'Angika', + android: false, + ios: false, + }, + { + code3: 'apa', + code2: '', + name: 'Apache languages', + en: 'Apache languages', + android: false, + ios: false, + }, + { + code3: 'ara', + code2: 'ar', + name: 'العربية', + en: 'Arabic', + android: true, + ios: false, + }, + { + code3: 'arc', + code2: '', + name: 'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', + en: 'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', + android: false, + ios: false, + }, + { + code3: 'arg', + code2: 'an', + name: 'Aragonese', + en: 'Aragonese', + android: false, + ios: false, + }, + { + code3: 'arm', + code2: 'hy', + name: 'Armenian', + en: 'Armenian', + android: false, + ios: false, + }, + { + code3: 'arn', + code2: '', + name: 'Mapudungun; Mapuche', + en: 'Mapudungun; Mapuche', + android: false, + ios: false, + }, + { + code3: 'arp', + code2: '', + name: 'Arapaho', + en: 'Arapaho', + android: false, + ios: false, + }, + { + code3: 'art', + code2: '', + name: 'Artificial languages', + en: 'Artificial languages', + android: false, + ios: false, + }, + { + code3: 'arw', + code2: '', + name: 'Arawak', + en: 'Arawak', + android: false, + ios: false, + }, + { + code3: 'asm', + code2: 'as', + name: 'Assamese', + en: 'Assamese', + android: false, + ios: false, + }, + { + code3: 'ast', + code2: '', + name: 'Asturian', + en: 'Asturian', + android: false, + ios: false, + }, + { + code3: 'ath', + code2: '', + name: 'Athapascan languages', + en: 'Athapascan languages', + android: false, + ios: false, + }, + { + code3: 'aus', + code2: '', + name: 'Australian languages', + en: 'Australian languages', + android: false, + ios: false, + }, + { + code3: 'ava', + code2: 'av', + name: 'Avaric', + en: 'Avaric', + android: false, + ios: false, + }, + { + code3: 'ave', + code2: 'ae', + name: 'Avestan', + en: 'Avestan', + android: false, + ios: false, + }, + { + code3: 'awa', + code2: '', + name: 'Awadhi', + en: 'Awadhi', + android: false, + ios: false, + }, + { + code3: 'aym', + code2: 'ay', + name: 'Aymara', + en: 'Aymara', + android: false, + ios: false, + }, + { + code3: 'aze', + code2: 'az', + name: 'azərbaycan', + en: 'Azerbaijani', + android: false, + ios: false, + }, + { + code3: 'bad', + code2: '', + name: 'Banda languages', + en: 'Banda languages', + android: false, + ios: false, + }, + { + code3: 'bai', + code2: '', + name: 'Bamileke languages', + en: 'Bamileke languages', + android: false, + ios: false, + }, + { + code3: 'bak', + code2: 'ba', + name: 'Bashkir', + en: 'Bashkir', + android: false, + ios: false, + }, + { + code3: 'bal', + code2: '', + name: 'Baluchi', + en: 'Baluchi', + android: false, + ios: false, + }, + { + code3: 'bam', + code2: 'bm', + name: 'Bambara', + en: 'Bambara', + android: false, + ios: false, + }, + { + code3: 'ban', + code2: '', + name: 'Balinese', + en: 'Balinese', + android: false, + ios: false, + }, + { + code3: 'baq', + code2: 'eu', + name: 'Basque', + en: 'Basque', + android: false, + ios: false, + }, + { + code3: 'bas', + code2: '', + name: 'Basa', + en: 'Basa', + android: false, + ios: false, + }, + { + code3: 'bat', + code2: '', + name: 'Baltic languages', + en: 'Baltic languages', + android: false, + ios: false, + }, + { + code3: 'bej', + code2: '', + name: 'Beja; Bedawiyet', + en: 'Beja; Bedawiyet', + android: false, + ios: false, + }, + { + code3: 'bel', + code2: 'be', + name: 'Belarusian', + en: 'Belarusian', + android: true, + ios: false, + }, + { + code3: 'bem', + code2: '', + name: 'Bemba', + en: 'Bemba', + android: false, + ios: false, + }, + { + code3: 'ben', + code2: 'bn', + name: 'বাংলা', + en: 'Bangla', + android: true, + ios: false, + }, + { + code3: 'ber', + code2: '', + name: 'Berber languages', + en: 'Berber languages', + android: false, + ios: false, + }, + { + code3: 'bho', + code2: '', + name: 'Bhojpuri', + en: 'Bhojpuri', + android: false, + ios: false, + }, + { + code3: 'bih', + code2: 'bh', + name: 'Bhojpuri', + en: 'Bhojpuri', + android: false, + ios: false, + }, + { + code3: 'bik', + code2: '', + name: 'Bikol', + en: 'Bikol', + android: false, + ios: false, + }, + { + code3: 'bin', + code2: '', + name: 'Bini; Edo', + en: 'Bini; Edo', + android: false, + ios: false, + }, + { + code3: 'bis', + code2: 'bi', + name: 'Bislama', + en: 'Bislama', + android: false, + ios: false, + }, + { + code3: 'bla', + code2: '', + name: 'Siksika', + en: 'Siksika', + android: false, + ios: false, + }, + { + code3: 'bnt', + code2: '', + name: 'Bantu languages', + en: 'Bantu languages', + android: false, + ios: false, + }, + { + code3: 'bod', + code2: 'bo', + name: 'Tibetan', + en: 'Tibetan', + android: false, + ios: false, + }, + { + code3: 'bos', + code2: 'bs', + name: 'bosanski', + en: 'Bosnian', + android: false, + ios: false, + }, + { + code3: 'bra', + code2: '', + name: 'Braj', + en: 'Braj', + android: false, + ios: false, + }, + { + code3: 'bre', + code2: 'br', + name: 'Breton', + en: 'Breton', + android: false, + ios: false, + }, + { + code3: 'btk', + code2: '', + name: 'Batak languages', + en: 'Batak languages', + android: false, + ios: false, + }, + { + code3: 'bua', + code2: '', + name: 'Buriat', + en: 'Buriat', + android: false, + ios: false, + }, + { + code3: 'bug', + code2: '', + name: 'Buginese', + en: 'Buginese', + android: false, + ios: false, + }, + { + code3: 'bul', + code2: 'bg', + name: 'български', + en: 'Bulgarian', + android: true, + ios: false, + }, + { + code3: 'bur', + code2: 'my', + name: 'Burmese', + en: 'Burmese', + android: false, + ios: false, + }, + { + code3: 'byn', + code2: '', + name: 'Blin; Bilin', + en: 'Blin; Bilin', + android: false, + ios: false, + }, + { + code3: 'cad', + code2: '', + name: 'Caddo', + en: 'Caddo', + android: false, + ios: false, + }, + { + code3: 'cai', + code2: '', + name: 'Central American Indian languages', + en: 'Central American Indian languages', + android: false, + ios: false, + }, + { + code3: 'car', + code2: '', + name: 'Galibi Carib', + en: 'Galibi Carib', + android: false, + ios: false, + }, + { + code3: 'cat', + code2: 'ca', + name: 'català', + en: 'Catalan', + android: true, + ios: false, + }, + { + code3: 'cau', + code2: '', + name: 'Caucasian languages', + en: 'Caucasian languages', + android: false, + ios: false, + }, + { + code3: 'ceb', + code2: '', + name: 'Cebuano', + en: 'Cebuano', + android: false, + ios: false, + }, + { + code3: 'cel', + code2: '', + name: 'Celtic languages', + en: 'Celtic languages', + android: false, + ios: false, + }, + { + code3: 'ces', + code2: 'cs', + name: 'čeština', + en: 'Czech', + android: true, + ios: false, + }, + { + code3: 'cha', + code2: 'ch', + name: 'Chamorro', + en: 'Chamorro', + android: false, + ios: false, + }, + { + code3: 'chb', + code2: '', + name: 'Chibcha', + en: 'Chibcha', + android: false, + ios: false, + }, + { + code3: 'che', + code2: 'ce', + name: 'Chechen', + en: 'Chechen', + android: false, + ios: false, + }, + { + code3: 'chg', + code2: '', + name: 'Chagatai', + en: 'Chagatai', + android: false, + ios: false, + }, + { + code3: 'chi', + code2: 'zh', + name: '中文', + en: 'Chinese', + android: true, + ios: false, + }, + { + code3: 'chk', + code2: '', + name: 'Chuukese', + en: 'Chuukese', + android: false, + ios: false, + }, + { + code3: 'chm', + code2: '', + name: 'Mari', + en: 'Mari', + android: false, + ios: false, + }, + { + code3: 'chn', + code2: '', + name: 'Chinook jargon', + en: 'Chinook jargon', + android: false, + ios: false, + }, + { + code3: 'cho', + code2: '', + name: 'Choctaw', + en: 'Choctaw', + android: false, + ios: false, + }, + { + code3: 'chp', + code2: '', + name: 'Chipewyan; Dene Suline', + en: 'Chipewyan; Dene Suline', + android: false, + ios: false, + }, + { + code3: 'chr', + code2: '', + name: 'Cherokee', + en: 'Cherokee', + android: false, + ios: false, + }, + { + code3: 'chu', + code2: 'cu', + name: 'Church Slavic', + en: 'Church Slavic', + android: false, + ios: false, + }, + { + code3: 'chv', + code2: 'cv', + name: 'Chuvash', + en: 'Chuvash', + android: false, + ios: false, + }, + { + code3: 'chy', + code2: '', + name: 'Cheyenne', + en: 'Cheyenne', + android: false, + ios: false, + }, + { + code3: 'cmc', + code2: '', + name: 'Chamic languages', + en: 'Chamic languages', + android: false, + ios: false, + }, + { + code3: 'cnr', + code2: '', + name: 'srpski (Crna Gora)', + en: 'Serbian (Montenegro)', + android: false, + ios: false, + }, + { + code3: 'cop', + code2: '', + name: 'Coptic', + en: 'Coptic', + android: false, + ios: false, + }, + { + code3: 'cor', + code2: 'kw', + name: 'Cornish', + en: 'Cornish', + android: false, + ios: false, + }, + { + code3: 'cos', + code2: 'co', + name: 'Corsican', + en: 'Corsican', + android: false, + ios: false, + }, + { + code3: 'cpe', + code2: '', + name: 'Creoles and pidgins, English based', + en: 'Creoles and pidgins, English based', + android: false, + ios: false, + }, + { + code3: 'cpf', + code2: '', + name: 'Creoles and pidgins, French-based', + en: 'Creoles and pidgins, French-based', + android: false, + ios: false, + }, + { + code3: 'cpp', + code2: '', + name: 'Creoles and pidgins, Portuguese-based', + en: 'Creoles and pidgins, Portuguese-based', + android: false, + ios: false, + }, + { + code3: 'cre', + code2: 'cr', + name: 'Cree', + en: 'Cree', + android: false, + ios: false, + }, + { + code3: 'crh', + code2: '', + name: 'Crimean Tatar; Crimean Turkish', + en: 'Crimean Tatar; Crimean Turkish', + android: false, + ios: false, + }, + { + code3: 'crp', + code2: '', + name: 'Creoles and pidgins', + en: 'Creoles and pidgins', + android: false, + ios: false, + }, + { + code3: 'csb', + code2: '', + name: 'Kashubian', + en: 'Kashubian', + android: false, + ios: false, + }, + { + code3: 'cus', + code2: '', + name: 'Cushitic languages', + en: 'Cushitic languages', + android: false, + ios: false, + }, + { + code3: 'cym', + code2: 'cy', + name: 'Welsh', + en: 'Welsh', + android: true, + ios: false, + }, + { + code3: 'cze', + code2: 'cs', + name: 'čeština', + en: 'Czech', + android: true, + ios: false, + }, + { + code3: 'dak', + code2: '', + name: 'Dakota', + en: 'Dakota', + android: false, + ios: false, + }, + { + code3: 'dan', + code2: 'da', + name: 'dansk', + en: 'Danish', + android: true, + ios: false, + }, + { + code3: 'dar', + code2: '', + name: 'Dargwa', + en: 'Dargwa', + android: false, + ios: false, + }, + { + code3: 'day', + code2: '', + name: 'Land Dayak languages', + en: 'Land Dayak languages', + android: false, + ios: false, + }, + { + code3: 'del', + code2: '', + name: 'Delaware', + en: 'Delaware', + android: false, + ios: false, + }, + { + code3: 'den', + code2: '', + name: 'Slave (Athapascan)', + en: 'Slave (Athapascan)', + android: false, + ios: false, + }, + { + code3: 'deu', + code2: 'de', + name: 'Deutsch', + en: 'German', + android: true, + ios: true, + }, + { + code3: 'dgr', + code2: '', + name: 'Dogrib', + en: 'Dogrib', + android: false, + ios: false, + }, + { + code3: 'din', + code2: '', + name: 'Dinka', + en: 'Dinka', + android: false, + ios: false, + }, + { + code3: 'div', + code2: 'dv', + name: 'Divehi', + en: 'Divehi', + android: false, + ios: false, + }, + { + code3: 'doi', + code2: '', + name: 'Dogri', + en: 'Dogri', + android: false, + ios: false, + }, + { + code3: 'dra', + code2: '', + name: 'Dravidian languages', + en: 'Dravidian languages', + android: false, + ios: false, + }, + { + code3: 'dsb', + code2: '', + name: 'Lower Sorbian', + en: 'Lower Sorbian', + android: false, + ios: false, + }, + { + code3: 'dua', + code2: '', + name: 'Duala', + en: 'Duala', + android: false, + ios: false, + }, + { + code3: 'dum', + code2: '', + name: 'Dutch, Middle (ca.1050-1350)', + en: 'Dutch, Middle (ca.1050-1350)', + android: false, + ios: false, + }, + { + code3: 'dut', + code2: 'nl', + name: 'Nederlands', + en: 'Dutch', + android: true, + ios: true, + }, + { + code3: 'dyu', + code2: '', + name: 'Dyula', + en: 'Dyula', + android: false, + ios: false, + }, + { + code3: 'dzo', + code2: 'dz', + name: 'Dzongkha', + en: 'Dzongkha', + android: false, + ios: false, + }, + { + code3: 'efi', + code2: '', + name: 'Efik', + en: 'Efik', + android: false, + ios: false, + }, + { + code3: 'egy', + code2: '', + name: 'Egyptian (Ancient)', + en: 'Egyptian (Ancient)', + android: false, + ios: false, + }, + { + code3: 'eka', + code2: '', + name: 'Ekajuk', + en: 'Ekajuk', + android: false, + ios: false, + }, + { + code3: 'ell', + code2: 'el', + name: 'Ελληνικά', + en: 'Greek', + android: true, + ios: false, + }, + { + code3: 'elx', + code2: '', + name: 'Elamite', + en: 'Elamite', + android: false, + ios: false, + }, + { + code3: 'eng', + code2: 'en', + name: 'English', + en: 'English', + android: true, + ios: true, + }, + { + code3: 'enm', + code2: '', + name: 'English, Middle (1100-1500)', + en: 'English, Middle (1100-1500)', + android: false, + ios: false, + }, + { + code3: 'epo', + code2: 'eo', + name: 'Esperanto', + en: 'Esperanto', + android: true, + ios: false, + }, + { + code3: 'est', + code2: 'et', + name: 'eesti', + en: 'Estonian', + android: true, + ios: false, + }, + { + code3: 'eus', + code2: 'eu', + name: 'Basque', + en: 'Basque', + android: false, + ios: false, + }, + { + code3: 'ewe', + code2: 'ee', + name: 'Ewe', + en: 'Ewe', + android: false, + ios: false, + }, + { + code3: 'ewo', + code2: '', + name: 'Ewondo', + en: 'Ewondo', + android: false, + ios: false, + }, + { + code3: 'fan', + code2: '', + name: 'Fang', + en: 'Fang', + android: false, + ios: false, + }, + { + code3: 'fao', + code2: 'fo', + name: 'Faroese', + en: 'Faroese', + android: false, + ios: false, + }, + { + code3: 'fas', + code2: 'fa', + name: 'فارسی', + en: 'Persian', + android: true, + ios: false, + }, + { + code3: 'fat', + code2: '', + name: 'Akan', + en: 'Akan', + android: false, + ios: false, + }, + { + code3: 'fij', + code2: 'fj', + name: 'Fijian', + en: 'Fijian', + android: false, + ios: false, + }, + { + code3: 'fil', + code2: '', + name: 'Filipino', + en: 'Filipino', + android: false, + ios: false, + }, + { + code3: 'fin', + code2: 'fi', + name: 'suomi', + en: 'Finnish', + android: true, + ios: false, + }, + { + code3: 'fiu', + code2: '', + name: 'Finno-Ugrian languages', + en: 'Finno-Ugrian languages', + android: false, + ios: false, + }, + { + code3: 'fon', + code2: '', + name: 'Fon', + en: 'Fon', + android: false, + ios: false, + }, + { + code3: 'fra', + code2: 'fr', + name: 'français', + en: 'French', + android: true, + ios: true, + }, + { + code3: 'fre', + code2: 'fr', + name: 'français', + en: 'French', + android: true, + ios: true, + }, + { + code3: 'frm', + code2: '', + name: 'French, Middle (ca.1400-1600)', + en: 'French, Middle (ca.1400-1600)', + android: false, + ios: false, + }, + { + code3: 'fro', + code2: '', + name: 'French, Old (842-ca.1400)', + en: 'French, Old (842-ca.1400)', + android: false, + ios: false, + }, + { + code3: 'frr', + code2: '', + name: 'Northern Frisian', + en: 'Northern Frisian', + android: false, + ios: false, + }, + { + code3: 'frs', + code2: '', + name: 'Eastern Frisian', + en: 'Eastern Frisian', + android: false, + ios: false, + }, + { + code3: 'fry', + code2: 'fy', + name: 'Western Frisian', + en: 'Western Frisian', + android: false, + ios: false, + }, + { + code3: 'ful', + code2: 'ff', + name: 'Fulah', + en: 'Fulah', + android: false, + ios: false, + }, + { + code3: 'fur', + code2: '', + name: 'Friulian', + en: 'Friulian', + android: false, + ios: false, + }, + { + code3: 'gaa', + code2: '', + name: 'Ga', + en: 'Ga', + android: false, + ios: false, + }, + { + code3: 'gay', + code2: '', + name: 'Gayo', + en: 'Gayo', + android: false, + ios: false, + }, + { + code3: 'gba', + code2: '', + name: 'Gbaya', + en: 'Gbaya', + android: false, + ios: false, + }, + { + code3: 'gem', + code2: '', + name: 'Germanic languages', + en: 'Germanic languages', + android: false, + ios: false, + }, + { + code3: 'geo', + code2: 'ka', + name: 'Georgian', + en: 'Georgian', + android: true, + ios: false, + }, + { + code3: 'ger', + code2: 'de', + name: 'Deutsch', + en: 'German', + android: true, + ios: true, + }, + { + code3: 'gez', + code2: '', + name: 'Geez', + en: 'Geez', + android: false, + ios: false, + }, + { + code3: 'gil', + code2: '', + name: 'Gilbertese', + en: 'Gilbertese', + android: false, + ios: false, + }, + { + code3: 'gla', + code2: 'gd', + name: 'Scottish Gaelic', + en: 'Scottish Gaelic', + android: false, + ios: false, + }, + { + code3: 'gle', + code2: 'ga', + name: 'Irish', + en: 'Irish', + android: true, + ios: false, + }, + { + code3: 'glg', + code2: 'gl', + name: 'Galician', + en: 'Galician', + android: true, + ios: false, + }, + { + code3: 'glv', + code2: 'gv', + name: 'Manx', + en: 'Manx', + android: false, + ios: false, + }, + { + code3: 'gmh', + code2: '', + name: 'German, Middle High (ca.1050-1500)', + en: 'German, Middle High (ca.1050-1500)', + android: false, + ios: false, + }, + { + code3: 'goh', + code2: '', + name: 'German, Old High (ca.750-1050)', + en: 'German, Old High (ca.750-1050)', + android: false, + ios: false, + }, + { + code3: 'gon', + code2: '', + name: 'Gondi', + en: 'Gondi', + android: false, + ios: false, + }, + { + code3: 'gor', + code2: '', + name: 'Gorontalo', + en: 'Gorontalo', + android: false, + ios: false, + }, + { + code3: 'got', + code2: '', + name: 'Gothic', + en: 'Gothic', + android: false, + ios: false, + }, + { + code3: 'grb', + code2: '', + name: 'Grebo', + en: 'Grebo', + android: false, + ios: false, + }, + { + code3: 'grc', + code2: '', + name: 'Ancient Greek', + en: 'Ancient Greek', + android: false, + ios: false, + }, + { + code3: 'gre', + code2: 'el', + name: 'Ελληνικά', + en: 'Greek', + android: true, + ios: false, + }, + { + code3: 'grn', + code2: 'gn', + name: 'Guarani', + en: 'Guarani', + android: false, + ios: false, + }, + { + code3: 'gsw', + code2: '', + name: 'Swiss German; Alemannic; Alsatian', + en: 'Swiss German; Alemannic; Alsatian', + android: false, + ios: false, + }, + { + code3: 'guj', + code2: 'gu', + name: 'ગુજરાતી', + en: 'Gujarati', + android: true, + ios: false, + }, + { + code3: 'gwi', + code2: '', + name: "Gwich'in", + en: "Gwich'in", + android: false, + ios: false, + }, + { + code3: 'hai', + code2: '', + name: 'Haida', + en: 'Haida', + android: false, + ios: false, + }, + { + code3: 'hat', + code2: 'ht', + name: 'Haitian Creole', + en: 'Haitian Creole', + android: true, + ios: false, + }, + { + code3: 'hau', + code2: 'ha', + name: 'Hausa', + en: 'Hausa', + android: false, + ios: false, + }, + { + code3: 'haw', + code2: '', + name: 'Hawaiian', + en: 'Hawaiian', + android: false, + ios: false, + }, + { + code3: 'heb', + code2: 'he', + name: 'עברית', + en: 'Hebrew', + android: true, + ios: false, + }, + { + code3: 'her', + code2: 'hz', + name: 'Herero', + en: 'Herero', + android: false, + ios: false, + }, + { + code3: 'hil', + code2: '', + name: 'Hiligaynon', + en: 'Hiligaynon', + android: false, + ios: false, + }, + { + code3: 'him', + code2: '', + name: 'Himachali languages; Western Pahari languages', + en: 'Himachali languages; Western Pahari languages', + android: false, + ios: false, + }, + { + code3: 'hin', + code2: 'hi', + name: 'हिन्दी', + en: 'Hindi', + android: true, + ios: true, + }, + { + code3: 'hit', + code2: '', + name: 'Hittite', + en: 'Hittite', + android: false, + ios: false, + }, + { + code3: 'hmn', + code2: '', + name: 'Hmong', + en: 'Hmong', + android: false, + ios: false, + }, + { + code3: 'hmo', + code2: 'ho', + name: 'Hiri Motu', + en: 'Hiri Motu', + android: false, + ios: false, + }, + { + code3: 'hrv', + code2: 'hr', + name: 'hrvatski', + en: 'Croatian', + android: true, + ios: false, + }, + { + code3: 'hsb', + code2: '', + name: 'Upper Sorbian', + en: 'Upper Sorbian', + android: false, + ios: false, + }, + { + code3: 'hun', + code2: 'hu', + name: 'magyar', + en: 'Hungarian', + android: true, + ios: false, + }, + { + code3: 'hup', + code2: '', + name: 'Hupa', + en: 'Hupa', + android: false, + ios: false, + }, + { + code3: 'hye', + code2: 'hy', + name: 'Armenian', + en: 'Armenian', + android: false, + ios: false, + }, + { + code3: 'iba', + code2: '', + name: 'Iban', + en: 'Iban', + android: false, + ios: false, + }, + { + code3: 'ibo', + code2: 'ig', + name: 'Igbo', + en: 'Igbo', + android: false, + ios: false, + }, + { + code3: 'ice', + code2: 'is', + name: 'Icelandic', + en: 'Icelandic', + android: true, + ios: false, + }, + { + code3: 'ido', + code2: 'io', + name: 'Ido', + en: 'Ido', + android: false, + ios: false, + }, + { + code3: 'iii', + code2: 'ii', + name: 'Sichuan Yi; Nuosu', + en: 'Sichuan Yi; Nuosu', + android: false, + ios: false, + }, + { + code3: 'ijo', + code2: '', + name: 'Ijo languages', + en: 'Ijo languages', + android: false, + ios: false, + }, + { + code3: 'iku', + code2: 'iu', + name: 'Inuktitut', + en: 'Inuktitut', + android: false, + ios: false, + }, + { + code3: 'ile', + code2: 'ie', + name: 'Interlingue', + en: 'Interlingue', + android: false, + ios: false, + }, + { + code3: 'ilo', + code2: '', + name: 'Iloko', + en: 'Iloko', + android: false, + ios: false, + }, + { + code3: 'ina', + code2: 'ia', + name: 'Interlingua', + en: 'Interlingua', + android: false, + ios: false, + }, + { + code3: 'inc', + code2: '', + name: 'Indic languages', + en: 'Indic languages', + android: false, + ios: false, + }, + { + code3: 'ind', + code2: 'id', + name: 'Indonesia', + en: 'Indonesian', + android: true, + ios: false, + }, + { + code3: 'ine', + code2: '', + name: 'Indo-European languages', + en: 'Indo-European languages', + android: false, + ios: false, + }, + { + code3: 'inh', + code2: '', + name: 'Ingush', + en: 'Ingush', + android: false, + ios: false, + }, + { + code3: 'ipk', + code2: 'ik', + name: 'Inupiaq', + en: 'Inupiaq', + android: false, + ios: false, + }, + { + code3: 'ira', + code2: '', + name: 'Iranian languages', + en: 'Iranian languages', + android: false, + ios: false, + }, + { + code3: 'iro', + code2: '', + name: 'Iroquoian languages', + en: 'Iroquoian languages', + android: false, + ios: false, + }, + { + code3: 'isl', + code2: 'is', + name: 'Icelandic', + en: 'Icelandic', + android: true, + ios: false, + }, + { + code3: 'ita', + code2: 'it', + name: 'italiano', + en: 'Italian', + android: true, + ios: true, + }, + { + code3: 'jav', + code2: 'jv', + name: 'Javanese', + en: 'Javanese', + android: false, + ios: false, + }, + { + code3: 'jbo', + code2: '', + name: 'Lojban', + en: 'Lojban', + android: false, + ios: false, + }, + { + code3: 'jpn', + code2: 'ja', + name: '日本語', + en: 'Japanese', + android: true, + ios: true, + }, + { + code3: 'jpr', + code2: '', + name: 'Judeo-Persian', + en: 'Judeo-Persian', + android: false, + ios: false, + }, + { + code3: 'jrb', + code2: '', + name: 'Judeo-Arabic', + en: 'Judeo-Arabic', + android: false, + ios: false, + }, + { + code3: 'kaa', + code2: '', + name: 'Kara-Kalpak', + en: 'Kara-Kalpak', + android: false, + ios: false, + }, + { + code3: 'kab', + code2: '', + name: 'Kabyle', + en: 'Kabyle', + android: false, + ios: false, + }, + { + code3: 'kac', + code2: '', + name: 'Kachin; Jingpho', + en: 'Kachin; Jingpho', + android: false, + ios: false, + }, + { + code3: 'kal', + code2: 'kl', + name: 'Kalaallisut', + en: 'Kalaallisut', + android: false, + ios: false, + }, + { + code3: 'kam', + code2: '', + name: 'Kamba', + en: 'Kamba', + android: false, + ios: false, + }, + { + code3: 'kan', + code2: 'kn', + name: 'ಕನ್ನಡ', + en: 'Kannada', + android: true, + ios: false, + }, + { + code3: 'kar', + code2: '', + name: 'Karen languages', + en: 'Karen languages', + android: false, + ios: false, + }, + { + code3: 'kas', + code2: 'ks', + name: 'Kashmiri', + en: 'Kashmiri', + android: false, + ios: false, + }, + { + code3: 'kat', + code2: 'ka', + name: 'Georgian', + en: 'Georgian', + android: true, + ios: false, + }, + { + code3: 'kau', + code2: 'kr', + name: 'Kanuri', + en: 'Kanuri', + android: false, + ios: false, + }, + { + code3: 'kaw', + code2: '', + name: 'Kawi', + en: 'Kawi', + android: false, + ios: false, + }, + { + code3: 'kaz', + code2: 'kk', + name: 'Kazakh', + en: 'Kazakh', + android: false, + ios: false, + }, + { + code3: 'kbd', + code2: '', + name: 'Kabardian', + en: 'Kabardian', + android: false, + ios: false, + }, + { + code3: 'kha', + code2: '', + name: 'Khasi', + en: 'Khasi', + android: false, + ios: false, + }, + { + code3: 'khi', + code2: '', + name: 'Khoisan languages', + en: 'Khoisan languages', + android: false, + ios: false, + }, + { + code3: 'khm', + code2: 'km', + name: 'Khmer', + en: 'Khmer', + android: false, + ios: false, + }, + { + code3: 'kho', + code2: '', + name: 'Khotanese; Sakan', + en: 'Khotanese; Sakan', + android: false, + ios: false, + }, + { + code3: 'kik', + code2: 'ki', + name: 'Kikuyu; Gikuyu', + en: 'Kikuyu; Gikuyu', + android: false, + ios: false, + }, + { + code3: 'kin', + code2: 'rw', + name: 'Kinyarwanda', + en: 'Kinyarwanda', + android: false, + ios: false, + }, + { + code3: 'kir', + code2: 'ky', + name: 'Kyrgyz', + en: 'Kyrgyz', + android: false, + ios: false, + }, + { + code3: 'kmb', + code2: '', + name: 'Kimbundu', + en: 'Kimbundu', + android: false, + ios: false, + }, + { + code3: 'kok', + code2: '', + name: 'कोंकणी', + en: 'Konkani', + android: false, + ios: false, + }, + { + code3: 'kom', + code2: 'kv', + name: 'Komi', + en: 'Komi', + android: false, + ios: false, + }, + { + code3: 'kon', + code2: 'kg', + name: 'Kongo', + en: 'Kongo', + android: false, + ios: false, + }, + { + code3: 'kor', + code2: 'ko', + name: '한국어', + en: 'Korean', + android: true, + ios: true, + }, + { + code3: 'kos', + code2: '', + name: 'Kosraean', + en: 'Kosraean', + android: false, + ios: false, + }, + { + code3: 'kpe', + code2: '', + name: 'Kpelle', + en: 'Kpelle', + android: false, + ios: false, + }, + { + code3: 'krc', + code2: '', + name: 'Karachay-Balkar', + en: 'Karachay-Balkar', + android: false, + ios: false, + }, + { + code3: 'krl', + code2: '', + name: 'Karelian', + en: 'Karelian', + android: false, + ios: false, + }, + { + code3: 'kro', + code2: '', + name: 'Kru languages', + en: 'Kru languages', + android: false, + ios: false, + }, + { + code3: 'kru', + code2: '', + name: 'Kurukh', + en: 'Kurukh', + android: false, + ios: false, + }, + { + code3: 'kua', + code2: 'kj', + name: 'Kuanyama; Kwanyama', + en: 'Kuanyama; Kwanyama', + android: false, + ios: false, + }, + { + code3: 'kum', + code2: '', + name: 'Kumyk', + en: 'Kumyk', + android: false, + ios: false, + }, + { + code3: 'kur', + code2: 'ku', + name: 'Kurdish', + en: 'Kurdish', + android: false, + ios: false, + }, + { + code3: 'kut', + code2: '', + name: 'Kutenai', + en: 'Kutenai', + android: false, + ios: false, + }, + { + code3: 'lad', + code2: '', + name: 'Ladino', + en: 'Ladino', + android: false, + ios: false, + }, + { + code3: 'lah', + code2: '', + name: 'Lahnda', + en: 'Lahnda', + android: false, + ios: false, + }, + { + code3: 'lam', + code2: '', + name: 'Lamba', + en: 'Lamba', + android: false, + ios: false, + }, + { + code3: 'lao', + code2: 'lo', + name: 'Lao', + en: 'Lao', + android: false, + ios: false, + }, + { + code3: 'lat', + code2: 'la', + name: 'Latin', + en: 'Latin', + android: false, + ios: false, + }, + { + code3: 'lav', + code2: 'lv', + name: 'latviešu', + en: 'Latvian', + android: true, + ios: false, + }, + { + code3: 'lez', + code2: '', + name: 'Lezghian', + en: 'Lezghian', + android: false, + ios: false, + }, + { + code3: 'lim', + code2: 'li', + name: 'Limburgish', + en: 'Limburgish', + android: false, + ios: false, + }, + { + code3: 'lin', + code2: 'ln', + name: 'Lingala', + en: 'Lingala', + android: false, + ios: false, + }, + { + code3: 'lit', + code2: 'lt', + name: 'lietuvių', + en: 'Lithuanian', + android: true, + ios: false, + }, + { + code3: 'lol', + code2: '', + name: 'Mongo', + en: 'Mongo', + android: false, + ios: false, + }, + { + code3: 'loz', + code2: '', + name: 'Lozi', + en: 'Lozi', + android: false, + ios: false, + }, + { + code3: 'ltz', + code2: 'lb', + name: 'Luxembourgish', + en: 'Luxembourgish', + android: false, + ios: false, + }, + { + code3: 'lua', + code2: '', + name: 'Luba-Lulua', + en: 'Luba-Lulua', + android: false, + ios: false, + }, + { + code3: 'lub', + code2: 'lu', + name: 'Luba-Katanga', + en: 'Luba-Katanga', + android: false, + ios: false, + }, + { + code3: 'lug', + code2: 'lg', + name: 'Ganda', + en: 'Ganda', + android: false, + ios: false, + }, + { + code3: 'lui', + code2: '', + name: 'Luiseno', + en: 'Luiseno', + android: false, + ios: false, + }, + { + code3: 'lun', + code2: '', + name: 'Lunda', + en: 'Lunda', + android: false, + ios: false, + }, + { + code3: 'luo', + code2: '', + name: 'Luo (Kenya and Tanzania)', + en: 'Luo (Kenya and Tanzania)', + android: false, + ios: false, + }, + { + code3: 'lus', + code2: '', + name: 'Mizo', + en: 'Mizo', + android: false, + ios: false, + }, + { + code3: 'mac', + code2: 'mk', + name: 'Macedonian', + en: 'Macedonian', + android: true, + ios: false, + }, + { + code3: 'mad', + code2: '', + name: 'Madurese', + en: 'Madurese', + android: false, + ios: false, + }, + { + code3: 'mag', + code2: '', + name: 'Magahi', + en: 'Magahi', + android: false, + ios: false, + }, + { + code3: 'mah', + code2: 'mh', + name: 'Marshallese', + en: 'Marshallese', + android: false, + ios: false, + }, + { + code3: 'mai', + code2: '', + name: 'Maithili', + en: 'Maithili', + android: false, + ios: false, + }, + { + code3: 'mak', + code2: '', + name: 'Makasar', + en: 'Makasar', + android: false, + ios: false, + }, + { + code3: 'mal', + code2: 'ml', + name: 'മലയാളം', + en: 'Malayalam', + android: false, + ios: false, + }, + { + code3: 'man', + code2: '', + name: 'Mandingo', + en: 'Mandingo', + android: false, + ios: false, + }, + { + code3: 'mao', + code2: 'mi', + name: 'Māori', + en: 'Māori', + android: false, + ios: false, + }, + { + code3: 'map', + code2: '', + name: 'Austronesian languages', + en: 'Austronesian languages', + android: false, + ios: false, + }, + { + code3: 'mar', + code2: 'mr', + name: 'मराठी', + en: 'Marathi', + android: true, + ios: false, + }, + { + code3: 'mas', + code2: '', + name: 'Masai', + en: 'Masai', + android: false, + ios: false, + }, + { + code3: 'may', + code2: 'ms', + name: 'Melayu', + en: 'Malay', + android: true, + ios: false, + }, + { + code3: 'mdf', + code2: '', + name: 'Moksha', + en: 'Moksha', + android: false, + ios: false, + }, + { + code3: 'mdr', + code2: '', + name: 'Mandar', + en: 'Mandar', + android: false, + ios: false, + }, + { + code3: 'men', + code2: '', + name: 'Mende', + en: 'Mende', + android: false, + ios: false, + }, + { + code3: 'mga', + code2: '', + name: 'Irish, Middle (900-1200)', + en: 'Irish, Middle (900-1200)', + android: false, + ios: false, + }, + { + code3: 'mic', + code2: '', + name: "Mi'kmaq; Micmac", + en: "Mi'kmaq; Micmac", + android: false, + ios: false, + }, + { + code3: 'min', + code2: '', + name: 'Minangkabau', + en: 'Minangkabau', + android: false, + ios: false, + }, + { + code3: 'mis', + code2: '', + name: 'Uncoded languages', + en: 'Uncoded languages', + android: false, + ios: false, + }, + { + code3: 'mkd', + code2: 'mk', + name: 'Macedonian', + en: 'Macedonian', + android: true, + ios: false, + }, + { + code3: 'mkh', + code2: '', + name: 'Mon-Khmer languages', + en: 'Mon-Khmer languages', + android: false, + ios: false, + }, + { + code3: 'mlg', + code2: 'mg', + name: 'Malagasy', + en: 'Malagasy', + android: false, + ios: false, + }, + { + code3: 'mlt', + code2: 'mt', + name: 'Maltese', + en: 'Maltese', + android: true, + ios: false, + }, + { + code3: 'mnc', + code2: '', + name: 'Manchu', + en: 'Manchu', + android: false, + ios: false, + }, + { + code3: 'mni', + code2: '', + name: 'Manipuri', + en: 'Manipuri', + android: false, + ios: false, + }, + { + code3: 'mno', + code2: '', + name: 'Manobo languages', + en: 'Manobo languages', + android: false, + ios: false, + }, + { + code3: 'moh', + code2: '', + name: 'Mohawk', + en: 'Mohawk', + android: false, + ios: false, + }, + { + code3: 'mon', + code2: 'mn', + name: 'Mongolian', + en: 'Mongolian', + android: false, + ios: false, + }, + { + code3: 'mos', + code2: '', + name: 'Mossi', + en: 'Mossi', + android: false, + ios: false, + }, + { + code3: 'mri', + code2: 'mi', + name: 'Māori', + en: 'Māori', + android: false, + ios: false, + }, + { + code3: 'msa', + code2: 'ms', + name: 'Melayu', + en: 'Malay', + android: true, + ios: false, + }, + { + code3: 'mul', + code2: '', + name: 'Multiple languages', + en: 'Multiple languages', + android: false, + ios: false, + }, + { + code3: 'mun', + code2: '', + name: 'Munda languages', + en: 'Munda languages', + android: false, + ios: false, + }, + { + code3: 'mus', + code2: '', + name: 'Creek', + en: 'Creek', + android: false, + ios: false, + }, + { + code3: 'mwl', + code2: '', + name: 'Mirandese', + en: 'Mirandese', + android: false, + ios: false, + }, + { + code3: 'mwr', + code2: '', + name: 'Marwari', + en: 'Marwari', + android: false, + ios: false, + }, + { + code3: 'mya', + code2: 'my', + name: 'Burmese', + en: 'Burmese', + android: false, + ios: false, + }, + { + code3: 'myn', + code2: '', + name: 'Mayan languages', + en: 'Mayan languages', + android: false, + ios: false, + }, + { + code3: 'myv', + code2: '', + name: 'Erzya', + en: 'Erzya', + android: false, + ios: false, + }, + { + code3: 'nah', + code2: '', + name: 'Nahuatl languages', + en: 'Nahuatl languages', + android: false, + ios: false, + }, + { + code3: 'nai', + code2: '', + name: 'North American Indian languages', + en: 'North American Indian languages', + android: false, + ios: false, + }, + { + code3: 'nap', + code2: '', + name: 'Neapolitan', + en: 'Neapolitan', + android: false, + ios: false, + }, + { + code3: 'nau', + code2: 'na', + name: 'Nauru', + en: 'Nauru', + android: false, + ios: false, + }, + { + code3: 'nav', + code2: 'nv', + name: 'Navajo', + en: 'Navajo', + android: false, + ios: false, + }, + { + code3: 'nbl', + code2: 'nr', + name: 'South Ndebele', + en: 'South Ndebele', + android: false, + ios: false, + }, + { + code3: 'nde', + code2: 'nd', + name: 'North Ndebele', + en: 'North Ndebele', + android: false, + ios: false, + }, + { + code3: 'ndo', + code2: 'ng', + name: 'Ndonga', + en: 'Ndonga', + android: false, + ios: false, + }, + { + code3: 'nds', + code2: '', + name: 'Low German; Low Saxon; German, Low; Saxon, Low', + en: 'Low German; Low Saxon; German, Low; Saxon, Low', + android: false, + ios: false, + }, + { + code3: 'nep', + code2: 'ne', + name: 'Nepali', + en: 'Nepali', + android: false, + ios: false, + }, + { + code3: 'new', + code2: '', + name: 'Nepal Bhasa; Newari', + en: 'Nepal Bhasa; Newari', + android: false, + ios: false, + }, + { + code3: 'nia', + code2: '', + name: 'Nias', + en: 'Nias', + android: false, + ios: false, + }, + { + code3: 'nic', + code2: '', + name: 'Niger-Kordofanian languages', + en: 'Niger-Kordofanian languages', + android: false, + ios: false, + }, + { + code3: 'niu', + code2: '', + name: 'Niuean', + en: 'Niuean', + android: false, + ios: false, + }, + { + code3: 'nld', + code2: 'nl', + name: 'Nederlands', + en: 'Dutch', + android: true, + ios: true, + }, + { + code3: 'nno', + code2: 'nn', + name: 'Norwegian Nynorsk', + en: 'Norwegian Nynorsk', + android: false, + ios: false, + }, + { + code3: 'nob', + code2: 'nb', + name: 'norsk bokmål', + en: 'Norwegian Bokmål', + android: false, + ios: false, + }, + { + code3: 'nog', + code2: '', + name: 'Nogai', + en: 'Nogai', + android: false, + ios: false, + }, + { + code3: 'non', + code2: '', + name: 'Norse, Old', + en: 'Norse, Old', + android: false, + ios: false, + }, + { + code3: 'nor', + code2: 'no', + name: 'norsk', + en: 'Norwegian', + android: true, + ios: false, + }, + { + code3: 'nqo', + code2: '', + name: "N'Ko", + en: "N'Ko", + android: false, + ios: false, + }, + { + code3: 'nso', + code2: '', + name: 'Northern Sotho', + en: 'Northern Sotho', + android: false, + ios: false, + }, + { + code3: 'nub', + code2: '', + name: 'Nubian languages', + en: 'Nubian languages', + android: false, + ios: false, + }, + { + code3: 'nwc', + code2: '', + name: 'Classical Newari; Old Newari; Classical Nepal Bhasa', + en: 'Classical Newari; Old Newari; Classical Nepal Bhasa', + android: false, + ios: false, + }, + { + code3: 'nya', + code2: 'ny', + name: 'Nyanja', + en: 'Nyanja', + android: false, + ios: false, + }, + { + code3: 'nym', + code2: '', + name: 'Nyamwezi', + en: 'Nyamwezi', + android: false, + ios: false, + }, + { + code3: 'nyn', + code2: '', + name: 'Nyankole', + en: 'Nyankole', + android: false, + ios: false, + }, + { + code3: 'nyo', + code2: '', + name: 'Nyoro', + en: 'Nyoro', + android: false, + ios: false, + }, + { + code3: 'nzi', + code2: '', + name: 'Nzima', + en: 'Nzima', + android: false, + ios: false, + }, + { + code3: 'oci', + code2: 'oc', + name: 'Occitan', + en: 'Occitan', + android: false, + ios: false, + }, + { + code3: 'oji', + code2: 'oj', + name: 'Ojibwa', + en: 'Ojibwa', + android: false, + ios: false, + }, + { + code3: 'ori', + code2: 'or', + name: 'Odia', + en: 'Odia', + android: false, + ios: false, + }, + { + code3: 'orm', + code2: 'om', + name: 'Oromo', + en: 'Oromo', + android: false, + ios: false, + }, + { + code3: 'osa', + code2: '', + name: 'Osage', + en: 'Osage', + android: false, + ios: false, + }, + { + code3: 'oss', + code2: 'os', + name: 'Ossetic', + en: 'Ossetic', + android: false, + ios: false, + }, + { + code3: 'ota', + code2: '', + name: 'Turkish, Ottoman (1500-1928)', + en: 'Turkish, Ottoman (1500-1928)', + android: false, + ios: false, + }, + { + code3: 'oto', + code2: '', + name: 'Otomian languages', + en: 'Otomian languages', + android: false, + ios: false, + }, + { + code3: 'paa', + code2: '', + name: 'Papuan languages', + en: 'Papuan languages', + android: false, + ios: false, + }, + { + code3: 'pag', + code2: '', + name: 'Pangasinan', + en: 'Pangasinan', + android: false, + ios: false, + }, + { + code3: 'pal', + code2: '', + name: 'Pahlavi', + en: 'Pahlavi', + android: false, + ios: false, + }, + { + code3: 'pam', + code2: '', + name: 'Pampanga; Kapampangan', + en: 'Pampanga; Kapampangan', + android: false, + ios: false, + }, + { + code3: 'pan', + code2: 'pa', + name: 'ਪੰਜਾਬੀ', + en: 'Punjabi', + android: false, + ios: false, + }, + { + code3: 'pap', + code2: '', + name: 'Papiamento', + en: 'Papiamento', + android: false, + ios: false, + }, + { + code3: 'pau', + code2: '', + name: 'Palauan', + en: 'Palauan', + android: false, + ios: false, + }, + { + code3: 'peo', + code2: '', + name: 'Persian, Old (ca.600-400 B.C.)', + en: 'Persian, Old (ca.600-400 B.C.)', + android: false, + ios: false, + }, + { + code3: 'per', + code2: 'fa', + name: 'فارسی', + en: 'Persian', + android: true, + ios: false, + }, + { + code3: 'phi', + code2: '', + name: 'Philippine languages', + en: 'Philippine languages', + android: false, + ios: false, + }, + { + code3: 'phn', + code2: '', + name: 'Phoenician', + en: 'Phoenician', + android: false, + ios: false, + }, + { + code3: 'pli', + code2: 'pi', + name: 'Pali', + en: 'Pali', + android: false, + ios: false, + }, + { + code3: 'pol', + code2: 'pl', + name: 'polski', + en: 'Polish', + android: true, + ios: true, + }, + { + code3: 'pon', + code2: '', + name: 'Pohnpeian', + en: 'Pohnpeian', + android: false, + ios: false, + }, + { + code3: 'por', + code2: 'pt', + name: 'português', + en: 'Portuguese', + android: true, + ios: true, + }, + { + code3: 'pra', + code2: '', + name: 'Prakrit languages', + en: 'Prakrit languages', + android: false, + ios: false, + }, + { + code3: 'pro', + code2: '', + name: 'Provençal, Old (to 1500);Occitan, Old (to 1500)', + en: 'Provençal, Old (to 1500);Occitan, Old (to 1500)', + android: false, + ios: false, + }, + { + code3: 'pus', + code2: 'ps', + name: 'Pashto', + en: 'Pashto', + android: false, + ios: false, + }, + { + code3: 'que', + code2: 'qu', + name: 'Quechua', + en: 'Quechua', + android: false, + ios: false, + }, + { + code3: 'raj', + code2: '', + name: 'Rajasthani', + en: 'Rajasthani', + android: false, + ios: false, + }, + { + code3: 'rap', + code2: '', + name: 'Rapanui', + en: 'Rapanui', + android: false, + ios: false, + }, + { + code3: 'rar', + code2: '', + name: 'Rarotongan; Cook Islands Maori', + en: 'Rarotongan; Cook Islands Maori', + android: false, + ios: false, + }, + { + code3: 'roa', + code2: '', + name: 'Romance languages', + en: 'Romance languages', + android: false, + ios: false, + }, + { + code3: 'roh', + code2: 'rm', + name: 'Romansh', + en: 'Romansh', + android: false, + ios: false, + }, + { + code3: 'rom', + code2: '', + name: 'Romany', + en: 'Romany', + android: false, + ios: false, + }, + { + code3: 'rum', + code2: 'ro', + name: 'română', + en: 'Romanian', + android: true, + ios: false, + }, + { + code3: 'ron', + code2: 'ro', + name: 'română', + en: 'Romanian', + android: true, + ios: false, + }, + { + code3: 'run', + code2: 'rn', + name: 'Rundi', + en: 'Rundi', + android: false, + ios: false, + }, + { + code3: 'rup', + code2: '', + name: 'Aromanian; Arumanian; Macedo-Romanian', + en: 'Aromanian; Arumanian; Macedo-Romanian', + android: false, + ios: false, + }, + { + code3: 'rus', + code2: 'ru', + name: 'русский', + en: 'Russian', + android: true, + ios: true, + }, + { + code3: 'sad', + code2: '', + name: 'Sandawe', + en: 'Sandawe', + android: false, + ios: false, + }, + { + code3: 'sag', + code2: 'sg', + name: 'Sango', + en: 'Sango', + android: false, + ios: false, + }, + { + code3: 'sah', + code2: '', + name: 'Yakut', + en: 'Yakut', + android: false, + ios: false, + }, + { + code3: 'sai', + code2: '', + name: 'South American Indian languages', + en: 'South American Indian languages', + android: false, + ios: false, + }, + { + code3: 'sal', + code2: '', + name: 'Salishan languages', + en: 'Salishan languages', + android: false, + ios: false, + }, + { + code3: 'sam', + code2: '', + name: 'Samaritan Aramaic', + en: 'Samaritan Aramaic', + android: false, + ios: false, + }, + { + code3: 'san', + code2: 'sa', + name: 'Sanskrit', + en: 'Sanskrit', + android: false, + ios: false, + }, + { + code3: 'sas', + code2: '', + name: 'Sasak', + en: 'Sasak', + android: false, + ios: false, + }, + { + code3: 'sat', + code2: '', + name: 'Santali', + en: 'Santali', + android: false, + ios: false, + }, + { + code3: 'scn', + code2: '', + name: 'Sicilian', + en: 'Sicilian', + android: false, + ios: false, + }, + { + code3: 'sco', + code2: '', + name: 'Scots', + en: 'Scots', + android: false, + ios: false, + }, + { + code3: 'sel', + code2: '', + name: 'Selkup', + en: 'Selkup', + android: false, + ios: false, + }, + { + code3: 'sem', + code2: '', + name: 'Semitic languages', + en: 'Semitic languages', + android: false, + ios: false, + }, + { + code3: 'sga', + code2: '', + name: 'Irish, Old (to 900)', + en: 'Irish, Old (to 900)', + android: false, + ios: false, + }, + { + code3: 'sgn', + code2: '', + name: 'Sign Languages', + en: 'Sign Languages', + android: false, + ios: false, + }, + { + code3: 'shn', + code2: '', + name: 'Shan', + en: 'Shan', + android: false, + ios: false, + }, + { + code3: 'sid', + code2: '', + name: 'Sidamo', + en: 'Sidamo', + android: false, + ios: false, + }, + { + code3: 'sin', + code2: 'si', + name: 'Sinhala', + en: 'Sinhala', + android: false, + ios: false, + }, + { + code3: 'sio', + code2: '', + name: 'Siouan languages', + en: 'Siouan languages', + android: false, + ios: false, + }, + { + code3: 'sit', + code2: '', + name: 'Sino-Tibetan languages', + en: 'Sino-Tibetan languages', + android: false, + ios: false, + }, + { + code3: 'sla', + code2: '', + name: 'Slavic languages', + en: 'Slavic languages', + android: false, + ios: false, + }, + { + code3: 'slo', + code2: 'sk', + name: 'slovenčina', + en: 'Slovak', + android: true, + ios: false, + }, + { + code3: 'slk', + code2: 'sk', + name: 'slovenčina', + en: 'Slovak', + android: true, + ios: false, + }, + { + code3: 'slv', + code2: 'sl', + name: 'slovenščina', + en: 'Slovenian', + android: true, + ios: false, + }, + { + code3: 'sma', + code2: '', + name: 'Southern Sami', + en: 'Southern Sami', + android: false, + ios: false, + }, + { + code3: 'sme', + code2: 'se', + name: 'Northern Sami', + en: 'Northern Sami', + android: false, + ios: false, + }, + { + code3: 'smi', + code2: '', + name: 'Sami languages', + en: 'Sami languages', + android: false, + ios: false, + }, + { + code3: 'smj', + code2: '', + name: 'Lule Sami', + en: 'Lule Sami', + android: false, + ios: false, + }, + { + code3: 'smn', + code2: '', + name: 'Inari Sami', + en: 'Inari Sami', + android: false, + ios: false, + }, + { + code3: 'smo', + code2: 'sm', + name: 'Samoan', + en: 'Samoan', + android: false, + ios: false, + }, + { + code3: 'sms', + code2: '', + name: 'Skolt Sami', + en: 'Skolt Sami', + android: false, + ios: false, + }, + { + code3: 'sna', + code2: 'sn', + name: 'Shona', + en: 'Shona', + android: false, + ios: false, + }, + { + code3: 'snd', + code2: 'sd', + name: 'سنڌي', + en: 'Sindhi', + android: false, + ios: false, + }, + { + code3: 'snk', + code2: '', + name: 'Soninke', + en: 'Soninke', + android: false, + ios: false, + }, + { + code3: 'sog', + code2: '', + name: 'Sogdian', + en: 'Sogdian', + android: false, + ios: false, + }, + { + code3: 'som', + code2: 'so', + name: 'Somali', + en: 'Somali', + android: false, + ios: false, + }, + { + code3: 'son', + code2: '', + name: 'Songhai languages', + en: 'Songhai languages', + android: false, + ios: false, + }, + { + code3: 'sot', + code2: 'st', + name: 'Southern Sotho', + en: 'Southern Sotho', + android: false, + ios: false, + }, + { + code3: 'spa', + code2: 'es', + name: 'español', + en: 'Spanish', + android: true, + ios: true, + }, + { + code3: 'sqi', + code2: 'sq', + name: 'Albanian', + en: 'Albanian', + android: true, + ios: false, + }, + { + code3: 'srd', + code2: 'sc', + name: 'Sardinian', + en: 'Sardinian', + android: false, + ios: false, + }, + { + code3: 'srn', + code2: '', + name: 'Sranan Tongo', + en: 'Sranan Tongo', + android: false, + ios: false, + }, + { + code3: 'srp', + code2: 'sr', + name: 'српски', + en: 'Serbian', + android: false, + ios: false, + }, + { + code3: 'srr', + code2: '', + name: 'Serer', + en: 'Serer', + android: false, + ios: false, + }, + { + code3: 'ssa', + code2: '', + name: 'Nilo-Saharan languages', + en: 'Nilo-Saharan languages', + android: false, + ios: false, + }, + { + code3: 'ssw', + code2: 'ss', + name: 'Swati', + en: 'Swati', + android: false, + ios: false, + }, + { + code3: 'suk', + code2: '', + name: 'Sukuma', + en: 'Sukuma', + android: false, + ios: false, + }, + { + code3: 'sun', + code2: 'su', + name: 'Sundanese', + en: 'Sundanese', + android: false, + ios: false, + }, + { + code3: 'sus', + code2: '', + name: 'Susu', + en: 'Susu', + android: false, + ios: false, + }, + { + code3: 'sux', + code2: '', + name: 'Sumerian', + en: 'Sumerian', + android: false, + ios: false, + }, + { + code3: 'swa', + code2: 'sw', + name: 'Kiswahili', + en: 'Swahili', + android: true, + ios: false, + }, + { + code3: 'swe', + code2: 'sv', + name: 'svenska', + en: 'Swedish', + android: true, + ios: false, + }, + { + code3: 'syc', + code2: '', + name: 'Classical Syriac', + en: 'Classical Syriac', + android: false, + ios: false, + }, + { + code3: 'syr', + code2: '', + name: 'Syriac', + en: 'Syriac', + android: false, + ios: false, + }, + { + code3: 'tah', + code2: 'ty', + name: 'Tahitian', + en: 'Tahitian', + android: false, + ios: false, + }, + { + code3: 'tai', + code2: '', + name: 'Tai languages', + en: 'Tai languages', + android: false, + ios: false, + }, + { + code3: 'tam', + code2: 'ta', + name: 'தமிழ்', + en: 'Tamil', + android: true, + ios: false, + }, + { + code3: 'tat', + code2: 'tt', + name: 'Tatar', + en: 'Tatar', + android: false, + ios: false, + }, + { + code3: 'tel', + code2: 'te', + name: 'తెలుగు', + en: 'Telugu', + android: true, + ios: false, + }, + { + code3: 'tem', + code2: '', + name: 'Timne', + en: 'Timne', + android: false, + ios: false, + }, + { + code3: 'ter', + code2: '', + name: 'Tereno', + en: 'Tereno', + android: false, + ios: false, + }, + { + code3: 'tet', + code2: '', + name: 'Tetum', + en: 'Tetum', + android: false, + ios: false, + }, + { + code3: 'tgk', + code2: 'tg', + name: 'Tajik', + en: 'Tajik', + android: false, + ios: false, + }, + { + code3: 'tgl', + code2: 'tl', + name: 'Filipino', + en: 'Filipino', + android: true, + ios: false, + }, + { + code3: 'tha', + code2: 'th', + name: 'ไทย', + en: 'Thai', + android: true, + ios: true, + }, + { + code3: 'tib', + code2: 'bo', + name: 'Tibetan', + en: 'Tibetan', + android: false, + ios: false, + }, + { + code3: 'tig', + code2: '', + name: 'Tigre', + en: 'Tigre', + android: false, + ios: false, + }, + { + code3: 'tir', + code2: 'ti', + name: 'Tigrinya', + en: 'Tigrinya', + android: false, + ios: false, + }, + { + code3: 'tiv', + code2: '', + name: 'Tiv', + en: 'Tiv', + android: false, + ios: false, + }, + { + code3: 'tkl', + code2: '', + name: 'Tokelau', + en: 'Tokelau', + android: false, + ios: false, + }, + { + code3: 'tlh', + code2: '', + name: 'Klingon; tlhIngan-Hol', + en: 'Klingon; tlhIngan-Hol', + android: false, + ios: false, + }, + { + code3: 'tli', + code2: '', + name: 'Tlingit', + en: 'Tlingit', + android: false, + ios: false, + }, + { + code3: 'tmh', + code2: '', + name: 'Tamashek', + en: 'Tamashek', + android: false, + ios: false, + }, + { + code3: 'tog', + code2: '', + name: 'Tonga (Nyasa)', + en: 'Tonga (Nyasa)', + android: false, + ios: false, + }, + { + code3: 'ton', + code2: 'to', + name: 'Tongan', + en: 'Tongan', + android: false, + ios: false, + }, + { + code3: 'tpi', + code2: '', + name: 'Tok Pisin', + en: 'Tok Pisin', + android: false, + ios: false, + }, + { + code3: 'tsi', + code2: '', + name: 'Tsimshian', + en: 'Tsimshian', + android: false, + ios: false, + }, + { + code3: 'tsn', + code2: 'tn', + name: 'Tswana', + en: 'Tswana', + android: false, + ios: false, + }, + { + code3: 'tso', + code2: 'ts', + name: 'Tsonga', + en: 'Tsonga', + android: false, + ios: false, + }, + { + code3: 'tuk', + code2: 'tk', + name: 'Turkmen', + en: 'Turkmen', + android: false, + ios: false, + }, + { + code3: 'tum', + code2: '', + name: 'Tumbuka', + en: 'Tumbuka', + android: false, + ios: false, + }, + { + code3: 'tup', + code2: '', + name: 'Tupi languages', + en: 'Tupi languages', + android: false, + ios: false, + }, + { + code3: 'tur', + code2: 'tr', + name: 'Türkçe', + en: 'Turkish', + android: true, + ios: true, + }, + { + code3: 'tut', + code2: '', + name: 'Altaic languages', + en: 'Altaic languages', + android: false, + ios: false, + }, + { + code3: 'tvl', + code2: '', + name: 'Tuvalu', + en: 'Tuvalu', + android: false, + ios: false, + }, + { + code3: 'twi', + code2: 'tw', + name: 'Akan', + en: 'Akan', + android: false, + ios: false, + }, + { + code3: 'tyv', + code2: '', + name: 'Tuvinian', + en: 'Tuvinian', + android: false, + ios: false, + }, + { + code3: 'udm', + code2: '', + name: 'Udmurt', + en: 'Udmurt', + android: false, + ios: false, + }, + { + code3: 'uga', + code2: '', + name: 'Ugaritic', + en: 'Ugaritic', + android: false, + ios: false, + }, + { + code3: 'uig', + code2: 'ug', + name: 'Uyghur', + en: 'Uyghur', + android: false, + ios: false, + }, + { + code3: 'ukr', + code2: 'uk', + name: 'українська', + en: 'Ukrainian', + android: true, + ios: true, + }, + { + code3: 'umb', + code2: '', + name: 'Umbundu', + en: 'Umbundu', + android: false, + ios: false, + }, + { + code3: 'und', + code2: '', + name: 'Undetermined', + en: 'Undetermined', + android: false, + ios: false, + }, + { + code3: 'urd', + code2: 'ur', + name: 'اردو', + en: 'Urdu', + android: true, + ios: false, + }, + { + code3: 'uzb', + code2: 'uz', + name: 'o‘zbek', + en: 'Uzbek', + android: false, + ios: false, + }, + { + code3: 'vai', + code2: '', + name: 'Vai', + en: 'Vai', + android: false, + ios: false, + }, + { + code3: 'ven', + code2: 've', + name: 'Venda', + en: 'Venda', + android: false, + ios: false, + }, + { + code3: 'vie', + code2: 'vi', + name: 'Tiếng Việt', + en: 'Vietnamese', + android: true, + ios: true, + }, + { + code3: 'vol', + code2: 'vo', + name: 'Volapük', + en: 'Volapük', + android: false, + ios: false, + }, + { + code3: 'vot', + code2: '', + name: 'Votic', + en: 'Votic', + android: false, + ios: false, + }, + { + code3: 'wak', + code2: '', + name: 'Wakashan languages', + en: 'Wakashan languages', + android: false, + ios: false, + }, + { + code3: 'wal', + code2: '', + name: 'Wolaitta; Wolaytta', + en: 'Wolaitta; Wolaytta', + android: false, + ios: false, + }, + { + code3: 'war', + code2: '', + name: 'Waray', + en: 'Waray', + android: false, + ios: false, + }, + { + code3: 'was', + code2: '', + name: 'Washo', + en: 'Washo', + android: false, + ios: false, + }, + { + code3: 'wel', + code2: 'cy', + name: 'Welsh', + en: 'Welsh', + android: true, + ios: false, + }, + { + code3: 'wen', + code2: '', + name: 'Sorbian languages', + en: 'Sorbian languages', + android: false, + ios: false, + }, + { + code3: 'wln', + code2: 'wa', + name: 'Walloon', + en: 'Walloon', + android: false, + ios: false, + }, + { + code3: 'wol', + code2: 'wo', + name: 'Wolof', + en: 'Wolof', + android: false, + ios: false, + }, + { + code3: 'xal', + code2: '', + name: 'Kalmyk; Oirat', + en: 'Kalmyk; Oirat', + android: false, + ios: false, + }, + { + code3: 'xho', + code2: 'xh', + name: 'Xhosa', + en: 'Xhosa', + android: false, + ios: false, + }, + { + code3: 'yao', + code2: '', + name: 'Yao', + en: 'Yao', + android: false, + ios: false, + }, + { + code3: 'yap', + code2: '', + name: 'Yapese', + en: 'Yapese', + android: false, + ios: false, + }, + { + code3: 'yid', + code2: 'yi', + name: 'Yiddish', + en: 'Yiddish', + android: false, + ios: false, + }, + { + code3: 'yor', + code2: 'yo', + name: 'Yoruba', + en: 'Yoruba', + android: false, + ios: false, + }, + { + code3: 'ypk', + code2: '', + name: 'Yupik languages', + en: 'Yupik languages', + android: false, + ios: false, + }, + { + code3: 'zap', + code2: '', + name: 'Zapotec', + en: 'Zapotec', + android: false, + ios: false, + }, + { + code3: 'zbl', + code2: '', + name: 'Blissymbols; Blissymbolics; Bliss', + en: 'Blissymbols; Blissymbolics; Bliss', + android: false, + ios: false, + }, + { + code3: 'zen', + code2: '', + name: 'Zenaga', + en: 'Zenaga', + android: false, + ios: false, + }, + { + code3: 'zgh', + code2: '', + name: 'Standard Moroccan Tamazight', + en: 'Standard Moroccan Tamazight', + android: false, + ios: false, + }, + { + code3: 'zha', + code2: 'za', + name: 'Zhuang; Chuang', + en: 'Zhuang; Chuang', + android: false, + ios: false, + }, + { + code3: 'zho', + code2: 'zh', + name: '中文', + en: 'Chinese', + android: true, + ios: true, + }, + { + code3: 'znd', + code2: '', + name: 'Zande languages', + en: 'Zande languages', + android: false, + ios: false, + }, + { + code3: 'zul', + code2: 'zu', + name: 'Zulu', + en: 'Zulu', + android: false, + ios: false, + }, + { + code3: 'zun', + code2: '', + name: 'Zuni', + en: 'Zuni', + android: false, + ios: false, + }, + { + code3: 'zza', + code2: '', + name: 'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', + en: 'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', + android: false, + ios: false, + }, +]; +export var LANGUAGES_MAP_CODE2 = Object.fromEntries(LANGUAGES.map(function (lang) { return [lang.code2, lang]; })); +export var LANGUAGES_MAP_CODE3 = Object.fromEntries(LANGUAGES.map(function (lang) { return [lang.code3, lang]; })); +// some additional manual mappings (not clear if these should be in the "official" mappings) +if (LANGUAGES_MAP_CODE2.fa) { + LANGUAGES_MAP_CODE3.pes = LANGUAGES_MAP_CODE2.fa; +} diff --git a/src/logger/__tests__/logDump.test.js b/src/logger/__tests__/logDump.test.js new file mode 100644 index 0000000000..880a9fa48e --- /dev/null +++ b/src/logger/__tests__/logDump.test.js @@ -0,0 +1,36 @@ +import { expect, test } from '@jest/globals'; +import { add, getEntries } from '#/logger/logDump'; +import { LogContext, LogLevel } from '#/logger/types'; +test('works', function () { + var items = [ + { + id: '1', + level: LogLevel.Debug, + context: LogContext.Default, + message: 'hello', + metadata: {}, + timestamp: Date.now(), + }, + { + id: '2', + level: LogLevel.Debug, + context: LogContext.Default, + message: 'hello', + metadata: {}, + timestamp: Date.now(), + }, + { + id: '3', + level: LogLevel.Debug, + context: LogContext.Default, + message: 'hello', + metadata: {}, + timestamp: Date.now(), + }, + ]; + for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { + var item = items_1[_i]; + add(item); + } + expect(getEntries()).toEqual(items.reverse()); +}); diff --git a/src/logger/__tests__/logger.test.js b/src/logger/__tests__/logger.test.js new file mode 100644 index 0000000000..65b97c0766 --- /dev/null +++ b/src/logger/__tests__/logger.test.js @@ -0,0 +1,304 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { beforeAll, describe, expect, jest, test } from '@jest/globals'; +import * as Sentry from '@sentry/react-native'; +import { nanoid } from 'nanoid/non-secure'; +import { Logger } from '#/logger'; +import { sentryTransport } from '#/logger/transports/sentry'; +import { LogLevel } from '#/logger/types'; +jest.mock('@sentry/react-native', function () { return ({ + addBreadcrumb: jest.fn(), + captureException: jest.fn(), + captureMessage: jest.fn(), +}); }); +beforeAll(function () { + jest.useFakeTimers(); +}); +describe('general functionality', function () { + test('default params', function () { + var logger = new Logger(); + expect(logger.level).toEqual(LogLevel.Info); + }); + test('can override default params', function () { + var logger = new Logger({ + level: LogLevel.Debug, + }); + expect(logger.level).toEqual(LogLevel.Debug); + }); + test('contextFilter overrides level', function () { + var logger = new Logger({ + level: LogLevel.Info, + contextFilter: 'test', + }); + expect(logger.level).toEqual(LogLevel.Debug); + }); + test('supports extra metadata', function () { + var timestamp = Date.now(); + var logger = new Logger({}); + var mockTransport = jest.fn(); + logger.addTransport(mockTransport); + var extra = { foo: true, __metadata__: {} }; + logger.warn('message', extra); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Warn, undefined, 'message', extra, timestamp); + }); + test('supports inherited metadata', function () { + var timestamp = Date.now(); + var logger = new Logger({ + metadata: { bar: true }, + }); + var mockTransport = jest.fn(); + logger.addTransport(mockTransport); + var extra = { foo: true, __metadata__: { bar: true } }; + logger.warn('message', extra); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Warn, undefined, 'message', extra, timestamp); + }); + test('supports nullish/falsy metadata', function () { + var timestamp = Date.now(); + var logger = new Logger({}); + var mockTransport = jest.fn(); + var remove = logger.addTransport(mockTransport); + // @ts-expect-error testing the JS case + logger.warn('a', null); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Warn, undefined, 'a', { __metadata__: {} }, timestamp); + // @ts-expect-error testing the JS case + logger.warn('b', false); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Warn, undefined, 'b', { __metadata__: {} }, timestamp); + // @ts-expect-error testing the JS case + logger.warn('c', 0); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Warn, undefined, 'c', { __metadata__: {} }, timestamp); + remove(); + logger.addTransport(function (level, context, message, metadata) { + expect(typeof metadata).toEqual('object'); + }); + // @ts-expect-error testing the JS case + logger.warn('message', null); + }); + test('sentryTransport', function () { + var message = 'message'; + var timestamp = Date.now(); + var sentryTimestamp = timestamp / 1000; + /* + sentryTransport( + LogLevel.Debug, + Logger.Context.Default, + message, + {}, + timestamp, + ) + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ + category: Logger.Context.Default, + message, + data: {__context__: 'logger'}, + type: 'default', + level: LogLevel.Debug, + timestamp: sentryTimestamp, + }) + */ + sentryTransport(LogLevel.Info, Logger.Context.Default, message, { type: 'info', prop: true }, timestamp); + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ + category: Logger.Context.Default, + message: message, + data: { prop: true, __context__: 'logger' }, + type: 'info', + level: LogLevel.Info, + timestamp: sentryTimestamp, + }); + sentryTransport(LogLevel.Log, Logger.Context.Default, message, {}, timestamp); + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ + category: Logger.Context.Default, + message: message, + data: { __context__: 'logger' }, + type: 'default', + level: 'log', + timestamp: sentryTimestamp, + }); + jest.runAllTimers(); + expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { + level: 'log', + tags: { category: 'logger' }, + extra: { __context__: 'logger' }, + }); + sentryTransport(LogLevel.Warn, Logger.Context.Default, message, {}, timestamp); + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ + category: Logger.Context.Default, + message: message, + data: { __context__: 'logger' }, + type: 'default', + level: 'warning', + timestamp: sentryTimestamp, + }); + jest.runAllTimers(); + expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { + level: 'warning', + tags: { category: 'logger' }, + extra: { __context__: 'logger' }, + }); + var e = new Error('error'); + var tags = { + prop: 'prop', + }; + sentryTransport(LogLevel.Error, Logger.Context.Default, e, { + tags: tags, + prop: true, + }, timestamp); + expect(Sentry.captureException).toHaveBeenCalledWith(e, { + tags: __assign(__assign({}, tags), { category: 'logger' }), + extra: { + prop: true, + __context__: 'logger', + }, + }); + }); + test('sentryTransport serializes errors', function () { + var message = 'message'; + var timestamp = Date.now(); + var sentryTimestamp = timestamp / 1000; + sentryTransport(LogLevel.Info, undefined, message, { error: new Error('foo') }, timestamp); + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ + message: message, + data: { error: 'Error: foo' }, + type: 'default', + level: LogLevel.Info, + timestamp: sentryTimestamp, + }); + }); + test('add/remove transport', function () { + var timestamp = Date.now(); + var logger = new Logger({}); + var mockTransport = jest.fn(); + var remove = logger.addTransport(mockTransport); + logger.warn('warn'); + remove(); + logger.warn('warn'); + // only called once bc it was removed + expect(mockTransport).toHaveBeenNthCalledWith(1, LogLevel.Warn, undefined, 'warn', { __metadata__: {} }, timestamp); + }); +}); +describe('create', function () { + test('create', function () { + var mockTransport = jest.fn(); + var timestamp = Date.now(); + var message = nanoid(); + var logger = Logger.create(Logger.Context.Default); + logger.addTransport(mockTransport); + logger.info(message, {}); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Info, Logger.Context.Default, message, { __metadata__: {} }, timestamp); + }); +}); +describe('debug contexts', function () { + test('specific', function () { + var mockTransport = jest.fn(); + var timestamp = Date.now(); + var message = nanoid(); + var logger = new Logger({ + // @ts-ignore + context: 'specific', + level: LogLevel.Debug, + }); + logger.addTransport(mockTransport); + logger.debug(message, {}); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Debug, 'specific', message, { __metadata__: {} }, timestamp); + }); + test('namespaced', function () { + var mockTransport = jest.fn(); + var timestamp = Date.now(); + var message = nanoid(); + var logger = new Logger({ + // @ts-ignore + context: 'namespace:foo', + contextFilter: 'namespace:*', + level: LogLevel.Debug, + }); + logger.addTransport(mockTransport); + logger.debug(message, {}); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Debug, 'namespace:foo', message, { __metadata__: {} }, timestamp); + }); + test('ignores inactive', function () { + var mockTransport = jest.fn(); + var timestamp = Date.now(); + var message = nanoid(); + var logger = new Logger({ + // @ts-ignore + context: 'namespace:bar:baz', + contextFilter: 'namespace:foo:*', + }); + logger.addTransport(mockTransport); + logger.debug(message, {}); + expect(mockTransport).not.toHaveBeenCalledWith(LogLevel.Debug, 'namespace:bar:baz', message, { __metadata__: {} }, timestamp); + }); +}); +describe('supports levels', function () { + test('debug', function () { + var timestamp = Date.now(); + var logger = new Logger({ + level: LogLevel.Debug, + }); + var message = nanoid(); + var mockTransport = jest.fn(); + logger.addTransport(mockTransport); + logger.debug(message); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Debug, undefined, message, { __metadata__: {} }, timestamp); + logger.info(message); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Info, undefined, message, { __metadata__: {} }, timestamp); + logger.warn(message); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Warn, undefined, message, { __metadata__: {} }, timestamp); + var e = new Error(message); + logger.error(e); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, undefined, e, { __metadata__: {} }, timestamp); + }); + test('info', function () { + var timestamp = Date.now(); + var logger = new Logger({ + level: LogLevel.Info, + }); + var message = nanoid(); + var mockTransport = jest.fn(); + logger.addTransport(mockTransport); + logger.debug(message); + expect(mockTransport).not.toHaveBeenCalled(); + logger.info(message); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Info, undefined, message, { __metadata__: {} }, timestamp); + }); + test('warn', function () { + var timestamp = Date.now(); + var logger = new Logger({ + level: LogLevel.Warn, + }); + var message = nanoid(); + var mockTransport = jest.fn(); + logger.addTransport(mockTransport); + logger.debug(message); + expect(mockTransport).not.toHaveBeenCalled(); + logger.info(message); + expect(mockTransport).not.toHaveBeenCalled(); + logger.warn(message); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Warn, undefined, message, { __metadata__: {} }, timestamp); + }); + test('error', function () { + var timestamp = Date.now(); + var logger = new Logger({ + level: LogLevel.Error, + }); + var message = nanoid(); + var mockTransport = jest.fn(); + logger.addTransport(mockTransport); + logger.debug(message); + expect(mockTransport).not.toHaveBeenCalled(); + logger.info(message); + expect(mockTransport).not.toHaveBeenCalled(); + logger.warn(message); + expect(mockTransport).not.toHaveBeenCalled(); + var e = new Error('original message'); + logger.error(e); + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, undefined, e, { __metadata__: {} }, timestamp); + }); +}); diff --git a/src/logger/index.js b/src/logger/index.js new file mode 100644 index 0000000000..7751320194 --- /dev/null +++ b/src/logger/index.js @@ -0,0 +1,136 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { nanoid } from 'nanoid/non-secure'; +import { add } from '#/logger/logDump'; +import { consoleTransport } from '#/logger/transports/console'; +import { sentryTransport } from '#/logger/transports/sentry'; +import { LogContext, LogLevel, } from '#/logger/types'; +import { enabledLogLevels } from '#/logger/util'; +import { ENV } from '#/env'; +var TRANSPORTS = (function configureTransports() { + switch (ENV) { + case 'production': { + return [sentryTransport].filter(Boolean); + } + case 'test': { + return []; + } + default: { + return [consoleTransport]; + } + } +})(); +var Logger = /** @class */ (function () { + function Logger(_a) { + var _b = _a === void 0 ? {} : _a, level = _b.level, context = _b.context, contextFilter = _b.contextFilter, _c = _b.metadata, ambientMetadata = _c === void 0 ? {} : _c; + this.context = undefined; + this.contextFilter = ''; + this.ambientMetadata = {}; + this.debugContextRegexes = []; + this.transports = []; + this.context = context; + this.level = level || LogLevel.Info; + this.contextFilter = contextFilter || ''; + this.ambientMetadata = ambientMetadata; + if (this.contextFilter) { + this.level = LogLevel.Debug; + } + this.debugContextRegexes = (this.contextFilter || '') + .split(',') + .map(function (filter) { + return new RegExp(filter.replace(/[^\w:*-]/, '').replace(/\*/g, '.*')); + }); + } + Logger.create = function (context, metadata) { + if (metadata === void 0) { metadata = {}; } + var logger = new Logger({ + level: process.env.EXPO_PUBLIC_LOG_LEVEL, + context: context, + contextFilter: process.env.EXPO_PUBLIC_LOG_DEBUG || '', + metadata: metadata, + }); + for (var _i = 0, TRANSPORTS_1 = TRANSPORTS; _i < TRANSPORTS_1.length; _i++) { + var transport = TRANSPORTS_1[_i]; + logger.addTransport(transport); + } + return logger; + }; + Logger.prototype.debug = function (message, metadata) { + if (metadata === void 0) { metadata = {}; } + this.transport({ level: LogLevel.Debug, message: message, metadata: metadata }); + }; + Logger.prototype.info = function (message, metadata) { + if (metadata === void 0) { metadata = {}; } + this.transport({ level: LogLevel.Info, message: message, metadata: metadata }); + }; + Logger.prototype.log = function (message, metadata) { + if (metadata === void 0) { metadata = {}; } + this.transport({ level: LogLevel.Log, message: message, metadata: metadata }); + }; + Logger.prototype.warn = function (message, metadata) { + if (metadata === void 0) { metadata = {}; } + this.transport({ level: LogLevel.Warn, message: message, metadata: metadata }); + }; + Logger.prototype.error = function (error, metadata) { + if (metadata === void 0) { metadata = {}; } + this.transport({ level: LogLevel.Error, message: error, metadata: metadata }); + }; + Logger.prototype.addTransport = function (transport) { + var _this = this; + this.transports.push(transport); + return function () { + _this.transports.splice(_this.transports.indexOf(transport), 1); + }; + }; + Logger.prototype.transport = function (_a) { + var _this = this; + var level = _a.level, message = _a.message, _b = _a.metadata, metadata = _b === void 0 ? {} : _b; + if (level === LogLevel.Debug && + !!this.contextFilter && + !!this.context && + !this.debugContextRegexes.find(function (reg) { return reg.test(_this.context); })) + return; + var timestamp = Date.now(); + var meta = __assign({ __metadata__: this.ambientMetadata }, metadata); + // send every log to syslog + add({ + id: nanoid(), + timestamp: timestamp, + level: level, + context: this.context, + message: message, + metadata: meta, + }); + if (!enabledLogLevels[this.level].includes(level)) + return; + for (var _i = 0, _c = this.transports; _i < _c.length; _i++) { + var transport = _c[_i]; + transport(level, this.context, message, meta, timestamp); + } + }; + Logger.Level = LogLevel; + Logger.Context = LogContext; + return Logger; +}()); +export { Logger }; +/** + * Default logger instance. See `@/logger/README` for docs. + * + * Basic usage: + * + * `logger.debug(message[, metadata])` + * `logger.info(message[, metadata])` + * `logger.log(message[, metadata])` + * `logger.warn(message[, metadata])` + * `logger.error(error[, metadata])` + */ +export var logger = Logger.create(Logger.Context.Default); diff --git a/src/logger/logDump.js b/src/logger/logDump.js new file mode 100644 index 0000000000..967d4842e2 --- /dev/null +++ b/src/logger/logDump.js @@ -0,0 +1,8 @@ +var entries = []; +export function add(entry) { + entries.unshift(entry); + entries = entries.slice(0, 500); +} +export function getEntries() { + return entries; +} diff --git a/src/logger/sentry/lib/index.js b/src/logger/sentry/lib/index.js new file mode 100644 index 0000000000..42104f9b14 --- /dev/null +++ b/src/logger/sentry/lib/index.js @@ -0,0 +1 @@ +export * as Sentry from '@sentry/react-native'; diff --git a/src/logger/sentry/lib/index.web.js b/src/logger/sentry/lib/index.web.js new file mode 100644 index 0000000000..42104f9b14 --- /dev/null +++ b/src/logger/sentry/lib/index.web.js @@ -0,0 +1 @@ +export * as Sentry from '@sentry/react-native'; diff --git a/src/logger/sentry/setup/index.js b/src/logger/sentry/setup/index.js new file mode 100644 index 0000000000..8a519537f3 --- /dev/null +++ b/src/logger/sentry/setup/index.js @@ -0,0 +1,31 @@ +import { init } from '@sentry/react-native'; +import * as env from '#/env'; +init({ + enabled: !env.IS_DEV && !!env.SENTRY_DSN, + autoSessionTracking: false, + dsn: env.SENTRY_DSN, + debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production + environment: env.ENV, + dist: env.BUNDLE_IDENTIFIER, + release: env.RELEASE_VERSION, + ignoreErrors: [ + /* + * Unknown internals errors + */ + "t is not defined", + "Can't find variable: t", + /* + * Un-useful errors + */ + "Network request failed", + ], + /** + * Does not affect traces of error events or other logs, just disables + * automatically attaching stack traces to events. This helps us group events + * and prevents explosions of separate issues. + * + * @see https://docs.sentry.io/platforms/react-native/configuration/options/#attach-stacktrace + */ + attachStacktrace: false, + sampleRate: env.IS_INTERNAL ? 1.0 : 0.1, +}); diff --git a/src/logger/transports/console.js b/src/logger/transports/console.js new file mode 100644 index 0000000000..e9a2e67327 --- /dev/null +++ b/src/logger/transports/console.js @@ -0,0 +1,95 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import format from 'date-fns/format'; +import { LogLevel } from '#/logger/types'; +import { prepareMetadata } from '#/logger/util'; +import { IS_WEB } from '#/env'; +/** + * Used in dev mode to nicely log to the console + */ +export var consoleTransport = function (level, context, message, metadata, timestamp) { + var _a, _b; + var hasMetadata = Object.keys(metadata).length; + if (IS_WEB) { + var cssColor = (_a = {}, + _a[LogLevel.Debug] = 'magenta', + _a[LogLevel.Info] = 'dodgerblue', + _a[LogLevel.Log] = 'green', + _a[LogLevel.Warn] = 'orange', + _a[LogLevel.Error] = 'red', + _a)[level]; + var timestampStr = format(timestamp, 'HH:mm:ss'); + var contextStr = context ? " (".concat(context, ")") : ''; + var messageStr = message ? " ".concat(message.toString()) : ''; + var styledPart = "%c".concat(timestampStr).concat(contextStr, "%c").concat(messageStr); + var styles = ["color: ".concat(cssColor, "; font-weight: bold"), 'color: inherit']; + if (hasMetadata) { + console.groupCollapsed.apply(console, __spreadArray([styledPart], styles, false)); + console.log(prepareMetadata(metadata)); + console.groupEnd(); + } + else { + console.log.apply(console, __spreadArray([styledPart], styles, false)); + } + if (message instanceof Error) { + // for stacktrace + console.error(message); + } + } + else { + var colorize = withColor((_b = {}, + _b[LogLevel.Debug] = colors.magenta, + _b[LogLevel.Info] = colors.blue, + _b[LogLevel.Log] = colors.green, + _b[LogLevel.Warn] = colors.yellow, + _b[LogLevel.Error] = colors.red, + _b)[level]); + var msg = "".concat(colorize(format(timestamp, 'HH:mm:ss'))); + if (context) { + msg += " ".concat(colorize("(".concat(context, ")"))); + } + if (message) { + msg += " ".concat(message.toString()); + } + if (hasMetadata) { + msg += " ".concat(JSON.stringify(prepareMetadata(metadata), null, 2)); + } + console.log(msg); + if (message instanceof Error) { + // for stacktrace + console.error(message); + } + } +}; +/** + * Color handling copied from Kleur + * + * @see https://github.com/lukeed/kleur/blob/fa3454483899ddab550d08c18c028e6db1aab0e5/colors.mjs#L13 + */ +var colors = { + default: [0, 0], + blue: [36, 39], + green: [32, 39], + magenta: [35, 39], + red: [31, 39], + yellow: [33, 39], +}; +function withColor(_a) { + var x = _a[0], y = _a[1]; + var rgx = new RegExp("\\x1b\\[".concat(y, "m"), 'g'); + var open = "\u001B[".concat(x, "m"), close = "\u001B[".concat(y, "m"); + return function (txt) { + if (txt == null) + return txt; + return (open + + (~('' + txt).indexOf(close) ? txt.replace(rgx, close + open) : txt) + + close); + }; +} diff --git a/src/logger/transports/sentry.js b/src/logger/transports/sentry.js new file mode 100644 index 0000000000..b521c7706e --- /dev/null +++ b/src/logger/transports/sentry.js @@ -0,0 +1,104 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { isNetworkError } from '#/lib/strings/errors'; +import { Sentry } from '#/logger/sentry/lib'; +import { LogLevel } from '#/logger/types'; +import { prepareMetadata } from '#/logger/util'; +export var sentryTransport = function (level, context, message, _a, timestamp) { + var _b; + var type = _a.type, tags = _a.tags, metadata = __rest(_a, ["type", "tags"]); + // Skip debug messages entirely for now - esb + if (level === LogLevel.Debug) + return; + var meta = __assign({ __context__: context }, prepareMetadata(metadata)); + var _tags = tags || {}; + _tags = __assign({ + // use `category` to match breadcrumbs + category: context }, tags); + /** + * If a string, report a breadcrumb + */ + if (typeof message === 'string') { + var severity = (_b = {}, + _b[LogLevel.Debug] = 'debug', + _b[LogLevel.Info] = 'info', + _b[LogLevel.Log] = 'log', + _b[LogLevel.Warn] = 'warning', + _b[LogLevel.Error] = 'error', + _b)[level]; + Sentry.addBreadcrumb({ + category: context, + message: message, + data: meta, + type: type || 'default', + level: severity, + timestamp: timestamp / 1000, // Sentry expects seconds + }); + // We don't want to send any network errors to sentry + if (isNetworkError(message)) { + return; + } + /** + * Send all higher levels with `captureMessage`, with appropriate severity + * level + */ + if (level === 'error' || level === 'warn' || level === 'log') { + // Defer non-critical messages so they're sent in a batch + queueMessageForSentry(message, { + level: severity, + tags: _tags, + extra: meta, + }); + } + } + else { + /** + * It's otherwise an Error and should be reported with captureException + */ + Sentry.captureException(message, { + tags: _tags, + extra: meta, + }); + } +}; +var queuedMessages = []; +var sentrySendTimeout = null; +function queueMessageForSentry(message, captureContext) { + queuedMessages.push([message, captureContext]); + if (!sentrySendTimeout) { + // Throttle sending messages with a leading delay + // so that we can get Sentry out of the critical path. + sentrySendTimeout = setTimeout(function () { + sentrySendTimeout = null; + sendQueuedMessages(); + }, 7000); + } +} +function sendQueuedMessages() { + while (queuedMessages.length > 0) { + var record = queuedMessages.shift(); + if (record) { + Sentry.captureMessage(record[0], record[1]); + } + } +} diff --git a/src/logger/types.js b/src/logger/types.js new file mode 100644 index 0000000000..f61b6f5d2b --- /dev/null +++ b/src/logger/types.js @@ -0,0 +1,33 @@ +/** + * DO NOT IMPORT THIS DIRECTLY + * + * Logger contexts, defined here and used via `Logger.Context.*` static prop. + */ +export var LogContext; +(function (LogContext) { + LogContext["Default"] = "logger"; + LogContext["Session"] = "session"; + LogContext["Notifications"] = "notifications"; + LogContext["ConversationAgent"] = "conversation-agent"; + LogContext["DMsAgent"] = "dms-agent"; + LogContext["ReportDialog"] = "report-dialog"; + LogContext["FeedFeedback"] = "feed-feedback"; + LogContext["PostSource"] = "post-source"; + LogContext["AgeAssurance"] = "age-assurance"; + LogContext["PolicyUpdate"] = "policy-update"; + LogContext["Geolocation"] = "geolocation"; + LogContext["Drafts"] = "drafts"; + /** + * METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this + * context + */ + LogContext["Metric"] = "metric"; +})(LogContext || (LogContext = {})); +export var LogLevel; +(function (LogLevel) { + LogLevel["Debug"] = "debug"; + LogLevel["Info"] = "info"; + LogLevel["Log"] = "log"; + LogLevel["Warn"] = "warn"; + LogLevel["Error"] = "error"; +})(LogLevel || (LogLevel = {})); diff --git a/src/logger/util.js b/src/logger/util.js new file mode 100644 index 0000000000..cf55bcc4c0 --- /dev/null +++ b/src/logger/util.js @@ -0,0 +1,42 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var _a; +import { LogLevel } from '#/logger/types'; +export var enabledLogLevels = (_a = {}, + _a[LogLevel.Debug] = [ + LogLevel.Debug, + LogLevel.Info, + LogLevel.Log, + LogLevel.Warn, + LogLevel.Error, + ], + _a[LogLevel.Info] = [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error], + _a[LogLevel.Log] = [LogLevel.Log, LogLevel.Warn, LogLevel.Error], + _a[LogLevel.Warn] = [LogLevel.Warn, LogLevel.Error], + _a[LogLevel.Error] = [LogLevel.Error], + _a); +export function prepareMetadata(metadata) { + return Object.keys(metadata).reduce(function (acc, key) { + var _a; + var value = metadata[key]; + if (value instanceof Error) { + value = value.toString(); + } + if (typeof value === 'object' && + value !== null && + Object.keys(value).length === 0 && + value.constructor === Object) { + return acc; + } + return __assign(__assign({}, acc), (_a = {}, _a[key] = value, _a)); + }, {}); +} diff --git a/src/platform/crypto.js b/src/platform/crypto.js new file mode 100644 index 0000000000..bd7fa91934 --- /dev/null +++ b/src/platform/crypto.js @@ -0,0 +1,7 @@ +// HACK +// expo-modules-core tries to require('crypto') in uuid.web.js +// and while it tries to detect web crypto before doing so, our +// build fails when it tries to do this require. We use a babel +// and tsconfig alias to direct it here +// -prf +export default crypto; diff --git a/src/platform/markBundleStartTime.web.js b/src/platform/markBundleStartTime.web.js new file mode 100644 index 0000000000..6849a704bb --- /dev/null +++ b/src/platform/markBundleStartTime.web.js @@ -0,0 +1,2 @@ +// @ts-ignore Web-only. On RN, this is set by Metro. +window.__BUNDLE_START_TIME__ = performance.now(); diff --git a/src/platform/polyfills.js b/src/platform/polyfills.js new file mode 100644 index 0000000000..c6ba5bae56 --- /dev/null +++ b/src/platform/polyfills.js @@ -0,0 +1 @@ +import 'fast-text-encoding'; diff --git a/src/platform/polyfills.web.js b/src/platform/polyfills.web.js new file mode 100644 index 0000000000..612a37902f --- /dev/null +++ b/src/platform/polyfills.web.js @@ -0,0 +1,27 @@ +import 'array.prototype.findlast/auto'; +import 'setimmediate'; +if (process.env.NODE_ENV !== 'production') { + // In development, react-native-web's tries to validate that + // text is wrapped into . It doesn't catch all cases but is useful. + // Unfortunately, it only does that via console.error so it's easy to miss. + // This is a hack to get it showing as a redbox on the web so we catch it early. + var realConsoleError_1 = console.error; + var thrownErrors_1 = new WeakSet(); + console.error = function consoleErrorWrapper(msgOrError) { + if (typeof msgOrError === 'string' && + msgOrError.startsWith('Unexpected text node')) { + if (msgOrError === + 'Unexpected text node: . A text node cannot be a child of a .') { + // This is due to a stray empty string. + // React already handles this fine, so RNW warning is a false positive. Ignore. + return; + } + var err = new Error(msgOrError); + thrownErrors_1.add(err); + throw err; + } + else if (!thrownErrors_1.has(msgOrError)) { + return realConsoleError_1.apply(this, arguments); + } + }; +} diff --git a/src/routes.js b/src/routes.js new file mode 100644 index 0000000000..57079565ee --- /dev/null +++ b/src/routes.js @@ -0,0 +1,87 @@ +import { Router } from '#/lib/routes/router'; +export var router = new Router({ + Home: ['/', '/download'], + Search: '/search', + Feeds: '/feeds', + Notifications: '/notifications', + NotificationsActivityList: '/notifications/activity', + LegacyNotificationSettings: '/notifications/settings', + Settings: '/settings', + Lists: '/lists', + // moderation + Moderation: '/moderation', + ModerationModlists: '/moderation/modlists', + ModerationMutedAccounts: '/moderation/muted-accounts', + ModerationBlockedAccounts: '/moderation/blocked-accounts', + ModerationInteractionSettings: '/moderation/interaction-settings', + ModerationVerificationSettings: '/moderation/verification-settings', + // profiles, threads, lists + Profile: ['/profile/:name', '/profile/:name/rss'], + ProfileFollowers: '/profile/:name/followers', + ProfileFollows: '/profile/:name/follows', + ProfileKnownFollowers: '/profile/:name/known-followers', + ProfileSearch: '/profile/:name/search', + ProfileList: '/profile/:name/lists/:rkey', + PostThread: '/profile/:name/post/:rkey', + PostLikedBy: '/profile/:name/post/:rkey/liked-by', + PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', + PostQuotes: '/profile/:name/post/:rkey/quotes', + ProfileFeed: '/profile/:name/feed/:rkey', + ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by', + ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by', + // debug + Debug: '/sys/debug', + DebugMod: '/sys/debug-mod', + Log: '/sys/log', + // settings + LanguageSettings: '/settings/language', + AppPasswords: '/settings/app-passwords', + PreferencesFollowingFeed: '/settings/following-feed', + PreferencesThreads: '/settings/threads', + PreferencesExternalEmbeds: '/settings/external-embeds', + AccessibilitySettings: '/settings/accessibility', + AppearanceSettings: '/settings/appearance', + SavedFeeds: '/settings/saved-feeds', + AccountSettings: '/settings/account', + PrivacyAndSecuritySettings: '/settings/privacy-and-security', + ActivityPrivacySettings: '/settings/privacy-and-security/activity', + ContentAndMediaSettings: '/settings/content-and-media', + InterestsSettings: '/settings/interests', + AboutSettings: '/settings/about', + AppIconSettings: '/settings/app-icon', + NotificationSettings: '/settings/notifications', + ReplyNotificationSettings: '/settings/notifications/replies', + MentionNotificationSettings: '/settings/notifications/mentions', + QuoteNotificationSettings: '/settings/notifications/quotes', + LikeNotificationSettings: '/settings/notifications/likes', + RepostNotificationSettings: '/settings/notifications/reposts', + NewFollowerNotificationSettings: '/settings/notifications/new-followers', + LikesOnRepostsNotificationSettings: '/settings/notifications/likes-on-reposts', + RepostsOnRepostsNotificationSettings: '/settings/notifications/reposts-on-reposts', + ActivityNotificationSettings: '/settings/notifications/activity', + MiscellaneousNotificationSettings: '/settings/notifications/miscellaneous', + FindContactsSettings: '/settings/find-contacts', + // support + Support: '/support', + PrivacyPolicy: '/support/privacy', + TermsOfService: '/support/tos', + CommunityGuidelines: '/support/community-guidelines', + CopyrightPolicy: '/support/copyright', + // hashtags + Hashtag: '/hashtag/:tag', + Topic: '/topic/:topic', + // DMs + Messages: '/messages', + MessagesSettings: '/messages/settings', + MessagesInbox: '/messages/inbox', + MessagesConversation: '/messages/:conversation', + // starter packs + Start: '/start/:name/:rkey', + StarterPackEdit: '/starter-pack/edit/:rkey', + StarterPack: '/starter-pack/:name/:rkey', + StarterPackShort: '/starter-pack-short/:code', + StarterPackWizard: '/starter-pack/create', + VideoFeed: '/video-feed', + Bookmarks: '/saved', + FindContactsFlow: '/find-contacts', +}); diff --git a/src/screens/Bookmarks/components/EmptyState.js b/src/screens/Bookmarks/components/EmptyState.js new file mode 100644 index 0000000000..53ddf4c0be --- /dev/null +++ b/src/screens/Bookmarks/components/EmptyState.js @@ -0,0 +1,28 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme } from '#/alf'; +import { ButtonText } from '#/components/Button'; +import { BookmarkDeleteLarge } from '#/components/icons/Bookmark'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export function EmptyState() { + var t = useTheme(); + var _ = useLingui()._; + return (_jsxs(View, { style: [ + a.align_center, + { + paddingVertical: 64, + }, + ], children: [_jsx(BookmarkDeleteLarge, { width: 64, fill: t.atoms.text_contrast_medium.color }), _jsx(View, { style: [a.pt_sm], children: _jsx(Text, { style: [ + a.text_lg, + a.font_medium, + a.text_center, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Nothing saved yet" }) }) }), _jsx(View, { style: [a.pt_2xl], children: _jsx(Link, { to: "/", action: "navigate", label: _(msg({ + message: "Go home", + context: "Button to go back to the home timeline", + })), size: "small", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { context: "Button to go back to the home timeline", children: "Go home" }) }) }) })] })); +} diff --git a/src/screens/Bookmarks/index.js b/src/screens/Bookmarks/index.js new file mode 100644 index 0000000000..6d811674ab --- /dev/null +++ b/src/screens/Bookmarks/index.js @@ -0,0 +1,269 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { AppBskyFeedDefs, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect, useNavigation, } from '@react-navigation/native'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { usePostViewTracking } from '#/lib/hooks/usePostViewTracking'; +import { useBookmarkMutation } from '#/state/queries/bookmarks/useBookmarkMutation'; +import { useBookmarksQuery } from '#/state/queries/bookmarks/useBookmarksQuery'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { Post } from '#/view/com/post/Post'; +import { EmptyState } from '#/view/com/util/EmptyState'; +import { List } from '#/view/com/util/List'; +import { PostFeedLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { BookmarkDeleteLarge, BookmarkFilled } from '#/components/icons/Bookmark'; +import { CircleQuestion_Stroke2_Corner2_Rounded as QuestionIcon } from '#/components/icons/CircleQuestion'; +import * as Layout from '#/components/Layout'; +import { ListFooter } from '#/components/Lists'; +import * as Skele from '#/components/Skeleton'; +import * as toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS } from '#/env'; +export function BookmarksScreen(_a) { + var setMinimalShellMode = useSetMinimalShellMode(); + var ax = useAnalytics(); + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + ax.metric('bookmarks:view', {}); + }, [setMinimalShellMode, ax])); + return (_jsxs(Layout.Screen, { testID: "bookmarksScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Saved Posts" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(BookmarksInner, {})] })); +} +function BookmarksInner() { + var _this = this; + var _a; + var initialNumToRender = useInitialNumToRender(); + var cleanError = useCleanError(); + var _b = useState(false), isPTRing = _b[0], setIsPTRing = _b[1]; + var trackPostView = usePostViewTracking('Bookmarks'); + var _c = useBookmarksQuery(), data = _c.data, isLoading = _c.isLoading, isFetchingNextPage = _c.isFetchingNextPage, hasNextPage = _c.hasNextPage, fetchNextPage = _c.fetchNextPage, error = _c.error, refetch = _c.refetch; + var cleanedError = useMemo(function () { + var _a = cleanError(error), raw = _a.raw, clean = _a.clean; + return clean || raw; + }, [error, cleanError]); + var onRefresh = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTRing(true); + _a.label = 1; + case 1: + _a.trys.push([1, , 3, 4]); + return [4 /*yield*/, refetch()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + setIsPTRing(false); + return [7 /*endfinally*/]; + case 4: return [2 /*return*/]; + } + }); + }); }, [refetch, setIsPTRing]); + var onEndReached = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (isFetchingNextPage || !hasNextPage || error) + return [2 /*return*/]; + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPage()]; + case 2: + _b.sent(); + return [3 /*break*/, 4]; + case 3: + _a = _b.sent(); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]); + var items = useMemo(function () { + var i = []; + if (isLoading) { + i.push({ type: 'loading', key: 'loading' }); + } + else if (error || !data) { + // handled in Footer + } + else { + var bookmarks = data.pages.flatMap(function (p) { return p.bookmarks; }); + if (bookmarks.length > 0) { + for (var _i = 0, bookmarks_1 = bookmarks; _i < bookmarks_1.length; _i++) { + var bookmark = bookmarks_1[_i]; + if (AppBskyFeedDefs.isNotFoundPost(bookmark.item)) { + i.push({ + type: 'bookmarkNotFound', + key: bookmark.item.uri, + bookmark: __assign(__assign({}, bookmark), { item: bookmark.item }), + }); + } + if (AppBskyFeedDefs.isPostView(bookmark.item)) { + i.push({ + type: 'bookmark', + key: bookmark.item.uri, + bookmark: __assign(__assign({}, bookmark), { item: bookmark.item }), + }); + } + } + } + else { + i.push({ type: 'empty', key: 'empty' }); + } + } + return i; + }, [isLoading, error, data]); + var isEmpty = items.length === 1 && ((_a = items[0]) === null || _a === void 0 ? void 0 : _a.type) === 'empty'; + return (_jsx(List, { data: items, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTRing, onRefresh: onRefresh, onEndReached: onEndReached, onEndReachedThreshold: 4, onItemSeen: function (item) { + if (item.type === 'bookmark') { + trackPostView(item.bookmark.item); + } + }, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanedError, onRetry: fetchNextPage, style: [isEmpty && a.border_t_0] }), initialNumToRender: initialNumToRender, windowSize: 9, maxToRenderPerBatch: IS_IOS ? 5 : 1, updateCellsBatchingPeriod: 40, sideBorders: false })); +} +function BookmarkNotFound(_a) { + var _this = this; + var hideTopBorder = _a.hideTopBorder, post = _a.post; + var t = useTheme(); + var _ = useLingui()._; + var bookmark = useBookmarkMutation().mutateAsync; + var cleanError = useCleanError(); + var remove = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1, _a, raw, clean; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, bookmark({ action: 'delete', uri: post.uri })]; + case 1: + _b.sent(); + toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Removed from saved posts"], ["Removed from saved posts"])))), { + type: 'info', + }); + return [3 /*break*/, 3]; + case 2: + e_1 = _b.sent(); + _a = cleanError(e_1), raw = _a.raw, clean = _a.clean; + toast.show(clean || raw || e_1, { + type: 'error', + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_start, + a.px_xl, + a.py_lg, + a.gap_sm, + !hideTopBorder && a.border_t, + t.atoms.border_contrast_low, + ], children: [_jsx(Skele.Circle, { size: 42, children: _jsx(QuestionIcon, { size: "lg", fill: t.atoms.text_contrast_low.color }) }), _jsxs(View, { style: [a.flex_1, a.gap_2xs], children: [_jsxs(View, { style: [a.flex_row, a.gap_xs], children: [_jsx(Skele.Text, { style: [a.text_md, { width: 80 }] }), _jsx(Skele.Text, { style: [a.text_md, { width: 100 }] })] }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + a.italic, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "This post was deleted by its author" }) })] }), _jsxs(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Remove from saved posts"], ["Remove from saved posts"])))), size: "tiny", color: "secondary", onPress: remove, children: [_jsx(ButtonIcon, { icon: BookmarkFilled }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Remove" }) })] })] })); +} +function BookmarkItem(_a) { + var item = _a.item, hideTopBorder = _a.hideTopBorder; + var ax = useAnalytics(); + return (_jsx(Post, { post: item.bookmark.item, hideTopBorder: hideTopBorder, onBeforePress: function () { + ax.metric('bookmarks:post-clicked', {}); + } })); +} +function BookmarksEmpty() { + var t = useTheme(); + var _ = useLingui()._; + var navigation = useNavigation(); + return (_jsx(EmptyState, { icon: BookmarkDeleteLarge, message: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Nothing saved yet"], ["Nothing saved yet"])))), textStyle: [t.atoms.text_contrast_medium, a.font_medium], button: { + label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Button to go back to the home timeline"], ["Button to go back to the home timeline"])))), + text: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Go home"], ["Go home"])))), + onPress: function () { return navigation.navigate('Home'); }, + size: 'small', + color: 'secondary', + }, style: [a.pt_3xl] })); +} +function renderItem(_a) { + var item = _a.item, index = _a.index; + switch (item.type) { + case 'loading': { + return _jsx(PostFeedLoadingPlaceholder, {}); + } + case 'empty': { + return _jsx(BookmarksEmpty, {}); + } + case 'bookmark': { + return _jsx(BookmarkItem, { item: item, hideTopBorder: index === 0 }); + } + case 'bookmarkNotFound': { + return (_jsx(BookmarkNotFound, { post: item.bookmark.item, hideTopBorder: index === 0 })); + } + default: + return null; + } +} +var keyExtractor = function (item) { return item.key; }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/screens/Deactivated.js b/src/screens/Deactivated.js new file mode 100644 index 0000000000..2cdf342ee4 --- /dev/null +++ b/src/screens/Deactivated.js @@ -0,0 +1,163 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAccountSwitcher } from '#/lib/hooks/useAccountSwitcher'; +import { logger } from '#/logger'; +import { useAgent, useSession, useSessionApi, } from '#/state/session'; +import { agentToSessionAccountOrThrow } from '#/state/session/agent'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { Logo } from '#/view/icons/Logo'; +import { atoms as a, useTheme } from '#/alf'; +import { AccountList } from '#/components/AccountList'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Divider } from '#/components/Divider'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +var COL_WIDTH = 400; +export function Deactivated() { + var _this = this; + var _ = useLingui()._; + var t = useTheme(); + var insets = useSafeAreaInsets(); + var _a = useSession(), currentAccount = _a.currentAccount, accounts = _a.accounts; + var _b = useAccountSwitcher(), onPressSwitchAccount = _b.onPressSwitchAccount, pendingDid = _b.pendingDid; + var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut; + var hasOtherAccounts = accounts.length > 1; + var _c = useSessionApi(), logoutCurrentAccount = _c.logoutCurrentAccount, resumeSession = _c.resumeSession; + var agent = useAgent(); + var _d = React.useState(false), pending = _d[0], setPending = _d[1]; + var _e = React.useState(), error = _e[0], setError = _e[1]; + var queryClient = useQueryClient(); + var onSelectAccount = React.useCallback(function (account) { + if (account.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + onPressSwitchAccount(account, 'SwitchAccount'); + } + }, [currentAccount, onPressSwitchAccount]); + var onPressAddAccount = React.useCallback(function () { + setShowLoggedOut(true); + }, [setShowLoggedOut]); + var onPressLogout = React.useCallback(function () { + if (IS_WEB) { + // We're switching accounts, which remounts the entire app. + // On mobile, this gets us Home, but on the web we also need reset the URL. + // We can't change the URL via a navigate() call because the navigator + // itself is about to unmount, and it calls pushState() too late. + // So we change the URL ourselves. The navigator will pick it up on remount. + history.pushState(null, '', '/'); + } + logoutCurrentAccount('Deactivated'); + }, [logoutCurrentAccount]); + var handleActivate = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var account, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 4, 5, 6]); + setPending(true); + return [4 /*yield*/, agent.com.atproto.server.activateAccount()]; + case 1: + _a.sent(); + return [4 /*yield*/, queryClient.resetQueries()]; + case 2: + _a.sent(); + account = agentToSessionAccountOrThrow(agent); + return [4 /*yield*/, resumeSession(__assign(__assign({}, account), { active: true, status: undefined }))]; + case 3: + _a.sent(); + return [3 /*break*/, 6]; + case 4: + e_1 = _a.sent(); + switch (e_1.message) { + case 'Bad token scope': + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You're signed in with an App Password. Please sign in with your main password to continue deactivating your account."], ["You're signed in with an App Password. Please sign in with your main password to continue deactivating your account."]))))); + break; + default: + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Something went wrong, please try again"], ["Something went wrong, please try again"]))))); + break; + } + logger.error(e_1, { + message: 'Failed to activate account', + }); + return [3 /*break*/, 6]; + case 5: + setPending(false); + return [7 /*endfinally*/]; + case 6: return [2 /*return*/]; + } + }); + }); }, [_, agent, queryClient, resumeSession]); + return (_jsx(View, { style: [a.util_screen_outer, a.flex_1], children: _jsx(Layout.Content, { ignoreTabletLayoutOffset: true, contentContainerStyle: [ + a.px_2xl, + { + paddingTop: IS_WEB ? 64 : insets.top + 16, + paddingBottom: IS_WEB ? 64 : insets.bottom, + }, + ], children: _jsxs(View, { style: [a.w_full, { marginHorizontal: 'auto', maxWidth: COL_WIDTH }], children: [_jsx(View, { style: [a.w_full, a.justify_center, a.align_center, a.pb_5xl], children: _jsx(Logo, { width: 40 }) }), _jsxs(View, { style: [a.gap_xs, a.pb_3xl], children: [_jsx(Text, { style: [a.text_xl, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { children: "Welcome back!" }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug], children: _jsxs(Trans, { children: ["You previously deactivated @", currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.handle, "."] }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug, a.pb_md], children: _jsx(Trans, { children: "You can reactivate your account to continue logging in. Your profile and posts will be visible to other users." }) }), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Reactivate your account"], ["Reactivate your account"])))), size: "large", variant: "solid", color: "primary", onPress: handleActivate, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Yes, reactivate my account" }) }), pending && _jsx(ButtonIcon, { icon: Loader, position: "right" })] }), _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Cancel reactivation and sign out"], ["Cancel reactivation and sign out"])))), size: "large", variant: "solid", color: "secondary", onPress: onPressLogout, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] }), error && (_jsxs(View, { style: [ + a.flex_row, + a.gap_sm, + a.mt_md, + a.p_md, + a.rounded_sm, + t.atoms.bg_contrast_25, + ], children: [_jsx(CircleInfo, { size: "md", fill: t.palette.negative_400 }), _jsx(Text, { style: [a.flex_1, a.leading_snug], children: error })] }))] }), _jsx(View, { style: [a.pb_3xl], children: _jsx(Divider, {}) }), hasOtherAccounts ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: [t.atoms.text_contrast_medium, a.pb_md, a.leading_snug], children: _jsx(Trans, { children: "Or, sign in to one of your other accounts." }) }), _jsx(AccountList, { onSelectAccount: onSelectAccount, onSelectOther: onPressAddAccount, otherLabel: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Add account"], ["Add account"])))), pendingDid: pendingDid })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { style: [t.atoms.text_contrast_medium, a.pb_md, a.leading_snug], children: _jsx(Trans, { children: "Or, continue with another account." }) }), _jsx(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Sign in or create an account"], ["Sign in or create an account"])))), size: "large", variant: "solid", color: "secondary", onPress: function () { return setShowLoggedOut(true); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Sign in or create an account" }) }) })] }))] }) }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/E2E/SharedPreferencesTesterScreen.js b/src/screens/E2E/SharedPreferencesTesterScreen.js new file mode 100644 index 0000000000..323ebee861 --- /dev/null +++ b/src/screens/E2E/SharedPreferencesTesterScreen.js @@ -0,0 +1,103 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { ScrollView } from '#/view/com/util/Views'; +import { atoms as a } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +import { SharedPrefs } from '../../../modules/expo-bluesky-swiss-army'; +export function SharedPreferencesTesterScreen() { + var _this = this; + var _a = React.useState(''), currentTestOutput = _a[0], setCurrentTestOutput = _a[1]; + return (_jsx(Layout.Screen, { children: _jsx(ScrollView, { contentContainerStyle: { backgroundColor: 'red' }, children: _jsxs(View, { style: [a.flex_1], children: [_jsx(View, { children: _jsx(Text, { testID: "testOutput", children: currentTestOutput }) }), _jsxs(View, { style: [a.flex_wrap], children: [_jsx(Button, { label: "btn", testID: "setStringBtn", style: [a.self_center], variant: "solid", color: "primary", size: "small", onPress: function () { return __awaiter(_this, void 0, void 0, function () { + var str; + return __generator(this, function (_a) { + SharedPrefs.removeValue('testerString'); + SharedPrefs.setValue('testerString', 'Hello'); + str = SharedPrefs.getString('testerString'); + console.log(JSON.stringify(str)); + setCurrentTestOutput("".concat(str)); + return [2 /*return*/]; + }); + }); }, children: _jsx(ButtonText, { children: "Set String" }) }), _jsx(Button, { label: "btn", testID: "removeStringBtn", style: [a.self_center], variant: "solid", color: "primary", size: "small", onPress: function () { return __awaiter(_this, void 0, void 0, function () { + var str; + return __generator(this, function (_a) { + SharedPrefs.removeValue('testerString'); + str = SharedPrefs.getString('testerString'); + setCurrentTestOutput("".concat(str)); + return [2 /*return*/]; + }); + }); }, children: _jsx(ButtonText, { children: "Remove String" }) }), _jsx(Button, { label: "btn", testID: "setBoolBtn", style: [a.self_center], variant: "solid", color: "primary", size: "small", onPress: function () { return __awaiter(_this, void 0, void 0, function () { + var bool; + return __generator(this, function (_a) { + SharedPrefs.removeValue('testerBool'); + SharedPrefs.setValue('testerBool', true); + bool = SharedPrefs.getBool('testerBool'); + setCurrentTestOutput("".concat(bool)); + return [2 /*return*/]; + }); + }); }, children: _jsx(ButtonText, { children: "Set Bool" }) }), _jsx(Button, { label: "btn", testID: "setNumberBtn", style: [a.self_center], variant: "solid", color: "primary", size: "small", onPress: function () { return __awaiter(_this, void 0, void 0, function () { + var num; + return __generator(this, function (_a) { + SharedPrefs.removeValue('testerNumber'); + SharedPrefs.setValue('testerNumber', 123); + num = SharedPrefs.getNumber('testerNumber'); + setCurrentTestOutput("".concat(num)); + return [2 /*return*/]; + }); + }); }, children: _jsx(ButtonText, { children: "Set Number" }) }), _jsx(Button, { label: "btn", testID: "addToSetBtn", style: [a.self_center], variant: "solid", color: "primary", size: "small", onPress: function () { return __awaiter(_this, void 0, void 0, function () { + var contains; + return __generator(this, function (_a) { + SharedPrefs.removeFromSet('testerSet', 'Hello!'); + SharedPrefs.addToSet('testerSet', 'Hello!'); + contains = SharedPrefs.setContains('testerSet', 'Hello!'); + setCurrentTestOutput("".concat(contains)); + return [2 /*return*/]; + }); + }); }, children: _jsx(ButtonText, { children: "Add to Set" }) }), _jsx(Button, { label: "btn", testID: "removeFromSetBtn", style: [a.self_center], variant: "solid", color: "primary", size: "small", onPress: function () { return __awaiter(_this, void 0, void 0, function () { + var contains; + return __generator(this, function (_a) { + SharedPrefs.removeFromSet('testerSet', 'Hello!'); + contains = SharedPrefs.setContains('testerSet', 'Hello!'); + setCurrentTestOutput("".concat(contains)); + return [2 /*return*/]; + }); + }); }, children: _jsx(ButtonText, { children: "Remove from Set" }) })] })] }) }) })); +} diff --git a/src/screens/Feeds/NoFollowingFeed.js b/src/screens/Feeds/NoFollowingFeed.js new file mode 100644 index 0000000000..d86d16960e --- /dev/null +++ b/src/screens/Feeds/NoFollowingFeed.js @@ -0,0 +1,42 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { TIMELINE_SAVED_FEED } from '#/lib/constants'; +import { useAddSavedFeedsMutation } from '#/state/queries/preferences'; +import { atoms as a, useTheme } from '#/alf'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export function NoFollowingFeed(_a) { + var onAddFeed = _a.onAddFeed; + var t = useTheme(); + var _ = useLingui()._; + var addSavedFeeds = useAddSavedFeedsMutation().mutateAsync; + var addRecommendedFeeds = function (e) { + e.preventDefault(); + addSavedFeeds([ + __assign(__assign({}, TIMELINE_SAVED_FEED), { pinned: true }), + ]); + onAddFeed === null || onAddFeed === void 0 ? void 0 : onAddFeed(); + // prevent navigation + return false; + }; + return (_jsx(View, { style: [a.flex_row, a.flex_wrap, a.align_center, a.py_md, a.px_lg], children: _jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Looks like you're missing a following feed.", ' ', _jsx(InlineLinkText, { to: "#", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Add the default feed of only people you follow"], ["Add the default feed of only people you follow"])))), onPress: addRecommendedFeeds, style: [a.leading_snug], children: "Click here to add one." })] }) }) })); +} +var templateObject_1; diff --git a/src/screens/Feeds/NoSavedFeedsOfAnyType.js b/src/screens/Feeds/NoSavedFeedsOfAnyType.js new file mode 100644 index 0000000000..1793426a92 --- /dev/null +++ b/src/screens/Feeds/NoSavedFeedsOfAnyType.js @@ -0,0 +1,89 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { TID } from '@atproto/common-web'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { RECOMMENDED_SAVED_FEEDS } from '#/lib/constants'; +import { useOverwriteSavedFeedsMutation } from '#/state/queries/preferences'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { Text } from '#/components/Typography'; +/** + * Explicitly named, since the CTA in this component will overwrite all saved + * feeds if pressed. It should only be presented to the user if they actually + * have no other feeds saved. + */ +export function NoSavedFeedsOfAnyType(_a) { + var _this = this; + var onAddRecommendedFeeds = _a.onAddRecommendedFeeds; + var t = useTheme(); + var _ = useLingui()._; + var _b = useOverwriteSavedFeedsMutation(), isPending = _b.isPending, overwriteSavedFeeds = _b.mutateAsync; + var addRecommendedFeeds = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + onAddRecommendedFeeds === null || onAddRecommendedFeeds === void 0 ? void 0 : onAddRecommendedFeeds(); + return [4 /*yield*/, overwriteSavedFeeds(RECOMMENDED_SAVED_FEEDS.map(function (f) { return (__assign(__assign({}, f), { id: TID.nextStr() })); }))]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(View, { style: [a.flex_row, a.flex_wrap, a.justify_between, a.p_xl, a.gap_md], children: [_jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_medium, { maxWidth: 310 }], children: _jsx(Trans, { children: "Looks like you haven't saved any feeds! Use our recommendations or browse more below." }) }), _jsxs(Button, { disabled: isPending, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Apply default recommended feeds"], ["Apply default recommended feeds"])))), size: "small", color: "primary_subtle", onPress: addRecommendedFeeds, children: [_jsx(ButtonIcon, { icon: Plus }), _jsx(ButtonText, { children: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Use recommended"], ["Use recommended"])))) })] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/FindContactsFlowScreen.js b/src/screens/FindContactsFlowScreen.js new file mode 100644 index 0000000000..b12d9f39fe --- /dev/null +++ b/src/screens/FindContactsFlowScreen.js @@ -0,0 +1,45 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useCallback, useLayoutEffect, useState } from 'react'; +import { LayoutAnimationConfig } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { usePreventRemove } from '@react-navigation/native'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { ErrorScreen } from '#/view/com/util/error/ErrorScreen'; +import { FindContactsFlow } from '#/components/contacts/FindContactsFlow'; +import { useFindContactsFlowState } from '#/components/contacts/state'; +import * as Layout from '#/components/Layout'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { IS_NATIVE } from '#/env'; +export function FindContactsFlowScreen(_a) { + var navigation = _a.navigation; + var _ = useLingui()._; + var _b = useFindContactsFlowState(), state = _b[0], dispatch = _b[1]; + var _c = useState('Forward'), transitionDirection = _c[0], setTransitionDirection = _c[1]; + var overrideGoBack = state.step === '2: verify number'; + usePreventRemove(overrideGoBack, function () { + setTransitionDirection('Backward'); + dispatch({ type: 'BACK' }); + setTimeout(function () { + setTransitionDirection('Forward'); + }); + }); + var setMinimalShellMode = useSetMinimalShellMode(); + var effect = useCallback(function () { + setMinimalShellMode(true); + return function () { return setMinimalShellMode(false); }; + }, [setMinimalShellMode]); + useLayoutEffect(effect); + return (_jsx(Layout.Screen, { children: IS_NATIVE ? (_jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: _jsx(ScreenTransition, { direction: transitionDirection, children: _jsx(FindContactsFlow, { state: state, dispatch: dispatch, onCancel: function () { + return navigation.canGoBack() + ? navigation.goBack() + : navigation.navigate('FindContactsFlow', undefined, { + pop: true, + }); + }, context: "Standalone" }) }, state.step) })) : (_jsx(ErrorScreen, { title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Not available on this platform."], ["Not available on this platform."])))), message: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please use the native app to sync your contacts."], ["Please use the native app to sync your contacts."])))), showHeader: true })) })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Hashtag.js b/src/screens/Hashtag.js new file mode 100644 index 0000000000..6b14bf25fb --- /dev/null +++ b/src/screens/Hashtag.js @@ -0,0 +1,199 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { HITSLOP_10 } from '#/lib/constants'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { usePostViewTracking } from '#/lib/hooks/usePostViewTracking'; +import { shareUrl } from '#/lib/sharing'; +import { cleanError } from '#/lib/strings/errors'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { enforceLen } from '#/lib/strings/helpers'; +import { useSearchPostsQuery } from '#/state/queries/search-posts'; +import { useSession } from '#/state/session'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { useCloseAllActiveElements } from '#/state/util'; +import { Pager } from '#/view/com/pager/Pager'; +import { TabBar } from '#/view/com/pager/TabBar'; +import { Post } from '#/view/com/post/Post'; +import { List } from '#/view/com/util/List'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share } from '#/components/icons/ArrowOutOfBox'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import { ListFooter, ListMaybePlaceholder } from '#/components/Lists'; +import { SearchError } from '#/components/SearchError'; +import { Text } from '#/components/Typography'; +var renderItem = function (_a) { + var item = _a.item; + return _jsx(Post, { post: item }); +}; +var keyExtractor = function (item, index) { + return "".concat(item.uri, "-").concat(index); +}; +export default function HashtagScreen(_a) { + var route = _a.route; + var _b = route.params, tag = _b.tag, author = _b.author; + var _ = useLingui()._; + var decodedTag = React.useMemo(function () { + return decodeURIComponent(tag); + }, [tag]); + var isCashtag = decodedTag.startsWith('$'); + var fullTag = React.useMemo(function () { + // Cashtags already include the $ prefix, hashtags need # added + return isCashtag ? decodedTag : "#".concat(decodedTag); + }, [decodedTag, isCashtag]); + var headerTitle = React.useMemo(function () { + // Keep cashtags uppercase, lowercase hashtags + var displayTag = isCashtag ? fullTag.toUpperCase() : fullTag.toLowerCase(); + return enforceLen(displayTag, 24, true, 'middle'); + }, [fullTag, isCashtag]); + var sanitizedAuthor = React.useMemo(function () { + if (!author) + return ''; + return sanitizeHandle(author); + }, [author]); + var onShare = React.useCallback(function () { + var url = new URL('https://bsky.app'); + url.pathname = "/hashtag/".concat(decodeURIComponent(tag)); + if (author) { + url.searchParams.set('author', author); + } + shareUrl(url.toString()); + }, [tag, author]); + var _c = React.useState(0), activeTab = _c[0], setActiveTab = _c[1]; + var setMinimalShellMode = useSetMinimalShellMode(); + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var onPageSelected = React.useCallback(function (index) { + setMinimalShellMode(false); + setActiveTab(index); + }, [setMinimalShellMode]); + var sections = React.useMemo(function () { + return [ + { + title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Top"], ["Top"])))), + component: (_jsx(HashtagScreenTab, { fullTag: fullTag, author: author, sort: "top", active: activeTab === 0 })), + }, + { + title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Latest"], ["Latest"])))), + component: (_jsx(HashtagScreenTab, { fullTag: fullTag, author: author, sort: "latest", active: activeTab === 1 })), + }, + ]; + }, [_, fullTag, author, activeTab]); + return (_jsx(Layout.Screen, { children: _jsx(Pager, { onPageSelected: onPageSelected, renderTabBar: function (props) { return (_jsxs(Layout.Center, { style: [a.z_10, web([a.sticky, { top: 0 }])], children: [_jsxs(Layout.Header.Outer, { noBottomBorder: true, children: [_jsx(Layout.Header.BackButton, {}), _jsxs(Layout.Header.Content, { children: [_jsx(Layout.Header.TitleText, { children: headerTitle }), author && (_jsx(Layout.Header.SubtitleText, { children: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["From @", ""], ["From @", ""])), sanitizedAuthor)) }))] }), _jsx(Layout.Header.Slot, { children: _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Share"], ["Share"])))), size: "small", variant: "ghost", color: "primary", shape: "round", onPress: onShare, hitSlop: HITSLOP_10, style: [{ right: -3 }], children: _jsx(ButtonIcon, { icon: Share, size: "md" }) }) })] }), _jsx(TabBar, __assign({ items: sections.map(function (section) { return section.title; }) }, props))] })); }, initialPage: 0, children: sections.map(function (section, i) { return (_jsx(View, { children: section.component }, i)); }) }) })); +} +function HashtagScreenTab(_a) { + var _this = this; + var fullTag = _a.fullTag, author = _a.author, sort = _a.sort, active = _a.active; + var _ = useLingui()._; + var initialNumToRender = useInitialNumToRender(); + var _b = React.useState(false), isPTR = _b[0], setIsPTR = _b[1]; + var t = useTheme(); + var hasSession = useSession().hasSession; + var trackPostView = usePostViewTracking('Hashtag'); + var isCashtag = fullTag.startsWith('$'); + var queryParam = React.useMemo(function () { + // Cashtags need # prefix for search: "#$BTC" or "#$BTC from:author" + var searchTag = isCashtag ? "#".concat(fullTag) : fullTag; + if (!author) + return searchTag; + return "".concat(searchTag, " from:").concat(author); + }, [fullTag, author, isCashtag]); + var _c = useSearchPostsQuery({ query: queryParam, sort: sort, enabled: active }), data = _c.data, isFetched = _c.isFetched, isFetchingNextPage = _c.isFetchingNextPage, isLoading = _c.isLoading, isError = _c.isError, error = _c.error, refetch = _c.refetch, fetchNextPage = _c.fetchNextPage, hasNextPage = _c.hasNextPage; + var posts = React.useMemo(function () { + return (data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { return page.posts; })) || []; + }, [data]); + var onRefresh = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTR(true); + return [4 /*yield*/, refetch()]; + case 1: + _a.sent(); + setIsPTR(false); + return [2 /*return*/]; + } + }); + }); }, [refetch]); + var onEndReached = React.useCallback(function () { + if (isFetchingNextPage || !hasNextPage || error) + return; + fetchNextPage(); + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]); + var closeAllActiveElements = useCloseAllActiveElements(); + var requestSwitchToAccount = useLoggedOutViewControls().requestSwitchToAccount; + var showSignIn = function () { + closeAllActiveElements(); + requestSwitchToAccount({ requestedAccount: 'none' }); + }; + var showCreateAccount = function () { + closeAllActiveElements(); + requestSwitchToAccount({ requestedAccount: 'new' }); + }; + if (!hasSession) { + return (_jsx(SearchError, { title: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Search is currently unavailable when logged out"], ["Search is currently unavailable when logged out"])))), children: _jsx(Text, { style: [a.text_md, a.text_center, a.leading_snug], children: _jsxs(Trans, { children: [_jsx(InlineLinkText, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Sign in"], ["Sign in"])))), to: '#', onPress: showSignIn, children: "Sign in" }), _jsx(Text, { style: t.atoms.text_contrast_medium, children: " or " }), _jsx(InlineLinkText, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Create an account"], ["Create an account"])))), to: '#', onPress: showCreateAccount, children: "create an account" }), _jsx(Text, { children: " " }), _jsx(Text, { style: t.atoms.text_contrast_medium, children: "to search for news, sports, politics, and everything else happening on Bluesky." })] }) }) })); + } + return (_jsx(_Fragment, { children: posts.length < 1 ? (_jsx(ListMaybePlaceholder, { isLoading: isLoading || !isFetched, isError: isError, onRetry: refetch, emptyType: "results", emptyMessage: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["We couldn't find any results for that tag."], ["We couldn't find any results for that tag."])))) })) : (_jsx(List, { data: posts, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTR, onRefresh: onRefresh, onEndReached: onEndReached, onEndReachedThreshold: 4, onItemSeen: trackPostView, + // @ts-ignore web only -prf + desktopFixedHeight: true, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage }), initialNumToRender: initialNumToRender, windowSize: 11 })) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/screens/Home/NoFeedsPinned.js b/src/screens/Home/NoFeedsPinned.js new file mode 100644 index 0000000000..58330f3b5b --- /dev/null +++ b/src/screens/Home/NoFeedsPinned.js @@ -0,0 +1,127 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { TID } from '@atproto/common-web'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED } from '#/lib/constants'; +import { useOverwriteSavedFeedsMutation } from '#/state/queries/preferences'; +import { CenteredView } from '#/view/com/util/Views'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useHeaderOffset } from '#/components/hooks/useHeaderOffset'; +import { ListSparkle_Stroke2_Corner0_Rounded as ListSparkle } from '#/components/icons/ListSparkle'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export function NoFeedsPinned(_a) { + var _this = this; + var preferences = _a.preferences; + var _ = useLingui()._; + var headerOffset = useHeaderOffset(); + var _b = useOverwriteSavedFeedsMutation(), isPending = _b.isPending, overwriteSavedFeeds = _b.mutateAsync; + var addRecommendedFeeds = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var skippedTimeline, skippedDiscover, remainingSavedFeeds, _i, _a, savedFeed, toSave; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + skippedTimeline = false; + skippedDiscover = false; + remainingSavedFeeds = []; + // remove first instance of both timeline and discover, since we're going to overwrite them + for (_i = 0, _a = preferences.savedFeeds; _i < _a.length; _i++) { + savedFeed = _a[_i]; + if (savedFeed.type === 'timeline' && !skippedTimeline) { + skippedTimeline = true; + } + else if (savedFeed.value === DISCOVER_SAVED_FEED.value && + !skippedDiscover) { + skippedDiscover = true; + } + else { + remainingSavedFeeds.push(savedFeed); + } + } + toSave = __spreadArray([ + __assign(__assign({}, DISCOVER_SAVED_FEED), { pinned: true, id: TID.nextStr() }), + __assign(__assign({}, TIMELINE_SAVED_FEED), { pinned: true, id: TID.nextStr() }) + ], remainingSavedFeeds, true); + return [4 /*yield*/, overwriteSavedFeeds(toSave)]; + case 1: + _b.sent(); + return [2 /*return*/]; + } + }); + }); }, [overwriteSavedFeeds, preferences.savedFeeds]); + return (_jsx(CenteredView, { sideBorders: true, style: [a.h_full_vh], children: _jsxs(View, { style: [ + a.align_center, + a.h_full_vh, + a.py_3xl, + a.px_xl, + { + paddingTop: headerOffset + a.py_3xl.paddingTop, + }, + ], children: [_jsxs(View, { style: [a.align_center, a.gap_sm, a.pb_xl], children: [_jsx(Text, { style: [a.text_xl, a.font_semi_bold], children: _jsx(Trans, { children: "Whoops!" }) }), _jsx(Text, { style: [a.text_md, a.text_center, a.leading_snug, { maxWidth: 340 }], children: _jsx(Trans, { children: "Looks like you unpinned all your feeds. But don't worry, you can add some below \uD83D\uDE04" }) })] }), _jsxs(View, { style: [a.flex_row, a.gap_md, a.justify_center, a.flex_wrap], children: [_jsxs(Button, { disabled: isPending, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Apply default recommended feeds"], ["Apply default recommended feeds"])))), size: "large", variant: "solid", color: "primary", onPress: addRecommendedFeeds, children: [_jsx(ButtonIcon, { icon: Plus, position: "left" }), _jsx(ButtonText, { children: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Add recommended feeds"], ["Add recommended feeds"])))) })] }), _jsxs(Link, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Browse other feeds"], ["Browse other feeds"])))), to: "/feeds", size: "large", variant: "solid", color: "secondary", children: [_jsx(ButtonIcon, { icon: ListSparkle, position: "left" }), _jsx(ButtonText, { children: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Browse other feeds"], ["Browse other feeds"])))) })] })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/List/ListHiddenScreen.js b/src/screens/List/ListHiddenScreen.js new file mode 100644 index 0000000000..adc1662d87 --- /dev/null +++ b/src/screens/List/ListHiddenScreen.js @@ -0,0 +1,176 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { AppBskyGraphDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { useGoBack } from '#/lib/hooks/useGoBack'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { RQKEY_ROOT as listQueryRoot, useListBlockMutation, useListMuteMutation, } from '#/state/queries/list'; +import { useRemoveFeedMutation, } from '#/state/queries/preferences'; +import { useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { CenteredView } from '#/view/com/util/Views'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { EyeSlash_Stroke2_Corner0_Rounded as EyeSlash } from '#/components/icons/EyeSlash'; +import { Loader } from '#/components/Loader'; +import { useHider } from '#/components/moderation/Hider'; +import { Text } from '#/components/Typography'; +export function ListHiddenScreen(_a) { + var _this = this; + var _b, _c, _d, _e, _f, _g; + var list = _a.list, preferences = _a.preferences; + var _ = useLingui()._; + var t = useTheme(); + var currentAccount = useSession().currentAccount; + var gtMobile = useBreakpoints().gtMobile; + var isOwner = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === list.creator.did; + var goBack = useGoBack(); + var queryClient = useQueryClient(); + var isModList = list.purpose === AppBskyGraphDefs.MODLIST; + var _h = React.useState(false), isProcessing = _h[0], setIsProcessing = _h[1]; + var listBlockMutation = useListBlockMutation(); + var listMuteMutation = useListMuteMutation(); + var removeSavedFeed = useRemoveFeedMutation().mutateAsync; + var setIsContentVisible = useHider().setIsContentVisible; + var savedFeedConfig = preferences.savedFeeds.find(function (f) { return f.value === list.uri; }); + var onUnsubscribe = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1, e_2; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + setIsProcessing(true); + if (!((_a = list.viewer) === null || _a === void 0 ? void 0 : _a.muted)) return [3 /*break*/, 4]; + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, listMuteMutation.mutateAsync({ uri: list.uri, mute: false })]; + case 2: + _c.sent(); + return [3 /*break*/, 4]; + case 3: + e_1 = _c.sent(); + setIsProcessing(false); + logger.error('Failed to unmute list', { message: e_1 }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."]))))); + return [2 /*return*/]; + case 4: + if (!((_b = list.viewer) === null || _b === void 0 ? void 0 : _b.blocked)) return [3 /*break*/, 8]; + _c.label = 5; + case 5: + _c.trys.push([5, 7, , 8]); + return [4 /*yield*/, listBlockMutation.mutateAsync({ uri: list.uri, block: false })]; + case 6: + _c.sent(); + return [3 /*break*/, 8]; + case 7: + e_2 = _c.sent(); + setIsProcessing(false); + logger.error('Failed to unblock list', { message: e_2 }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."]))))); + return [2 /*return*/]; + case 8: + queryClient.invalidateQueries({ + queryKey: [listQueryRoot], + }); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Unsubscribed from list"], ["Unsubscribed from list"]))))); + setIsProcessing(false); + return [2 /*return*/]; + } + }); + }); }; + var onRemoveList = function () { return __awaiter(_this, void 0, void 0, function () { + var e_3; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!savedFeedConfig) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, removeSavedFeed(savedFeedConfig)]; + case 2: + _a.sent(); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Removed from saved feeds"], ["Removed from saved feeds"]))))); + return [3 /*break*/, 5]; + case 3: + e_3 = _a.sent(); + logger.error('Failed to remove list from saved feeds', { message: e_3 }); + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."]))))); + return [3 /*break*/, 5]; + case 4: + setIsProcessing(false); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(CenteredView, { style: [ + a.flex_1, + a.align_center, + a.gap_5xl, + !gtMobile && a.justify_between, + t.atoms.border_contrast_low, + { paddingTop: 175, paddingBottom: 110 }, + ], sideBorders: true, children: [_jsxs(View, { style: [a.w_full, a.align_center, a.gap_lg], children: [_jsx(EyeSlash, { style: { color: t.atoms.text_contrast_medium.color }, height: 42, width: 42 }), _jsxs(View, { style: [a.gap_sm, a.align_center], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_3xl], children: ((_b = list.creator.viewer) === null || _b === void 0 ? void 0 : _b.blocking) || ((_c = list.creator.viewer) === null || _c === void 0 ? void 0 : _c.blockedBy) ? (_jsx(Trans, { children: "Creator has been blocked" })) : (_jsx(Trans, { children: "List has been hidden" })) }), _jsx(Text, { style: [ + a.text_md, + a.text_center, + a.px_md, + t.atoms.text_contrast_high, + { lineHeight: 1.4 }, + ], children: ((_d = list.creator.viewer) === null || _d === void 0 ? void 0 : _d.blocking) || ((_e = list.creator.viewer) === null || _e === void 0 ? void 0 : _e.blockedBy) ? (_jsx(Trans, { children: "Either the creator of this list has blocked you or you have blocked the creator." })) : isOwner ? (_jsx(Trans, { children: "This list \u2013 created by you \u2013 contains possible violations of Bluesky's community guidelines in its name or description." })) : (_jsxs(Trans, { children: ["This list \u2013 created by", ' ', _jsx(Text, { style: [a.font_semi_bold], children: sanitizeHandle(list.creator.handle, '@') }), ' ', "\u2013 contains possible violations of Bluesky's community guidelines in its name or description."] })) })] })] }), _jsxs(View, { style: [a.gap_md, gtMobile ? { width: 350 } : [a.w_full, a.px_lg]], children: [_jsxs(View, { style: [a.gap_md], children: [savedFeedConfig ? (_jsxs(Button, { variant: "solid", color: "secondary", size: "large", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Remove from saved feeds"], ["Remove from saved feeds"])))), onPress: onRemoveList, disabled: isProcessing, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Remove from saved feeds" }) }), isProcessing ? (_jsx(ButtonIcon, { icon: Loader, position: "right" })) : null] })) : null, isOwner ? (_jsx(Button, { variant: "solid", color: "secondary", size: "large", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Show list anyway"], ["Show list anyway"])))), onPress: function () { return setIsContentVisible(true); }, disabled: isProcessing, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Show anyway" }) }) })) : ((_f = list.viewer) === null || _f === void 0 ? void 0 : _f.muted) || ((_g = list.viewer) === null || _g === void 0 ? void 0 : _g.blocked) ? (_jsxs(Button, { variant: "solid", color: "secondary", size: "large", label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Unsubscribe from list"], ["Unsubscribe from list"])))), onPress: function () { + if (isModList) { + onUnsubscribe(); + } + else { + onRemoveList(); + } + }, disabled: isProcessing, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Unsubscribe from list" }) }), isProcessing ? (_jsx(ButtonIcon, { icon: Loader, position: "right" })) : null] })) : null] }), _jsx(Button, { variant: "solid", color: "primary", label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Return to previous page"], ["Return to previous page"])))), onPress: goBack, size: "large", disabled: isProcessing, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go Back" }) }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Log.js b/src/screens/Log.js new file mode 100644 index 0000000000..a66b763d17 --- /dev/null +++ b/src/screens/Log.js @@ -0,0 +1,78 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { LayoutAnimation, Pressable, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { getEntries } from '#/logger/logDump'; +import { useSetMinimalShellMode, useTickEveryMinute } from '#/state/shell'; +import { atoms as a, useTheme } from '#/alf'; +import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon, ChevronTop_Stroke2_Corner0_Rounded as ChevronTopIcon, } from '#/components/icons/Chevron'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon } from '#/components/icons/CircleInfo'; +import { Warning_Stroke2_Corner0_Rounded as WarningIcon } from '#/components/icons/Warning'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +export function LogScreen(_a) { + var t = useTheme(); + var _ = useLingui()._; + var setMinimalShellMode = useSetMinimalShellMode(); + var _b = useState([]), expanded = _b[0], setExpanded = _b[1]; + var timeAgo = useGetTimeAgo(); + var tick = useTickEveryMinute(); + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var toggler = function (id) { return function () { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + if (expanded.includes(id)) { + setExpanded(expanded.filter(function (v) { return v !== id; })); + } + else { + setExpanded(__spreadArray(__spreadArray([], expanded, true), [id], false)); + } + }; }; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "System log" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: getEntries() + .slice(0) + .map(function (entry) { + return (_jsxs(View, { children: [_jsxs(Pressable, { style: [ + a.flex_row, + a.align_center, + a.py_md, + a.px_sm, + a.border_b, + t.atoms.border_contrast_low, + t.atoms.bg, + a.gap_sm, + ], onPress: toggler(entry.id), accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["View debug entry"], ["View debug entry"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Opens additional details for a debug entry"], ["Opens additional details for a debug entry"])))), children: [entry.level === 'warn' || entry.level === 'error' ? (_jsx(WarningIcon, { size: "sm", fill: t.palette.negative_500 })) : (_jsx(CircleInfoIcon, { size: "sm" })), _jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.justify_start, + a.align_center, + a.gap_sm, + ], children: [entry.context && (_jsxs(Text, { style: [t.atoms.text_contrast_medium], children: ["(", String(entry.context), ")"] })), _jsx(Text, { children: String(entry.message) })] }), entry.metadata && + Object.keys(entry.metadata).length > 0 && + (expanded.includes(entry.id) ? (_jsx(ChevronTopIcon, { size: "sm", style: [t.atoms.text_contrast_low] })) : (_jsx(ChevronBottomIcon, { size: "sm", style: [t.atoms.text_contrast_low] }))), _jsx(Text, { style: [{ minWidth: 40 }, t.atoms.text_contrast_medium], children: timeAgo(entry.timestamp, tick) })] }), expanded.includes(entry.id) && (_jsx(View, { style: [ + t.atoms.bg_contrast_25, + a.rounded_xs, + a.p_sm, + a.border_b, + t.atoms.border_contrast_low, + ], children: _jsx(View, { style: [a.px_sm, a.py_xs], children: _jsx(Text, { style: [a.leading_snug, { fontFamily: 'monospace' }], children: JSON.stringify(entry.metadata, null, 2) }) }) }))] }, "entry-".concat(entry.id))); + }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Login/ChooseAccountForm.js b/src/screens/Login/ChooseAccountForm.js new file mode 100644 index 0000000000..a6b5a64290 --- /dev/null +++ b/src/screens/Login/ChooseAccountForm.js @@ -0,0 +1,123 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { logger } from '#/logger'; +import { useSession, useSessionApi } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, web } from '#/alf'; +import { AccountList } from '#/components/AccountList'; +import { Button, ButtonText } from '#/components/Button'; +import * as TextField from '#/components/forms/TextField'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import { FormContainer } from './FormContainer'; +export var ChooseAccountForm = function (_a) { + var onSelectAccount = _a.onSelectAccount, onPressBack = _a.onPressBack; + var _b = useState(null), pendingDid = _b[0], setPendingDid = _b[1]; + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var resumeSession = useSessionApi().resumeSession; + var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut; + var onSelect = useCallback(function (account) { return __awaiter(void 0, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (pendingDid) { + // The session API isn't resilient to race conditions so let's just ignore this. + return [2 /*return*/]; + } + if (!account.accessJwt) { + // Move to login form. + onSelectAccount(account); + return [2 /*return*/]; + } + if (account.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + setShowLoggedOut(false); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Already signed in as @", ""], ["Already signed in as @", ""])), account.handle))); + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + setPendingDid(account.did); + return [4 /*yield*/, resumeSession(account, true)]; + case 2: + _a.sent(); + ax.metric('account:loggedIn', { + logContext: 'ChooseAccountForm', + withPassword: false, + }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Signed in as @", ""], ["Signed in as @", ""])), account.handle))); + return [3 /*break*/, 5]; + case 3: + e_1 = _a.sent(); + logger.error('choose account: initSession failed', { + message: e_1 instanceof Error ? e_1.message : 'Unknown error', + }); + // Move to login form. + onSelectAccount(account); + return [3 /*break*/, 5]; + case 4: + setPendingDid(null); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }, [ + currentAccount, + resumeSession, + pendingDid, + onSelectAccount, + setShowLoggedOut, + _, + ax, + ]); + return (_jsxs(FormContainer, { testID: "chooseAccountForm", titleText: _jsx(Trans, { children: "Select account" }), style: web([a.py_2xl]), children: [_jsxs(View, { children: [IS_WEB && (_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Sign in as..." }) })), _jsx(AccountList, { onSelectAccount: onSelect, onSelectOther: function () { return onSelectAccount(); }, pendingDid: pendingDid })] }), IS_WEB && (_jsxs(View, { style: [a.flex_row], children: [_jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Back"], ["Back"])))), color: "secondary", size: "large", onPress: onPressBack, children: _jsx(ButtonText, { children: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Back"], ["Back"])))) }) }), _jsx(View, { style: [a.flex_1] })] }))] })); +}; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Login/ForgotPasswordForm.js b/src/screens/Login/ForgotPasswordForm.js new file mode 100644 index 0000000000..760fd8e0db --- /dev/null +++ b/src/screens/Login/ForgotPasswordForm.js @@ -0,0 +1,114 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useState } from 'react'; +import { Keyboard, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import * as EmailValidator from 'email-validator'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { Agent } from '#/state/session/agent'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { FormError } from '#/components/forms/FormError'; +import { HostingProvider } from '#/components/forms/HostingProvider'; +import * as TextField from '#/components/forms/TextField'; +import { At_Stroke2_Corner0_Rounded as At } from '#/components/icons/At'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +import { FormContainer } from './FormContainer'; +export var ForgotPasswordForm = function (_a) { + var error = _a.error, serviceUrl = _a.serviceUrl, serviceDescription = _a.serviceDescription, setError = _a.setError, setServiceUrl = _a.setServiceUrl, onPressBack = _a.onPressBack, onEmailSent = _a.onEmailSent; + var t = useTheme(); + var _b = useState(false), isProcessing = _b[0], setIsProcessing = _b[1]; + var _c = useState(''), email = _c[0], setEmail = _c[1]; + var _ = useLingui()._; + var onPressSelectService = React.useCallback(function () { + Keyboard.dismiss(); + }, []); + var onPressNext = function () { return __awaiter(void 0, void 0, void 0, function () { + var agent, e_1, errMsg; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!EmailValidator.validate(email)) { + return [2 /*return*/, setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Your email appears to be invalid."], ["Your email appears to be invalid."])))))]; + } + setError(''); + setIsProcessing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + agent = new Agent(null, { service: serviceUrl }); + return [4 /*yield*/, agent.com.atproto.server.requestPasswordReset({ email: email })]; + case 2: + _a.sent(); + onEmailSent(); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + errMsg = e_1.toString(); + logger.warn('Failed to request password reset', { error: e_1 }); + setIsProcessing(false); + if (isNetworkError(e_1)) { + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Unable to contact your service. Please check your Internet connection."], ["Unable to contact your service. Please check your Internet connection."]))))); + } + else { + setError(cleanError(errMsg)); + } + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(FormContainer, { testID: "forgotPasswordForm", titleText: _jsx(Trans, { children: "Reset password" }), children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Hosting provider" }) }), _jsx(HostingProvider, { serviceUrl: serviceUrl, onSelectServiceUrl: setServiceUrl, onOpenDialog: onPressSelectService })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Email address" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: At }), _jsx(TextField.Input, { testID: "forgotPasswordEmail", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Enter your email address"], ["Enter your email address"])))), autoCapitalize: "none", autoFocus: true, autoCorrect: false, autoComplete: "email", value: email, onChangeText: setEmail, editable: !isProcessing, accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Sets email for password reset"], ["Sets email for password reset"])))) })] })] }), _jsx(Text, { style: [t.atoms.text_contrast_high, a.leading_snug], children: _jsx(Trans, { children: "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." }) }), _jsx(FormError, { error: error }), _jsxs(View, { style: [web([a.flex_row, a.align_center]), a.pt_md], children: [IS_WEB && (_jsxs(_Fragment, { children: [_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Back"], ["Back"])))), color: "secondary", size: "large", onPress: onPressBack, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Back" }) }) }), _jsx(View, { style: a.flex_1 })] })), !serviceDescription ? (_jsxs(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Connecting to service..."], ["Connecting to service..."])))), size: "large", color: "secondary", disabled: true, children: [_jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: "Connecting..." })] })) : (_jsxs(Button, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Next"], ["Next"])))), accessibilityHint: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Navigates to the next screen"], ["Navigates to the next screen"])))), color: "primary", size: "large", onPress: onPressNext, disabled: isProcessing, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Next" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] }))] }), _jsx(View, { style: [ + t.atoms.border_contrast_medium, + a.border_t, + a.pt_xl, + a.mt_md, + a.flex_row, + a.justify_center, + ], children: _jsx(Button, { testID: "skipSendEmailButton", onPress: onEmailSent, label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Go to next"], ["Go to next"])))), accessibilityHint: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Navigates to the next screen"], ["Navigates to the next screen"])))), size: "large", variant: "ghost", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Already have a code?" }) }) }) })] })); +}; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10; diff --git a/src/screens/Login/FormContainer.js b/src/screens/Login/FormContainer.js new file mode 100644 index 0000000000..7e141d5567 --- /dev/null +++ b/src/screens/Login/FormContainer.js @@ -0,0 +1,10 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useBreakpoints, useGutters } from '#/alf'; +import { Text } from '#/components/Typography'; +export function FormContainer(_a) { + var testID = _a.testID, titleText = _a.titleText, children = _a.children, style = _a.style; + var gtMobile = useBreakpoints().gtMobile; + var gutter = useGutters([0, 'wide']); + return (_jsxs(View, { testID: testID, style: [a.gap_md, a.flex_1, !gtMobile && gutter, style], children: [titleText && !gtMobile && (_jsx(Text, { style: [a.text_3xl, a.font_bold], children: titleText })), children] })); +} diff --git a/src/screens/Login/LoginForm.js b/src/screens/Login/LoginForm.js new file mode 100644 index 0000000000..3a920ecc2a --- /dev/null +++ b/src/screens/Login/LoginForm.js @@ -0,0 +1,216 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useRef, useState } from 'react'; +import { Keyboard, View } from 'react-native'; +import { ComAtprotoServerCreateSession, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useRequestNotificationsPermission } from '#/lib/notifications/notifications'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { createFullHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { useSetHasCheckedForStarterPack } from '#/state/preferences/used-starter-packs'; +import { useSessionApi } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { atoms as a, ios, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { FormError } from '#/components/forms/FormError'; +import { HostingProvider } from '#/components/forms/HostingProvider'; +import * as TextField from '#/components/forms/TextField'; +import { At_Stroke2_Corner0_Rounded as At } from '#/components/icons/At'; +import { Lock_Stroke2_Corner0_Rounded as Lock } from '#/components/icons/Lock'; +import { Ticket_Stroke2_Corner0_Rounded as Ticket } from '#/components/icons/Ticket'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_IOS, IS_WEB } from '#/env'; +import { FormContainer } from './FormContainer'; +export var LoginForm = function (_a) { + var error = _a.error, serviceUrl = _a.serviceUrl, serviceDescription = _a.serviceDescription, initialHandle = _a.initialHandle, setError = _a.setError, setServiceUrl = _a.setServiceUrl, onPressRetryConnect = _a.onPressRetryConnect, onPressBack = _a.onPressBack, onPressForgotPassword = _a.onPressForgotPassword, onAttemptSuccess = _a.onAttemptSuccess, onAttemptFailed = _a.onAttemptFailed; + var t = useTheme(); + var _b = useState(false), isProcessing = _b[0], setIsProcessing = _b[1]; + var _c = useState('none'), errorField = _c[0], setErrorField = _c[1]; + var _d = useState(false), isAuthFactorTokenNeeded = _d[0], setIsAuthFactorTokenNeeded = _d[1]; + var identifierValueRef = useRef(initialHandle || ''); + var passwordValueRef = useRef(''); + var _e = useState(''), authFactorToken = _e[0], setAuthFactorToken = _e[1]; + var identifierRef = useRef(null); + var passwordRef = useRef(null); + var hasFocusedOnce = useRef(false); + var _ = useLingui()._; + var login = useSessionApi().login; + var requestNotificationsPermission = useRequestNotificationsPermission(); + var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut; + var setHasCheckedForStarterPack = useSetHasCheckedForStarterPack(); + var onPressSelectService = useCallback(function () { + Keyboard.dismiss(); + }, []); + var onPressNext = function () { return __awaiter(void 0, void 0, void 0, function () { + var identifier, password, fullIdent, matched, _i, _a, domain, e_1, errMsg; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (isProcessing) + return [2 /*return*/]; + Keyboard.dismiss(); + setError(''); + setErrorField('none'); + identifier = identifierValueRef.current.toLowerCase().trim(); + password = passwordValueRef.current; + if (!identifier) { + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Please enter your username"], ["Please enter your username"]))))); + setErrorField('identifier'); + return [2 /*return*/]; + } + if (!password) { + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please enter your password"], ["Please enter your password"]))))); + setErrorField('password'); + return [2 /*return*/]; + } + setIsProcessing(true); + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + fullIdent = identifier; + if (!identifier.includes('@') && // not an email + !identifier.includes('.') && // not a domain + serviceDescription && + serviceDescription.availableUserDomains.length > 0) { + matched = false; + for (_i = 0, _a = serviceDescription.availableUserDomains; _i < _a.length; _i++) { + domain = _a[_i]; + if (fullIdent.endsWith(domain)) { + matched = true; + } + } + if (!matched) { + fullIdent = createFullHandle(identifier, serviceDescription.availableUserDomains[0]); + } + } + // TODO remove double login + return [4 /*yield*/, login({ + service: serviceUrl, + identifier: fullIdent, + password: password, + authFactorToken: authFactorToken.trim(), + }, 'LoginForm')]; + case 2: + // TODO remove double login + _b.sent(); + onAttemptSuccess(); + setShowLoggedOut(false); + setHasCheckedForStarterPack(true); + requestNotificationsPermission('Login'); + return [3 /*break*/, 4]; + case 3: + e_1 = _b.sent(); + errMsg = e_1.toString(); + setIsProcessing(false); + if (e_1 instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError) { + setIsAuthFactorTokenNeeded(true); + } + else { + onAttemptFailed(); + if (errMsg.includes('Token is invalid')) { + logger.debug('Failed to login due to invalid 2fa token', { + error: errMsg, + }); + setError(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Invalid 2FA confirmation code."], ["Invalid 2FA confirmation code."]))))); + setErrorField('2fa'); + } + else if (errMsg.includes('Authentication Required') || + errMsg.includes('Invalid identifier or password')) { + logger.debug('Failed to login due to invalid credentials', { + error: errMsg, + }); + setError(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Incorrect username or password"], ["Incorrect username or password"]))))); + } + else if (isNetworkError(e_1)) { + logger.warn('Failed to login due to network error', { error: errMsg }); + setError(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Unable to contact your service. Please check your Internet connection."], ["Unable to contact your service. Please check your Internet connection."]))))); + } + else { + logger.warn('Failed to login', { error: errMsg }); + setError(cleanError(errMsg)); + } + } + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(FormContainer, { testID: "loginForm", titleText: _jsx(Trans, { children: "Sign in" }), children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Hosting provider" }) }), _jsx(HostingProvider, { serviceUrl: serviceUrl, onSelectServiceUrl: setServiceUrl, onOpenDialog: onPressSelectService })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Account" }) }), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(TextField.Root, { isInvalid: errorField === 'identifier', children: [_jsx(TextField.Icon, { icon: At }), _jsx(TextField.Input, { testID: "loginUsernameInput", inputRef: identifierRef, label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Username or email address"], ["Username or email address"])))), autoCapitalize: "none", autoFocus: !IS_IOS, autoCorrect: false, autoComplete: "username", returnKeyType: "next", textContentType: "username", defaultValue: initialHandle || '', onChangeText: function (v) { + identifierValueRef.current = v; + if (errorField) + setErrorField('none'); + }, onSubmitEditing: function () { + var _a; + (_a = passwordRef.current) === null || _a === void 0 ? void 0 : _a.focus(); + }, blurOnSubmit: false, editable: !isProcessing, accessibilityHint: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Enter the username or email address you used when you created your account"], ["Enter the username or email address you used when you created your account"])))) })] }), _jsxs(TextField.Root, { isInvalid: errorField === 'password', children: [_jsx(TextField.Icon, { icon: Lock }), _jsx(TextField.Input, { testID: "loginPasswordInput", inputRef: passwordRef, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Password"], ["Password"])))), autoCapitalize: "none", autoCorrect: false, autoComplete: "current-password", returnKeyType: "done", enablesReturnKeyAutomatically: true, secureTextEntry: true, clearButtonMode: "while-editing", onChangeText: function (v) { + passwordValueRef.current = v; + if (errorField) + setErrorField('none'); + }, onSubmitEditing: onPressNext, blurOnSubmit: false, editable: !isProcessing, accessibilityHint: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Enter your password"], ["Enter your password"])))), onLayout: ios(function () { + var _a; + if (hasFocusedOnce.current) + return; + hasFocusedOnce.current = true; + // kinda dumb, but if we use `autoFocus` to focus + // the username input, it happens before the password + // input gets rendered. this breaks the password autofill + // on iOS (it only does the username part). delaying + // it until both inputs are rendered fixes the autofill -sfn + (_a = identifierRef.current) === null || _a === void 0 ? void 0 : _a.focus(); + }) }), _jsx(Button, { testID: "forgotPasswordButton", onPress: onPressForgotPassword, label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Forgot password?"], ["Forgot password?"])))), accessibilityHint: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Opens password reset form"], ["Opens password reset form"])))), variant: "solid", color: "secondary", style: [ + a.rounded_sm, + // t.atoms.bg_contrast_100, + { marginLeft: 'auto', left: 6, padding: 6 }, + a.z_10, + ], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Forgot?" }) }) })] })] })] }), isAuthFactorTokenNeeded && (_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "2FA Confirmation" }) }), _jsxs(TextField.Root, { isInvalid: errorField === '2fa', children: [_jsx(TextField.Icon, { icon: Ticket }), _jsx(TextField.Input, { testID: "loginAuthFactorTokenInput", label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Confirmation code"], ["Confirmation code"])))), autoCapitalize: "none", autoFocus: true, autoCorrect: false, autoComplete: "one-time-code", returnKeyType: "done", blurOnSubmit: false, value: authFactorToken, onChangeText: function (text) { + setAuthFactorToken(text); + if (errorField) + setErrorField('none'); + }, onSubmitEditing: onPressNext, editable: !isProcessing, accessibilityHint: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Input the code which has been emailed to you"], ["Input the code which has been emailed to you"])))), style: { + textTransform: authFactorToken === '' ? 'none' : 'uppercase', + } })] }), _jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium, a.mt_sm], children: _jsx(Trans, { children: "Check your email for a sign in code and enter it here." }) })] })), _jsx(FormError, { error: error }), _jsxs(View, { style: [a.pt_md, web([a.justify_between, a.flex_row])], children: [IS_WEB && (_jsx(Button, { label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Back"], ["Back"])))), color: "secondary", size: "large", onPress: onPressBack, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Back" }) }) })), !serviceDescription && error ? (_jsx(Button, { testID: "loginRetryButton", label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Retry"], ["Retry"])))), accessibilityHint: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Retries signing in"], ["Retries signing in"])))), color: "primary_subtle", size: "large", onPress: onPressRetryConnect, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }) })) : !serviceDescription ? (_jsxs(Button, { label: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Connecting to service..."], ["Connecting to service..."])))), size: "large", color: "secondary", disabled: true, children: [_jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: "Connecting..." })] })) : (_jsxs(Button, { testID: "loginNextButton", label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Sign in"], ["Sign in"])))), accessibilityHint: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Navigates to the next screen"], ["Navigates to the next screen"])))), color: "primary", size: "large", onPress: onPressNext, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Sign in" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] }))] })] })); +}; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19; diff --git a/src/screens/Login/PasswordUpdatedForm.js b/src/screens/Login/PasswordUpdatedForm.js new file mode 100644 index 0000000000..f809ffda5b --- /dev/null +++ b/src/screens/Login/PasswordUpdatedForm.js @@ -0,0 +1,20 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useBreakpoints, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { Text } from '#/components/Typography'; +import { FormContainer } from './FormContainer'; +export var PasswordUpdatedForm = function (_a) { + var onPressNext = _a.onPressNext; + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + return (_jsxs(FormContainer, { testID: "passwordUpdatedForm", style: [a.gap_2xl, !gtMobile && a.mt_5xl], children: [_jsx(Text, { style: [a.text_3xl, a.font_bold, a.text_center], children: _jsx(Trans, { children: "Password updated!" }) }), _jsx(Text, { style: [a.text_center, a.mx_auto, { maxWidth: '80%' }], children: _jsx(Trans, { children: "You can now sign in with your new password." }) }), _jsx(View, { style: web([a.flex_row, a.justify_center]), children: _jsx(Button, { onPress: onPressNext, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close alert"], ["Close alert"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Closes password update alert"], ["Closes password update alert"])))), color: "primary", size: "large", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Okay" }) }) }) })] })); +}; +var templateObject_1, templateObject_2; diff --git a/src/screens/Login/SetNewPasswordForm.js b/src/screens/Login/SetNewPasswordForm.js new file mode 100644 index 0000000000..253f1e22d3 --- /dev/null +++ b/src/screens/Login/SetNewPasswordForm.js @@ -0,0 +1,127 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { checkAndFormatResetCode } from '#/lib/strings/password'; +import { logger } from '#/logger'; +import { Agent } from '#/state/session/agent'; +import { atoms as a, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { FormError } from '#/components/forms/FormError'; +import * as TextField from '#/components/forms/TextField'; +import { Lock_Stroke2_Corner0_Rounded as Lock } from '#/components/icons/Lock'; +import { Ticket_Stroke2_Corner0_Rounded as Ticket } from '#/components/icons/Ticket'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import { FormContainer } from './FormContainer'; +export var SetNewPasswordForm = function (_a) { + var error = _a.error, serviceUrl = _a.serviceUrl, setError = _a.setError, onPressBack = _a.onPressBack, onPasswordSet = _a.onPasswordSet; + var _ = useLingui()._; + var ax = useAnalytics(); + var _b = useState(false), isProcessing = _b[0], setIsProcessing = _b[1]; + var _c = useState(''), resetCode = _c[0], setResetCode = _c[1]; + var _d = useState(''), password = _d[0], setPassword = _d[1]; + var onPressNext = function () { return __awaiter(void 0, void 0, void 0, function () { + var formattedCode, agent, e_1, errMsg; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + formattedCode = checkAndFormatResetCode(resetCode); + if (!formattedCode) { + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You have entered an invalid code. It should look like XXXXX-XXXXX."], ["You have entered an invalid code. It should look like XXXXX-XXXXX."]))))); + ax.metric('signin:passwordResetFailure', {}); + return [2 /*return*/]; + } + // TODO Better password strength check + if (!password) { + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please enter a password."], ["Please enter a password."]))))); + return [2 /*return*/]; + } + setError(''); + setIsProcessing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + agent = new Agent(null, { service: serviceUrl }); + return [4 /*yield*/, agent.com.atproto.server.resetPassword({ + token: formattedCode, + password: password, + })]; + case 2: + _a.sent(); + onPasswordSet(); + ax.metric('signin:passwordResetSuccess', {}); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + errMsg = e_1.toString(); + logger.warn('Failed to set new password', { error: e_1 }); + ax.metric('signin:passwordResetFailure', {}); + setIsProcessing(false); + if (isNetworkError(e_1)) { + setError(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Unable to contact your service. Please check your Internet connection."], ["Unable to contact your service. Please check your Internet connection."]))))); + } + else { + setError(cleanError(errMsg)); + } + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var onBlur = function () { + var formattedCode = checkAndFormatResetCode(resetCode); + if (!formattedCode) { + setError(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["You have entered an invalid code. It should look like XXXXX-XXXXX."], ["You have entered an invalid code. It should look like XXXXX-XXXXX."]))))); + return; + } + setResetCode(formattedCode); + }; + return (_jsxs(FormContainer, { testID: "setNewPasswordForm", titleText: _jsx(Trans, { children: "Set new password" }), children: [_jsx(Text, { style: [a.leading_snug, a.mb_sm], children: _jsx(Trans, { children: "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." }) }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Reset code" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: Ticket }), _jsx(TextField.Input, { testID: "resetCodeInput", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Looks like XXXXX-XXXXX"], ["Looks like XXXXX-XXXXX"])))), autoCapitalize: "none", autoFocus: true, autoCorrect: false, autoComplete: "off", value: resetCode, onChangeText: setResetCode, onFocus: function () { return setError(''); }, onBlur: onBlur, editable: !isProcessing, accessibilityHint: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Input code sent to your email for password reset"], ["Input code sent to your email for password reset"])))) })] })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "New password" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: Lock }), _jsx(TextField.Input, { testID: "newPasswordInput", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Enter a password"], ["Enter a password"])))), autoCapitalize: "none", autoCorrect: false, returnKeyType: "done", secureTextEntry: true, autoComplete: "new-password", passwordRules: "minlength: 8;", clearButtonMode: "while-editing", value: password, onChangeText: setPassword, onSubmitEditing: onPressNext, editable: !isProcessing, accessibilityHint: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Input new password"], ["Input new password"])))) })] })] }), _jsx(FormError, { error: error }), _jsxs(View, { style: [web([a.flex_row, a.align_center]), a.pt_lg], children: [IS_WEB && (_jsxs(_Fragment, { children: [_jsx(Button, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Back"], ["Back"])))), variant: "solid", color: "secondary", size: "large", onPress: onPressBack, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Back" }) }) }), _jsx(View, { style: a.flex_1 })] })), _jsxs(Button, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Next"], ["Next"])))), color: "primary", size: "large", onPress: onPressNext, disabled: isProcessing, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Next" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] })] })] })); +}; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10; diff --git a/src/screens/Login/components/AuthLayout/Header/index.js b/src/screens/Login/components/AuthLayout/Header/index.js new file mode 100644 index 0000000000..ad370c9c03 --- /dev/null +++ b/src/screens/Login/components/AuthLayout/Header/index.js @@ -0,0 +1,85 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useContext } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_30 } from '#/lib/constants'; +import { Logomark } from '#/view/icons/Logomark'; +import { atoms as a, platform, useGutters, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft } from '#/components/icons/Arrow'; +import { BUTTON_VISUAL_ALIGNMENT_OFFSET, Header, HEADER_SLOT_SIZE, } from '#/components/Layout'; +import { IS_WEB } from '#/env'; +import { AuthLayoutNavigationContext } from '../context'; +/** + * This is a simplified version of `Layout.Header` for the auth screens. + */ +export var Slot = Header.Slot; +export function Outer(_a) { + var children = _a.children; + var t = useTheme(); + var gutters = useGutters([0, 'wide']); + return (_jsx(View, { style: [ + a.w_full, + a.flex_row, + a.align_center, + a.gap_sm, + gutters, + platform({ + native: [a.pb_xs, { minHeight: 48 }], + web: [a.py_xs, { minHeight: 52 }], + }), + t.atoms.border_contrast_low, + ], children: children })); +} +export function Content(_a) { + var children = _a.children; + return (_jsx(View, { style: [a.flex_1, a.justify_center, { minHeight: HEADER_SLOT_SIZE }], children: IS_WEB ? children : _jsx(Logo, {}) })); +} +export function Logo() { + var t = useTheme(); + return _jsx(Logomark, { fill: t.palette.primary_500, style: [a.mx_auto] }); +} +export function BackButton(_a) { + var onPress = _a.onPress, style = _a.style, props = __rest(_a, ["onPress", "style"]); + var _ = useLingui()._; + var navigation = useContext(AuthLayoutNavigationContext); + var onPressBack = function (evt) { + onPress === null || onPress === void 0 ? void 0 : onPress(evt); + if (evt.defaultPrevented) + return; + navigation === null || navigation === void 0 ? void 0 : navigation.goBack(); + }; + return (_jsx(Slot, { children: _jsx(Button, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go back"], ["Go back"])))), onPress: onPressBack, size: "small", variant: "ghost", color: "secondary", shape: "round", hitSlop: HITSLOP_30, style: [ + { marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET }, + a.bg_transparent, + style, + ] }, props, { children: _jsx(ButtonIcon, { icon: ArrowLeft, size: "lg" }) })) })); +} +var templateObject_1; diff --git a/src/screens/Login/components/AuthLayout/Header/index.web.js b/src/screens/Login/components/AuthLayout/Header/index.web.js new file mode 100644 index 0000000000..96cc04d73b --- /dev/null +++ b/src/screens/Login/components/AuthLayout/Header/index.web.js @@ -0,0 +1,16 @@ +import { Header } from '#/components/Layout'; +// TEMP: Web needs a much more comprehensive layout change to support the new design +// so to do this incrementally, we'll do native only and migrate to web later +export var Slot = Header.Slot; +export function Outer(_a) { + return null; +} +export function Content(_a) { + return null; +} +export function Logo() { + return null; +} +export function BackButton(_a) { + return null; +} diff --git a/src/screens/Login/components/AuthLayout/context.js b/src/screens/Login/components/AuthLayout/context.js new file mode 100644 index 0000000000..10d5bb5b5d --- /dev/null +++ b/src/screens/Login/components/AuthLayout/context.js @@ -0,0 +1,2 @@ +import { createContext } from 'react'; +export var AuthLayoutNavigationContext = createContext(null); diff --git a/src/screens/Login/components/AuthLayout/index.js b/src/screens/Login/components/AuthLayout/index.js new file mode 100644 index 0000000000..9ec5744632 --- /dev/null +++ b/src/screens/Login/components/AuthLayout/index.js @@ -0,0 +1 @@ +export * as Header from './Header'; diff --git a/src/screens/Login/index.js b/src/screens/Login/index.js new file mode 100644 index 0000000000..8000876243 --- /dev/null +++ b/src/screens/Login/index.js @@ -0,0 +1,148 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect, useRef, useState } from 'react'; +import { KeyboardAvoidingView } from 'react-native'; +import Animated, { FadeIn, LayoutAnimationConfig } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { DEFAULT_SERVICE } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useServiceQuery } from '#/state/queries/service'; +import { useSession } from '#/state/session'; +import { useLoggedOutView } from '#/state/shell/logged-out'; +import { LoggedOutLayout } from '#/view/com/util/layouts/LoggedOutLayout'; +import { ForgotPasswordForm } from '#/screens/Login/ForgotPasswordForm'; +import { LoginForm } from '#/screens/Login/LoginForm'; +import { PasswordUpdatedForm } from '#/screens/Login/PasswordUpdatedForm'; +import { SetNewPasswordForm } from '#/screens/Login/SetNewPasswordForm'; +import { atoms as a, native } from '#/alf'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { useAnalytics } from '#/analytics'; +import { ChooseAccountForm } from './ChooseAccountForm'; +import * as AuthLayout from './components/AuthLayout'; +import { AuthLayoutNavigationContext } from './components/AuthLayout/context'; +var Forms; +(function (Forms) { + Forms[Forms["Login"] = 0] = "Login"; + Forms[Forms["ChooseAccount"] = 1] = "ChooseAccount"; + Forms[Forms["ForgotPassword"] = 2] = "ForgotPassword"; + Forms[Forms["SetNewPassword"] = 3] = "SetNewPassword"; + Forms[Forms["PasswordUpdated"] = 4] = "PasswordUpdated"; +})(Forms || (Forms = {})); +var OrderedForms = [ + Forms.ChooseAccount, + Forms.Login, + Forms.ForgotPassword, + Forms.SetNewPassword, + Forms.PasswordUpdated, +]; +export var Login = function (_a) { + var onPressBack = _a.onPressBack; + var _ = useLingui()._; + var failedAttemptCountRef = useRef(0); + var startTimeRef = useRef(Date.now()); + var accounts = useSession().accounts; + var requestedAccountSwitchTo = useLoggedOutView().requestedAccountSwitchTo; + var requestedAccount = accounts.find(function (acc) { return acc.did === requestedAccountSwitchTo; }); + var _b = useState(''), error = _b[0], setError = _b[1]; + var _c = useState((requestedAccount === null || requestedAccount === void 0 ? void 0 : requestedAccount.service) || DEFAULT_SERVICE), serviceUrl = _c[0], setServiceUrl = _c[1]; + var _d = useState((requestedAccount === null || requestedAccount === void 0 ? void 0 : requestedAccount.handle) || ''), initialHandle = _d[0], setInitialHandle = _d[1]; + var _e = useState(requestedAccount + ? Forms.Login + : accounts.length + ? Forms.ChooseAccount + : Forms.Login), currentForm = _e[0], setCurrentForm = _e[1]; + var _f = useState('Forward'), screenTransitionDirection = _f[0], setScreenTransitionDirection = _f[1]; + var ax = useAnalytics(); + var _g = useServiceQuery(serviceUrl), serviceDescription = _g.data, serviceError = _g.error, refetchService = _g.refetch; + var onSelectAccount = function (account) { + if (account === null || account === void 0 ? void 0 : account.service) { + setServiceUrl(account.service); + } + setInitialHandle((account === null || account === void 0 ? void 0 : account.handle) || ''); + gotoForm(Forms.Login); + }; + var gotoForm = function (form) { + setError(''); + var index = OrderedForms.indexOf(currentForm); + var nextIndex = OrderedForms.indexOf(form); + setScreenTransitionDirection(index < nextIndex ? 'Forward' : 'Backward'); + setCurrentForm(form); + }; + useEffect(function () { + if (serviceError) { + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Unable to contact your service. Please check your Internet connection."], ["Unable to contact your service. Please check your Internet connection."]))))); + logger.warn("Failed to fetch service description for ".concat(serviceUrl), { + error: String(serviceError), + }); + ax.metric('signin:hostingProviderFailedResolution', {}); + } + else { + setError(''); + } + }, [serviceError, serviceUrl, _]); + var onPressForgotPassword = function () { + gotoForm(Forms.ForgotPassword); + ax.metric('signin:forgotPasswordPressed', {}); + }; + var handlePressBack = function () { + onPressBack(); + setScreenTransitionDirection('Backward'); + ax.metric('signin:backPressed', { + failedAttemptsCount: failedAttemptCountRef.current, + }); + }; + var onAttemptSuccess = function () { + ax.metric('signin:success', { + isUsingCustomProvider: serviceUrl !== DEFAULT_SERVICE, + timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000), + failedAttemptsCount: failedAttemptCountRef.current, + }); + }; + var onAttemptFailed = function () { + failedAttemptCountRef.current += 1; + }; + var content = null; + var title = ''; + var description = ''; + var goBack = null; + switch (currentForm) { + case Forms.Login: + title = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Sign in"], ["Sign in"])))); + description = _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Enter your username and password"], ["Enter your username and password"])))); + goBack = function () { + return accounts.length ? gotoForm(Forms.ChooseAccount) : handlePressBack(); + }; + content = (_jsx(LoginForm, { error: error, serviceUrl: serviceUrl, serviceDescription: serviceDescription, initialHandle: initialHandle, setError: setError, onAttemptFailed: onAttemptFailed, onAttemptSuccess: onAttemptSuccess, setServiceUrl: setServiceUrl, onPressBack: goBack, onPressForgotPassword: onPressForgotPassword, onPressRetryConnect: refetchService })); + break; + case Forms.ChooseAccount: + title = _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Sign in"], ["Sign in"])))); + description = _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Select from an existing account"], ["Select from an existing account"])))); + goBack = handlePressBack; + content = (_jsx(ChooseAccountForm, { onSelectAccount: onSelectAccount, onPressBack: goBack })); + break; + case Forms.ForgotPassword: + title = _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Forgot Password"], ["Forgot Password"])))); + description = _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Let's get your password reset!"], ["Let's get your password reset!"])))); + goBack = function () { return gotoForm(Forms.Login); }; + content = (_jsx(ForgotPasswordForm, { error: error, serviceUrl: serviceUrl, serviceDescription: serviceDescription, setError: setError, setServiceUrl: setServiceUrl, onPressBack: goBack, onEmailSent: function () { return gotoForm(Forms.SetNewPassword); } })); + break; + case Forms.SetNewPassword: + title = _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Forgot Password"], ["Forgot Password"])))); + description = _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Let's get your password reset!"], ["Let's get your password reset!"])))); + goBack = function () { return gotoForm(Forms.ForgotPassword); }; + content = (_jsx(SetNewPasswordForm, { error: error, serviceUrl: serviceUrl, setError: setError, onPressBack: goBack, onPasswordSet: function () { return gotoForm(Forms.PasswordUpdated); } })); + break; + case Forms.PasswordUpdated: + title = _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Password updated"], ["Password updated"])))); + description = _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["You can now sign in with your new password."], ["You can now sign in with your new password."])))); + content = (_jsx(PasswordUpdatedForm, { onPressNext: function () { return gotoForm(Forms.Login); } })); + break; + } + var navigation = goBack ? { goBack: goBack } : null; + return (_jsx(AuthLayoutNavigationContext, { value: navigation, children: _jsx(Animated.View, { style: a.flex_1, entering: native(FadeIn.duration(90)), children: _jsxs(KeyboardAvoidingView, { testID: "signIn", behavior: "padding", style: a.flex_1, children: [_jsxs(AuthLayout.Header.Outer, { children: [_jsx(AuthLayout.Header.BackButton, {}), _jsx(AuthLayout.Header.Content, {}), _jsx(AuthLayout.Header.Slot, {})] }), _jsx(LoggedOutLayout, { leadin: "", title: title, description: description, scrollable: true, children: _jsx(LayoutAnimationConfig, { skipEntering: true, children: _jsx(ScreenTransition, { direction: screenTransitionDirection, children: content }, currentForm) }) })] }) }) })); +}; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11; diff --git a/src/screens/Messages/ChatList.js b/src/screens/Messages/ChatList.js new file mode 100644 index 0000000000..9d943d7af9 --- /dev/null +++ b/src/screens/Messages/ChatList.js @@ -0,0 +1,311 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { useAnimatedRef } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect, useIsFocused } from '@react-navigation/native'; +import { useAppState } from '#/lib/appState'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { cleanError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { listenSoftReset } from '#/state/events'; +import { MESSAGE_SCREEN_POLL_INTERVAL } from '#/state/messages/convo/const'; +import { useMessagesEventBus } from '#/state/messages/events'; +import { useLeftConvos } from '#/state/queries/messages/leave-conversation'; +import { useListConvosQuery } from '#/state/queries/messages/list-conversations'; +import { useSession } from '#/state/session'; +import { List } from '#/view/com/util/List'; +import { ChatListLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { AgeRestrictedScreen } from '#/components/ageAssurance/AgeRestrictedScreen'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { NewChat } from '#/components/dms/dialogs/NewChatDialog'; +import { useRefreshOnFocus } from '#/components/hooks/useRefreshOnFocus'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon } from '#/components/icons/ArrowRotate'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon } from '#/components/icons/CircleInfo'; +import { Message_Stroke2_Corner0_Rounded as MessageIcon } from '#/components/icons/Message'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import { SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon } from '#/components/icons/SettingsGear2'; +import * as Layout from '#/components/Layout'; +import { Link } from '#/components/Link'; +import { ListFooter } from '#/components/Lists'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +import { ChatListItem } from './components/ChatListItem'; +import { InboxPreview } from './components/InboxPreview'; +function renderItem(_a) { + var item = _a.item; + switch (item.type) { + case 'INBOX': + return _jsx(InboxPreview, { profiles: item.profiles }); + case 'CONVERSATION': + return _jsx(ChatListItem, { convo: item.conversation }); + } +} +function keyExtractor(item) { + return item.type === 'INBOX' ? 'INBOX' : item.conversation.id; +} +export function MessagesScreen(props) { + var _ = useLingui()._; + var aaCopy = useAgeAssuranceCopy(); + return (_jsx(AgeRestrictedScreen, { screenTitle: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Chats"], ["Chats"])))), infoText: aaCopy.chatsInfoText, rightHeaderSlot: _jsx(Link, { to: "/messages/settings", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Chat settings"], ["Chat settings"])))), size: "small", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Chat settings" }) }) }), children: _jsx(MessagesScreenInner, __assign({}, props)) })); +} +export function MessagesScreenInner(_a) { + var _this = this; + var _b, _c; + var navigation = _a.navigation, route = _a.route; + var _ = useLingui()._; + var t = useTheme(); + var currentAccount = useSession().currentAccount; + var newChatControl = useDialogControl(); + var scrollElRef = useAnimatedRef(); + var pushToConversation = (_b = route.params) === null || _b === void 0 ? void 0 : _b.pushToConversation; + // Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on + // this tab. We should immediately push to the conversation after pressing the notification. + // After we push, reset with `setParams` so that this effect will fire next time we press a notification, even if + // the conversation is the same as before + useEffect(function () { + if (pushToConversation) { + navigation.navigate('MessagesConversation', { + conversation: pushToConversation, + }); + navigation.setParams({ pushToConversation: undefined }); + } + }, [navigation, pushToConversation]); + // Request the poll interval to be 10s (or whatever the MESSAGE_SCREEN_POLL_INTERVAL is set to in the future) + // but only when the screen is active + var messagesBus = useMessagesEventBus(); + var state = useAppState(); + var isActive = state === 'active'; + useFocusEffect(useCallback(function () { + if (isActive) { + var unsub_1 = messagesBus.requestPollInterval(MESSAGE_SCREEN_POLL_INTERVAL); + return function () { return unsub_1(); }; + } + }, [messagesBus, isActive])); + var initialNumToRender = useInitialNumToRender({ minItemHeight: 80 }); + var _d = useState(false), isPTRing = _d[0], setIsPTRing = _d[1]; + var _e = useListConvosQuery({ status: 'accepted' }), data = _e.data, isLoading = _e.isLoading, isFetchingNextPage = _e.isFetchingNextPage, hasNextPage = _e.hasNextPage, fetchNextPage = _e.fetchNextPage, isError = _e.isError, error = _e.error, refetch = _e.refetch; + var _f = useListConvosQuery({ + status: 'request', + }), inboxData = _f.data, refetchInbox = _f.refetch; + useRefreshOnFocus(refetch); + useRefreshOnFocus(refetchInbox); + var leftConvos = useLeftConvos(); + var inboxAllConvos = (_c = inboxData === null || inboxData === void 0 ? void 0 : inboxData.pages.flatMap(function (page) { return page.convos; }).filter(function (convo) { + return !leftConvos.includes(convo.id) && + !convo.muted && + convo.members.every(function (member) { return member.handle !== 'missing.invalid'; }); + })) !== null && _c !== void 0 ? _c : []; + var hasInboxConvos = (inboxAllConvos === null || inboxAllConvos === void 0 ? void 0 : inboxAllConvos.length) > 0; + var inboxUnreadConvos = inboxAllConvos.filter(function (convo) { return convo.unreadCount > 0; }); + var inboxUnreadConvoMembers = inboxUnreadConvos + .map(function (x) { return x.members.find(function (y) { return y.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); }) + .filter(function (x) { return !!x; }); + var conversations = useMemo(function () { + if (data === null || data === void 0 ? void 0 : data.pages) { + var conversations_1 = data.pages + .flatMap(function (page) { return page.convos; }) + // filter out convos that are actively being left + .filter(function (convo) { return !leftConvos.includes(convo.id); }); + return __spreadArray(__spreadArray([], (hasInboxConvos + ? [ + { + type: 'INBOX', + count: inboxUnreadConvoMembers.length, + profiles: inboxUnreadConvoMembers.slice(0, 3), + }, + ] + : []), true), conversations_1.map(function (convo) { + return ({ + type: 'CONVERSATION', + conversation: convo, + }); + }), true); + } + return []; + }, [data, leftConvos, hasInboxConvos, inboxUnreadConvoMembers]); + var onRefresh = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTRing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, Promise.all([refetch(), refetchInbox()])]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + logger.error('Failed to refresh conversations', { message: err_1 }); + return [3 /*break*/, 4]; + case 4: + setIsPTRing(false); + return [2 /*return*/]; + } + }); + }); }, [refetch, refetchInbox, setIsPTRing]); + var onEndReached = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isFetchingNextPage || !hasNextPage || isError) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPage()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_2 = _a.sent(); + logger.error('Failed to load more conversations', { message: err_2 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]); + var onNewChat = useCallback(function (conversation) { + return navigation.navigate('MessagesConversation', { conversation: conversation }); + }, [navigation]); + var onSoftReset = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_3; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: 0, + }); + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, refetch()]; + case 2: + _b.sent(); + return [3 /*break*/, 4]; + case 3: + err_3 = _b.sent(); + logger.error('Failed to refresh conversations', { message: err_3 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [scrollElRef, refetch]); + var isScreenFocused = useIsFocused(); + useEffect(function () { + if (!isScreenFocused) { + return; + } + return listenSoftReset(onSoftReset); + }, [onSoftReset, isScreenFocused]); + // NOTE(APiligrim) + // Show empty state only if there are no conversations at all + var activeConversations = conversations.filter(function (item) { return item.type === 'CONVERSATION'; }); + if (activeConversations.length === 0) { + return (_jsxs(Layout.Screen, { children: [_jsx(Header, { newChatControl: newChatControl }), _jsxs(Layout.Center, { children: [!isLoading && hasInboxConvos && (_jsx(InboxPreview, { profiles: inboxUnreadConvoMembers })), isLoading ? (_jsx(ChatListLoadingPlaceholder, {})) : (_jsx(_Fragment, { children: isError ? (_jsx(_Fragment, { children: _jsxs(View, { style: [a.pt_3xl, a.align_center], children: [_jsx(CircleInfoIcon, { width: 48, fill: t.atoms.text_contrast_low.color }), _jsx(Text, { style: [a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Whoops!" }) }), _jsx(Text, { style: [ + a.text_md, + a.pb_xl, + a.text_center, + a.leading_snug, + t.atoms.text_contrast_medium, + { maxWidth: 360 }, + ], children: cleanError(error) || + _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to load conversations"], ["Failed to load conversations"])))) }), _jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Reload conversations"], ["Reload conversations"])))), size: "small", color: "secondary_inverted", variant: "solid", onPress: function () { return refetch(); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), _jsx(ButtonIcon, { icon: RetryIcon, position: "right" })] })] }) })) : (_jsx(_Fragment, { children: _jsxs(View, { style: [a.pt_3xl, a.align_center], children: [_jsx(MessageIcon, { width: 48, fill: t.palette.primary_500 }), _jsx(Text, { style: [a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Nothing here" }) }), _jsx(Text, { style: [ + a.text_md, + a.pb_xl, + a.text_center, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "You have no conversations yet. Start one!" }) })] }) })) }))] }), !isLoading && !isError && (_jsx(NewChat, { onNewChat: onNewChat, control: newChatControl }))] })); + } + return (_jsxs(Layout.Screen, { testID: "messagesScreen", children: [_jsx(Header, { newChatControl: newChatControl }), _jsx(NewChat, { onNewChat: onNewChat, control: newChatControl }), _jsx(List, { ref: scrollElRef, data: conversations, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTRing, onRefresh: onRefresh, onEndReached: onEndReached, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage, style: { borderColor: 'transparent' }, hasNextPage: hasNextPage }), onEndReachedThreshold: IS_NATIVE ? 1.5 : 0, initialNumToRender: initialNumToRender, windowSize: 11, desktopFixedHeight: true, sideBorders: false })] })); +} +function Header(_a) { + var newChatControl = _a.newChatControl; + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var requireEmailVerification = useRequireEmailVerification(); + var openChatControl = useCallback(function () { + newChatControl.open(); + }, [newChatControl]); + var wrappedOpenChatControl = requireEmailVerification(openChatControl, { + instructions: [ + _jsx(Trans, { children: "Before you can message another user, you must first verify your email." }, "new-chat"), + ], + }); + var settingsLink = (_jsx(Link, { to: "/messages/settings", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Chat settings"], ["Chat settings"])))), size: "small", variant: "ghost", color: "secondary", shape: "round", style: [a.justify_center], children: _jsx(ButtonIcon, { icon: SettingsIcon, size: "lg" }) })); + return (_jsx(Layout.Header.Outer, { children: gtMobile ? (_jsxs(_Fragment, { children: [_jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Chats" }) }) }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: [settingsLink, _jsxs(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["New chat"], ["New chat"])))), color: "primary", size: "small", variant: "solid", onPress: wrappedOpenChatControl, children: [_jsx(ButtonIcon, { icon: PlusIcon, position: "left" }), _jsx(ButtonText, { children: _jsx(Trans, { children: "New chat" }) })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Layout.Header.MenuButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Chats" }) }) }), _jsx(Layout.Header.Slot, { children: settingsLink })] })) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/Messages/Conversation.js b/src/screens/Messages/Conversation.js new file mode 100644 index 0000000000..8b3b4847c8 --- /dev/null +++ b/src/screens/Messages/Conversation.js @@ -0,0 +1,157 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React, { useCallback, useEffect } from 'react'; +import { View } from 'react-native'; +import { moderateProfile, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect, useNavigation, useRoute, } from '@react-navigation/native'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { useMaybeProfileShadow } from '#/state/cache/profile-shadow'; +import { useEmail } from '#/state/email-verification'; +import { ConvoProvider, isConvoActive, useConvo } from '#/state/messages/convo'; +import { ConvoStatus } from '#/state/messages/convo/types'; +import { useCurrentConvoId } from '#/state/messages/current-convo-id'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { MessagesList } from '#/screens/Messages/components/MessagesList'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { AgeRestrictedScreen } from '#/components/ageAssurance/AgeRestrictedScreen'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog'; +import { MessagesListBlockedFooter } from '#/components/dms/MessagesListBlockedFooter'; +import { MessagesListHeader } from '#/components/dms/MessagesListHeader'; +import { Error } from '#/components/Error'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import { IS_WEB } from '#/env'; +export function MessagesConversationScreen(props) { + var _ = useLingui()._; + var aaCopy = useAgeAssuranceCopy(); + return (_jsx(AgeRestrictedScreen, { screenTitle: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Conversation"], ["Conversation"])))), infoText: aaCopy.chatsInfoText, children: _jsx(MessagesConversationScreenInner, __assign({}, props)) })); +} +export function MessagesConversationScreenInner(_a) { + var route = _a.route; + var gtMobile = useBreakpoints().gtMobile; + var setMinimalShellMode = useSetMinimalShellMode(); + var convoId = route.params.conversation; + var setCurrentConvoId = useCurrentConvoId().setCurrentConvoId; + useFocusEffect(useCallback(function () { + setCurrentConvoId(convoId); + if (IS_WEB && !gtMobile) { + setMinimalShellMode(true); + } + else { + setMinimalShellMode(false); + } + return function () { + setCurrentConvoId(undefined); + setMinimalShellMode(false); + }; + }, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode])); + return (_jsx(Layout.Screen, { testID: "convoScreen", style: web([{ minHeight: 0 }, a.flex_1]), children: _jsx(ConvoProvider, { convoId: convoId, children: _jsx(Inner, {}) }, convoId) })); +} +function Inner() { + var _a; + var t = useTheme(); + var convoState = useConvo(); + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + var recipientUnshadowed = useProfileQuery({ + did: (_a = convoState.recipients) === null || _a === void 0 ? void 0 : _a[0].did, + }).data; + var recipient = useMaybeProfileShadow(recipientUnshadowed); + var moderation = React.useMemo(function () { + if (!recipient || !moderationOpts) + return null; + return moderateProfile(recipient, moderationOpts); + }, [recipient, moderationOpts]); + // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, + // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be + // empty. So, we also check for that possible state as well and render once we can. + var _b = React.useState(false), hasScrolled = _b[0], setHasScrolled = _b[1]; + var readyToShow = hasScrolled || + (isConvoActive(convoState) && + !convoState.isFetchingHistory && + convoState.items.length === 0); + // Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this + // state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added. + React.useEffect(function () { + if (convoState.status === ConvoStatus.Initializing) { + setHasScrolled(false); + } + }, [convoState.status]); + if (convoState.status === ConvoStatus.Error) { + return (_jsxs(_Fragment, { children: [_jsx(Layout.Center, { style: [a.flex_1], children: moderation ? (_jsx(MessagesListHeader, { moderation: moderation, profile: recipient })) : (_jsx(MessagesListHeader, {})) }), _jsx(Error, { title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Something went wrong"], ["Something went wrong"])))), message: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["We couldn't load this conversation"], ["We couldn't load this conversation"])))), onRetry: function () { return convoState.error.retry(); }, sideBorders: false })] })); + } + return (_jsxs(Layout.Center, { style: [a.flex_1], children: [!readyToShow && + (moderation ? (_jsx(MessagesListHeader, { moderation: moderation, profile: recipient })) : (_jsx(MessagesListHeader, {}))), _jsxs(View, { style: [a.flex_1], children: [moderation && recipient ? (_jsx(InnerReady, { moderation: moderation, recipient: recipient, hasScrolled: hasScrolled, setHasScrolled: setHasScrolled })) : (_jsx(View, { style: [a.align_center, a.gap_sm, a.flex_1] })), !readyToShow && (_jsx(View, { style: [ + a.absolute, + a.z_10, + a.w_full, + a.h_full, + a.justify_center, + a.align_center, + t.atoms.bg, + ], children: _jsx(View, { style: [{ marginBottom: 75 }], children: _jsx(Loader, { size: "xl" }) }) }))] })] })); +} +function InnerReady(_a) { + var moderation = _a.moderation, recipient = _a.recipient, hasScrolled = _a.hasScrolled, setHasScrolled = _a.setHasScrolled; + var convoState = useConvo(); + var navigation = useNavigation(); + var params = useRoute().params; + var needsEmailVerification = useEmail().needsEmailVerification; + var emailDialogControl = useEmailDialogControl(); + /** + * Must be non-reactive, otherwise the update to open the global dialog will + * cause a re-render loop. + */ + var maybeBlockForEmailVerification = useNonReactiveCallback(function () { + if (needsEmailVerification) { + /* + * HACKFIX + * + * Load bearing timeout, to bump this state update until the after the + * `navigator.addListener('state')` handler closes elements from + * `shell/index.*.tsx` - sfn & esb + */ + setTimeout(function () { + return emailDialogControl.open({ + id: EmailDialogScreenID.Verify, + instructions: [ + _jsx(Trans, { children: "Before you can message another user, you must first verify your email." }, "pre-compose"), + ], + onCloseWithoutVerifying: function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Messages', { animation: 'pop' }); + } + }, + }); + }); + } + }); + useEffect(function () { + maybeBlockForEmailVerification(); + }, [maybeBlockForEmailVerification]); + return (_jsxs(_Fragment, { children: [_jsx(MessagesListHeader, { profile: recipient, moderation: moderation }), isConvoActive(convoState) && (_jsx(MessagesList, { hasScrolled: hasScrolled, setHasScrolled: setHasScrolled, blocked: moderation === null || moderation === void 0 ? void 0 : moderation.blocked, hasAcceptOverride: !!params.accept, footer: _jsx(MessagesListBlockedFooter, { recipient: recipient, convoId: convoState.convo.id, hasMessages: convoState.items.length > 0, moderation: moderation }) }))] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Messages/Inbox.js b/src/screens/Messages/Inbox.js new file mode 100644 index 0000000000..efbc544195 --- /dev/null +++ b/src/screens/Messages/Inbox.js @@ -0,0 +1,237 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; +import { useAppState } from '#/lib/appState'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { cleanError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { MESSAGE_SCREEN_POLL_INTERVAL } from '#/state/messages/convo/const'; +import { useMessagesEventBus } from '#/state/messages/events'; +import { useLeftConvos } from '#/state/queries/messages/leave-conversation'; +import { useListConvosQuery } from '#/state/queries/messages/list-conversations'; +import { useUpdateAllRead } from '#/state/queries/messages/update-all-read'; +import { FAB } from '#/view/com/util/fab/FAB'; +import { List } from '#/view/com/util/List'; +import { ChatListLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { AgeRestrictedScreen } from '#/components/ageAssurance/AgeRestrictedScreen'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useRefreshOnFocus } from '#/components/hooks/useRefreshOnFocus'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon } from '#/components/icons/Arrow'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon } from '#/components/icons/ArrowRotate'; +import { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon } from '#/components/icons/CircleInfo'; +import { Message_Stroke2_Corner0_Rounded as MessageIcon } from '#/components/icons/Message'; +import * as Layout from '#/components/Layout'; +import { ListFooter } from '#/components/Lists'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +import { RequestListItem } from './components/RequestListItem'; +export function MessagesInboxScreen(props) { + var _ = useLingui()._; + var aaCopy = useAgeAssuranceCopy(); + return (_jsx(AgeRestrictedScreen, { screenTitle: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Chat requests"], ["Chat requests"])))), infoText: aaCopy.chatsInfoText, children: _jsx(MessagesInboxScreenInner, __assign({}, props)) })); +} +export function MessagesInboxScreenInner(_a) { + var gtTablet = useBreakpoints().gtTablet; + var listConvosQuery = useListConvosQuery({ status: 'request' }); + var data = listConvosQuery.data; + var leftConvos = useLeftConvos(); + var conversations = useMemo(function () { + if (data === null || data === void 0 ? void 0 : data.pages) { + var convos = data.pages + .flatMap(function (page) { return page.convos; }) + // filter out convos that are actively being left + .filter(function (convo) { return !leftConvos.includes(convo.id); }); + return convos; + } + return []; + }, [data, leftConvos]); + var hasUnreadConvos = useMemo(function () { + return conversations.some(function (conversation) { + return conversation.members.every(function (member) { return member.handle !== 'missing.invalid'; }) && conversation.unreadCount > 0; + }); + }, [conversations]); + return (_jsxs(Layout.Screen, { testID: "messagesInboxScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { align: gtTablet ? 'left' : 'platform', children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Chat requests" }) }) }), hasUnreadConvos && gtTablet ? (_jsx(MarkAsReadHeaderButton, {})) : (_jsx(Layout.Header.Slot, {}))] }), _jsx(RequestList, { listConvosQuery: listConvosQuery, conversations: conversations, hasUnreadConvos: hasUnreadConvos })] })); +} +function RequestList(_a) { + var _this = this; + var listConvosQuery = _a.listConvosQuery, conversations = _a.conversations, hasUnreadConvos = _a.hasUnreadConvos; + var _ = useLingui()._; + var t = useTheme(); + var navigation = useNavigation(); + // Request the poll interval to be 10s (or whatever the MESSAGE_SCREEN_POLL_INTERVAL is set to in the future) + // but only when the screen is active + var messagesBus = useMessagesEventBus(); + var state = useAppState(); + var isActive = state === 'active'; + useFocusEffect(useCallback(function () { + if (isActive) { + var unsub_1 = messagesBus.requestPollInterval(MESSAGE_SCREEN_POLL_INTERVAL); + return function () { return unsub_1(); }; + } + }, [messagesBus, isActive])); + var initialNumToRender = useInitialNumToRender({ minItemHeight: 130 }); + var _b = useState(false), isPTRing = _b[0], setIsPTRing = _b[1]; + var isLoading = listConvosQuery.isLoading, isFetchingNextPage = listConvosQuery.isFetchingNextPage, hasNextPage = listConvosQuery.hasNextPage, fetchNextPage = listConvosQuery.fetchNextPage, isError = listConvosQuery.isError, error = listConvosQuery.error, refetch = listConvosQuery.refetch; + useRefreshOnFocus(refetch); + var onRefresh = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTRing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, refetch()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + logger.error('Failed to refresh conversations', { message: err_1 }); + return [3 /*break*/, 4]; + case 4: + setIsPTRing(false); + return [2 /*return*/]; + } + }); + }); }, [refetch, setIsPTRing]); + var onEndReached = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isFetchingNextPage || !hasNextPage || isError) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPage()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_2 = _a.sent(); + logger.error('Failed to load more conversations', { message: err_2 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]); + if (conversations.length < 1) { + return (_jsx(Layout.Center, { children: isLoading ? (_jsx(ChatListLoadingPlaceholder, {})) : (_jsx(_Fragment, { children: isError ? (_jsx(_Fragment, { children: _jsxs(View, { style: [a.pt_3xl, a.align_center], children: [_jsx(CircleInfoIcon, { width: 48, fill: t.atoms.text_contrast_low.color }), _jsx(Text, { style: [a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Whoops!" }) }), _jsx(Text, { style: [ + a.text_md, + a.pb_xl, + a.text_center, + a.leading_snug, + t.atoms.text_contrast_medium, + { maxWidth: 360 }, + ], children: cleanError(error) || _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to load conversations"], ["Failed to load conversations"])))) }), _jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Reload conversations"], ["Reload conversations"])))), size: "small", color: "secondary_inverted", variant: "solid", onPress: function () { return refetch(); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), _jsx(ButtonIcon, { icon: RetryIcon, position: "right" })] })] }) })) : (_jsx(_Fragment, { children: _jsxs(View, { style: [a.pt_3xl, a.align_center], children: [_jsx(MessageIcon, { width: 48, fill: t.palette.primary_500 }), _jsx(Text, { style: [a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { comment: "Title message shown in chat requests inbox when it's empty", children: "Inbox zero!" }) }), _jsx(Text, { style: [ + a.text_md, + a.pb_xl, + a.text_center, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "You don't have any chat requests at the moment." }) }), _jsxs(Button, { variant: "solid", color: "secondary", size: "small", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Go back"], ["Go back"])))), onPress: function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Messages', { animation: 'pop' }); + } + }, children: [_jsx(ButtonIcon, { icon: ArrowLeftIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Back to Chats" }) })] })] }) })) })) })); + } + return (_jsxs(_Fragment, { children: [_jsx(List, { data: conversations, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTRing, onRefresh: onRefresh, onEndReached: onEndReached, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage, style: { borderColor: 'transparent' }, hasNextPage: hasNextPage }), onEndReachedThreshold: IS_NATIVE ? 1.5 : 0, initialNumToRender: initialNumToRender, windowSize: 11, desktopFixedHeight: true, sideBorders: false }), hasUnreadConvos && _jsx(MarkAllReadFAB, {})] })); +} +function keyExtractor(item) { + return item.id; +} +function renderItem(_a) { + var item = _a.item; + return _jsx(RequestListItem, { convo: item }); +} +function MarkAllReadFAB() { + var _ = useLingui()._; + var t = useTheme(); + var markAllRead = useUpdateAllRead('request', { + onMutate: function () { + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Marked all as read"], ["Marked all as read"])))), 'check'); + }, + onError: function () { + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Failed to mark all requests as read"], ["Failed to mark all requests as read"])))), 'xmark'); + }, + }).mutate; + return (_jsx(FAB, { testID: "markAllAsReadFAB", onPress: function () { return markAllRead(); }, icon: _jsx(CheckIcon, { size: "lg", fill: t.palette.white }), accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Mark all as read"], ["Mark all as read"])))), accessibilityHint: "" })); +} +function MarkAsReadHeaderButton() { + var _ = useLingui()._; + var markAllRead = useUpdateAllRead('request', { + onMutate: function () { + Toast.show(_(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Marked all as read"], ["Marked all as read"])))), 'check'); + }, + onError: function () { + Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Failed to mark all requests as read"], ["Failed to mark all requests as read"])))), 'xmark'); + }, + }).mutate; + return (_jsxs(Button, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Mark all as read"], ["Mark all as read"])))), size: "small", color: "secondary", variant: "solid", onPress: function () { return markAllRead(); }, children: [_jsx(ButtonIcon, { icon: CheckIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Mark all as read" }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10; diff --git a/src/screens/Messages/Settings.js b/src/screens/Messages/Settings.js new file mode 100644 index 0000000000..1a7a79d1f6 --- /dev/null +++ b/src/screens/Messages/Settings.js @@ -0,0 +1,66 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useUpdateActorDeclaration } from '#/state/queries/messages/actor-declaration'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Divider } from '#/components/Divider'; +import * as Toggle from '#/components/forms/Toggle'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +import { useBackgroundNotificationPreferences } from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'; +export function MessagesSettingsScreen(props) { + return _jsx(MessagesSettingsScreenInner, __assign({}, props)); +} +export function MessagesSettingsScreenInner(_a) { + var _b, _c, _d; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var profile = useProfileQuery({ + did: currentAccount.did, + }).data; + var _e = useBackgroundNotificationPreferences(), preferences = _e.preferences, setPref = _e.setPref; + var updateDeclaration = useUpdateActorDeclaration({ + onError: function () { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to update settings"], ["Failed to update settings"])))), 'xmark'); + }, + }).mutate; + var onSelectMessagesFrom = useCallback(function (keys) { + var key = keys[0]; + if (!key) + return; + updateDeclaration(key); + }, [updateDeclaration]); + var onSelectSoundSetting = useCallback(function (keys) { + var key = keys[0]; + if (!key) + return; + setPref('playSoundChat', key === 'enabled'); + }, [setPref]); + return (_jsxs(Layout.Screen, { testID: "messagesSettingsScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Chat Settings" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(View, { style: [a.p_lg, a.gap_md], children: [_jsx(Text, { style: [a.text_lg, a.font_semi_bold], children: _jsx(Trans, { children: "Allow new messages from" }) }), _jsx(Toggle.Group, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Allow new messages from"], ["Allow new messages from"])))), type: "radio", values: [ + (_d = (_c = (_b = profile === null || profile === void 0 ? void 0 : profile.associated) === null || _b === void 0 ? void 0 : _b.chat) === null || _c === void 0 ? void 0 : _c.allowIncoming) !== null && _d !== void 0 ? _d : 'following', + ], onChange: onSelectMessagesFrom, children: _jsxs(View, { children: [_jsxs(Toggle.Item, { name: "all", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Everyone"], ["Everyone"])))), style: [a.justify_between, a.py_sm], children: [_jsx(Toggle.LabelText, { children: _jsx(Trans, { children: "Everyone" }) }), _jsx(Toggle.Radio, {})] }), _jsxs(Toggle.Item, { name: "following", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Users I follow"], ["Users I follow"])))), style: [a.justify_between, a.py_sm], children: [_jsx(Toggle.LabelText, { children: _jsx(Trans, { children: "Users I follow" }) }), _jsx(Toggle.Radio, {})] }), _jsxs(Toggle.Item, { name: "none", label: _(msg({ context: 'allow messages from', message: "No one" })), style: [a.justify_between, a.py_sm], children: [_jsx(Toggle.LabelText, { children: _jsx(Trans, { context: "allow messages from", children: "No one" }) }), _jsx(Toggle.Radio, {})] })] }) }), _jsx(Admonition, { type: "tip", children: _jsx(Trans, { children: "You can continue ongoing conversations regardless of which setting you choose." }) }), IS_NATIVE && (_jsxs(_Fragment, { children: [_jsx(Divider, { style: a.my_md }), _jsx(Text, { style: [a.text_lg, a.font_semi_bold], children: _jsx(Trans, { children: "Notification Sounds" }) }), _jsx(Toggle.Group, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Notification sounds"], ["Notification sounds"])))), type: "radio", values: [preferences.playSoundChat ? 'enabled' : 'disabled'], onChange: onSelectSoundSetting, children: _jsxs(View, { children: [_jsxs(Toggle.Item, { name: "enabled", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Enabled"], ["Enabled"])))), style: [a.justify_between, a.py_sm], children: [_jsx(Toggle.LabelText, { children: _jsx(Trans, { children: "Enabled" }) }), _jsx(Toggle.Radio, {})] }), _jsxs(Toggle.Item, { name: "disabled", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Disabled"], ["Disabled"])))), style: [a.justify_between, a.py_sm], children: [_jsx(Toggle.LabelText, { children: _jsx(Trans, { children: "Disabled" }) }), _jsx(Toggle.Radio, {})] })] }) })] }))] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/screens/Messages/components/ChatDisabled.js b/src/screens/Messages/components/ChatDisabled.js new file mode 100644 index 0000000000..677ee8a50c --- /dev/null +++ b/src/screens/Messages/components/ChatDisabled.js @@ -0,0 +1,119 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { ToolsOzoneReportDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation } from '@tanstack/react-query'; +import { BLUESKY_MOD_SERVICE_HEADERS } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useAgent, useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +export function ChatDisabled() { + var t = useTheme(); + return (_jsx(View, { style: [a.p_md], children: _jsxs(View, { style: [a.align_start, a.p_xl, a.rounded_md, t.atoms.bg_contrast_25], children: [_jsx(Text, { style: [ + a.text_md, + a.font_semi_bold, + a.pb_sm, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Your chats have been disabled" }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Our moderators have reviewed reports and decided to disable your access to chats on Bluesky." }) }), _jsx(AppealDialog, {})] }) })); +} +function AppealDialog() { + var control = Dialog.useDialogControl(); + var _ = useLingui()._; + return (_jsxs(_Fragment, { children: [_jsx(Button, { testID: "appealDisabledChatBtn", variant: "ghost", color: "secondary", size: "small", onPress: control.open, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Appeal this decision"], ["Appeal this decision"])))), style: a.mt_sm, children: _jsx(ButtonText, { children: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Appeal this decision"], ["Appeal this decision"])))) }) }), _jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, {})] })] })); +} +function DialogInner() { + var _this = this; + var _ = useLingui()._; + var control = Dialog.useDialogContext(); + var _a = useState(''), details = _a[0], setDetails = _a[1]; + var gtMobile = useBreakpoints().gtMobile; + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + var _b = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!currentAccount) + throw new Error('No current account, should be unreachable'); + return [4 /*yield*/, agent.createModerationReport({ + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: currentAccount.did, + }, + reason: details, + }, { + encoding: 'application/json', + headers: BLUESKY_MOD_SERVICE_HEADERS, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onError: function (err) { + logger.error('Failed to submit chat appeal', { message: err }); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to submit appeal, please try again."], ["Failed to submit appeal, please try again."])))), 'xmark'); + }, + onSuccess: function () { + control.close(); + Toast.show(_(msg({ message: 'Appeal submitted', context: 'toast' }))); + }, + }), mutate = _b.mutate, isPending = _b.isPending; + var onSubmit = useCallback(function () { return mutate(); }, [mutate]); + var onBack = useCallback(function () { return control.close(); }, [control]); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Appeal this decision"], ["Appeal this decision"])))), children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold, a.pb_xs, a.leading_tight], children: _jsx(Trans, { children: "Appeal this decision" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "This appeal will be sent to Bluesky's moderation service." }) }), _jsx(View, { style: [a.my_md], children: _jsx(Dialog.Input, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Text input field"], ["Text input field"])))), placeholder: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Please explain why you think your chats were incorrectly disabled"], ["Please explain why you think your chats were incorrectly disabled"])))), value: details, onChangeText: setDetails, autoFocus: true, numberOfLines: 3, multiline: true, maxLength: 300 }) }), _jsxs(View, { style: gtMobile + ? [a.flex_row, a.justify_between] + : [{ flexDirection: 'column-reverse' }, a.gap_sm], children: [_jsx(Button, { testID: "backBtn", variant: "solid", color: "secondary", size: "large", onPress: onBack, label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Back"], ["Back"])))), children: _jsx(ButtonText, { children: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Back"], ["Back"])))) }) }), _jsxs(Button, { testID: "submitBtn", variant: "solid", color: "primary", size: "large", onPress: onSubmit, label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Submit"], ["Submit"])))), children: [_jsx(ButtonText, { children: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Submit"], ["Submit"])))) }), isPending && _jsx(ButtonIcon, { icon: Loader })] })] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10; diff --git a/src/screens/Messages/components/ChatListItem.js b/src/screens/Messages/components/ChatListItem.js new file mode 100644 index 0000000000..49fc5dfe9a --- /dev/null +++ b/src/screens/Messages/components/ChatListItem.js @@ -0,0 +1,323 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React, { useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { AppBskyEmbedRecord, ChatBskyConvoDefs, moderateProfile, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { GestureActionView } from '#/lib/custom-animations/GestureActionView'; +import { useHaptics } from '#/lib/haptics'; +import { decrementBadgeCount } from '#/lib/notifications/notifications'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { postUriToRelativePath, toBskyAppUrl, toShortUrl, } from '#/lib/strings/url-helpers'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { precacheConvoQuery, useMarkAsReadMutation, } from '#/state/queries/messages/conversation'; +import { precacheProfile } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { TimeElapsed } from '#/view/com/util/TimeElapsed'; +import { PreviewableUserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import * as tokens from '#/alf/tokens'; +import { useDialogControl } from '#/components/Dialog'; +import { ConvoMenu } from '#/components/dms/ConvoMenu'; +import { LeaveConvoPrompt } from '#/components/dms/LeaveConvoPrompt'; +import { Bell2Off_Filled_Corner0_Rounded as BellStroke } from '#/components/icons/Bell2'; +import { Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen } from '#/components/icons/EnveopeOpen'; +import { Trash_Stroke2_Corner0_Rounded } from '#/components/icons/Trash'; +import { Link } from '#/components/Link'; +import { useMenuControl } from '#/components/Menu'; +import { PostAlerts } from '#/components/moderation/PostAlerts'; +import { createPortalGroup } from '#/components/Portal'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +export var ChatListItemPortal = createPortalGroup(); +export var ChatListItem = function (_a) { + var convo = _a.convo, _b = _a.showMenu, showMenu = _b === void 0 ? true : _b, children = _a.children; + var currentAccount = useSession().currentAccount; + var moderationOpts = useModerationOpts(); + var otherUser = convo.members.find(function (member) { return member.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); + if (!otherUser || !moderationOpts) { + return null; + } + return (_jsx(ChatListItemReady, { convo: convo, profile: otherUser, moderationOpts: moderationOpts, showMenu: showMenu, children: children })); +}; +ChatListItem = React.memo(ChatListItem); +function ChatListItemReady(_a) { + var convo = _a.convo, profileUnshadowed = _a.profile, moderationOpts = _a.moderationOpts, showMenu = _a.showMenu, children = _a.children; + var ax = useAnalytics(); + var t = useTheme(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var menuControl = useMenuControl(); + var leaveConvoControl = useDialogControl(); + var gtMobile = useBreakpoints().gtMobile; + var profile = useProfileShadow(profileUnshadowed); + var markAsRead = useMarkAsReadMutation().mutate; + var moderation = React.useMemo(function () { return moderateProfile(profile, moderationOpts); }, [profile, moderationOpts]); + var playHaptic = useHaptics(); + var queryClient = useQueryClient(); + var isUnread = convo.unreadCount > 0; + var verification = useSimpleVerificationState({ + profile: profile, + }); + var blockInfo = useMemo(function () { + var modui = moderation.ui('profileView'); + var blocks = modui.alerts.filter(function (alert) { return alert.type === 'blocking'; }); + var listBlocks = blocks.filter(function (alert) { return alert.source.type === 'list'; }); + var userBlock = blocks.find(function (alert) { return alert.source.type === 'user'; }); + return { + listBlocks: listBlocks, + userBlock: userBlock, + }; + }, [moderation]); + var isDeletedAccount = profile.handle === 'missing.invalid'; + var displayName = isDeletedAccount + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Deleted Account"], ["Deleted Account"])))) + : sanitizeDisplayName(profile.displayName || profile.handle, moderation.ui('displayName')); + var isDimStyle = convo.muted || moderation.blocked || isDeletedAccount; + var _b = useMemo(function () { + var _a; + var lastMessage = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["No messages yet"], ["No messages yet"])))); + var lastMessageSentAt = null; + var latestReportableMessage; + if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { + var isFromMe = ((_a = convo.lastMessage.sender) === null || _a === void 0 ? void 0 : _a.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + if (!isFromMe) { + latestReportableMessage = convo.lastMessage; + } + if (convo.lastMessage.text) { + if (isFromMe) { + lastMessage = _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You: ", ""], ["You: ", ""])), convo.lastMessage.text)); + } + else { + lastMessage = convo.lastMessage.text; + } + } + else if (convo.lastMessage.embed) { + var defaultEmbeddedContentMessage = _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["(contains embedded content)"], ["(contains embedded content)"])))); + if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) { + var embed = convo.lastMessage.embed; + if (AppBskyEmbedRecord.isViewRecord(embed.record)) { + var record = embed.record; + var path = postUriToRelativePath(record.uri, { + handle: record.author.handle, + }); + var href = path ? toBskyAppUrl(path) : undefined; + var short = href + ? toShortUrl(href) + : defaultEmbeddedContentMessage; + if (isFromMe) { + lastMessage = _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["You: ", ""], ["You: ", ""])), short)); + } + else { + lastMessage = short; + } + } + } + else { + if (isFromMe) { + lastMessage = _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["You: ", ""], ["You: ", ""])), defaultEmbeddedContentMessage)); + } + else { + lastMessage = defaultEmbeddedContentMessage; + } + } + } + lastMessageSentAt = convo.lastMessage.sentAt; + } + if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { + lastMessageSentAt = convo.lastMessage.sentAt; + lastMessage = isDeletedAccount + ? _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Conversation deleted"], ["Conversation deleted"])))) + : _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Message deleted"], ["Message deleted"])))); + } + if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) { + if (!lastMessageSentAt || + new Date(lastMessageSentAt) < + new Date(convo.lastReaction.reaction.createdAt)) { + var isFromMe = convo.lastReaction.reaction.sender.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var lastMessageText = convo.lastReaction.message.text; + var fallbackMessage = _(msg({ + message: 'a message', + comment: "If last message does not contain text, fall back to \"{user} reacted to {a message}\"", + })); + if (isFromMe) { + lastMessage = _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["You reacted ", " to ", ""], ["You reacted ", " to ", ""])), convo.lastReaction.reaction.value, lastMessageText + ? "\"".concat(convo.lastReaction.message.text, "\"") + : fallbackMessage)); + } + else { + var senderDid_1 = convo.lastReaction.reaction.sender.did; + var sender = convo.members.find(function (member) { return member.did === senderDid_1; }); + if (sender) { + lastMessage = _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["", " reacted ", " to ", ""], ["", " reacted ", " to ", ""])), sanitizeDisplayName(sender.displayName || sender.handle), convo.lastReaction.reaction.value, lastMessageText + ? "\"".concat(convo.lastReaction.message.text, "\"") + : fallbackMessage)); + } + else { + lastMessage = _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Someone reacted ", " to ", ""], ["Someone reacted ", " to ", ""])), convo.lastReaction.reaction.value, lastMessageText + ? "\"".concat(convo.lastReaction.message.text, "\"") + : fallbackMessage)); + } + } + } + } + return { + lastMessage: lastMessage, + lastMessageSentAt: lastMessageSentAt, + latestReportableMessage: latestReportableMessage, + }; + }, [ + _, + convo.lastMessage, + convo.lastReaction, + currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + isDeletedAccount, + convo.members, + ]), lastMessage = _b.lastMessage, lastMessageSentAt = _b.lastMessageSentAt, latestReportableMessage = _b.latestReportableMessage; + var _c = useState(false), showActions = _c[0], setShowActions = _c[1]; + var onMouseEnter = useCallback(function () { + setShowActions(true); + }, []); + var onMouseLeave = useCallback(function () { + setShowActions(false); + }, []); + var onFocus = useCallback(function (e) { + if (e.nativeEvent.relatedTarget == null) + return; + setShowActions(true); + }, []); + var onPress = useCallback(function (e) { + precacheProfile(queryClient, profile); + precacheConvoQuery(queryClient, convo); + decrementBadgeCount(convo.unreadCount); + if (isDeletedAccount) { + e.preventDefault(); + menuControl.open(); + return false; + } + else { + ax.metric('chat:open', { logContext: 'ChatsList' }); + } + }, [ax, isDeletedAccount, menuControl, queryClient, profile, convo]); + var onLongPress = useCallback(function () { + playHaptic(); + menuControl.open(); + }, [playHaptic, menuControl]); + var markReadAction = { + threshold: 120, + color: t.palette.primary_500, + icon: EnvelopeOpen, + action: function () { + markAsRead({ + convoId: convo.id, + }); + }, + }; + var deleteAction = { + threshold: 225, + color: t.palette.negative_500, + icon: Trash_Stroke2_Corner0_Rounded, + action: function () { + leaveConvoControl.open(); + }, + }; + var actions = isUnread + ? { + leftFirst: markReadAction, + leftSecond: deleteAction, + } + : { + leftFirst: deleteAction, + }; + var hasUnread = convo.unreadCount > 0 && !isDeletedAccount; + return (_jsx(ChatListItemPortal.Provider, { children: _jsx(GestureActionView, { actions: actions, children: _jsxs(View, { onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, + // @ts-expect-error web only + onFocus: onFocus, onBlur: onMouseLeave, style: [a.relative, t.atoms.bg], children: [_jsx(View, { style: [ + a.z_10, + a.absolute, + { top: tokens.space.md, left: tokens.space.lg }, + ], children: _jsx(PreviewableUserAvatar, { profile: profile, size: 52, moderation: moderation.ui('avatar') }) }), _jsx(Link, { to: "/messages/".concat(convo.id), label: displayName, accessibilityHint: !isDeletedAccount + ? _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Go to conversation with ", ""], ["Go to conversation with ", ""])), profile.handle)) + : _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["This conversation is with a deleted or a deactivated account. Press for options"], ["This conversation is with a deleted or a deactivated account. Press for options"])))), accessibilityActions: IS_NATIVE + ? [ + { + name: 'magicTap', + label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Open conversation options"], ["Open conversation options"])))), + }, + { + name: 'longpress', + label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Open conversation options"], ["Open conversation options"])))), + }, + ] + : undefined, onPress: onPress, onLongPress: IS_NATIVE ? onLongPress : undefined, onAccessibilityAction: onLongPress, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed, focused = _a.focused; + return (_jsxs(View, { style: [ + a.flex_row, + isDeletedAccount ? a.align_center : a.align_start, + a.flex_1, + a.px_lg, + a.py_md, + a.gap_md, + (hovered || pressed || focused) && t.atoms.bg_contrast_25, + ], children: [_jsx(View, { style: { width: 52, height: 52 } }), _jsxs(View, { style: [a.flex_1, a.justify_center, web({ paddingRight: 45 })], children: [_jsxs(View, { style: [a.w_full, a.flex_row, a.align_end, a.pb_2xs], children: [_jsx(View, { style: [a.flex_shrink], children: _jsx(Text, { emoji: true, numberOfLines: 1, style: [ + a.text_md, + t.atoms.text, + a.font_semi_bold, + { lineHeight: 21 }, + isDimStyle && t.atoms.text_contrast_medium, + ], children: displayName }) }), verification.showBadge && (_jsx(View, { style: [a.pl_xs, a.self_center], children: _jsx(VerificationCheck, { width: 14, verifier: verification.role === 'verifier' }) })), lastMessageSentAt && (_jsx(View, { style: [a.pl_xs], children: _jsx(TimeElapsed, { timestamp: lastMessageSentAt, children: function (_a) { + var timeElapsed = _a.timeElapsed; + return (_jsxs(Text, { style: [ + a.text_sm, + { lineHeight: 21 }, + t.atoms.text_contrast_medium, + web({ whiteSpace: 'preserve nowrap' }), + ], children: ["\u00B7 ", timeElapsed] })); + } }) })), (convo.muted || moderation.blocked) && (_jsxs(Text, { style: [ + a.text_sm, + { lineHeight: 21 }, + t.atoms.text_contrast_medium, + web({ whiteSpace: 'preserve nowrap' }), + ], children: [' ', "\u00B7", ' ', _jsx(BellStroke, { size: "xs", style: [t.atoms.text_contrast_medium] })] }))] }), !isDeletedAccount && (_jsxs(Text, { numberOfLines: 1, style: [ + a.text_sm, + t.atoms.text_contrast_medium, + a.pb_xs, + ], children: ["@", profile.handle] })), _jsx(Text, { emoji: true, numberOfLines: 2, style: [ + a.text_sm, + a.leading_snug, + hasUnread ? a.font_semi_bold : t.atoms.text_contrast_high, + isDimStyle && t.atoms.text_contrast_medium, + ], children: lastMessage }), _jsx(PostAlerts, { modui: moderation.ui('contentList'), size: "lg", style: [a.pt_xs] }), children] }), hasUnread && (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + { + backgroundColor: isDimStyle + ? t.palette.contrast_200 + : t.palette.primary_500, + height: 7, + width: 7, + top: 15, + right: 12, + }, + ] }))] })); + } }), _jsx(ChatListItemPortal.Outlet, {}), showMenu && (_jsx(ConvoMenu, { convo: convo, profile: profile, control: menuControl, currentScreen: "list", showMarkAsRead: convo.unreadCount > 0, hideTrigger: IS_NATIVE, blockInfo: blockInfo, style: [ + a.absolute, + a.h_full, + a.self_end, + a.justify_center, + { + right: tokens.space.lg, + opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0, + }, + ], latestReportableMessage: latestReportableMessage })), _jsx(LeaveConvoPrompt, { control: leaveConvoControl, convoId: convo.id, currentScreen: "list" })] }) }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15; diff --git a/src/screens/Messages/components/ChatStatusInfo.js b/src/screens/Messages/components/ChatStatusInfo.js new file mode 100644 index 0000000000..136e7570e0 --- /dev/null +++ b/src/screens/Messages/components/ChatStatusInfo.js @@ -0,0 +1,33 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { LeaveConvoPrompt } from '#/components/dms/LeaveConvoPrompt'; +import { KnownFollowers } from '#/components/KnownFollowers'; +import { usePromptControl } from '#/components/Prompt'; +import { AcceptChatButton, DeleteChatButton, RejectMenu } from './RequestButtons'; +export function ChatStatusInfo(_a) { + var convoState = _a.convoState; + var t = useTheme(); + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + var currentAccount = useSession().currentAccount; + var leaveConvoControl = usePromptControl(); + var onAcceptChat = useCallback(function () { + convoState.markConvoAccepted(); + }, [convoState]); + var otherUser = convoState.recipients.find(function (user) { return user.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); + if (!moderationOpts) { + return null; + } + return (_jsxs(View, { style: [t.atoms.bg, a.p_lg, a.gap_md, a.align_center], children: [otherUser && (_jsx(KnownFollowers, { profile: otherUser, moderationOpts: moderationOpts, showIfEmpty: true })), _jsxs(View, { style: [a.flex_row, a.gap_md, a.w_full, otherUser && a.pt_sm], children: [otherUser && (_jsx(RejectMenu, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Block or report"], ["Block or report"])))), convo: convoState.convo, profile: otherUser, color: "negative_subtle", size: "small", currentScreen: "conversation" })), _jsx(DeleteChatButton, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Delete"], ["Delete"])))), convo: convoState.convo, color: "secondary", size: "small", currentScreen: "conversation", onPress: leaveConvoControl.open }), _jsx(LeaveConvoPrompt, { convoId: convoState.convo.id, control: leaveConvoControl, currentScreen: "conversation", hasMessages: false })] }), _jsx(View, { style: [a.w_full, a.flex_row], children: _jsx(AcceptChatButton, { onAcceptConvo: onAcceptChat, convo: convoState.convo, color: "primary_subtle", size: "small", currentScreen: "conversation" }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Messages/components/InboxPreview.js b/src/screens/Messages/components/InboxPreview.js new file mode 100644 index 0000000000..9bf0076d52 --- /dev/null +++ b/src/screens/Messages/components/InboxPreview.js @@ -0,0 +1,46 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme } from '#/alf'; +import { AvatarStack } from '#/components/AvatarStack'; +import { ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon } from '#/components/icons/Arrow'; +import { Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon } from '#/components/icons/Envelope'; +import { Link } from '#/components/Link'; +export function InboxPreview(_a) { + var profiles = _a.profiles; + var _ = useLingui()._; + var t = useTheme(); + return (_jsxs(Link, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Chat request inbox"], ["Chat request inbox"])))), style: [ + a.flex_1, + a.px_xl, + a.py_sm, + a.flex_row, + a.align_center, + a.gap_md, + a.border_t, + { marginTop: a.border_t.borderTopWidth * -1 }, + a.border_b, + t.atoms.border_contrast_low, + { minHeight: 44 }, + a.rounded_0, + ], to: "/messages/inbox", color: "secondary", variant: "solid", children: [_jsxs(View, { style: [a.relative], children: [_jsx(ButtonIcon, { icon: EnvelopeIcon, size: "lg" }), profiles.length > 0 && (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + a.z_20, + { + top: -4, + right: -5, + width: 10, + height: 10, + backgroundColor: t.palette.primary_500, + }, + ] }))] }), _jsx(ButtonText, { style: [a.flex_1, a.font_semi_bold, a.text_left], numberOfLines: 1, children: _jsx(Trans, { children: "Chat requests" }) }), _jsx(AvatarStack, { profiles: profiles, backgroundColor: t.atoms.bg_contrast_25.backgroundColor }), _jsx(ButtonIcon, { icon: ArrowRightIcon, size: "lg" })] })); +} +var templateObject_1; diff --git a/src/screens/Messages/components/MessageInput.js b/src/screens/Messages/components/MessageInput.js new file mode 100644 index 0000000000..e42df261db --- /dev/null +++ b/src/screens/Messages/components/MessageInput.js @@ -0,0 +1,141 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { Pressable, TextInput, useWindowDimensions, View } from 'react-native'; +import { useFocusedInputHandler, useReanimatedKeyboardAnimation, } from 'react-native-keyboard-controller'; +import Animated, { measure, useAnimatedProps, useAnimatedRef, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { countGraphemes } from 'unicode-segmenter/grapheme'; +import { HITSLOP_10, MAX_DM_GRAPHEME_LENGTH } from '#/lib/constants'; +import { useHaptics } from '#/lib/haptics'; +import { useEmail } from '#/state/email-verification'; +import { useMessageDraft, useSaveMessageDraft, } from '#/state/messages/message-drafts'; +import * as Toast from '#/view/com/util/Toast'; +import { android, atoms as a, useTheme } from '#/alf'; +import { useSharedInputStyles } from '#/components/forms/TextField'; +import { PaperPlane_Stroke2_Corner0_Rounded as PaperPlane } from '#/components/icons/PaperPlane'; +import { IS_IOS, IS_WEB } from '#/env'; +import { useExtractEmbedFromFacets } from './MessageInputEmbed'; +var AnimatedTextInput = Animated.createAnimatedComponent(TextInput); +export function MessageInput(_a) { + var onSendMessage = _a.onSendMessage, hasEmbed = _a.hasEmbed, setEmbed = _a.setEmbed, children = _a.children; + var _ = useLingui()._; + var t = useTheme(); + var playHaptic = useHaptics(); + var _b = useMessageDraft(), getDraft = _b.getDraft, clearDraft = _b.clearDraft; + // Input layout + var topInset = useSafeAreaInsets().top; + var windowHeight = useWindowDimensions().height; + var keyboardHeight = useReanimatedKeyboardAnimation().height; + var maxHeight = useSharedValue(undefined); + var isInputScrollable = useSharedValue(false); + var inputStyles = useSharedInputStyles(); + var _c = useState(false), isFocused = _c[0], setIsFocused = _c[1]; + var _d = useState(getDraft), message = _d[0], setMessage = _d[1]; + var inputRef = useAnimatedRef(); + var _e = useState(false), shouldEnforceClear = _e[0], setShouldEnforceClear = _e[1]; + var needsEmailVerification = useEmail().needsEmailVerification; + useSaveMessageDraft(message); + useExtractEmbedFromFacets(message, setEmbed); + var onSubmit = useCallback(function () { + if (needsEmailVerification) { + return; + } + if (!hasEmbed && message.trim() === '') { + return; + } + if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Message is too long"], ["Message is too long"])))), 'xmark'); + return; + } + clearDraft(); + onSendMessage(message); + playHaptic(); + setEmbed(undefined); + setMessage(''); + if (IS_IOS) { + setShouldEnforceClear(true); + } + if (IS_WEB) { + // Pressing the send button causes the text input to lose focus, so we need to + // re-focus it after sending + setTimeout(function () { + var _a; + (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); + }, 100); + } + }, [ + needsEmailVerification, + hasEmbed, + message, + clearDraft, + onSendMessage, + playHaptic, + setEmbed, + inputRef, + _, + ]); + useFocusedInputHandler({ + onChangeText: function () { + 'worklet'; + var measurement = measure(inputRef); + if (!measurement) + return; + var max = windowHeight - -keyboardHeight.get() - topInset - 150; + var availableSpace = max - measurement.height; + maxHeight.set(max); + isInputScrollable.set(availableSpace < 30); + }, + }, [windowHeight, topInset]); + var animatedStyle = useAnimatedStyle(function () { return ({ + maxHeight: maxHeight.get(), + }); }); + var animatedProps = useAnimatedProps(function () { return ({ + scrollEnabled: isInputScrollable.get(), + }); }); + return (_jsxs(View, { style: [a.px_md, a.pb_sm, a.pt_xs], children: [children, _jsxs(View, { style: [ + a.w_full, + a.flex_row, + t.atoms.bg_contrast_25, + { + padding: a.p_sm.padding - 2, + paddingLeft: a.p_md.padding - 2, + borderWidth: 1, + borderRadius: 23, + borderColor: 'transparent', + }, + isFocused && inputStyles.chromeFocus, + ], children: [_jsx(AnimatedTextInput, { accessibilityLabel: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Message input field"], ["Message input field"])))), accessibilityHint: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Type your message here"], ["Type your message here"])))), placeholder: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Write a message"], ["Write a message"])))), placeholderTextColor: t.palette.contrast_500, value: message, onChange: function (evt) { + // bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen + // including the button we just pressed - and this overrides clearing the input! so we watch for the + // next change and double make sure the input is cleared. It should *always* send an onChange event after + // clearing via setMessage('') that happens in onSubmit() + // -sfn + if (IS_IOS && shouldEnforceClear) { + setShouldEnforceClear(false); + setMessage(''); + return; + } + var text = evt.nativeEvent.text; + setMessage(text); + }, multiline: true, style: [ + a.flex_1, + a.text_md, + a.px_sm, + t.atoms.text, + android({ paddingTop: 0 }), + { paddingBottom: IS_IOS ? 5 : 0 }, + animatedStyle, + ], keyboardAppearance: t.scheme, submitBehavior: "newline", onFocus: function () { return setIsFocused(true); }, onBlur: function () { return setIsFocused(false); }, ref: inputRef, hitSlop: HITSLOP_10, animatedProps: animatedProps, editable: !needsEmailVerification }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Send message"], ["Send message"])))), accessibilityHint: "", hitSlop: HITSLOP_10, style: [ + a.rounded_full, + a.align_center, + a.justify_center, + { height: 30, width: 30, backgroundColor: t.palette.primary_500 }, + ], onPress: onSubmit, disabled: needsEmailVerification, children: _jsx(PaperPlane, { fill: t.palette.white, style: [a.relative, { left: 1 }] }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/screens/Messages/components/MessageInput.web.js b/src/screens/Messages/components/MessageInput.web.js new file mode 100644 index 0000000000..ce281352cc --- /dev/null +++ b/src/screens/Messages/components/MessageInput.web.js @@ -0,0 +1,179 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Pressable, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { flushSync } from 'react-dom'; +import TextareaAutosize from 'react-textarea-autosize'; +import { countGraphemes } from 'unicode-segmenter/grapheme'; +import { MAX_DM_GRAPHEME_LENGTH } from '#/lib/constants'; +import { useWebMediaQueries } from '#/lib/hooks/useWebMediaQueries'; +import { useMessageDraft, useSaveMessageDraft, } from '#/state/messages/message-drafts'; +import { textInputWebEmitter } from '#/view/com/composer/text-input/textInputWebEmitter'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, flatten, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { useSharedInputStyles } from '#/components/forms/TextField'; +import { EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile } from '#/components/icons/Emoji'; +import { PaperPlane_Stroke2_Corner0_Rounded as PaperPlane } from '#/components/icons/PaperPlane'; +import { IS_WEB_SAFARI, IS_WEB_TOUCH_DEVICE } from '#/env'; +import { useExtractEmbedFromFacets } from './MessageInputEmbed'; +export function MessageInput(_a) { + var onSendMessage = _a.onSendMessage, hasEmbed = _a.hasEmbed, setEmbed = _a.setEmbed, children = _a.children, openEmojiPicker = _a.openEmojiPicker; + var isMobile = useWebMediaQueries().isMobile; + var _ = useLingui()._; + var t = useTheme(); + var _b = useMessageDraft(), getDraft = _b.getDraft, clearDraft = _b.clearDraft; + var _c = React.useState(getDraft), message = _c[0], setMessage = _c[1]; + var inputStyles = useSharedInputStyles(); + var isComposing = React.useRef(false); + var _d = React.useState(false), isFocused = _d[0], setIsFocused = _d[1]; + var _e = React.useState(false), isHovered = _e[0], setIsHovered = _e[1]; + var _f = React.useState(38), textAreaHeight = _f[0], setTextAreaHeight = _f[1]; + var textAreaRef = React.useRef(null); + var onSubmit = React.useCallback(function () { + if (!hasEmbed && message.trim() === '') { + return; + } + if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Message is too long"], ["Message is too long"])))), 'xmark'); + return; + } + clearDraft(); + onSendMessage(message); + setMessage(''); + setEmbed(undefined); + }, [message, onSendMessage, _, clearDraft, hasEmbed, setEmbed]); + var onKeyDown = React.useCallback(function (e) { + // Don't submit the form when the Japanese or any other IME is composing + if (isComposing.current) + return; + // see https://github.com/bluesky-social/social-app/issues/4178 + // see https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/ + // see https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html + // + // On Safari, the final keydown event to dismiss the IME - which is the enter key - is also "Enter" below. + // Obviously, this causes problems because the final dismissal should _not_ submit the text, but should just + // stop the IME editing. This is the behavior of Chrome and Firefox, but not Safari. + // + // Keycode is deprecated, however the alternative seems to only be to compare the timestamp from the + // onCompositionEnd event to the timestamp of the keydown event, which is not reliable. For example, this hack + // uses that method: https://github.com/ProseMirror/prosemirror-view/pull/44. However, from my 500ms resulted in + // far too long of a delay, and a subsequent enter press would often just end up doing nothing. A shorter time + // frame was also not great, since it was too short to be reliable (i.e. an older system might have a larger + // time gap between the two events firing. + if (IS_WEB_SAFARI && e.key === 'Enter' && e.keyCode === 229) { + return; + } + if (e.key === 'Enter') { + if (e.shiftKey) + return; + e.preventDefault(); + onSubmit(); + } + }, [onSubmit]); + var onChange = React.useCallback(function (e) { + setMessage(e.target.value); + }, []); + var onEmojiInserted = React.useCallback(function (emoji) { + var _a; + if (!textAreaRef.current) { + return; + } + var position = (_a = textAreaRef.current.selectionStart) !== null && _a !== void 0 ? _a : 0; + textAreaRef.current.focus(); + flushSync(function () { + setMessage(function (message) { + return message.slice(0, position) + emoji.native + message.slice(position); + }); + }); + textAreaRef.current.selectionStart = position + emoji.native.length; + textAreaRef.current.selectionEnd = position + emoji.native.length; + }, [setMessage]); + React.useEffect(function () { + textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted); + return function () { + textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted); + }; + }, [onEmojiInserted]); + useSaveMessageDraft(message); + useExtractEmbedFromFacets(message, setEmbed); + return (_jsxs(View, { style: a.p_sm, children: [children, _jsxs(View, { style: [ + a.flex_row, + t.atoms.bg_contrast_25, + { + paddingRight: a.p_sm.padding - 2, + paddingLeft: a.p_sm.padding - 2, + borderWidth: 1, + borderRadius: 23, + borderColor: 'transparent', + height: textAreaHeight + 23, + }, + isHovered && inputStyles.chromeHover, + isFocused && inputStyles.chromeFocus, + ], + // @ts-expect-error web only + onMouseEnter: function () { return setIsHovered(true); }, onMouseLeave: function () { return setIsHovered(false); }, children: [_jsx(Button, { onPress: function (e) { + e.currentTarget.measure(function (_fx, _fy, _width, _height, px, py) { + openEmojiPicker === null || openEmojiPicker === void 0 ? void 0 : openEmojiPicker({ + top: py, + left: px, + right: px, + bottom: py, + nextFocusRef: textAreaRef, + }); + }); + }, style: [ + a.rounded_full, + a.overflow_hidden, + a.align_center, + a.justify_center, + { + marginTop: 5, + height: 30, + width: 30, + }, + ], label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Open emoji picker"], ["Open emoji picker"])))), children: function (state) { return (_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.align_center, + a.justify_center, + { + backgroundColor: state.hovered || state.focused || state.pressed + ? t.atoms.bg.backgroundColor + : undefined, + }, + ], children: _jsx(EmojiSmile, { size: "lg" }) })); } }), _jsx(TextareaAutosize, { ref: textAreaRef, style: flatten([ + a.flex_1, + a.px_sm, + a.border_0, + t.atoms.text, + { + paddingTop: 10, + backgroundColor: 'transparent', + resize: 'none', + }, + ]), maxRows: 12, placeholder: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Write a message"], ["Write a message"])))), defaultValue: "", value: message, dirName: "ltr", autoFocus: true, onFocus: function () { return setIsFocused(true); }, onBlur: function () { return setIsFocused(false); }, onCompositionStart: function () { + isComposing.current = true; + }, onCompositionEnd: function () { + isComposing.current = false; + }, onHeightChange: function (height) { return setTextAreaHeight(height); }, onChange: onChange, + // On mobile web phones, we want to keep the same behavior as the native app. Do not submit the message + // in these cases. + onKeyDown: IS_WEB_TOUCH_DEVICE && isMobile ? undefined : onKeyDown }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Send message"], ["Send message"])))), accessibilityHint: "", style: [ + a.rounded_full, + a.align_center, + a.justify_center, + { + height: 30, + width: 30, + marginTop: 5, + backgroundColor: t.palette.primary_500, + }, + ], onPress: onSubmit, children: _jsx(PaperPlane, { fill: t.palette.white, style: [a.relative, { left: 1 }] }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Messages/components/MessageInputEmbed.js b/src/screens/Messages/components/MessageInputEmbed.js new file mode 100644 index 0000000000..1b3066d096 --- /dev/null +++ b/src/screens/Messages/components/MessageInputEmbed.js @@ -0,0 +1,128 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { LayoutAnimation, View } from 'react-native'; +import { AppBskyFeedPost, AppBskyRichtextFacet, AtUri, moderatePost, RichText as RichTextAPI, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import { makeProfileLink } from '#/lib/routes/links'; +import { convertBskyAppUrlIfNeeded, isBskyPostUrl, makeRecordUri, } from '#/lib/strings/url-helpers'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { usePostQuery } from '#/state/queries/post'; +import { PostMeta } from '#/view/com/util/PostMeta'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Loader } from '#/components/Loader'; +import * as MediaPreview from '#/components/MediaPreview'; +import { ContentHider } from '#/components/moderation/ContentHider'; +import { PostAlerts } from '#/components/moderation/PostAlerts'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import * as bsky from '#/types/bsky'; +export function useMessageEmbed() { + var route = useRoute(); + var navigation = useNavigation(); + var embedFromParams = route.params.embed; + var _a = useState(embedFromParams), embedUri = _a[0], setEmbed = _a[1]; + if (embedFromParams && embedUri !== embedFromParams) { + setEmbed(embedFromParams); + } + return { + embedUri: embedUri, + setEmbed: useCallback(function (embedUrl) { + if (!embedUrl) { + navigation.setParams({ embed: '' }); + setEmbed(undefined); + return; + } + if (embedFromParams) + return; + var url = convertBskyAppUrlIfNeeded(embedUrl); + var _a = url.split('/').filter(Boolean), _0 = _a[0], user = _a[1], _1 = _a[2], rkey = _a[3]; + var uri = makeRecordUri(user, 'app.bsky.feed.post', rkey); + setEmbed(uri); + }, [embedFromParams, navigation]), + }; +} +export function useExtractEmbedFromFacets(message, setEmbed) { + var _a; + var rt = new RichTextAPI({ text: message }); + rt.detectFacetsWithoutResolution(); + var uriFromFacet; + for (var _i = 0, _b = (_a = rt.facets) !== null && _a !== void 0 ? _a : []; _i < _b.length; _i++) { + var facet = _b[_i]; + for (var _c = 0, _d = facet.features; _c < _d.length; _c++) { + var feature = _d[_c]; + if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) { + uriFromFacet = feature.uri; + break; + } + } + } + useEffect(function () { + if (uriFromFacet) { + setEmbed(uriFromFacet); + } + }, [uriFromFacet, setEmbed]); +} +export function MessageInputEmbed(_a) { + var embedUri = _a.embedUri, setEmbed = _a.setEmbed; + var t = useTheme(); + var _ = useLingui()._; + var _b = usePostQuery(embedUri), post = _b.data, status = _b.status; + var moderationOpts = useModerationOpts(); + var moderation = useMemo(function () { + return moderationOpts && post ? moderatePost(post, moderationOpts) : undefined; + }, [moderationOpts, post]); + var _c = useMemo(function () { + if (post && + bsky.dangerousIsType(post.record, AppBskyFeedPost.isRecord)) { + return { + rt: new RichTextAPI({ + text: post.record.text, + facets: post.record.facets, + }), + record: post.record, + }; + } + return { rt: undefined, record: undefined }; + }, [post]), rt = _c.rt, record = _c.record; + if (!embedUri) { + return null; + } + var content = null; + switch (status) { + case 'pending': + content = (_jsx(View, { style: [a.flex_1, { minHeight: 64 }, a.justify_center, a.align_center], children: _jsx(Loader, {}) })); + break; + case 'error': + content = (_jsx(View, { style: [a.flex_1, { minHeight: 64 }, a.justify_center, a.align_center], children: _jsx(Text, { style: a.text_center, children: "Could not fetch post" }) })); + break; + case 'success': + var itemUrip = new AtUri(post.uri); + var itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey); + if (!post || !moderation || !rt || !record) { + return null; + } + content = (_jsxs(View, { style: [ + a.flex_1, + t.atoms.bg, + t.atoms.border_contrast_low, + a.rounded_md, + a.border, + a.p_sm, + a.mb_sm, + ], pointerEvents: "none", children: [_jsx(PostMeta, { showAvatar: true, author: post.author, moderation: moderation, timestamp: post.indexedAt, postHref: itemHref, style: a.flex_0 }), _jsxs(ContentHider, { modui: moderation.ui('contentView'), children: [_jsx(PostAlerts, { modui: moderation.ui('contentView'), style: a.py_xs }), rt.text && (_jsx(View, { style: a.mt_xs, children: _jsx(RichText, { enableTags: true, testID: "postText", value: rt, style: [a.text_sm, t.atoms.text_contrast_high], authorHandle: post.author.handle, numberOfLines: 3 }) })), _jsx(MediaPreview.Embed, { embed: post.embed, style: a.mt_sm })] })] })); + break; + } + return (_jsxs(View, { style: [a.flex_row, a.gap_sm], children: [content, _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Remove embed"], ["Remove embed"])))), onPress: function () { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setEmbed(undefined); + }, size: "tiny", variant: "solid", color: "secondary", shape: "round", children: _jsx(ButtonIcon, { icon: X }) })] })); +} +var templateObject_1; diff --git a/src/screens/Messages/components/MessageListError.js b/src/screens/Messages/components/MessageListError.js new file mode 100644 index 0000000000..ecc26c909e --- /dev/null +++ b/src/screens/Messages/components/MessageListError.js @@ -0,0 +1,48 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { ConvoItemError } from '#/state/messages/convo/types'; +import { atoms as a, useTheme } from '#/alf'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export function MessageListError(_a) { + var item = _a.item; + var t = useTheme(); + var _ = useLingui()._; + var _b = React.useMemo(function () { + var _a; + return (_a = {}, + _a[ConvoItemError.FirehoseFailed] = { + description: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["This chat was disconnected"], ["This chat was disconnected"])))), + help: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Press to attempt reconnection"], ["Press to attempt reconnection"])))), + cta: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Reconnect"], ["Reconnect"])))), + }, + _a[ConvoItemError.HistoryFailed] = { + description: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Failed to load past messages"], ["Failed to load past messages"])))), + help: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Press to retry"], ["Press to retry"])))), + cta: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Retry"], ["Retry"])))), + }, + _a)[item.code]; + }, [_, item.code]), description = _b.description, help = _b.help, cta = _b.cta; + return (_jsx(View, { style: [a.py_md, a.w_full, a.flex_row, a.justify_center], children: _jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.align_center, + a.justify_center, + a.gap_sm, + { maxWidth: 400 }, + ], children: [_jsx(CircleInfo, { size: "sm", fill: t.palette.negative_400 }), _jsxs(Text, { style: [a.leading_snug, t.atoms.text_contrast_medium], children: [description, " \u00B7", ' ', item.retry && (_jsx(InlineLinkText, { to: "#", label: help, onPress: function (e) { + var _a; + e.preventDefault(); + (_a = item.retry) === null || _a === void 0 ? void 0 : _a.call(item); + return false; + }, children: cta }))] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/Messages/components/MessagesList.js b/src/screens/Messages/components/MessagesList.js new file mode 100644 index 0000000000..feb970c012 --- /dev/null +++ b/src/screens/Messages/components/MessagesList.js @@ -0,0 +1,413 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { useKeyboardHandler } from 'react-native-keyboard-controller'; +import Animated, { runOnJS, scrollTo, useAnimatedRef, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; +import { AppBskyRichtextFacet, RichText, } from '@atproto/api'; +import { useHideBottomBarBorderForScreen } from '#/lib/hooks/useHideBottomBarBorder'; +import { ScrollProvider } from '#/lib/ScrollContext'; +import { shortenLinks, stripInvalidMentions } from '#/lib/strings/rich-text-manip'; +import { convertBskyAppUrlIfNeeded, isBskyPostUrl, } from '#/lib/strings/url-helpers'; +import { logger } from '#/logger'; +import { isConvoActive, useConvoActive, } from '#/state/messages/convo'; +import { ConvoStatus, } from '#/state/messages/convo/types'; +import { useGetPost } from '#/state/queries/post'; +import { useAgent } from '#/state/session'; +import { useShellLayout } from '#/state/shell/shell-layout'; +import { EmojiPicker, } from '#/view/com/composer/text-input/web/EmojiPicker'; +import { List } from '#/view/com/util/List'; +import { ChatDisabled } from '#/screens/Messages/components/ChatDisabled'; +import { MessageInput } from '#/screens/Messages/components/MessageInput'; +import { MessageListError } from '#/screens/Messages/components/MessageListError'; +import { ChatEmptyPill } from '#/components/dms/ChatEmptyPill'; +import { MessageItem } from '#/components/dms/MessageItem'; +import { NewMessagesPill } from '#/components/dms/NewMessagesPill'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +import { IS_WEB } from '#/env'; +import { ChatStatusInfo } from './ChatStatusInfo'; +import { MessageInputEmbed, useMessageEmbed } from './MessageInputEmbed'; +function MaybeLoader(_a) { + var isLoading = _a.isLoading; + return (_jsx(View, { style: { + height: 50, + width: '100%', + alignItems: 'center', + justifyContent: 'center', + }, children: isLoading && _jsx(Loader, { size: "xl" }) })); +} +function renderItem(_a) { + var item = _a.item; + if (item.type === 'message' || item.type === 'pending-message') { + return _jsx(MessageItem, { item: item }); + } + else if (item.type === 'deleted-message') { + return _jsx(Text, { children: "Deleted message" }); + } + else if (item.type === 'error') { + return _jsx(MessageListError, { item: item }); + } + return null; +} +function keyExtractor(item) { + return item.key; +} +function onScrollToIndexFailed() { + // Placeholder function. You have to give FlatList something or else it will error. +} +export function MessagesList(_a) { + var _this = this; + var hasScrolled = _a.hasScrolled, setHasScrolled = _a.setHasScrolled, blocked = _a.blocked, footer = _a.footer, hasAcceptOverride = _a.hasAcceptOverride; + var convoState = useConvoActive(); + var agent = useAgent(); + var getPost = useGetPost(); + var _b = useMessageEmbed(), embedUri = _b.embedUri, setEmbed = _b.setEmbed; + useHideBottomBarBorderForScreen(); + var flatListRef = useAnimatedRef(); + var _c = useState({ + show: false, + startContentOffset: 0, + }), newMessagesPill = _c[0], setNewMessagesPill = _c[1]; + var _d = useState({ + isOpen: false, + pos: { top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null }, + }), emojiPickerState = _d[0], setEmojiPickerState = _d[1]; + // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items + // are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to + // the bottom. + var isAtBottom = useSharedValue(true); + // This will be used on web to assist in determining if we need to maintain the content offset + var isAtTop = useSharedValue(true); + // Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing + // onStartReached to fire. + var prevContentHeight = useRef(0); + var prevItemCount = useRef(0); + // -- Keep track of background state and positioning for new pill + var layoutHeight = useSharedValue(0); + var didBackground = useRef(false); + useEffect(function () { + if (convoState.status === ConvoStatus.Backgrounded) { + didBackground.current = true; + } + }, [convoState.status]); + // -- Scroll handling + // Every time the content size changes, that means one of two things is happening: + // 1. New messages are being added from the log or from a message you have sent + // 2. Old messages are being prepended to the top + // + // The first time that the content size changes is when the initial items are rendered. Because we cannot rely on + // `initialScrollIndex`, we need to immediately scroll to the bottom of the list. That scroll will not be animated. + // + // Subsequent resizes will only scroll to the bottom if the user is at the bottom of the list (within 100 pixels of + // the bottom). Therefore, any new messages that come in or are sent will result in an animated scroll to end. However + // we will not scroll whenever new items get prepended to the top. + var onContentSizeChange = useCallback(function (_, height) { + var _a, _b, _c; + // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the + // previous off whenever we add new content to the previous offset whenever we add new content to the list. + if (IS_WEB && isAtTop.get() && hasScrolled) { + (_a = flatListRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + offset: height - prevContentHeight.current, + animated: false, + }); + } + // This number _must_ be the height of the MaybeLoader component + if (height > 50 && isAtBottom.get()) { + // If the size of the content is changing by more than the height of the screen, then we don't + // want to scroll further than the start of all the new content. Since we are storing the previous offset, + // we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill + // that can be pressed to immediately scroll to the end. + if (didBackground.current && + hasScrolled && + height - prevContentHeight.current > layoutHeight.get() - 50 && + convoState.items.length - prevItemCount.current > 1) { + (_b = flatListRef.current) === null || _b === void 0 ? void 0 : _b.scrollToOffset({ + offset: prevContentHeight.current - 65, + animated: true, + }); + setNewMessagesPill({ + show: true, + startContentOffset: prevContentHeight.current - 65, + }); + } + else { + (_c = flatListRef.current) === null || _c === void 0 ? void 0 : _c.scrollToOffset({ + offset: height, + animated: hasScrolled && height > prevContentHeight.current, + }); + // HACK Unfortunately, we need to call `setHasScrolled` after a brief delay, + // because otherwise there is too much of a delay between the time the content + // scrolls and the time the screen appears, causing a flicker. + // We cannot actually use a synchronous scroll here, because `onContentSizeChange` + // is actually async itself - all the info has to come across the bridge first. + if (!hasScrolled && !convoState.isFetchingHistory) { + setTimeout(function () { + setHasScrolled(true); + }, 100); + } + } + } + prevContentHeight.current = height; + prevItemCount.current = convoState.items.length; + didBackground.current = false; + }, [ + hasScrolled, + setHasScrolled, + convoState.isFetchingHistory, + convoState.items.length, + // these are stable + flatListRef, + isAtTop, + isAtBottom, + layoutHeight, + ]); + var onStartReached = useCallback(function () { + if (hasScrolled && prevContentHeight.current > layoutHeight.get()) { + convoState.fetchMessageHistory(); + } + }, [convoState, hasScrolled, layoutHeight]); + var onScroll = useCallback(function (e) { + 'worklet'; + layoutHeight.set(e.layoutMeasurement.height); + var bottomOffset = e.contentOffset.y + e.layoutMeasurement.height; + // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom + // when a new message is added, hence the 100 pixel offset + isAtBottom.set(e.contentSize.height - 100 < bottomOffset); + isAtTop.set(e.contentOffset.y <= 1); + if (newMessagesPill.show && + (e.contentOffset.y > newMessagesPill.startContentOffset + 200 || + isAtBottom.get())) { + runOnJS(setNewMessagesPill)({ + show: false, + startContentOffset: 0, + }); + } + }, [layoutHeight, newMessagesPill, isAtBottom, isAtTop]); + // -- Keyboard animation handling + var footerHeight = useShellLayout().footerHeight; + var keyboardHeight = useSharedValue(0); + var keyboardIsOpening = useSharedValue(false); + // In some cases - like when the emoji piker opens - we don't want to animate the scroll in the list onLayout event. + // We use this value to keep track of when we want to disable the animation. + var layoutScrollWithoutAnimation = useSharedValue(false); + useKeyboardHandler({ + onStart: function (e) { + 'worklet'; + // Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should + // just update the height here instead of having the `onMove` event do it (that event will not fire!) + if (e.duration === 0) { + layoutScrollWithoutAnimation.set(true); + keyboardHeight.set(e.height); + } + else { + keyboardIsOpening.set(true); + } + }, + onMove: function (e) { + 'worklet'; + keyboardHeight.set(e.height); + if (e.height > footerHeight.get()) { + scrollTo(flatListRef, 0, 1e7, false); + } + }, + onEnd: function (e) { + 'worklet'; + keyboardHeight.set(e.height); + if (e.height > footerHeight.get()) { + scrollTo(flatListRef, 0, 1e7, false); + } + keyboardIsOpening.set(false); + }, + }, [footerHeight]); + var animatedListStyle = useAnimatedStyle(function () { return ({ + marginBottom: Math.max(keyboardHeight.get(), footerHeight.get()), + }); }); + var animatedStickyViewStyle = useAnimatedStyle(function () { return ({ + transform: [ + { translateY: -Math.max(keyboardHeight.get(), footerHeight.get()) }, + ], + }); }); + // -- Message sending + var onSendMessage = useCallback(function (text) { return __awaiter(_this, void 0, void 0, function () { + var rt, embed, post_1, postLinkFacet, isAtStart, isAtEnd, error_1; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + rt = new RichText({ text: text.trimEnd() }, { cleanNewlines: true }); + // detect facets without resolution first - this is used to see if there's + // any post links in the text that we can embed. We do this first because + // we want to remove the post link from the text, re-trim, then detect facets + rt.detectFacetsWithoutResolution(); + if (!embedUri) return [3 /*break*/, 4]; + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, getPost({ uri: embedUri })]; + case 2: + post_1 = _b.sent(); + if (post_1) { + embed = { + $type: 'app.bsky.embed.record', + record: { + uri: post_1.uri, + cid: post_1.cid, + }, + }; + postLinkFacet = (_a = rt.facets) === null || _a === void 0 ? void 0 : _a.find(function (facet) { + return facet.features.find(function (feature) { + if (AppBskyRichtextFacet.isLink(feature)) { + if (isBskyPostUrl(feature.uri)) { + var url = convertBskyAppUrlIfNeeded(feature.uri); + var _a = url.split('/').filter(Boolean), _0 = _a[0], _1 = _a[1], _2 = _a[2], rkey = _a[3]; + // this might have a handle instead of a DID + // so just compare the rkey - not particularly dangerous + return post_1.uri.endsWith(rkey); + } + } + return false; + }); + }); + if (postLinkFacet) { + isAtStart = postLinkFacet.index.byteStart === 0; + isAtEnd = postLinkFacet.index.byteEnd === rt.unicodeText.graphemeLength; + // remove the post link from the text + if (isAtStart || isAtEnd) { + rt.delete(postLinkFacet.index.byteStart, postLinkFacet.index.byteEnd); + } + rt = new RichText({ text: rt.text.trim() }, { cleanNewlines: true }); + } + } + return [3 /*break*/, 4]; + case 3: + error_1 = _b.sent(); + logger.error('Failed to get post as quote for DM', { error: error_1 }); + return [3 /*break*/, 4]; + case 4: return [4 /*yield*/, rt.detectFacets(agent)]; + case 5: + _b.sent(); + rt = shortenLinks(rt); + rt = stripInvalidMentions(rt); + if (!hasScrolled) { + setHasScrolled(true); + } + convoState.sendMessage({ + text: rt.text, + facets: rt.facets, + embed: embed, + }); + return [2 /*return*/]; + } + }); + }); }, [agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled]); + // -- List layout changes (opening emoji keyboard, etc.) + var onListLayout = useCallback(function (e) { + var _a; + layoutHeight.set(e.nativeEvent.layout.height); + if (IS_WEB || !keyboardIsOpening.get()) { + (_a = flatListRef.current) === null || _a === void 0 ? void 0 : _a.scrollToEnd({ + animated: !layoutScrollWithoutAnimation.get(), + }); + layoutScrollWithoutAnimation.set(false); + } + }, [ + flatListRef, + keyboardIsOpening, + layoutScrollWithoutAnimation, + layoutHeight, + ]); + var scrollToEndOnPress = useCallback(function () { + var _a; + (_a = flatListRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + offset: prevContentHeight.current, + animated: true, + }); + }, [flatListRef]); + var onOpenEmojiPicker = useCallback(function (pos) { + setEmojiPickerState({ isOpen: true, pos: pos }); + }, []); + return (_jsxs(_Fragment, { children: [_jsx(ScrollProvider, { onScroll: onScroll, children: _jsx(List, { ref: flatListRef, data: convoState.items, renderItem: renderItem, keyExtractor: keyExtractor, disableFullWindowScroll: true, disableVirtualization: true, style: animatedListStyle, + // The extra two items account for the header and the footer components + initialNumToRender: IS_NATIVE ? 32 : 62, maxToRenderPerBatch: IS_WEB ? 32 : 62, keyboardDismissMode: "on-drag", keyboardShouldPersistTaps: "handled", maintainVisibleContentPosition: { + minIndexForVisible: 0, + }, removeClippedSubviews: false, sideBorders: false, onContentSizeChange: onContentSizeChange, onLayout: onListLayout, onStartReached: onStartReached, onScrollToIndexFailed: onScrollToIndexFailed, scrollEventThrottle: 100, ListHeaderComponent: _jsx(MaybeLoader, { isLoading: convoState.isFetchingHistory }) }) }), _jsx(Animated.View, { style: animatedStickyViewStyle, children: convoState.status === ConvoStatus.Disabled ? (_jsx(ChatDisabled, {})) : blocked ? (footer) : (_jsx(ConversationFooter, { convoState: convoState, hasAcceptOverride: hasAcceptOverride, children: _jsx(MessageInput, { onSendMessage: onSendMessage, hasEmbed: !!embedUri, setEmbed: setEmbed, openEmojiPicker: onOpenEmojiPicker, children: _jsx(MessageInputEmbed, { embedUri: embedUri, setEmbed: setEmbed }) }) })) }), IS_WEB && (_jsx(EmojiPicker, { pinToTop: true, state: emojiPickerState, close: function () { return setEmojiPickerState(function (prev) { return (__assign(__assign({}, prev), { isOpen: false })); }); } })), newMessagesPill.show && _jsx(NewMessagesPill, { onPress: scrollToEndOnPress })] })); +} +function getFooterState(convoState, hasAcceptOverride) { + if (convoState.items.length === 0) { + if (convoState.isFetchingHistory) { + return 'loading'; + } + else { + return 'new-chat'; + } + } + if (convoState.convo.status === 'request' && !hasAcceptOverride) { + return 'request'; + } + return 'standard'; +} +function ConversationFooter(_a) { + var convoState = _a.convoState, hasAcceptOverride = _a.hasAcceptOverride, children = _a.children; + if (!isConvoActive(convoState)) { + return null; + } + var footerState = getFooterState(convoState, hasAcceptOverride); + switch (footerState) { + case 'loading': + return null; + case 'new-chat': + return (_jsxs(_Fragment, { children: [_jsx(ChatEmptyPill, {}), children] })); + case 'request': + return _jsx(ChatStatusInfo, { convoState: convoState }); + case 'standard': + return children; + } +} diff --git a/src/screens/Messages/components/RequestButtons.js b/src/screens/Messages/components/RequestButtons.js new file mode 100644 index 0000000000..3834543f0b --- /dev/null +++ b/src/screens/Messages/components/RequestButtons.js @@ -0,0 +1,177 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { ChatBskyConvoDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { StackActions, useNavigation } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useEmail } from '#/state/email-verification'; +import { useAcceptConversation } from '#/state/queries/messages/accept-conversation'; +import { precacheConvoQuery } from '#/state/queries/messages/conversation'; +import { useLeaveConvo } from '#/state/queries/messages/leave-conversation'; +import { useProfileBlockMutationQueue } from '#/state/queries/profile'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText, } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog'; +import { AfterReportDialog } from '#/components/dms/AfterReportDialog'; +import { CircleX_Stroke2_Corner0_Rounded } from '#/components/icons/CircleX'; +import { Flag_Stroke2_Corner0_Rounded as FlagIcon } from '#/components/icons/Flag'; +import { PersonX_Stroke2_Corner0_Rounded as PersonXIcon } from '#/components/icons/Person'; +import { Loader } from '#/components/Loader'; +import * as Menu from '#/components/Menu'; +import { ReportDialog } from '#/components/moderation/ReportDialog'; +export function RejectMenu(_a) { + var convo = _a.convo, profile = _a.profile, _b = _a.size, size = _b === void 0 ? 'tiny' : _b, _c = _a.color, color = _c === void 0 ? 'secondary' : _c, label = _a.label, showDeleteConvo = _a.showDeleteConvo, currentScreen = _a.currentScreen, props = __rest(_a, ["convo", "profile", "size", "color", "label", "showDeleteConvo", "currentScreen"]); + var _ = useLingui()._; + var shadowedProfile = useProfileShadow(profile); + var navigation = useNavigation(); + var leaveConvo = useLeaveConvo(convo.id, { + onMutate: function () { + if (currentScreen === 'conversation') { + navigation.dispatch(StackActions.pop()); + } + }, + onError: function () { + Toast.show(_(msg({ + context: 'toast', + message: 'Failed to delete chat', + })), 'xmark'); + }, + }).mutate; + var queueBlock = useProfileBlockMutationQueue(shadowedProfile)[0]; + var onPressDelete = useCallback(function () { + Toast.show(_(msg({ + context: 'toast', + message: 'Chat deleted', + })), 'check'); + leaveConvo(); + }, [leaveConvo, _]); + var onPressBlock = useCallback(function () { + Toast.show(_(msg({ + context: 'toast', + message: 'Account blocked', + })), 'check'); + // block and also delete convo + queueBlock(); + leaveConvo(); + }, [queueBlock, leaveConvo, _]); + var reportControl = useDialogControl(); + var blockOrDeleteControl = useDialogControl(); + var lastMessage = ChatBskyConvoDefs.isMessageView(convo.lastMessage) + ? convo.lastMessage + : null; + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Reject chat request"], ["Reject chat request"])))), children: function (_a) { + var triggerProps = _a.props; + return (_jsx(Button, __assign({}, triggerProps, props, { label: triggerProps.accessibilityLabel, style: [a.flex_1], color: color, size: size, children: _jsx(ButtonText, { children: label || (_jsx(Trans, { comment: "Reject a chat request, this opens a menu with options", children: "Reject" })) }) }))); + } }), _jsx(Menu.Outer, { showCancel: true, children: _jsxs(Menu.Group, { children: [showDeleteConvo && (_jsxs(Menu.Item, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Delete conversation"], ["Delete conversation"])))), onPress: onPressDelete, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Delete conversation" }) }), _jsx(Menu.ItemIcon, { icon: CircleX_Stroke2_Corner0_Rounded })] })), _jsxs(Menu.Item, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Block account"], ["Block account"])))), onPress: onPressBlock, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Block account" }) }), _jsx(Menu.ItemIcon, { icon: PersonXIcon })] }), lastMessage && (_jsxs(Menu.Item, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Report conversation"], ["Report conversation"])))), onPress: reportControl.open, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Report conversation" }) }), _jsx(Menu.ItemIcon, { icon: FlagIcon })] }))] }) })] }), lastMessage && (_jsxs(_Fragment, { children: [_jsx(ReportDialog, { subject: { + view: 'convo', + convoId: convo.id, + message: lastMessage, + }, control: reportControl, onAfterSubmit: function () { + blockOrDeleteControl.open(); + } }), _jsx(AfterReportDialog, { control: blockOrDeleteControl, currentScreen: currentScreen, params: { + convoId: convo.id, + message: lastMessage, + } })] }))] })); +} +export function AcceptChatButton(_a) { + var convo = _a.convo, _b = _a.size, size = _b === void 0 ? 'tiny' : _b, _c = _a.color, color = _c === void 0 ? 'secondary_inverted' : _c, label = _a.label, currentScreen = _a.currentScreen, onAcceptConvo = _a.onAcceptConvo, props = __rest(_a, ["convo", "size", "color", "label", "currentScreen", "onAcceptConvo"]); + var _ = useLingui()._; + var queryClient = useQueryClient(); + var navigation = useNavigation(); + var needsEmailVerification = useEmail().needsEmailVerification; + var emailDialogControl = useEmailDialogControl(); + var _d = useAcceptConversation(convo.id, { + onMutate: function () { + onAcceptConvo === null || onAcceptConvo === void 0 ? void 0 : onAcceptConvo(); + if (currentScreen === 'list') { + precacheConvoQuery(queryClient, __assign(__assign({}, convo), { status: 'accepted' })); + navigation.navigate('MessagesConversation', { + conversation: convo.id, + accept: true, + }); + } + }, + onError: function () { + // Should we show a toast here? They'll be on the convo screen, and it'll make + // no difference if the request failed - when they send a message, the convo will be accepted + // automatically. The only difference is that when they back out of the convo (without sending a message), the conversation will be rejected. + // the list will still have this chat in it -sfn + Toast.show(_(msg({ + context: 'toast', + message: 'Failed to accept chat', + })), 'xmark'); + }, + }), acceptConvo = _d.mutate, isPending = _d.isPending; + var onPressAccept = useCallback(function () { + if (needsEmailVerification) { + emailDialogControl.open({ + id: EmailDialogScreenID.Verify, + instructions: [ + _jsx(Trans, { children: "Before you can accept this chat request, you must first verify your email." }, "request-btn"), + ], + }); + } + else { + acceptConvo(); + } + }, [acceptConvo, needsEmailVerification, emailDialogControl]); + return (_jsx(Button, __assign({}, props, { label: label || _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Accept chat request"], ["Accept chat request"])))), size: size, color: color, style: a.flex_1, onPress: onPressAccept, children: isPending ? (_jsx(ButtonIcon, { icon: Loader })) : (_jsx(ButtonText, { children: label || _jsx(Trans, { comment: "Accept a chat request", children: "Accept" }) })) }))); +} +export function DeleteChatButton(_a) { + var convo = _a.convo, _b = _a.size, size = _b === void 0 ? 'tiny' : _b, _c = _a.color, color = _c === void 0 ? 'secondary' : _c, label = _a.label, currentScreen = _a.currentScreen, props = __rest(_a, ["convo", "size", "color", "label", "currentScreen"]); + var _ = useLingui()._; + var navigation = useNavigation(); + var leaveConvo = useLeaveConvo(convo.id, { + onMutate: function () { + if (currentScreen === 'conversation') { + navigation.dispatch(StackActions.pop()); + } + }, + onError: function () { + Toast.show(_(msg({ + context: 'toast', + message: 'Failed to delete chat', + })), 'xmark'); + }, + }).mutate; + var onPressDelete = useCallback(function () { + Toast.show(_(msg({ + context: 'toast', + message: 'Chat deleted', + })), 'check'); + leaveConvo(); + }, [leaveConvo, _]); + return (_jsx(Button, __assign({ label: label || _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Delete chat"], ["Delete chat"])))), size: size, color: color, style: a.flex_1, onPress: onPressDelete }, props, { children: _jsx(ButtonText, { children: label || _jsx(Trans, { children: "Delete chat" }) }) }))); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/Messages/components/RequestListItem.js b/src/screens/Messages/components/RequestListItem.js new file mode 100644 index 0000000000..6564dbce1d --- /dev/null +++ b/src/screens/Messages/components/RequestListItem.js @@ -0,0 +1,32 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useSession } from '#/state/session'; +import { atoms as a, tokens } from '#/alf'; +import { KnownFollowers } from '#/components/KnownFollowers'; +import { Text } from '#/components/Typography'; +import { ChatListItem, ChatListItemPortal } from './ChatListItem'; +import { AcceptChatButton, DeleteChatButton, RejectMenu } from './RequestButtons'; +export function RequestListItem(_a) { + var convo = _a.convo; + var currentAccount = useSession().currentAccount; + var moderationOpts = useModerationOpts(); + var otherUser = convo.members.find(function (member) { return member.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); + if (!otherUser || !moderationOpts) { + return null; + } + var isDeletedAccount = otherUser.handle === 'missing.invalid'; + return (_jsx(View, { style: [a.relative, a.flex_1], children: _jsxs(ChatListItem, { convo: convo, showMenu: false, children: [_jsx(View, { style: [a.pt_xs, a.pb_2xs], children: _jsx(KnownFollowers, { profile: otherUser, moderationOpts: moderationOpts, minimal: true, showIfEmpty: true }) }), _jsx(View, { style: [a.pt_md, a.pb_xs, a.w_full, { opacity: 0 }], "aria-hidden": true, children: _jsx(Text, { style: [a.text_xs, a.leading_tight, a.font_semi_bold], children: _jsx(Trans, { comment: "Accept a chat request", children: "Accept Request" }) }) }), _jsx(ChatListItemPortal.Portal, { children: _jsx(View, { style: [ + a.absolute, + a.pr_md, + a.w_full, + a.flex_row, + a.align_center, + a.gap_sm, + { + bottom: tokens.space.md, + paddingLeft: tokens.space.lg + 52 + tokens.space.md, + }, + ], children: !isDeletedAccount ? (_jsxs(_Fragment, { children: [_jsx(AcceptChatButton, { convo: convo, currentScreen: "list" }), _jsx(RejectMenu, { convo: convo, profile: otherUser, showDeleteConvo: true, currentScreen: "list" })] })) : (_jsxs(_Fragment, { children: [_jsx(DeleteChatButton, { convo: convo, currentScreen: "list" }), _jsx(View, { style: a.flex_1 })] })) }) })] }) })); +} diff --git a/src/screens/Moderation/VerificationSettings.js b/src/screens/Moderation/VerificationSettings.js new file mode 100644 index 0000000000..f5f832dc22 --- /dev/null +++ b/src/screens/Moderation/VerificationSettings.js @@ -0,0 +1,45 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { urls } from '#/lib/constants'; +import { usePreferencesQuery, } from '#/state/queries/preferences'; +import { useSetVerificationPrefsMutation } from '#/state/queries/preferences'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import { atoms as a, useGutters } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import * as Toggle from '#/components/forms/Toggle'; +import { CircleCheck_Stroke2_Corner0_Rounded as CircleCheck } from '#/components/icons/CircleCheck'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { useAnalytics } from '#/analytics'; +export function Screen() { + var _ = useLingui()._; + var ax = useAnalytics(); + var gutters = useGutters(['base']); + var preferences = usePreferencesQuery().data; + return (_jsxs(Layout.Screen, { testID: "ModerationVerificationSettingsScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Verification Settings" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsx(SettingsList.Item, { children: _jsx(Admonition, { type: "tip", style: [a.flex_1], children: _jsxs(Trans, { children: ["Verifications on Bluesky work differently than on other platforms.", ' ', _jsx(InlineLinkText, { overridePresentation: true, to: urls.website.blog.initialVerificationAnnouncement, label: _(msg({ + message: "Learn more", + context: "english-only-resource", + })), onPress: function () { + ax.metric('verification:learn-more', { + location: 'verificationSettings', + }); + }, children: "Learn more here." })] }) }) }), preferences ? (_jsx(Inner, { preferences: preferences })) : (_jsx(View, { style: [gutters, a.justify_center, a.align_center], children: _jsx(Loader, { size: "xl" }) }))] }) })] })); +} +function Inner(_a) { + var preferences = _a.preferences; + var _ = useLingui()._; + var hideBadges = preferences.verificationPrefs.hideBadges; + var _b = useSetVerificationPrefsMutation(), setVerificationPrefs = _b.mutate, isPending = _b.isPending; + return (_jsx(Toggle.Item, { type: "checkbox", name: "hideBadges", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Hide verification badges"], ["Hide verification badges"])))), value: hideBadges, disabled: isPending, onChange: function (value) { + setVerificationPrefs({ hideBadges: value }); + }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: CircleCheck }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Hide verification badges" }) }), _jsx(Toggle.Platform, {})] }) })); +} +var templateObject_1; diff --git a/src/screens/Moderation/index.js b/src/screens/Moderation/index.js new file mode 100644 index 0000000000..a7caa35ebc --- /dev/null +++ b/src/screens/Moderation/index.js @@ -0,0 +1,265 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { Fragment, useCallback } from 'react'; +import { Linking, View } from 'react-native'; +import { LABELS } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { getLabelingServiceTitle, isAppLabeler } from '#/lib/moderation'; +import { logger } from '#/logger'; +import { useIsBirthdateUpdateAllowed } from '#/state/birthdate'; +import { useRemoveLabelersMutation } from '#/state/queries/labeler'; +import { useMyLabelersQuery, usePreferencesQuery, usePreferencesSetAdultContentMutation, } from '#/state/queries/preferences'; +import { isNonConfigurableModerationAuthority } from '#/state/session/additional-moderation-authorities'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import * as Admonition from '#/components/Admonition'; +import { AgeAssuranceAdmonition } from '#/components/ageAssurance/AgeAssuranceAdmonition'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { Divider } from '#/components/Divider'; +import * as Toggle from '#/components/forms/Toggle'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRight } from '#/components/icons/Chevron'; +import { CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign } from '#/components/icons/CircleBanSign'; +import { CircleCheck_Stroke2_Corner0_Rounded as CircleCheck } from '#/components/icons/CircleCheck'; +import { EditBig_Stroke2_Corner0_Rounded as EditBig } from '#/components/icons/EditBig'; +import { Filter_Stroke2_Corner0_Rounded as Filter } from '#/components/icons/Filter'; +import { Group3_Stroke2_Corner0_Rounded as Group } from '#/components/icons/Group'; +import { Person_Stroke2_Corner0_Rounded as Person } from '#/components/icons/Person'; +import * as LabelingService from '#/components/LabelingServiceCard'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText, Link } from '#/components/Link'; +import { ListMaybePlaceholder } from '#/components/Lists'; +import { Loader } from '#/components/Loader'; +import { GlobalLabelPreference } from '#/components/moderation/LabelPreference'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAgeAssurance } from '#/ageAssurance'; +import { IS_IOS } from '#/env'; +function ErrorState(_a) { + var error = _a.error; + var t = useTheme(); + return (_jsxs(View, { style: [a.p_xl], children: [_jsx(Text, { style: [ + a.text_md, + a.leading_normal, + a.pb_md, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." }) }), _jsx(View, { style: [ + a.relative, + a.py_md, + a.px_lg, + a.rounded_md, + a.mb_2xl, + t.atoms.bg_contrast_25, + ], children: _jsx(Text, { style: [a.text_md, a.leading_normal], children: error }) })] })); +} +export function ModerationScreen(_props) { + var _ = useLingui()._; + var _a = usePreferencesQuery(), isPreferencesLoading = _a.isLoading, preferencesError = _a.error, preferences = _a.data; + var isLoading = isPreferencesLoading; + var error = preferencesError; + return (_jsxs(Layout.Screen, { testID: "moderationScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Moderation" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: isLoading ? (_jsx(ListMaybePlaceholder, { isLoading: true, sideBorders: false })) : error || !preferences ? (_jsx(ErrorState, { error: (preferencesError === null || preferencesError === void 0 ? void 0 : preferencesError.toString()) || + _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Something went wrong, please try again."], ["Something went wrong, please try again."])))) })) : (_jsx(ModerationScreenInner, { preferences: preferences })) })] })); +} +function SubItem(_a) { + var title = _a.title, Icon = _a.icon, style = _a.style; + var t = useTheme(); + return (_jsxs(View, { style: [ + a.w_full, + a.flex_row, + a.align_center, + a.justify_between, + a.p_lg, + a.gap_sm, + style, + ], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_md], children: [_jsx(Icon, { size: "md", style: [t.atoms.text_contrast_medium] }), _jsx(Text, { style: [a.text_sm, a.font_semi_bold], children: title })] }), _jsx(ChevronRight, { size: "sm", style: [t.atoms.text_contrast_low, a.self_end, { paddingBottom: 2 }] })] })); +} +export function ModerationScreenInner(_a) { + var _this = this; + var preferences = _a.preferences; + var _ = useLingui()._; + var t = useTheme(); + var setMinimalShellMode = useSetMinimalShellMode(); + var gtMobile = useBreakpoints().gtMobile; + var mutedWordsDialogControl = useGlobalDialogsControlContext().mutedWordsDialogControl; + var _b = useMyLabelersQuery(), isLabelersLoading = _b.isLoading, labelers = _b.data, labelersError = _b.error; + var _c = useRemoveLabelersMutation(), removeLabelers = _c.mutateAsync, isRemovingLabelers = _c.isPending; + var aa = useAgeAssurance(); + var isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed(); + var aaCopy = useAgeAssuranceCopy(); + var subscribedDids = preferences.moderationPrefs.labelers.map(function (l) { return l.did; }); + var returnedDids = new Set(labelers === null || labelers === void 0 ? void 0 : labelers.map(function (l) { return l.creator.did; })); + var unavailableDids = subscribedDids.filter(function (did) { + return !returnedDids.has(did) && + !isAppLabeler(did) && + !isNonConfigurableModerationAuthority(did); + }); + var handleCleanup = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, removeLabelers({ dids: unavailableDids })]; + case 1: + _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Removed unavailable services"], ["Removed unavailable services"])))), { + type: 'success', + }); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + logger.error('Failed to remove unavailable labelers', { + safeMessage: e_1.message, + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var _d = usePreferencesSetAdultContentMutation(), setAdultContentPref = _d.mutateAsync, optimisticAdultContent = _d.variables; + var adultContentEnabled = !!((optimisticAdultContent && optimisticAdultContent.enabled) || + (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)); + var adultContentUIDisabledOnIOS = IS_IOS && !adultContentEnabled; + var adultContentUIDisabled = adultContentUIDisabledOnIOS; + if (aa.flags.adultContentDisabled) { + adultContentEnabled = false; + adultContentUIDisabled = true; + } + var onToggleAdultContentEnabled = useCallback(function (selected) { return __awaiter(_this, void 0, void 0, function () { + var e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, setAdultContentPref({ + enabled: selected, + })]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + e_2 = _a.sent(); + logger.error("Failed to set adult content pref", { + message: e_2.message, + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }, [setAdultContentPref]); + return (_jsxs(View, { style: [a.pt_2xl, a.px_lg, gtMobile && a.px_2xl], children: [aa.flags.adultContentDisabled && isBirthdateUpdateAllowed && (_jsx(View, { style: [a.pb_2xl], children: _jsx(Admonition.Admonition, { type: "tip", style: [a.pb_md], children: _jsxs(Trans, { children: ["Your declared age is under 18. Some settings below may be disabled. If this was a mistake, you may edit your birthdate in your", ' ', _jsx(InlineLinkText, { to: "/settings/account", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Go to account settings"], ["Go to account settings"])))), children: "account settings" }), "."] }) }) })), _jsx(Text, { style: [ + a.text_md, + a.font_semi_bold, + a.pb_md, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Moderation tools" }) }), _jsxs(View, { style: [ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + ], children: [_jsx(Link, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["View your default post interaction settings"], ["View your default post interaction settings"])))), testID: "interactionSettingsBtn", to: "/moderation/interaction-settings", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Interaction settings"], ["Interaction settings"])))), icon: EditBig, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Button, { testID: "mutedWordsBtn", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Open muted words and tags settings"], ["Open muted words and tags settings"])))), onPress: function () { return mutedWordsDialogControl.open(); }, children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Muted words & tags"], ["Muted words & tags"])))), icon: Filter, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Link, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["View your moderation lists"], ["View your moderation lists"])))), testID: "moderationlistsBtn", to: "/moderation/modlists", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Moderation lists"], ["Moderation lists"])))), icon: Group, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Link, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["View your muted accounts"], ["View your muted accounts"])))), testID: "mutedAccountsBtn", to: "/moderation/muted-accounts", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Muted accounts"], ["Muted accounts"])))), icon: Person, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Link, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["View your blocked accounts"], ["View your blocked accounts"])))), testID: "blockedAccountsBtn", to: "/moderation/blocked-accounts", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Blocked accounts"], ["Blocked accounts"])))), icon: CircleBanSign, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Link, { label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Manage verification settings"], ["Manage verification settings"])))), testID: "verificationSettingsBtn", to: "/moderation/verification-settings", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Verification settings"], ["Verification settings"])))), icon: CircleCheck, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } })] }), _jsx(Text, { style: [ + a.pt_2xl, + a.pb_md, + a.text_md, + a.font_semi_bold, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Content filters" }) }), _jsx(AgeAssuranceAdmonition, { style: [a.pb_md], children: aaCopy.notice }), _jsx(View, { style: [a.gap_md], children: _jsx(View, { style: [ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + ], children: aa.state.access === aa.Access.Full && (_jsxs(_Fragment, { children: [_jsxs(View, { style: [ + a.py_lg, + a.px_lg, + a.flex_row, + a.align_center, + a.justify_between, + adultContentUIDisabled && { opacity: 0.5 }, + ], children: [_jsx(Text, { style: [a.font_semi_bold, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "Enable adult content" }) }), _jsx(Toggle.Item, { label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Toggle to enable or disable adult content"], ["Toggle to enable or disable adult content"])))), disabled: adultContentUIDisabled, name: "adultContent", value: adultContentEnabled, onChange: onToggleAdultContentEnabled, children: _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Text, { style: [t.atoms.text_contrast_medium], children: adultContentEnabled ? (_jsx(Trans, { children: "Enabled" })) : (_jsx(Trans, { children: "Disabled" })) }), _jsx(Toggle.Switch, {})] }) })] }), adultContentUIDisabledOnIOS && (_jsx(View, { style: [a.pb_lg, a.px_lg], children: _jsx(Text, { children: _jsxs(Trans, { children: ["Adult content can only be enabled via the Web at", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["The Bluesky web application"], ["The Bluesky web application"])))), to: "", onPress: function (evt) { + evt.preventDefault(); + Linking.openURL('https://bsky.app/'); + return false; + }, children: "bsky.app" }), "."] }) }) })), adultContentEnabled && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(GlobalLabelPreference, { labelDefinition: LABELS.porn }), _jsx(Divider, {}), _jsx(GlobalLabelPreference, { labelDefinition: LABELS.sexual }), _jsx(Divider, {}), _jsx(GlobalLabelPreference, { labelDefinition: LABELS['graphic-media'] }), _jsx(Divider, {}), _jsx(GlobalLabelPreference, { labelDefinition: LABELS.nudity })] }))] })) }) }), _jsx(Text, { style: [ + a.text_md, + a.font_semi_bold, + a.pt_2xl, + a.pb_md, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Advanced" }) }), unavailableDids.length > 0 && (_jsx(Admonition.Outer, { type: "tip", style: [a.mb_md], children: _jsxs(Admonition.Row, { children: [_jsx(Admonition.Icon, {}), _jsx(Admonition.Content, { children: _jsx(Admonition.Text, { children: _jsx(Trans, { children: "Some moderation services in your list are no longer available." }) }) }), _jsxs(Admonition.Button, { color: "primary_subtle", label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Remove unavailable moderation services"], ["Remove unavailable moderation services"])))), onPress: handleCleanup, disabled: isRemovingLabelers, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Remove" }) }), isRemovingLabelers && _jsx(ButtonIcon, { icon: Loader })] })] }) })), isLabelersLoading ? (_jsx(View, { style: [a.w_full, a.align_center, a.p_lg], children: _jsx(Loader, { size: "xl" }) })) : labelersError || !labelers ? (_jsx(View, { style: [a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25], children: _jsx(Text, { children: _jsx(Trans, { children: "We were unable to load your configured labelers at this time." }) }) })) : (_jsx(View, { style: [a.rounded_sm, t.atoms.bg_contrast_25], children: labelers.map(function (labeler, i) { + return (_jsxs(Fragment, { children: [i !== 0 && _jsx(Divider, {}), _jsx(LabelingService.Link, { labeler: labeler, children: function (state) { return (_jsxs(LabelingService.Outer, { style: [ + i === 0 && { + borderTopLeftRadius: a.rounded_sm.borderRadius, + borderTopRightRadius: a.rounded_sm.borderRadius, + }, + i === labelers.length - 1 && { + borderBottomLeftRadius: a.rounded_sm.borderRadius, + borderBottomRightRadius: a.rounded_sm.borderRadius, + }, + (state.hovered || state.pressed) && [ + t.atoms.bg_contrast_50, + ], + ], children: [_jsx(LabelingService.Avatar, { avatar: labeler.creator.avatar }), _jsxs(LabelingService.Content, { children: [_jsx(LabelingService.Title, { value: getLabelingServiceTitle({ + displayName: labeler.creator.displayName, + handle: labeler.creator.handle, + }) }), _jsx(LabelingService.Description, { value: labeler.creator.description, handle: labeler.creator.handle }), isNonConfigurableModerationAuthority(labeler.creator.did) && _jsx(LabelingService.RegionalNotice, {})] })] })); } })] }, labeler.creator.did)); + }) })), _jsx(View, { style: { height: 150 } })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18; diff --git a/src/screens/ModerationInteractionSettings/index.js b/src/screens/ModerationInteractionSettings/index.js new file mode 100644 index 0000000000..28a2f5423d --- /dev/null +++ b/src/screens/ModerationInteractionSettings/index.js @@ -0,0 +1,122 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import deepEqual from 'fast-deep-equal'; +import { logger } from '#/logger'; +import { usePostInteractionSettingsMutation } from '#/state/queries/post-interaction-settings'; +import { createPostgateRecord } from '#/state/queries/postgate/util'; +import { usePreferencesQuery, } from '#/state/queries/preferences'; +import { threadgateAllowUISettingToAllowRecordValue, threadgateRecordToAllowUISetting, } from '#/state/queries/threadgate'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useGutters } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { PostInteractionSettingsForm } from '#/components/dialogs/PostInteractionSettingsDialog'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +export function Screen() { + var gutters = useGutters(['base']); + var preferences = usePreferencesQuery().data; + return (_jsxs(Layout.Screen, { testID: "ModerationInteractionSettingsScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Post Interaction Settings" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(View, { style: [gutters, a.gap_xl], children: [_jsx(Admonition, { type: "tip", children: _jsx(Trans, { children: "The following settings will be used as your defaults when creating new posts. You can edit these for a specific post from the composer." }) }), preferences ? (_jsx(Inner, { preferences: preferences })) : (_jsx(View, { style: [gutters, a.justify_center, a.align_center], children: _jsx(Loader, { size: "xl" }) }))] }) })] })); +} +function Inner(_a) { + var _this = this; + var preferences = _a.preferences; + var _ = useLingui()._; + var _b = usePostInteractionSettingsMutation(), setPostInteractionSettings = _b.mutateAsync, isPending = _b.isPending; + var _c = React.useState(undefined), error = _c[0], setError = _c[1]; + var allowUI = React.useMemo(function () { + return threadgateRecordToAllowUISetting({ + $type: 'app.bsky.feed.threadgate', + post: '', + createdAt: new Date().toString(), + allow: preferences.postInteractionSettings.threadgateAllowRules, + }); + }, [preferences.postInteractionSettings.threadgateAllowRules]); + var postgate = React.useMemo(function () { + return createPostgateRecord({ + post: '', + embeddingRules: preferences.postInteractionSettings.postgateEmbeddingRules, + }); + }, [preferences.postInteractionSettings.postgateEmbeddingRules]); + var _d = React.useState(allowUI), maybeEditedAllowUI = _d[0], setAllowUI = _d[1]; + var _e = React.useState(postgate), maybeEditedPostgate = _e[0], setEditedPostgate = _e[1]; + var wasEdited = React.useMemo(function () { + return (!deepEqual(allowUI, maybeEditedAllowUI) || + !deepEqual(postgate.embeddingRules, maybeEditedPostgate.embeddingRules)); + }, [postgate, allowUI, maybeEditedAllowUI, maybeEditedPostgate]); + var onSave = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + setError(''); + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, setPostInteractionSettings({ + threadgateAllowRules: threadgateAllowUISettingToAllowRecordValue(maybeEditedAllowUI), + postgateEmbeddingRules: (_a = maybeEditedPostgate.embeddingRules) !== null && _a !== void 0 ? _a : [], + })]; + case 2: + _b.sent(); + Toast.show(_(msg({ message: 'Settings saved', context: 'toast' }))); + return [3 /*break*/, 4]; + case 3: + e_1 = _b.sent(); + logger.error("Failed to save post interaction settings", { + source: 'ModerationInteractionSettingsScreen', + safeMessage: e_1.message, + }); + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to save settings. Please try again."], ["Failed to save settings. Please try again."]))))); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [_, maybeEditedPostgate, maybeEditedAllowUI, setPostInteractionSettings]); + return (_jsxs(_Fragment, { children: [_jsx(PostInteractionSettingsForm, { canSave: wasEdited, isSaving: isPending, onSave: onSave, postgate: maybeEditedPostgate, onChangePostgate: setEditedPostgate, threadgateAllowUISettings: maybeEditedAllowUI, onChangeThreadgateAllowUISettings: setAllowUI }), error && _jsx(Admonition, { type: "error", children: error })] })); +} +var templateObject_1; diff --git a/src/screens/Notifications/ActivityList.js b/src/screens/Notifications/ActivityList.js new file mode 100644 index 0000000000..7f4e68d134 --- /dev/null +++ b/src/screens/Notifications/ActivityList.js @@ -0,0 +1,20 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { PostFeed } from '#/view/com/posts/PostFeed'; +import { EmptyState } from '#/view/com/util/EmptyState'; +import { EditBig_Stroke1_Corner0_Rounded as EditIcon } from '#/components/icons/EditBig'; +import * as Layout from '#/components/Layout'; +import { ListFooter } from '#/components/Lists'; +export function NotificationsActivityListScreen(_a) { + var posts = _a.route.params.posts; + var uris = decodeURIComponent(posts); + var _ = useLingui()._; + return (_jsxs(Layout.Screen, { testID: "NotificationsActivityListScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(PostFeed, { feed: "posts|".concat(uris), disablePoll: true, renderEmptyState: function () { return (_jsx(EmptyState, { icon: EditIcon, iconSize: "2xl", message: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["No posts here"], ["No posts here"])))) })); }, renderEndOfFeed: function () { return _jsx(ListFooter, {}); } })] })); +} +var templateObject_1; diff --git a/src/screens/Onboarding/Layout.js b/src/screens/Onboarding/Layout.js new file mode 100644 index 0000000000..b3b1739884 --- /dev/null +++ b/src/screens/Onboarding/Layout.js @@ -0,0 +1,121 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useEffect, useRef, useState } from 'react'; +import { ScrollView, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useOnboardingDispatch } from '#/state/shell'; +import { useOnboardingInternalState } from '#/screens/Onboarding/state'; +import { atoms as a, native, tokens, useBreakpoints, useTheme, web, } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft } from '#/components/icons/Arrow'; +import { HEADER_SLOT_SIZE } from '#/components/Layout'; +import { createPortalGroup } from '#/components/Portal'; +import { P, Text } from '#/components/Typography'; +import { IS_ANDROID, IS_INTERNAL, IS_WEB } from '#/env'; +var ONBOARDING_COL_WIDTH = 420; +export var OnboardingControls = createPortalGroup(); +export var OnboardingHeaderSlot = createPortalGroup(); +export function Layout(_a) { + var _b; + var children = _a.children; + var _ = useLingui()._; + var t = useTheme(); + var insets = useSafeAreaInsets(); + var gtMobile = useBreakpoints().gtMobile; + var onboardDispatch = useOnboardingDispatch(); + var _c = useOnboardingInternalState(), state = _c.state, dispatch = _c.dispatch; + var scrollview = useRef(null); + var prevActiveStep = useRef(state.activeStep); + useEffect(function () { + var _a; + if (state.activeStep !== prevActiveStep.current) { + prevActiveStep.current = state.activeStep; + (_a = scrollview.current) === null || _a === void 0 ? void 0 : _a.scrollTo({ y: 0, animated: false }); + } + }, [state]); + var dialogLabel = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Set up your account"], ["Set up your account"])))); + var _d = useState(0), headerHeight = _d[0], setHeaderHeight = _d[1]; + var _e = useState(0), footerHeight = _e[0], setFooterHeight = _e[1]; + return (_jsxs(View, { "aria-modal": true, role: "dialog", "aria-role": "dialog", "aria-label": dialogLabel, accessibilityLabel: dialogLabel, accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Customizes your Bluesky experience"], ["Customizes your Bluesky experience"])))), style: [IS_WEB ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg], children: [!gtMobile ? (_jsx(View, { style: [ + web(a.fixed), + native(a.absolute), + a.top_0, + a.left_0, + a.right_0, + a.flex_row, + a.w_full, + a.justify_center, + a.z_20, + a.px_xl, + { paddingTop: ((_b = web(tokens.space.lg)) !== null && _b !== void 0 ? _b : 0) + insets.top }, + native([t.atoms.bg, a.pb_xs, { minHeight: 48 }]), + web(a.pointer_events_box_none), + ], onLayout: function (evt) { return setHeaderHeight(evt.nativeEvent.layout.height); }, children: _jsxs(View, { style: [ + a.w_full, + a.align_center, + a.flex_row, + a.justify_between, + web({ maxWidth: ONBOARDING_COL_WIDTH }), + web(a.pointer_events_box_none), + ], children: [_jsx(HeaderSlot, { children: state.canGoBack && (_jsx(Button, { color: "secondary", variant: "ghost", shape: "round", size: "small", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Go back to previous step"], ["Go back to previous step"])))), onPress: function () { return dispatch({ type: 'prev' }); }, children: _jsx(ButtonIcon, { icon: ArrowLeft, size: "lg" }) }, state.activeStep)) }), IS_INTERNAL && (_jsx(Button, { variant: "ghost", color: "negative", size: "tiny", onPress: function () { return onboardDispatch({ type: 'skip' }); }, + // DEV ONLY + label: "Clear onboarding state", children: _jsx(ButtonText, { children: "[DEV] Clear" }) })), _jsx(HeaderSlot, { children: _jsx(OnboardingHeaderSlot.Outlet, {}) })] }) })) : (_jsx(_Fragment, { children: IS_INTERNAL && (_jsx(View, { style: [ + a.absolute, + a.align_center, + a.z_10, + { top: 0, left: 0, right: 0 }, + ], children: _jsx(Button, { variant: "ghost", color: "negative", size: "tiny", onPress: function () { return onboardDispatch({ type: 'skip' }); }, + // DEV ONLY + label: "Clear onboarding state", children: _jsx(ButtonText, { children: "[DEV] Clear" }) }) })) })), _jsx(ScrollView, { ref: scrollview, style: [a.h_full, a.w_full], contentContainerStyle: { + borderWidth: 0, + minHeight: '100%', + paddingTop: gtMobile ? 40 : headerHeight, + paddingBottom: footerHeight, + }, showsVerticalScrollIndicator: !IS_ANDROID, scrollIndicatorInsets: { bottom: footerHeight - insets.bottom }, + // @ts-expect-error web only --prf + dataSet: { 'stable-gutters': 1 }, centerContent: gtMobile, children: _jsx(View, { style: [a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl], children: _jsx(View, { style: [a.flex_1, web({ maxWidth: ONBOARDING_COL_WIDTH })], children: _jsx(View, { style: [a.w_full, a.py_md], children: children }) }) }) }), _jsx(View, { onLayout: function (evt) { return setFooterHeight(evt.nativeEvent.layout.height); }, style: [ + IS_WEB ? a.fixed : a.absolute, + { bottom: 0, left: 0, right: 0 }, + t.atoms.bg, + t.atoms.border_contrast_low, + a.border_t, + a.align_center, + gtMobile ? a.px_5xl : a.px_xl, + IS_WEB + ? a.py_2xl + : { + paddingTop: tokens.space.md, + paddingBottom: insets.bottom + tokens.space.md, + }, + ], children: _jsxs(View, { style: [ + a.w_full, + { maxWidth: ONBOARDING_COL_WIDTH }, + gtMobile && [a.flex_row, a.justify_between, a.align_center], + ], children: [gtMobile && + (state.canGoBack ? (_jsx(Button, { color: "secondary", variant: "ghost", shape: "square", size: "small", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Go back to previous step"], ["Go back to previous step"])))), onPress: function () { return dispatch({ type: 'prev' }); }, children: _jsx(ButtonIcon, { icon: ArrowLeft, size: "lg" }) }, state.activeStep)) : (_jsx(View, { style: { height: 33 } }))), _jsx(OnboardingControls.Outlet, {})] }) })] })); +} +function HeaderSlot(_a) { + var children = _a.children; + return (_jsx(View, { style: [{ minHeight: HEADER_SLOT_SIZE, minWidth: HEADER_SLOT_SIZE }], children: children })); +} +export function OnboardingPosition() { + var state = useOnboardingInternalState().state; + var t = useTheme(); + return (_jsx(Text, { style: [a.text_sm, a.font_medium, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Step ", state.activeStepIndex + 1, " of ", state.totalSteps] }) })); +} +export function OnboardingTitleText(_a) { + var children = _a.children, style = _a.style; + return (_jsx(Text, { style: [a.text_3xl, a.font_bold, a.leading_snug, style], children: children })); +} +export function OnboardingDescriptionText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + return (_jsx(P, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium, style], children: children })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Onboarding/StepFindContacts/index.js b/src/screens/Onboarding/StepFindContacts/index.js new file mode 100644 index 0000000000..9b1f0991a5 --- /dev/null +++ b/src/screens/Onboarding/StepFindContacts/index.js @@ -0,0 +1,39 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { LayoutAnimationConfig } from 'react-native-reanimated'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useCallOnce } from '#/lib/once'; +import { FindContactsFlow } from '#/components/contacts/FindContactsFlow'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { useAnalytics } from '#/analytics'; +import { useOnboardingInternalState } from '../state'; +export function StepFindContacts(_a) { + var flowState = _a.flowState, flowDispatch = _a.flowDispatch; + var dispatch = useOnboardingInternalState().dispatch; + var ax = useAnalytics(); + useCallOnce(function () { + ax.metric('onboarding:contacts:begin', {}); + })(); + var _b = useState('Forward'), transitionDirection = _b[0], setTransitionDirection = _b[1]; + var isFinalStep = flowState.step === '4: view matches'; + var onSkip = useCallback(function () { + if (!isFinalStep) { + ax.metric('onboarding:contacts:skipPressed', {}); + } + dispatch({ type: 'next' }); + }, [dispatch, isFinalStep, ax]); + var canGoBack = flowState.step === '2: verify number'; + var onBack = useCallback(function () { + if (canGoBack) { + setTransitionDirection('Backward'); + flowDispatch({ type: 'BACK' }); + setTimeout(function () { + setTransitionDirection('Forward'); + }); + } + else { + dispatch({ type: 'prev' }); + } + }, [dispatch, flowDispatch, canGoBack]); + return (_jsx(SafeAreaView, { edges: ['left', 'top', 'right'], children: _jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: _jsx(ScreenTransition, { direction: transitionDirection, children: _jsx(FindContactsFlow, { context: "Onboarding", state: flowState, dispatch: flowDispatch, onCancel: onSkip, onBack: onBack }) }, flowState.step) }) })); +} diff --git a/src/screens/Onboarding/StepFindContacts/index.web.js b/src/screens/Onboarding/StepFindContacts/index.web.js new file mode 100644 index 0000000000..e28316e373 --- /dev/null +++ b/src/screens/Onboarding/StepFindContacts/index.web.js @@ -0,0 +1,3 @@ +export function StepFindContacts() { + throw new Error('StepFindContacts is not available on web'); +} diff --git a/src/screens/Onboarding/StepFindContactsIntro/index.js b/src/screens/Onboarding/StepFindContactsIntro/index.js new file mode 100644 index 0000000000..55efc80015 --- /dev/null +++ b/src/screens/Onboarding/StepFindContactsIntro/index.js @@ -0,0 +1,80 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import * as Contacts from 'expo-contacts'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQuery } from '@tanstack/react-query'; +import { urls } from '#/lib/constants'; +import { useCallOnce } from '#/lib/once'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonText } from '#/components/Button'; +import { ContactsHeroImage } from '#/components/contacts/components/HeroImage'; +import { InlineLinkText } from '#/components/Link'; +import { useAnalytics } from '#/analytics'; +import { OnboardingControls, OnboardingDescriptionText, OnboardingPosition, OnboardingTitleText, } from '../Layout'; +import { useOnboardingInternalState } from '../state'; +export function StepFindContactsIntro() { + var _this = this; + var ax = useAnalytics(); + var _ = useLingui()._; + var dispatch = useOnboardingInternalState().dispatch; + useCallOnce(function () { + ax.metric('onboarding:contacts:presented', {}); + })(); + var _a = useQuery({ + queryKey: ['contacts-available'], + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Contacts.isAvailableAsync()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); }); }, + }), isAvailable = _a.data, isSuccess = _a.isSuccess; + return (_jsxs(View, { style: [a.w_full, a.gap_sm], children: [_jsx(OnboardingPosition, {}), _jsx(ContactsHeroImage, {}), _jsx(OnboardingTitleText, { style: [a.mt_sm], children: _jsx(Trans, { children: "Bluesky is more fun with friends" }) }), _jsx(OnboardingDescriptionText, { children: _jsxs(Trans, { children: ["Find your friends on Bluesky by verifying your phone number and matching with your contacts. We protect your information and you control what happens next.", ' ', _jsx(InlineLinkText, { to: urls.website.blog.findFriendsAnnouncement, label: _(msg({ + message: "Learn more about importing contacts", + context: "english-only-resource", + })), style: [a.text_md, a.leading_snug], children: _jsx(Trans, { context: "english-only-resource", children: "Learn more" }) })] }) }), !isAvailable && isSuccess && (_jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Contact sync is not available on this device, as the app is unable to access your contacts." }) })), _jsx(OnboardingControls.Portal, { children: _jsxs(View, { style: [a.gap_md], children: [_jsx(Button, { onPress: function () { return dispatch({ type: 'next' }); }, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Import contacts"], ["Import contacts"])))), size: "large", color: "primary", disabled: !isAvailable, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Import contacts" }) }) }), _jsx(Button, { onPress: function () { return dispatch({ type: 'skip-contacts' }); }, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Skip"], ["Skip"])))), size: "large", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Skip" }) }) })] }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Onboarding/StepFindContactsIntro/index.web.js b/src/screens/Onboarding/StepFindContactsIntro/index.web.js new file mode 100644 index 0000000000..af627b67d1 --- /dev/null +++ b/src/screens/Onboarding/StepFindContactsIntro/index.web.js @@ -0,0 +1,3 @@ +export function StepFindContactsIntro() { + throw new Error('StepFindContactsIntro is not available on web'); +} diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.js b/src/screens/Onboarding/StepFinished/ValuePropositionPager.js new file mode 100644 index 0000000000..dde3723e95 --- /dev/null +++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.js @@ -0,0 +1,60 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useRef, useState } from 'react'; +import { View } from 'react-native'; +import PagerView from 'react-native-pager-view'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, tokens, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +import { PROP_1, PROP_2, PROP_3 } from './images'; +import { Dot, useValuePropText } from './ValuePropositionPager.shared'; +export function ValuePropositionPager(_a) { + var _b; + var step = _a.step, setStep = _a.setStep, avatarUri = _a.avatarUri; + var t = useTheme(); + var _c = useState(step), activePage = _c[0], setActivePage = _c[1]; + var ref = useRef(null); + if (step !== activePage) { + setActivePage(step); + (_b = ref.current) === null || _b === void 0 ? void 0 : _b.setPage(step); + } + var images = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]]; + return (_jsx(View, { style: [a.h_full, { marginHorizontal: tokens.space.xl * -1 }], children: _jsx(PagerView, { ref: ref, style: [a.flex_1], initialPage: step, onPageSelected: function (evt) { + var page = evt.nativeEvent.position; + if (step !== page) { + setActivePage(page); + setStep(page); + } + }, children: [0, 1, 2].map(function (page) { return (_jsx(Page, { page: page, image: images[page], avatarUri: avatarUri }, page)); }) }) })); +} +function Page(_a) { + var page = _a.page, image = _a.image, avatarUri = _a.avatarUri; + var _ = useLingui()._; + var t = useTheme(); + var _b = useValuePropText(page), title = _b.title, description = _b.description, alt = _b.alt; + return (_jsxs(View, { children: [_jsxs(View, { style: [ + a.relative, + a.align_center, + a.justify_center, + a.pointer_events_none, + ], children: [_jsx(Image, { source: image, style: [a.w_full, a.aspect_square], alt: alt, accessibilityIgnoresInvertColors: false }), page === 1 && (_jsx(Image, { source: avatarUri, style: [ + a.z_10, + a.absolute, + a.rounded_full, + { + width: "".concat((80 / 393) * 100, "%"), + height: "".concat((80 / 393) * 100, "%"), + }, + ], accessibilityIgnoresInvertColors: true, alt: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Your profile picture"], ["Your profile picture"])))) }))] }), _jsxs(View, { style: [a.mt_4xl, a.gap_2xl, a.px_xl, a.align_center], children: [_jsxs(View, { style: [a.flex_row, a.gap_sm], children: [_jsx(Dot, { active: page === 0 }), _jsx(Dot, { active: page === 1 }), _jsx(Dot, { active: page === 2 })] }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_bold, a.text_3xl, a.text_center], children: title }), _jsx(Text, { style: [ + t.atoms.text_contrast_medium, + a.text_md, + a.leading_snug, + a.text_center, + ], children: description })] })] })] }, page)); +} +var templateObject_1; diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.js b/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.js new file mode 100644 index 0000000000..367608ccfe --- /dev/null +++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.js @@ -0,0 +1,41 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +export function useValuePropText(step) { + var _ = useLingui()._; + return [ + { + title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Free your feed"], ["Free your feed"])))), + description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you."], ["No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you."])))), + alt: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens"], ["A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens"])))), + }, + { + title: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Find your people"], ["Find your people"])))), + description: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Ditch the trolls and clickbait. Find real people and conversations that matter to you."], ["Ditch the trolls and clickbait. Find real people and conversations that matter to you."])))), + alt: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Your profile picture surrounded by concentric circles of other users' profile pictures"], ["Your profile picture surrounded by concentric circles of other users' profile pictures"])))), + }, + { + title: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Forget the noise"], ["Forget the noise"])))), + description: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention."], ["No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention."])))), + alt: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["An illustration of several Bluesky posts alongside repost, like, and comment icons"], ["An illustration of several Bluesky posts alongside repost, like, and comment icons"])))), + }, + ][step]; +} +export function Dot(_a) { + var active = _a.active; + var t = useTheme(); + return (_jsx(View, { style: [ + a.rounded_full, + { width: 8, height: 8 }, + active + ? { backgroundColor: t.palette.primary_500 } + : t.atoms.bg_contrast_50, + ] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.js b/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.js new file mode 100644 index 0000000000..2db631f2bc --- /dev/null +++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.js @@ -0,0 +1,40 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +import { PROP_1, PROP_2, PROP_3 } from './images'; +import { Dot, useValuePropText } from './ValuePropositionPager.shared'; +export function ValuePropositionPager(_a) { + var step = _a.step, avatarUri = _a.avatarUri; + var t = useTheme(); + var _ = useLingui()._; + var image = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]][step]; + var _b = useValuePropText(step), title = _b.title, description = _b.description, alt = _b.alt; + return (_jsxs(View, { children: [_jsxs(View, { style: [ + a.relative, + a.align_center, + a.justify_center, + a.pointer_events_none, + ], children: [_jsx(Image, { source: image, style: [a.w_full, { aspectRatio: 1 }], alt: alt, accessibilityIgnoresInvertColors: false }), step === 1 && (_jsx(Image, { source: avatarUri, style: [ + a.z_10, + a.absolute, + a.rounded_full, + { + width: "".concat((80 / 393) * 100, "%"), + height: "".concat((80 / 393) * 100, "%"), + }, + ], accessibilityIgnoresInvertColors: true, alt: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Your profile picture"], ["Your profile picture"])))) }))] }), _jsxs(View, { style: [a.mt_4xl, a.gap_2xl, a.align_center], children: [_jsxs(View, { style: [a.flex_row, a.gap_sm], children: [_jsx(Dot, { active: step === 0 }), _jsx(Dot, { active: step === 1 }), _jsx(Dot, { active: step === 2 })] }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_bold, a.text_3xl, a.text_center], children: title }), _jsx(Text, { style: [ + t.atoms.text_contrast_medium, + a.text_md, + a.leading_snug, + a.text_center, + ], children: description })] })] })] })); +} +var templateObject_1; diff --git a/src/screens/Onboarding/StepFinished/images.js b/src/screens/Onboarding/StepFinished/images.js new file mode 100644 index 0000000000..f610edafd4 --- /dev/null +++ b/src/screens/Onboarding/StepFinished/images.js @@ -0,0 +1,25 @@ +import { platform } from '#/alf'; +export var PROP_1 = { + light: platform({ + native: require('../../../../assets/images/onboarding/value_prop_1_light.webp'), + web: require('../../../../assets/images/onboarding/value_prop_1_light_borderless.webp'), + }), + dim: platform({ + native: require('../../../../assets/images/onboarding/value_prop_1_dim.webp'), + web: require('../../../../assets/images/onboarding/value_prop_1_dim_borderless.webp'), + }), + dark: platform({ + native: require('../../../../assets/images/onboarding/value_prop_1_dark.webp'), + web: require('../../../../assets/images/onboarding/value_prop_1_dark_borderless.webp'), + }), +}; +export var PROP_2 = { + light: require('../../../../assets/images/onboarding/value_prop_2_light.webp'), + dim: require('../../../../assets/images/onboarding/value_prop_2_dim.webp'), + dark: require('../../../../assets/images/onboarding/value_prop_2_dark.webp'), +}; +export var PROP_3 = { + light: require('../../../../assets/images/onboarding/value_prop_3_light.webp'), + dim: require('../../../../assets/images/onboarding/value_prop_3_dim.webp'), + dark: require('../../../../assets/images/onboarding/value_prop_3_dark.webp'), +}; diff --git a/src/screens/Onboarding/StepFinished/index.js b/src/screens/Onboarding/StepFinished/index.js new file mode 100644 index 0000000000..5f4aa56480 --- /dev/null +++ b/src/screens/Onboarding/StepFinished/index.js @@ -0,0 +1,331 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { AppBskyGraphStarterpack, } from '@atproto/api'; +import { TID } from '@atproto/common-web'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { uploadBlob } from '#/lib/api'; +import { BSKY_APP_ACCOUNT_DID, DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED, VIDEO_SAVED_FEED, } from '#/lib/constants'; +import { useRequestNotificationsPermission } from '#/lib/notifications/notifications'; +import { logger } from '#/logger'; +import { useSetHasCheckedForStarterPack } from '#/state/preferences/used-starter-packs'; +import { getAllListMembers } from '#/state/queries/list-members'; +import { preferencesQueryKey } from '#/state/queries/preferences'; +import { RQKEY as profileRQKey } from '#/state/queries/profile'; +import { useAgent } from '#/state/session'; +import { useOnboardingDispatch } from '#/state/shell'; +import { useProgressGuideControls } from '#/state/shell/progress-guide'; +import { useActiveStarterPack, useSetActiveStarterPack, } from '#/state/shell/starter-pack'; +import { OnboardingControls, OnboardingHeaderSlot, } from '#/screens/Onboarding/Layout'; +import { useOnboardingInternalState, } from '#/screens/Onboarding/state'; +import { bulkWriteFollows } from '#/screens/Onboarding/util'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRight_Stroke2_Corner0_Rounded as ArrowRight } from '#/components/icons/Arrow'; +import { Loader } from '#/components/Loader'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import * as bsky from '#/types/bsky'; +import { ValuePropositionPager } from './ValuePropositionPager'; +export function StepFinished() { + var _this = this; + var _a = useOnboardingInternalState(), state = _a.state, dispatch = _a.dispatch; + var ax = useAnalytics(); + var onboardDispatch = useOnboardingDispatch(); + var _b = useState(false), saving = _b[0], setSaving = _b[1]; + var queryClient = useQueryClient(); + var agent = useAgent(); + var requestNotificationsPermission = useRequestNotificationsPermission(); + var activeStarterPack = useActiveStarterPack(); + var setActiveStarterPack = useSetActiveStarterPack(); + var setHasCheckedForStarterPack = useSetHasCheckedForStarterPack(); + var startProgressGuide = useProgressGuideControls().startProgressGuide; + var finishOnboarding = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var starterPack, listItems, spRes, e_1, e_2, interestsStepResults, profileStepResults_1, selectedInterests_1, e_3; + var _this = this; + var _a, _b, _c, _d, _e, _f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + setSaving(true); + if (!(activeStarterPack === null || activeStarterPack === void 0 ? void 0 : activeStarterPack.uri)) return [3 /*break*/, 8]; + _g.label = 1; + case 1: + _g.trys.push([1, 3, , 4]); + return [4 /*yield*/, agent.app.bsky.graph.getStarterPack({ + starterPack: activeStarterPack.uri, + })]; + case 2: + spRes = _g.sent(); + starterPack = spRes.data.starterPack; + return [3 /*break*/, 4]; + case 3: + e_1 = _g.sent(); + logger.error('Failed to fetch starter pack', { safeMessage: e_1 }); + return [3 /*break*/, 4]; + case 4: + _g.trys.push([4, 7, , 8]); + if (!(starterPack === null || starterPack === void 0 ? void 0 : starterPack.list)) return [3 /*break*/, 6]; + return [4 /*yield*/, getAllListMembers(agent, starterPack.list.uri)]; + case 5: + listItems = _g.sent(); + _g.label = 6; + case 6: return [3 /*break*/, 8]; + case 7: + e_2 = _g.sent(); + logger.error('Failed to fetch starter pack list items', { + safeMessage: e_2, + }); + return [3 /*break*/, 8]; + case 8: + _g.trys.push([8, 10, , 11]); + interestsStepResults = state.interestsStepResults, profileStepResults_1 = state.profileStepResults; + selectedInterests_1 = interestsStepResults.selectedInterests; + return [4 /*yield*/, Promise.all([ + bulkWriteFollows(agent, __spreadArray([BSKY_APP_ACCOUNT_DID], ((_a = listItems === null || listItems === void 0 ? void 0 : listItems.map(function (i) { return i.subject.did; })) !== null && _a !== void 0 ? _a : []), true), starterPack + ? { uri: starterPack.uri, cid: starterPack.cid } + : undefined), + (function () { return __awaiter(_this, void 0, void 0, function () { + var feedsToSave; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + // Interests need to get saved first, then we can write the feeds to prefs + return [4 /*yield*/, agent.setInterestsPref({ tags: selectedInterests_1 }) + // Default feeds that every user should have pinned when landing in the app + ]; + case 1: + // Interests need to get saved first, then we can write the feeds to prefs + _b.sent(); + feedsToSave = [ + __assign(__assign({}, DISCOVER_SAVED_FEED), { id: TID.nextStr() }), + __assign(__assign({}, TIMELINE_SAVED_FEED), { id: TID.nextStr() }), + __assign(__assign({}, VIDEO_SAVED_FEED), { id: TID.nextStr() }), + ]; + // Any starter pack feeds will be pinned _after_ the defaults + if (starterPack && ((_a = starterPack.feeds) === null || _a === void 0 ? void 0 : _a.length)) { + feedsToSave.push.apply(feedsToSave, starterPack.feeds.map(function (f) { return ({ + type: 'feed', + value: f.uri, + pinned: true, + id: TID.nextStr(), + }); })); + } + return [4 /*yield*/, agent.overwriteSavedFeeds(feedsToSave)]; + case 2: + _b.sent(); + return [2 /*return*/]; + } + }); + }); })(), + (function () { return __awaiter(_this, void 0, void 0, function () { + var imageUri, imageMime, blobPromise; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + imageUri = profileStepResults_1.imageUri, imageMime = profileStepResults_1.imageMime; + blobPromise = imageUri && imageMime + ? uploadBlob(agent, imageUri, imageMime) + : undefined; + return [4 /*yield*/, agent.upsertProfile(function (existing) { return __awaiter(_this, void 0, void 0, function () { + var next, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + next = existing !== null && existing !== void 0 ? existing : {}; + if (!blobPromise) return [3 /*break*/, 2]; + return [4 /*yield*/, blobPromise]; + case 1: + res = _a.sent(); + if (res.data.blob) { + next.avatar = res.data.blob; + } + _a.label = 2; + case 2: + if (starterPack) { + next.joinedViaStarterPack = { + uri: starterPack.uri, + cid: starterPack.cid, + }; + } + next.displayName = ''; + if (!next.createdAt) { + next.createdAt = new Date().toISOString(); + } + return [2 /*return*/, next]; + } + }); + }); })]; + case 1: + _a.sent(); + ax.metric('onboarding:finished:avatarResult', { + avatarResult: profileStepResults_1.isCreatedAvatar + ? 'created' + : profileStepResults_1.image + ? 'uploaded' + : 'default', + }); + return [2 /*return*/]; + } + }); + }); })(), + requestNotificationsPermission('AfterOnboarding'), + ])]; + case 9: + _g.sent(); + return [3 /*break*/, 11]; + case 10: + e_3 = _g.sent(); + logger.info("onboarding: bulk save failed"); + logger.error(e_3); + return [3 /*break*/, 11]; + case 11: + // Try to ensure that prefs and profile are up-to-date by the time we render Home. + return [4 /*yield*/, Promise.all([ + queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }), + queryClient.invalidateQueries({ + queryKey: profileRQKey((_c = (_b = agent.session) === null || _b === void 0 ? void 0 : _b.did) !== null && _c !== void 0 ? _c : ''), + }), + ]).catch(function (e) { + logger.error(e); + // Keep going. + })]; + case 12: + // Try to ensure that prefs and profile are up-to-date by the time we render Home. + _g.sent(); + setSaving(false); + setActiveStarterPack(undefined); + setHasCheckedForStarterPack(true); + startProgressGuide('follow-10'); + dispatch({ type: 'finish' }); + onboardDispatch({ type: 'finish' }); + ax.metric('onboarding:finished:nextPressed', { + usedStarterPack: Boolean(starterPack), + starterPackName: starterPack && + bsky.dangerousIsType(starterPack.record, AppBskyGraphStarterpack.isRecord) + ? starterPack.record.name + : undefined, + starterPackCreator: starterPack === null || starterPack === void 0 ? void 0 : starterPack.creator.did, + starterPackUri: starterPack === null || starterPack === void 0 ? void 0 : starterPack.uri, + profilesFollowed: (_d = listItems === null || listItems === void 0 ? void 0 : listItems.length) !== null && _d !== void 0 ? _d : 0, + feedsPinned: (_f = (_e = starterPack === null || starterPack === void 0 ? void 0 : starterPack.feeds) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0, + }); + if (starterPack && (listItems === null || listItems === void 0 ? void 0 : listItems.length)) { + ax.metric('starterPack:followAll', { + logContext: 'Onboarding', + starterPack: starterPack.uri, + count: listItems === null || listItems === void 0 ? void 0 : listItems.length, + }); + } + return [2 /*return*/]; + } + }); + }); }, [ + ax, + queryClient, + agent, + dispatch, + onboardDispatch, + activeStarterPack, + state, + requestNotificationsPermission, + setActiveStarterPack, + setHasCheckedForStarterPack, + startProgressGuide, + ]); + return (_jsx(ValueProposition, { finishOnboarding: finishOnboarding, saving: saving, state: state })); +} +function ValueProposition(_a) { + var finishOnboarding = _a.finishOnboarding, saving = _a.saving, state = _a.state; + var _b = useState(0), subStep = _b[0], setSubStep = _b[1]; + var _ = useLingui()._; + var ax = useAnalytics(); + var gtMobile = useBreakpoints().gtMobile; + var onPress = function () { + if (subStep === 2) { + finishOnboarding(); // has its own metrics + } + else if (subStep === 1) { + setSubStep(2); + ax.metric('onboarding:valueProp:stepTwo:nextPressed', {}); + } + else if (subStep === 0) { + setSubStep(1); + ax.metric('onboarding:valueProp:stepOne:nextPressed', {}); + } + }; + return (_jsxs(_Fragment, { children: [!gtMobile && (_jsx(OnboardingHeaderSlot.Portal, { children: _jsx(Button, { disabled: saving, variant: "ghost", color: "secondary", size: "small", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Skip introduction and start using your account"], ["Skip introduction and start using your account"])))), onPress: function () { + ax.metric('onboarding:valueProp:skipPressed', {}); + finishOnboarding(); + }, style: [a.bg_transparent], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Skip" }) }) }) })), _jsx(ValuePropositionPager, { step: subStep, setStep: function (ss) { return setSubStep(ss); }, avatarUri: state.profileStepResults.imageUri }), _jsx(OnboardingControls.Portal, { children: _jsxs(View, { style: gtMobile && [a.gap_md, a.flex_row], children: [gtMobile && (IS_WEB ? subStep !== 2 : true) && (_jsx(Button, { disabled: saving, color: "secondary", size: "large", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Skip introduction and start using your account"], ["Skip introduction and start using your account"])))), onPress: function () { return finishOnboarding(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Skip" }) }) })), _jsxs(Button, { testID: "onboardingFinish", disabled: saving, color: "primary", size: "large", label: subStep === 2 + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Complete onboarding and start using your account"], ["Complete onboarding and start using your account"])))) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Next"], ["Next"])))), onPress: onPress, children: [_jsx(ButtonText, { children: saving ? (_jsx(Trans, { children: "Finalizing" })) : subStep === 2 ? (_jsx(Trans, { children: "Let's go!" })) : (_jsx(Trans, { children: "Next" })) }), subStep === 2 && (_jsx(ButtonIcon, { icon: saving ? Loader : ArrowRight }))] }, state.activeStep)] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Onboarding/StepInterests/InterestButton.js b/src/screens/Onboarding/StepInterests/InterestButton.js new file mode 100644 index 0000000000..921d08bbe7 --- /dev/null +++ b/src/screens/Onboarding/StepInterests/InterestButton.js @@ -0,0 +1,68 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { useInterestsDisplayNames } from '#/lib/interests'; +import { capitalize } from '#/lib/strings/capitalize'; +import { atoms as a, native, useTheme } from '#/alf'; +import * as Toggle from '#/components/forms/Toggle'; +import { Text } from '#/components/Typography'; +export function InterestButton(_a) { + var interest = _a.interest; + var t = useTheme(); + var interestsDisplayNames = useInterestsDisplayNames(); + var ctx = Toggle.useItemContext(); + var styles = React.useMemo(function () { + var hovered = [ + { + backgroundColor: t.name === 'light' ? t.palette.contrast_200 : t.palette.contrast_50, + }, + ]; + var focused = []; + var pressed = []; + var selected = [ + { + backgroundColor: t.palette.contrast_900, + }, + ]; + var selectedHover = [ + { + backgroundColor: t.palette.contrast_800, + }, + ]; + var textSelected = [ + { + color: t.palette.contrast_100, + }, + ]; + return { + hovered: hovered, + focused: focused, + pressed: pressed, + selected: selected, + selectedHover: selectedHover, + textSelected: textSelected, + }; + }, [t]); + return (_jsx(View, { style: [ + { + backgroundColor: t.palette.contrast_100, + paddingVertical: 15, + }, + a.rounded_full, + a.px_2xl, + ctx.hovered ? styles.hovered : {}, + ctx.focused ? styles.hovered : {}, + ctx.pressed ? styles.hovered : {}, + ctx.selected ? styles.selected : {}, + ctx.selected && (ctx.hovered || ctx.focused || ctx.pressed) + ? styles.selectedHover + : {}, + ], children: _jsx(Text, { style: [ + { + color: t.palette.contrast_900, + }, + a.font_semi_bold, + native({ paddingTop: 2 }), + ctx.selected ? styles.textSelected : {}, + ], children: interestsDisplayNames[interest] || capitalize(interest) }) })); +} diff --git a/src/screens/Onboarding/StepInterests/index.js b/src/screens/Onboarding/StepInterests/index.js new file mode 100644 index 0000000000..828abfa5e5 --- /dev/null +++ b/src/screens/Onboarding/StepInterests/index.js @@ -0,0 +1,90 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { interests, useInterestsDisplayNames } from '#/lib/interests'; +import { capitalize } from '#/lib/strings/capitalize'; +import { logger } from '#/logger'; +import { OnboardingControls, OnboardingDescriptionText, OnboardingPosition, OnboardingTitleText, } from '#/screens/Onboarding/Layout'; +import { useOnboardingInternalState } from '#/screens/Onboarding/state'; +import { InterestButton } from '#/screens/Onboarding/StepInterests/InterestButton'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Toggle from '#/components/forms/Toggle'; +import { Loader } from '#/components/Loader'; +import { useAnalytics } from '#/analytics'; +export function StepInterests() { + var _this = this; + var _ = useLingui()._; + var ax = useAnalytics(); + var interestsDisplayNames = useInterestsDisplayNames(); + var _a = useOnboardingInternalState(), state = _a.state, dispatch = _a.dispatch; + var _b = React.useState(false), saving = _b[0], setSaving = _b[1]; + var _c = React.useState(state.interestsStepResults.selectedInterests.map(function (i) { return i; })), selectedInterests = _c[0], setSelectedInterests = _c[1]; + var saveInterests = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + setSaving(true); + try { + setSaving(false); + dispatch({ + type: 'setInterestsStepResults', + selectedInterests: selectedInterests, + }); + dispatch({ type: 'next' }); + ax.metric('onboarding:interests:nextPressed', { + selectedInterests: selectedInterests, + selectedInterestsLength: selectedInterests.length, + }); + } + catch (e) { + logger.info("onboading: error saving interests"); + logger.error(e); + } + return [2 /*return*/]; + }); + }); }, [ax, selectedInterests, setSaving, dispatch]); + return (_jsxs(View, { style: [a.align_start, a.gap_sm], testID: "onboardingInterests", children: [_jsx(OnboardingPosition, {}), _jsx(OnboardingTitleText, { children: _jsx(Trans, { children: "What are your interests?" }) }), _jsx(OnboardingDescriptionText, { children: _jsx(Trans, { children: "We'll use this to help customize your experience." }) }), _jsx(View, { style: [a.w_full, a.pt_lg], children: _jsx(Toggle.Group, { values: selectedInterests, onChange: setSelectedInterests, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select your interests from the options below"], ["Select your interests from the options below"])))), children: _jsx(View, { style: [a.flex_row, a.gap_md, a.flex_wrap], children: interests.map(function (interest) { return (_jsx(Toggle.Item, { name: interest, label: interestsDisplayNames[interest] || capitalize(interest), children: _jsx(InterestButton, { interest: interest }) }, interest)); }) }) }) }), _jsx(OnboardingControls.Portal, { children: _jsxs(Button, { disabled: saving, testID: "onboardingContinue", variant: "solid", color: "primary", size: "large", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Continue to next step"], ["Continue to next step"])))), onPress: saveInterests, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Continue" }) }), saving && _jsx(ButtonIcon, { icon: Loader })] }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Onboarding/StepProfile/AvatarCircle.js b/src/screens/Onboarding/StepProfile/AvatarCircle.js new file mode 100644 index 0000000000..a4eef33fbd --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCircle.js @@ -0,0 +1,39 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { Image as ExpoImage } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { AvatarCreatorCircle } from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle'; +import { useAvatar } from '#/screens/Onboarding/StepProfile/index'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { Pencil_Stroke2_Corner0_Rounded as Pencil } from '#/components/icons/Pencil'; +import { StreamingLive_Stroke2_Corner0_Rounded as StreamingLive } from '#/components/icons/StreamingLive'; +export function AvatarCircle(_a) { + var openLibrary = _a.openLibrary, openCreator = _a.openCreator; + var _ = useLingui()._; + var t = useTheme(); + var avatar = useAvatar().avatar; + var styles = React.useMemo(function () { return ({ + imageContainer: [ + a.rounded_full, + a.overflow_hidden, + a.align_center, + a.justify_center, + a.border, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + { + height: 200, + width: 200, + }, + ], + }); }, [t.atoms.bg_contrast_25, t.atoms.border_contrast_low]); + return (_jsxs(View, { children: [avatar.useCreatedAvatar ? (_jsx(AvatarCreatorCircle, { avatar: avatar, size: 200 })) : avatar.image ? (_jsx(ExpoImage, { source: avatar.image.path, style: styles.imageContainer, accessibilityIgnoresInvertColors: true, transition: { duration: 300, effect: 'cross-dissolve' } })) : (_jsx(View, { style: styles.imageContainer, children: _jsx(StreamingLive, { height: 100, width: 100, style: { color: t.palette.contrast_200 } }) })), _jsx(View, { style: [a.absolute, { bottom: 2, right: 2 }], children: _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select an avatar"], ["Select an avatar"])))), size: "large", shape: "round", variant: "solid", color: "primary", onPress: avatar.useCreatedAvatar ? openCreator : openLibrary, children: _jsx(ButtonIcon, { icon: Pencil }) }) })] })); +} +var templateObject_1; diff --git a/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.js b/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.js new file mode 100644 index 0000000000..61e2c2ead4 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.js @@ -0,0 +1,25 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +export function AvatarCreatorCircle(_a) { + var avatar = _a.avatar, _b = _a.size, size = _b === void 0 ? 125 : _b; + var t = useTheme(); + var Icon = avatar.placeholder.component; + var styles = React.useMemo(function () { return ({ + imageContainer: [ + a.rounded_full, + a.overflow_hidden, + a.align_center, + a.justify_center, + a.border, + t.atoms.border_contrast_high, + { + height: size, + width: size, + backgroundColor: avatar.backgroundColor, + }, + ], + }); }, [avatar.backgroundColor, size, t.atoms.border_contrast_high]); + return (_jsx(View, { children: _jsx(View, { style: styles.imageContainer, children: _jsx(Icon, { height: 85, width: 85, style: { color: t.palette.white } }) }) })); +} diff --git a/src/screens/Onboarding/StepProfile/AvatarCreatorItems.js b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.js new file mode 100644 index 0000000000..10cba59be4 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.js @@ -0,0 +1,78 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { avatarColors, emojiItems, emojiNames, } from '#/screens/Onboarding/StepProfile/types'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { Text } from '#/components/Typography'; +var ACTIVE_BORDER_WIDTH = 3; +var ACTIVE_BORDER_STYLES = { + top: -ACTIVE_BORDER_WIDTH, + bottom: -ACTIVE_BORDER_WIDTH, + left: -ACTIVE_BORDER_WIDTH, + right: -ACTIVE_BORDER_WIDTH, + opacity: 0.5, + borderWidth: 3, +}; +export function AvatarCreatorItems(_a) { + var type = _a.type, avatar = _a.avatar, setAvatar = _a.setAvatar; + var _ = useLingui()._; + var t = useTheme(); + var isEmojis = type === 'emojis'; + var onSelectEmoji = React.useCallback(function (emoji) { + setAvatar(function (prev) { return (__assign(__assign({}, prev), { placeholder: emojiItems[emoji] })); }); + }, [setAvatar]); + var onSelectColor = React.useCallback(function (color) { + setAvatar(function (prev) { return (__assign(__assign({}, prev), { backgroundColor: color })); }); + }, [setAvatar]); + return (_jsxs(View, { style: [a.w_full], children: [_jsx(Text, { style: [a.pb_md, t.atoms.text_contrast_medium], children: isEmojis ? (_jsx(Trans, { children: "Select an emoji" })) : (_jsx(Trans, { children: "Select a color" })) }), _jsx(View, { style: [ + a.flex_row, + a.align_start, + a.justify_start, + a.flex_wrap, + a.gap_md, + ], children: isEmojis + ? emojiNames.map(function (emojiName) { return (_jsxs(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select the ", " emoji as your avatar"], ["Select the ", " emoji as your avatar"])), emojiName)), size: "small", shape: "round", variant: "solid", color: "secondary", onPress: function () { return onSelectEmoji(emojiName); }, children: [_jsx(ButtonIcon, { icon: emojiItems[emojiName].component }), avatar.placeholder.name === emojiName && (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + ACTIVE_BORDER_STYLES, + { + borderColor: avatar.backgroundColor, + }, + ] }))] }, emojiName)); }) + : avatarColors.map(function (color) { return (_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Choose this color as your avatar"], ["Choose this color as your avatar"])))), size: "small", shape: "round", variant: "solid", onPress: function () { return onSelectColor(color); }, children: function (ctx) { return (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.rounded_full, + { + opacity: ctx.hovered || ctx.pressed ? 0.8 : 1, + backgroundColor: color, + }, + ] }), avatar.backgroundColor === color && (_jsx(View, { style: [ + a.absolute, + a.rounded_full, + ACTIVE_BORDER_STYLES, + { + borderColor: color, + }, + ] }))] })); } }, color)); }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.js b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.js new file mode 100644 index 0000000000..2efe16b7dd --- /dev/null +++ b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.js @@ -0,0 +1,89 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { useAvatar } from '#/screens/Onboarding/StepProfile/index'; +import { atoms as a } from '#/alf'; +var LazyViewShot = React.lazy( +// @ts-expect-error dynamic import +function () { return import('react-native-view-shot/src/index'); }); +var SIZE_MULTIPLIER = 5; +// This component is supposed to be invisible to the user. We only need this for ViewShot to have something to +// "screenshot". +export var PlaceholderCanvas = React.forwardRef(function PlaceholderCanvas(_a, ref) { + var _this = this; + var avatar = useAvatar().avatar; + var viewshotRef = React.useRef(null); + var Icon = avatar.placeholder.component; + var styles = React.useMemo(function () { return ({ + container: [a.absolute, { top: -2000 }], + imageContainer: [ + a.align_center, + a.justify_center, + { height: 150 * SIZE_MULTIPLIER, width: 150 * SIZE_MULTIPLIER }, + ], + }); }, []); + React.useImperativeHandle(ref, function () { return ({ + capture: function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!((_a = viewshotRef.current) === null || _a === void 0 ? void 0 : _a.capture)) return [3 /*break*/, 2]; + return [4 /*yield*/, viewshotRef.current.capture()]; + case 1: return [2 /*return*/, _b.sent()]; + case 2: return [2 /*return*/]; + } + }); + }); }, + }); }); + return (_jsx(View, { style: styles.container, children: _jsx(React.Suspense, { fallback: null, children: _jsx(LazyViewShot + // @ts-ignore this library doesn't have types + , { + // @ts-ignore this library doesn't have types + ref: viewshotRef, options: { + fileName: 'placeholderAvatar', + format: 'jpg', + quality: 0.8, + height: 150 * SIZE_MULTIPLIER, + width: 150 * SIZE_MULTIPLIER, + }, children: _jsx(View, { style: [ + styles.imageContainer, + { backgroundColor: avatar.backgroundColor }, + ], collapsable: false, children: _jsx(Icon, { height: 85 * SIZE_MULTIPLIER, width: 85 * SIZE_MULTIPLIER, style: { color: 'white' } }) }) }) }) })); +}); diff --git a/src/screens/Onboarding/StepProfile/index.js b/src/screens/Onboarding/StepProfile/index.js new file mode 100644 index 0000000000..61f8d32c91 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/index.js @@ -0,0 +1,265 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { Image as ExpoImage } from 'expo-image'; +import { launchImageLibraryAsync, UIImagePickerPreferredAssetRepresentationMode, } from 'expo-image-picker'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { usePhotoLibraryPermission } from '#/lib/hooks/usePermissions'; +import { compressIfNeeded } from '#/lib/media/manip'; +import { openCropper } from '#/lib/media/picker'; +import { getDataUriSize } from '#/lib/media/util'; +import { useRequestNotificationsPermission } from '#/lib/notifications/notifications'; +import { isCancelledError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { OnboardingControls, OnboardingDescriptionText, OnboardingPosition, OnboardingTitleText, } from '#/screens/Onboarding/Layout'; +import { useOnboardingInternalState } from '#/screens/Onboarding/state'; +import { AvatarCircle } from '#/screens/Onboarding/StepProfile/AvatarCircle'; +import { AvatarCreatorCircle } from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle'; +import { AvatarCreatorItems } from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'; +import { PlaceholderCanvas, } from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useSheetWrapper } from '#/components/Dialog/sheet-wrapper'; +import { CircleInfo_Stroke2_Corner0_Rounded } from '#/components/icons/CircleInfo'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE, IS_WEB } from '#/env'; +import { avatarColors, emojiItems } from './types'; +var AvatarContext = React.createContext({}); +AvatarContext.displayName = 'AvatarContext'; +export var useAvatar = function () { return React.useContext(AvatarContext); }; +var randomColor = avatarColors[Math.floor(Math.random() * avatarColors.length)]; +export function StepProfile() { + var _this = this; + var _a, _b, _c; + var ax = useAnalytics(); + var _ = useLingui()._; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var requestPhotoAccessIfNeeded = usePhotoLibraryPermission().requestPhotoAccessIfNeeded; + var requestNotificationsPermission = useRequestNotificationsPermission(); + var creatorControl = Dialog.useDialogControl(); + var _d = React.useState(''), error = _d[0], setError = _d[1]; + var _e = useOnboardingInternalState(), state = _e.state, dispatch = _e.dispatch; + var _f = React.useState({ + image: (_a = state.profileStepResults) === null || _a === void 0 ? void 0 : _a.image, + placeholder: ((_b = state.profileStepResults.creatorState) === null || _b === void 0 ? void 0 : _b.emoji) || emojiItems.at, + backgroundColor: ((_c = state.profileStepResults.creatorState) === null || _c === void 0 ? void 0 : _c.backgroundColor) || randomColor, + useCreatedAvatar: state.profileStepResults.isCreatedAvatar, + }), avatar = _f[0], setAvatar = _f[1]; + var canvasRef = React.useRef(null); + React.useEffect(function () { + requestNotificationsPermission('StartOnboarding'); + }, [requestNotificationsPermission]); + var sheetWrapper = useSheetWrapper(); + var openPicker = React.useCallback(function (opts) { return __awaiter(_this, void 0, void 0, function () { + var response; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, sheetWrapper(launchImageLibraryAsync(__assign(__assign({ exif: false, mediaTypes: ['images'], quality: 1 }, opts), { legacy: true, preferredAssetRepresentationMode: UIImagePickerPreferredAssetRepresentationMode.Automatic })))]; + case 1: + response = _b.sent(); + return [2 /*return*/, ((_a = response.assets) !== null && _a !== void 0 ? _a : []) + .slice(0, 1) + .filter(function (asset) { + var _a, _b, _c, _d; + if (!((_a = asset.mimeType) === null || _a === void 0 ? void 0 : _a.startsWith('image/')) || + (!((_b = asset.mimeType) === null || _b === void 0 ? void 0 : _b.endsWith('jpeg')) && + !((_c = asset.mimeType) === null || _c === void 0 ? void 0 : _c.endsWith('jpg')) && + !((_d = asset.mimeType) === null || _d === void 0 ? void 0 : _d.endsWith('png')))) { + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Only .jpg and .png files are supported"], ["Only .jpg and .png files are supported"]))))); + return false; + } + return true; + }) + .map(function (image) { return ({ + mime: 'image/jpeg', + height: image.height, + width: image.width, + path: image.uri, + size: getDataUriSize(image.uri), + }); })]; + } + }); + }); }, [_, setError, sheetWrapper]); + var onContinue = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var imageUri; + var _a, _b, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + imageUri = (_a = avatar === null || avatar === void 0 ? void 0 : avatar.image) === null || _a === void 0 ? void 0 : _a.path; + if (!(!imageUri || avatar.useCreatedAvatar)) return [3 /*break*/, 2]; + return [4 /*yield*/, ((_b = canvasRef.current) === null || _b === void 0 ? void 0 : _b.capture())]; + case 1: + imageUri = _e.sent(); + _e.label = 2; + case 2: + if (imageUri) { + dispatch({ + type: 'setProfileStepResults', + image: avatar.image, + imageUri: imageUri, + imageMime: (_d = (_c = avatar.image) === null || _c === void 0 ? void 0 : _c.mime) !== null && _d !== void 0 ? _d : 'image/jpeg', + isCreatedAvatar: avatar.useCreatedAvatar, + creatorState: { + emoji: avatar.placeholder, + backgroundColor: avatar.backgroundColor, + }, + }); + } + dispatch({ type: 'next' }); + ax.metric('onboarding:profile:nextPressed', {}); + return [2 /*return*/]; + } + }); + }); }, [ax, avatar, dispatch]); + var onDoneCreating = React.useCallback(function () { + setAvatar(function (prev) { return (__assign(__assign({}, prev), { image: undefined, useCreatedAvatar: true })); }); + creatorControl.close(); + }, [creatorControl]); + var openLibrary = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var items, image, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, requestPhotoAccessIfNeeded()]; + case 1: + if (!(_a.sent())) { + return [2 /*return*/]; + } + setError(''); + return [4 /*yield*/, sheetWrapper(openPicker({ + aspect: [1, 1], + }))]; + case 2: + items = _a.sent(); + image = items[0]; + if (!image) + return [2 /*return*/]; + if (!!IS_WEB) return [3 /*break*/, 6]; + _a.label = 3; + case 3: + _a.trys.push([3, 5, , 6]); + return [4 /*yield*/, openCropper({ + imageUri: image.path, + shape: 'circle', + aspectRatio: 1 / 1, + })]; + case 4: + image = _a.sent(); + return [3 /*break*/, 6]; + case 5: + e_1 = _a.sent(); + if (!isCancelledError(e_1)) { + logger.error('Failed to crop avatar in onboarding', { error: e_1 }); + } + return [3 /*break*/, 6]; + case 6: return [4 /*yield*/, compressIfNeeded(image, 1000000) + // If we are on mobile, prefetching the image will load the image into memory before we try and display it, + // stopping any brief flickers. + ]; + case 7: + image = _a.sent(); + if (!IS_NATIVE) return [3 /*break*/, 9]; + return [4 /*yield*/, ExpoImage.prefetch(image.path)]; + case 8: + _a.sent(); + _a.label = 9; + case 9: + setAvatar(function (prev) { return (__assign(__assign({}, prev), { image: image, useCreatedAvatar: false })); }); + return [2 /*return*/]; + } + }); + }); }, [ + requestPhotoAccessIfNeeded, + setAvatar, + openPicker, + setError, + sheetWrapper, + ]); + var onSecondaryPress = React.useCallback(function () { + if (avatar.useCreatedAvatar) { + openLibrary(); + } + else { + creatorControl.open(); + } + }, [avatar.useCreatedAvatar, creatorControl, openLibrary]); + var value = React.useMemo(function () { return ({ + avatar: avatar, + setAvatar: setAvatar, + }); }, [avatar]); + return (_jsxs(AvatarContext.Provider, { value: value, children: [_jsxs(View, { style: [a.align_start], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(OnboardingPosition, {}), _jsx(OnboardingTitleText, { children: _jsx(Trans, { children: "Give your profile a face" }) }), _jsx(OnboardingDescriptionText, { children: _jsx(Trans, { children: "Help people know you're not a bot by uploading a picture or creating an avatar." }) })] }), _jsxs(View, { style: [a.w_full, a.align_center, { paddingTop: gtMobile ? 80 : 60 }], children: [_jsx(AvatarCircle, { openLibrary: openLibrary, openCreator: creatorControl.open }), error && (_jsxs(View, { style: [ + a.flex_row, + a.gap_sm, + a.align_center, + a.mt_xl, + a.py_md, + a.px_lg, + a.border, + a.rounded_md, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + ], children: [_jsx(CircleInfo_Stroke2_Corner0_Rounded, { size: "sm" }), _jsx(Text, { style: [a.leading_snug], children: error })] }))] }), _jsx(OnboardingControls.Portal, { children: _jsxs(View, { style: [a.gap_md, gtMobile && a.flex_row_reverse], children: [_jsx(Button, { testID: "onboardingContinue", color: "primary", size: "large", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Continue to next step"], ["Continue to next step"])))), onPress: onContinue, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Continue" }) }) }), _jsx(Button, { testID: "onboardingAvatarCreator", color: "primary_subtle", size: "large", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Open avatar creator"], ["Open avatar creator"])))), onPress: onSecondaryPress, children: _jsx(ButtonText, { children: avatar.useCreatedAvatar ? (_jsx(Trans, { children: "Upload a photo instead" })) : (_jsx(Trans, { children: "Create an avatar instead" })) }) })] }) })] }), _jsx(Dialog.Outer, { control: creatorControl, children: _jsxs(Dialog.Inner, { label: "Avatar creator", style: [ + { + width: 'auto', + maxWidth: 410, + }, + ], children: [_jsx(View, { style: [a.align_center, { paddingTop: 20 }], children: _jsx(AvatarCreatorCircle, { avatar: avatar }) }), _jsxs(View, { style: [a.pt_3xl, a.gap_lg], children: [_jsx(AvatarCreatorItems, { type: "emojis", avatar: avatar, setAvatar: setAvatar }), _jsx(AvatarCreatorItems, { type: "colors", avatar: avatar, setAvatar: setAvatar })] }), _jsx(View, { style: [a.pt_4xl], children: _jsx(Button, { color: "primary", size: "large", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Done"], ["Done"])))), onPress: onDoneCreating, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Done" }) }) }) })] }) }), _jsx(PlaceholderCanvas, { ref: canvasRef })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Onboarding/StepProfile/types.js b/src/screens/Onboarding/StepProfile/types.js new file mode 100644 index 0000000000..e20e2b2036 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/types.js @@ -0,0 +1,111 @@ +import { Alien_Stroke2_Corner0_Rounded as Alien } from '#/components/icons/Alien'; +import { Apple_Stroke2_Corner0_Rounded as Apple } from '#/components/icons/Apple'; +import { At_Stroke2_Corner0_Rounded as At } from '#/components/icons/At'; +import { Atom_Stroke2_Corner0_Rounded as Atom } from '#/components/icons/Atom'; +import { Celebrate_Stroke2_Corner0_Rounded as Celebrate } from '#/components/icons/Celebrate'; +import { EmojiArc_Stroke2_Corner0_Rounded as EmojiArc, EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyes, } from '#/components/icons/Emoji'; +import { Explosion_Stroke2_Corner0_Rounded as Explosion } from '#/components/icons/Explosion'; +import { GameController_Stroke2_Corner0_Rounded as GameController } from '#/components/icons/GameController'; +import { Lab_Stroke2_Corner0_Rounded as Lab } from '#/components/icons/Lab'; +import { Leaf_Stroke2_Corner0_Rounded as Leaf } from '#/components/icons/Leaf'; +import { MusicNote_Stroke2_Corner0_Rounded as MusicNote } from '#/components/icons/MusicNote'; +import { Rose_Stroke2_Corner0_Rounded as Rose } from '#/components/icons/Rose'; +import { Shaka_Stroke2_Corner0_Rounded as Shaka } from '#/components/icons/Shaka'; +import { UFO_Stroke2_Corner0_Rounded as UFO } from '#/components/icons/UFO'; +import { Zap_Stroke2_Corner0_Rounded as Zap } from '#/components/icons/Zap'; +/** + * If you want to add or remove icons from the selection, just add the name to the `emojiNames` array and + * add the item to the `emojiItems` record.. + */ +export var emojiNames = [ + 'at', + 'arc', + 'heartEyes', + 'alien', + 'apple', + 'atom', + 'celebrate', + 'gameController', + 'leaf', + 'musicNote', + 'rose', + 'shaka', + 'ufo', + 'zap', + 'explosion', + 'lab', +]; +export var emojiItems = { + at: { + name: 'at', + component: At, + }, + arc: { + name: 'arc', + component: EmojiArc, + }, + heartEyes: { + name: 'heartEyes', + component: EmojiHeartEyes, + }, + alien: { + name: 'alien', + component: Alien, + }, + apple: { + name: 'apple', + component: Apple, + }, + atom: { + name: 'atom', + component: Atom, + }, + celebrate: { + name: 'celebrate', + component: Celebrate, + }, + gameController: { + name: 'gameController', + component: GameController, + }, + leaf: { + name: 'leaf', + component: Leaf, + }, + musicNote: { + name: 'musicNote', + component: MusicNote, + }, + rose: { + name: 'rose', + component: Rose, + }, + shaka: { + name: 'shaka', + component: Shaka, + }, + ufo: { + name: 'ufo', + component: UFO, + }, + zap: { + name: 'zap', + component: Zap, + }, + explosion: { + name: 'explosion', + component: Explosion, + }, + lab: { + name: 'lab', + component: Lab, + }, +}; +export var avatarColors = [ + '#FE8311', + '#FED811', + '#73DF84', + '#1185FE', + '#EF75EA', + '#F55454', +]; diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.js b/src/screens/Onboarding/StepSuggestedAccounts/index.js new file mode 100644 index 0000000000..9189b64e80 --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.js @@ -0,0 +1,303 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import * as bcp47Match from 'bcp-47-match'; +import { wait } from '#/lib/async/wait'; +import { popularInterests, useInterestsDisplayNames } from '#/lib/interests'; +import { isBlockedOrBlocking, isMuted } from '#/lib/moderation/blocked-and-muted'; +import { logger } from '#/logger'; +import { updateProfileShadow } from '#/state/cache/profile-shadow'; +import { useLanguagePrefs } from '#/state/preferences'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useAgent, useSession } from '#/state/session'; +import { OnboardingControls, OnboardingPosition, OnboardingTitleText, } from '#/screens/Onboarding/Layout'; +import { useOnboardingInternalState } from '#/screens/Onboarding/state'; +import { useSuggestedOnboardingUsers } from '#/screens/Search/util/useSuggestedOnboardingUsers'; +import { atoms as a, tokens, useBreakpoints, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon } from '#/components/icons/ArrowRotate'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import { boostInterests, InterestTabs } from '#/components/InterestTabs'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import * as toast from '#/components/Toast'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import { bulkWriteFollows } from '../util'; +export function StepSuggestedAccounts() { + var _this = this; + var _a; + var _ = useLingui()._; + var ax = useAnalytics(); + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + var queryClient = useQueryClient(); + var _b = useOnboardingInternalState(), state = _b.state, dispatch = _b.dispatch; + var _c = useState(null), selectedInterest = _c[0], setSelectedInterest = _c[1]; + // keeping track of who was followed via the follow all button + // so we can enable/disable the button without having to dig through the shadow cache + var _d = useState([]), followedUsers = _d[0], setFollowedUsers = _d[1]; + /* + * Special language handling copied wholesale from the Explore screen + */ + var contentLanguages = useLanguagePrefs().contentLanguages; + var useFullExperience = useMemo(function () { + if (contentLanguages.length === 0) + return true; + return bcp47Match.basicFilter('en', contentLanguages).length > 0; + }, [contentLanguages]); + var interestsDisplayNames = useInterestsDisplayNames(); + var interests = Object.keys(interestsDisplayNames) + .sort(boostInterests(popularInterests)) + .sort(boostInterests(state.interestsStepResults.selectedInterests)); + var _e = useSuggestedOnboardingUsers({ + category: selectedInterest || (useFullExperience ? null : interests[0]), + search: !useFullExperience, + overrideInterests: state.interestsStepResults.selectedInterests, + }), suggestedUsers = _e.data, isLoading = _e.isLoading, error = _e.error, isRefetching = _e.isRefetching, refetch = _e.refetch; + var isError = !!error; + var isEmpty = !isLoading && suggestedUsers && suggestedUsers.actors.length === 0; + var followableDids = (_a = suggestedUsers === null || suggestedUsers === void 0 ? void 0 : suggestedUsers.actors.filter(function (user) { + var _a; + return user.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) && + !isBlockedOrBlocking(user) && + !isMuted(user) && + !((_a = user.viewer) === null || _a === void 0 ? void 0 : _a.following) && + !followedUsers.includes(user.did); + }).map(function (user) { return user.did; })) !== null && _a !== void 0 ? _a : []; + var _f = useMutation({ + onMutate: function () { + ax.metric('onboarding:suggestedAccounts:followAllPressed', { + tab: selectedInterest !== null && selectedInterest !== void 0 ? selectedInterest : 'all', + numAccounts: followableDids.length, + }); + for (var i = 0; i < followableDids.length; i++) { + var did = followableDids[i]; + ax.metric('suggestedUser:follow', { + logContext: 'Onboarding', + location: 'FollowAll', + recId: suggestedUsers === null || suggestedUsers === void 0 ? void 0 : suggestedUsers.recId, + position: i, + suggestedDid: did, + category: selectedInterest, + }); + } + }, + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var _i, followableDids_1, did, uris, _a, followableDids_2, did, uri; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + for (_i = 0, followableDids_1 = followableDids; _i < followableDids_1.length; _i++) { + did = followableDids_1[_i]; + updateProfileShadow(queryClient, did, { + followingUri: 'pending', + }); + } + return [4 /*yield*/, wait(1e3, bulkWriteFollows(agent, followableDids))]; + case 1: + uris = _b.sent(); + for (_a = 0, followableDids_2 = followableDids; _a < followableDids_2.length; _a++) { + did = followableDids_2[_a]; + uri = uris.get(did); + updateProfileShadow(queryClient, did, { + followingUri: uri, + }); + } + return [2 /*return*/, followableDids]; + } + }); + }); }, + onSuccess: function (newlyFollowed) { + toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Followed all accounts!"], ["Followed all accounts!"])))), { type: 'success' }); + setFollowedUsers(function (followed) { return __spreadArray(__spreadArray([], followed, true), newlyFollowed, true); }); + }, + onError: function (e) { + logger.error('Failed to follow all suggested accounts during onboarding', { + safeMessage: e, + }); + toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to follow all suggested accounts, please try again"], ["Failed to follow all suggested accounts, please try again"])))), { type: 'error' }); + }, + }), followAll = _f.mutate, isFollowingAll = _f.isPending; + var canFollowAll = followableDids.length > 0 && !isFollowingAll; + // Track seen profiles - shared ref across all cards + var seenProfilesRef = useRef(new Set()); + var onProfileSeen = useCallback(function (did, position) { + if (!seenProfilesRef.current.has(did)) { + seenProfilesRef.current.add(did); + ax.metric('suggestedUser:seen', { + logContext: 'Onboarding', + recId: suggestedUsers === null || suggestedUsers === void 0 ? void 0 : suggestedUsers.recId, + position: position, + suggestedDid: did, + category: selectedInterest, + }); + } + }, [ax, selectedInterest, suggestedUsers === null || suggestedUsers === void 0 ? void 0 : suggestedUsers.recId]); + useEffect(function () { + if (error) { + logger.error('Failed to fetch suggested accounts during onboarding', { + safeMessage: error, + }); + } + }, [error]); + return (_jsxs(View, { style: [a.align_start, a.gap_sm], testID: "onboardingInterests", children: [_jsx(OnboardingPosition, {}), _jsx(OnboardingTitleText, { children: _jsx(Trans, { comment: "Accounts suggested to the user for them to follow", children: "Suggested for you" }) }), _jsxs(View, { style: [ + a.overflow_hidden, + a.mt_sm, + IS_WEB + ? [a.max_w_full, web({ minHeight: '100vh' })] + : { marginHorizontal: tokens.space.xl * -1 }, + a.flex_1, + a.justify_start, + ], children: [_jsx(TabBar, { selectedInterest: selectedInterest, onSelectInterest: setSelectedInterest, defaultTabLabel: _(msg({ + message: 'All', + comment: 'the default tab in the interests tab bar', + })), selectedInterests: state.interestsStepResults.selectedInterests }), isLoading || !moderationOpts ? (_jsx(View, { style: [ + a.flex_1, + a.mt_md, + a.align_center, + a.justify_center, + { minHeight: 400 }, + ], children: _jsx(Loader, { size: "xl" }) })) : isError ? (_jsx(View, { style: [a.flex_1, a.px_xl, a.pt_2xl], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "An error occurred while fetching suggested accounts." }) }) })) : isEmpty ? (_jsx(View, { style: [a.flex_1, a.px_xl, a.pt_2xl], children: _jsx(Admonition, { type: "apology", children: _jsx(Trans, { children: "Sorry, we're unable to load account suggestions at this time." }) }) })) : (_jsx(View, { style: [ + a.flex_1, + a.mt_md, + a.border_y, + t.atoms.border_contrast_low, + IS_WEB && [a.border_x, a.rounded_sm, a.overflow_hidden], + ], children: suggestedUsers === null || suggestedUsers === void 0 ? void 0 : suggestedUsers.actors.map(function (user, index) { return (_jsx(SuggestedProfileCard, { profile: user, moderationOpts: moderationOpts, position: index, category: selectedInterest, onSeen: onProfileSeen, recId: suggestedUsers.recId }, user.did)); }) }))] }), _jsx(OnboardingControls.Portal, { children: isError ? (_jsxs(View, { style: [a.gap_md, gtMobile ? a.flex_row : a.flex_col], children: [_jsxs(Button, { disabled: isRefetching, color: "secondary", size: "large", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Retry"], ["Retry"])))), onPress: function () { return void refetch(); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), _jsx(ButtonIcon, { icon: ArrowRotateCounterClockwiseIcon })] }), _jsx(Button, { color: "secondary", size: "large", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Skip to next step"], ["Skip to next step"])))), onPress: function () { return dispatch({ type: 'next' }); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Skip" }) }) })] })) : (_jsxs(View, { style: [a.gap_md, gtMobile ? a.flex_row : a.flex_col], children: [_jsxs(Button, { disabled: !canFollowAll, color: "secondary", size: "large", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Follow all accounts"], ["Follow all accounts"])))), onPress: function () { return followAll(); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Follow all" }) }), _jsx(ButtonIcon, { icon: isFollowingAll ? Loader : PlusIcon })] }), _jsx(Button, { disabled: isFollowingAll, color: "primary", size: "large", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Continue to next step"], ["Continue to next step"])))), onPress: function () { return dispatch({ type: 'next' }); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Continue" }) }) })] })) })] })); +} +function TabBar(_a) { + var selectedInterest = _a.selectedInterest, onSelectInterest = _a.onSelectInterest, selectedInterests = _a.selectedInterests, hideDefaultTab = _a.hideDefaultTab, defaultTabLabel = _a.defaultTabLabel; + var _ = useLingui()._; + var ax = useAnalytics(); + var interestsDisplayNames = useInterestsDisplayNames(); + var interests = Object.keys(interestsDisplayNames) + .sort(boostInterests(popularInterests)) + .sort(boostInterests(selectedInterests)); + return (_jsx(InterestTabs, { interests: hideDefaultTab ? interests : __spreadArray(['all'], interests, true), selectedInterest: selectedInterest || (hideDefaultTab ? interests[0] : 'all'), onSelectTab: function (tab) { + ax.metric('onboarding:suggestedAccounts:tabPressed', { tab: tab }); + onSelectInterest(tab === 'all' ? null : tab); + }, interestsDisplayNames: hideDefaultTab + ? interestsDisplayNames + : __assign({ all: defaultTabLabel || _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["For You"], ["For You"])))) }, interestsDisplayNames), gutterWidth: IS_WEB ? 0 : tokens.space.xl })); +} +function SuggestedProfileCard(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, position = _a.position, category = _a.category, onSeen = _a.onSeen, recId = _a.recId; + var t = useTheme(); + var ax = useAnalytics(); + var cardRef = useRef(null); + var hasTrackedRef = useRef(false); + useEffect(function () { + var node = cardRef.current; + if (!node || hasTrackedRef.current) + return; + if (IS_WEB && typeof IntersectionObserver !== 'undefined') { + var observer_1 = new IntersectionObserver(function (entries) { + var _a; + if (((_a = entries[0]) === null || _a === void 0 ? void 0 : _a.isIntersecting) && !hasTrackedRef.current) { + hasTrackedRef.current = true; + onSeen(profile.did, position); + observer_1.disconnect(); + } + }, { threshold: 0.5 }); + // @ts-ignore - web only + observer_1.observe(node); + return function () { return observer_1.disconnect(); }; + } + else { + // Native: use a short delay to account for initial layout + var timeout_1 = setTimeout(function () { + if (!hasTrackedRef.current) { + hasTrackedRef.current = true; + onSeen(profile.did, position); + } + }, 500); + return function () { return clearTimeout(timeout_1); }; + } + }, [onSeen, profile.did, position]); + return (_jsx(View, { ref: cardRef, style: [ + a.w_full, + a.py_lg, + a.px_xl, + position !== 0 && a.border_t, + t.atoms.border_contrast_low, + ], children: _jsxs(ProfileCard.Outer, { children: [_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts, disabledPreview: true }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.FollowButton, { profile: profile, moderationOpts: moderationOpts, withIcon: false, logContext: "OnboardingSuggestedAccounts", onFollow: function () { + ax.metric('suggestedUser:follow', { + logContext: 'Onboarding', + location: 'Card', + recId: recId, + position: position, + suggestedDid: profile.did, + category: category, + }); + } })] }), _jsx(ProfileCard.Description, { profile: profile, numberOfLines: 3 })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.js b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.js new file mode 100644 index 0000000000..55ae5c3846 --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.js @@ -0,0 +1,180 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { AppBskyGraphStarterpack } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { batchedUpdates } from '#/lib/batchedUpdates'; +import { isBlockedOrBlocking, isMuted } from '#/lib/moderation/blocked-and-muted'; +import { logger } from '#/logger'; +import { updateProfileShadow } from '#/state/cache/profile-shadow'; +import { getAllListMembers } from '#/state/queries/list-members'; +import { useAgent, useSession } from '#/state/session'; +import { bulkWriteFollows } from '#/screens/Onboarding/util'; +import { AvatarStack } from '#/screens/Search/components/StarterPackCard'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import * as bsky from '#/types/bsky'; +var IGNORED_ACCOUNT = 'did:plc:pifkcjimdcfwaxkanzhwxufp'; +export function StarterPackCard(_a) { + var _this = this; + var _b, _c; + var view = _a.view; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var gtPhone = useBreakpoints().gtPhone; + var agent = useAgent(); + var queryClient = useQueryClient(); + var record = view.record; + var _d = useState(false), isProcessing = _d[0], setIsProcessing = _d[1]; + var _e = useState(false), isFollowingAll = _e[0], setIsFollowingAll = _e[1]; + var onFollowAll = function () { return __awaiter(_this, void 0, void 0, function () { + var listItems, e_1, dids, followUris, e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!view.list) + return [2 /*return*/]; + setIsProcessing(true); + listItems = []; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, getAllListMembers(agent, view.list.uri)]; + case 2: + listItems = _a.sent(); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + setIsProcessing(false); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["An error occurred while trying to follow all"], ["An error occurred while trying to follow all"])))), { + type: 'error', + }); + logger.error('Failed to get list members for starter pack', { + safeMessage: e_1, + }); + return [2 /*return*/]; + case 4: + dids = listItems + .filter(function (li) { + var _a; + return li.subject.did !== IGNORED_ACCOUNT && + li.subject.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) && + !isBlockedOrBlocking(li.subject) && + !isMuted(li.subject) && + !((_a = li.subject.viewer) === null || _a === void 0 ? void 0 : _a.following); + }) + .map(function (li) { return li.subject.did; }); + _a.label = 5; + case 5: + _a.trys.push([5, 7, , 8]); + return [4 /*yield*/, bulkWriteFollows(agent, dids, { + uri: view.uri, + cid: view.cid, + })]; + case 6: + followUris = _a.sent(); + return [3 /*break*/, 8]; + case 7: + e_2 = _a.sent(); + setIsProcessing(false); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["An error occurred while trying to follow all"], ["An error occurred while trying to follow all"])))), { + type: 'error', + }); + logger.error('Failed to follow all accounts', { safeMessage: e_2 }); + return [3 /*break*/, 8]; + case 8: + setIsFollowingAll(true); + setIsProcessing(false); + batchedUpdates(function () { + for (var _i = 0, dids_1 = dids; _i < dids_1.length; _i++) { + var did = dids_1[_i]; + updateProfileShadow(queryClient, did, { + followingUri: followUris.get(did), + }); + } + }); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["All accounts have been followed!"], ["All accounts have been followed!"])))), { type: 'success' }); + ax.metric('starterPack:followAll', { + logContext: 'Onboarding', + starterPack: view.uri, + count: dids.length, + }); + return [2 /*return*/]; + } + }); + }); }; + if (!bsky.dangerousIsType(record, AppBskyGraphStarterpack.isRecord)) { + return null; + } + var profileCount = gtPhone ? 11 : 8; + var profiles = (_b = view.listItemsSample) === null || _b === void 0 ? void 0 : _b.slice(0, profileCount).map(function (item) { return item.subject; }); + return (_jsxs(View, { style: [ + a.w_full, + a.p_lg, + a.gap_md, + a.border, + a.rounded_lg, + a.overflow_hidden, + t.atoms.border_contrast_medium, + ], children: [_jsx(AvatarStack, { profiles: profiles !== null && profiles !== void 0 ? profiles : [], numPending: profileCount, total: (_c = view.list) === null || _c === void 0 ? void 0 : _c.listItemCount }), _jsxs(View, { style: [ + a.w_full, + a.flex_row, + a.align_end, + a.gap_lg, + web({ + position: 'static', + zIndex: 'unset', + }), + ], children: [_jsxs(View, { style: [a.flex_1, a.gap_2xs], children: [_jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_snug], numberOfLines: 1, children: record.name }), _jsx(Text, { emoji: true, style: [a.text_xs, t.atoms.text_contrast_medium, a.leading_snug], numberOfLines: 2, children: record.description })] }), _jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Follow all"], ["Follow all"])))), disabled: isProcessing || isFollowingAll, onPress: onFollowAll, color: "secondary", size: "small", style: [a.z_50], children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Follow all" }) }), isFollowingAll ? (_jsx(ButtonIcon, { icon: CheckIcon })) : (isProcessing && _jsx(ButtonIcon, { icon: Loader }))] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/index.js b/src/screens/Onboarding/StepSuggestedStarterpacks/index.js new file mode 100644 index 0000000000..1e8fc0c975 --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/index.js @@ -0,0 +1,42 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useOnboardingSuggestedStarterPacksQuery } from '#/state/queries/useOnboardingSuggestedStarterPacksQuery'; +import { OnboardingControls, OnboardingPosition, OnboardingTitleText, } from '#/screens/Onboarding/Layout'; +import { useOnboardingInternalState } from '#/screens/Onboarding/state'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon } from '#/components/icons/ArrowRotate'; +import { Loader } from '#/components/Loader'; +import { StarterPackCard } from './StarterPackCard'; +export function StepSuggestedStarterpacks() { + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var _a = useOnboardingInternalState(), state = _a.state, dispatch = _a.dispatch; + var _b = useOnboardingSuggestedStarterPacksQuery({ + enabled: true, + overrideInterests: state.interestsStepResults.selectedInterests, + }), suggestedStarterPacks = _b.data, isLoading = _b.isLoading, isError = _b.isError, isRefetching = _b.isRefetching, refetch = _b.refetch; + return (_jsxs(View, { style: [a.align_start, a.gap_sm], testID: "onboardingInterests", children: [_jsx(OnboardingPosition, {}), _jsx(OnboardingTitleText, { children: _jsx(Trans, { comment: "Starter packs suggested to the user for them to follow", children: "Find people to follow" }) }), _jsx(View, { style: [ + a.overflow_hidden, + a.flex_1, + a.justify_start, + a.w_full, + a.mt_sm, + ], children: isLoading || !moderationOpts ? (_jsx(View, { style: [ + a.flex_1, + a.align_center, + a.justify_center, + { minHeight: 400 }, + ], children: _jsx(Loader, { size: "xl" }) })) : isError ? (_jsx(View, { style: [a.flex_1, a.px_xl, a.pt_5xl], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "An error occurred while fetching suggested accounts." }) }) })) : (_jsx(View, { style: [a.flex_1], children: suggestedStarterPacks === null || suggestedStarterPacks === void 0 ? void 0 : suggestedStarterPacks.starterPacks.map(function (starterPack) { return (_jsx(View, { style: [a.pb_lg], children: _jsx(StarterPackCard, { view: starterPack }) }, starterPack.uri)); }) })) }), _jsx(OnboardingControls.Portal, { children: isError ? (_jsxs(View, { style: [a.gap_md, gtMobile ? a.flex_row : a.flex_col], children: [_jsxs(Button, { disabled: isRefetching, color: "secondary", size: "large", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Retry"], ["Retry"])))), onPress: function () { return refetch(); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), _jsx(ButtonIcon, { icon: ArrowRotateCounterClockwiseIcon })] }), _jsx(Button, { color: "secondary", size: "large", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Skip to next step"], ["Skip to next step"])))), onPress: function () { return dispatch({ type: 'next' }); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Skip" }) }) })] })) : (_jsx(View, { style: [a.gap_md, gtMobile ? a.flex_row : a.flex_col], children: _jsx(Button, { color: "primary", size: "large", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Continue to next step"], ["Continue to next step"])))), onPress: function () { return dispatch({ type: 'next' }); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Continue" }) }) }) })) })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Onboarding/index.js b/src/screens/Onboarding/index.js new file mode 100644 index 0000000000..5bf2024feb --- /dev/null +++ b/src/screens/Onboarding/index.js @@ -0,0 +1,44 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo, useReducer } from 'react'; +import { View } from 'react-native'; +import * as bcp47Match from 'bcp-47-match'; +import { useLanguagePrefs } from '#/state/preferences'; +import { Layout, OnboardingControls, OnboardingHeaderSlot, } from '#/screens/Onboarding/Layout'; +import { Context, createInitialOnboardingState, reducer, } from '#/screens/Onboarding/state'; +import { StepFinished } from '#/screens/Onboarding/StepFinished'; +import { StepInterests } from '#/screens/Onboarding/StepInterests'; +import { StepProfile } from '#/screens/Onboarding/StepProfile'; +import { atoms as a, useTheme } from '#/alf'; +import { useIsFindContactsFeatureEnabledBasedOnGeolocation } from '#/components/contacts/country-allowlist'; +import { useFindContactsFlowState } from '#/components/contacts/state'; +import { Portal } from '#/components/Portal'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { useAnalytics } from '#/analytics'; +import { ENV, IS_NATIVE } from '#/env'; +import { StepFindContacts } from './StepFindContacts'; +import { StepFindContactsIntro } from './StepFindContactsIntro'; +import { StepSuggestedAccounts } from './StepSuggestedAccounts'; +import { StepSuggestedStarterpacks } from './StepSuggestedStarterpacks'; +export function Onboarding() { + var t = useTheme(); + var ax = useAnalytics(); + var contentLanguages = useLanguagePrefs().contentLanguages; + var probablySpeaksEnglish = useMemo(function () { + if (contentLanguages.length === 0) + return true; + return bcp47Match.basicFilter('en', contentLanguages).length > 0; + }, [contentLanguages]); + // starter packs screen is currently geared towards english-speaking accounts + var showSuggestedStarterpacks = ENV !== 'e2e' && probablySpeaksEnglish; + var findContactsEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation(); + var showFindContacts = ENV !== 'e2e' && + IS_NATIVE && + findContactsEnabled && + !ax.features.enabled(ax.features.ImportContactsOnboardingDisable); + var _a = useReducer(reducer, { + starterPacksStepEnabled: showSuggestedStarterpacks, + findContactsStepEnabled: showFindContacts, + }, createInitialOnboardingState), state = _a[0], dispatch = _a[1]; + var _b = useFindContactsFlowState(), contactsFlowState = _b[0], contactsFlowDispatch = _b[1]; + return (_jsx(Portal, { children: _jsx(View, { style: [a.absolute, a.inset_0, t.atoms.bg], children: _jsx(OnboardingControls.Provider, { children: _jsx(OnboardingHeaderSlot.Provider, { children: _jsx(Context.Provider, { value: useMemo(function () { return ({ state: state, dispatch: dispatch }); }, [state, dispatch]), children: _jsx(ScreenTransition, { direction: state.stepTransitionDirection, style: a.flex_1, children: state.activeStep === 'find-contacts' ? (_jsx(StepFindContacts, { flowState: contactsFlowState, flowDispatch: contactsFlowDispatch })) : (_jsxs(Layout, { children: [state.activeStep === 'profile' && _jsx(StepProfile, {}), state.activeStep === 'interests' && _jsx(StepInterests, {}), state.activeStep === 'suggested-accounts' && (_jsx(StepSuggestedAccounts, {})), state.activeStep === 'suggested-starterpacks' && (_jsx(StepSuggestedStarterpacks, {})), state.activeStep === 'find-contacts-intro' && (_jsx(StepFindContactsIntro, {})), state.activeStep === 'finished' && _jsx(StepFinished, {})] })) }, state.activeStep) }) }) }) }) })); +} diff --git a/src/screens/Onboarding/state.js b/src/screens/Onboarding/state.js new file mode 100644 index 0000000000..c8f662b743 --- /dev/null +++ b/src/screens/Onboarding/state.js @@ -0,0 +1,148 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { createContext, useContext, useMemo } from 'react'; +import { logger } from '#/logger'; +export function createInitialOnboardingState(_a) { + var _b = _a === void 0 ? { starterPacksStepEnabled: true, findContactsStepEnabled: false } : _a, starterPacksStepEnabled = _b.starterPacksStepEnabled, findContactsStepEnabled = _b.findContactsStepEnabled; + var screens = { + profile: true, + interests: true, + 'suggested-accounts': true, + 'suggested-starterpacks': starterPacksStepEnabled, + 'find-contacts-intro': findContactsStepEnabled, + 'find-contacts': findContactsStepEnabled, + finished: true, + }; + return { + screens: screens, + activeStep: 'profile', + stepTransitionDirection: 'Forward', + interestsStepResults: { + selectedInterests: [], + }, + profileStepResults: { + isCreatedAvatar: false, + image: undefined, + imageUri: '', + imageMime: '', + }, + }; +} +export var Context = createContext(null); +Context.displayName = 'OnboardingContext'; +export function reducer(s, a) { + var _a; + var next = __assign({}, s); + var stepOrder = getStepOrder(s); + switch (a.type) { + case 'next': { + var nextIndex = stepOrder.indexOf(next.activeStep) + 1; + var nextStep = stepOrder[nextIndex]; + if (nextStep) { + next.activeStep = nextStep; + } + next.stepTransitionDirection = 'Forward'; + break; + } + case 'prev': { + var prevIndex = stepOrder.indexOf(next.activeStep) - 1; + var prevStep = stepOrder[prevIndex]; + if (prevStep) { + next.activeStep = prevStep; + } + next.stepTransitionDirection = 'Backward'; + break; + } + case 'skip-contacts': { + var nextIndex = stepOrder.indexOf('find-contacts') + 1; + var nextStep = (_a = stepOrder[nextIndex]) !== null && _a !== void 0 ? _a : 'finished'; + next.activeStep = nextStep; + next.stepTransitionDirection = 'Forward'; + break; + } + case 'finish': { + next = createInitialOnboardingState({ + starterPacksStepEnabled: s.screens['suggested-starterpacks'], + findContactsStepEnabled: s.screens['find-contacts'], + }); + break; + } + case 'setInterestsStepResults': { + next.interestsStepResults = { + selectedInterests: a.selectedInterests, + }; + break; + } + case 'setProfileStepResults': { + next.profileStepResults = { + isCreatedAvatar: a.isCreatedAvatar, + image: a.image, + imageUri: a.imageUri, + imageMime: a.imageMime, + creatorState: a.creatorState, + }; + break; + } + } + var state = __assign(__assign({}, next), { hasPrev: next.activeStep !== 'profile' }); + logger.debug("onboarding", { + hasPrev: state.hasPrev, + activeStep: state.activeStep, + interestsStepResults: { + selectedInterests: state.interestsStepResults.selectedInterests, + }, + profileStepResults: state.profileStepResults, + }); + if (s.activeStep !== state.activeStep) { + logger.debug("onboarding: step changed", { activeStep: state.activeStep }); + } + return state; +} +function getStepOrder(s) { + return [ + s.screens.profile && 'profile', + s.screens.interests && 'interests', + s.screens['suggested-accounts'] && 'suggested-accounts', + s.screens['suggested-starterpacks'] && 'suggested-starterpacks', + s.screens['find-contacts-intro'] && 'find-contacts-intro', + s.screens['find-contacts'] && 'find-contacts', + s.screens.finished && 'finished', + ].filter(function (x) { return !!x; }); +} +/** + * Note: not to be confused with `useOnboardingState`, which just determines if onboarding is active. + * This hook is for internal state of the onboarding flow (i.e. active step etc). + * + * This adds additional derived state to the onboarding context reducer. + */ +export function useOnboardingInternalState() { + var ctx = useContext(Context); + if (!ctx) { + throw new Error('useOnboardingInternalState must be used within OnboardingContext'); + } + var state = ctx.state, dispatch = ctx.dispatch; + return { + state: useMemo(function () { + var stepOrder = getStepOrder(state).filter(function (x) { return x !== 'find-contacts' && x !== 'finished'; }); + var canGoBack = state.activeStep !== stepOrder[0]; + return __assign(__assign({}, state), { canGoBack: canGoBack, + /** + * Note: for *display* purposes only, do not lean on this + * for navigation purposes! we merge certain steps! + */ + activeStepIndex: stepOrder.indexOf(state.activeStep === 'find-contacts' + ? 'find-contacts-intro' + : state.activeStep), totalSteps: stepOrder.length }); + }, [state]), + dispatch: dispatch, + }; +} diff --git a/src/screens/Onboarding/util.js b/src/screens/Onboarding/util.js new file mode 100644 index 0000000000..b479f4aaba --- /dev/null +++ b/src/screens/Onboarding/util.js @@ -0,0 +1,111 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { TID } from '@atproto/common-web'; +import chunk from 'lodash.chunk'; +import { until } from '#/lib/async/until'; +export function bulkWriteFollows(agent, dids, via) { + return __awaiter(this, void 0, void 0, function () { + var session, followRecords, followWrites, chunks, _i, chunks_1, chunk_1, followUris, _a, followWrites_1, r; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + session = agent.session; + if (!session) { + throw new Error("bulkWriteFollows failed: no session"); + } + followRecords = dids.map(function (did) { + return { + $type: 'app.bsky.graph.follow', + subject: did, + createdAt: new Date().toISOString(), + via: via, + }; + }); + followWrites = followRecords.map(function (r) { return ({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.follow', + rkey: TID.nextStr(), + value: r, + }); }); + chunks = chunk(followWrites, 50); + _i = 0, chunks_1 = chunks; + _b.label = 1; + case 1: + if (!(_i < chunks_1.length)) return [3 /*break*/, 4]; + chunk_1 = chunks_1[_i]; + return [4 /*yield*/, agent.com.atproto.repo.applyWrites({ + repo: session.did, + writes: chunk_1, + })]; + case 2: + _b.sent(); + _b.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: return [4 /*yield*/, whenFollowsIndexed(agent, session.did, function (res) { return !!res.data.follows.length; })]; + case 5: + _b.sent(); + followUris = new Map(); + for (_a = 0, followWrites_1 = followWrites; _a < followWrites_1.length; _a++) { + r = followWrites_1[_a]; + followUris.set(r.value.subject, "at://".concat(session.did, "/app.bsky.graph.follow/").concat(r.rkey)); + } + return [2 /*return*/, followUris]; + } + }); + }); +} +function whenFollowsIndexed(agent, actor, fn) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, until(5, // 5 tries + 1e3, // 1s delay between tries + fn, function () { + return agent.app.bsky.graph.getFollows({ + actor: actor, + limit: 1, + }); + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} diff --git a/src/screens/Post/PostLikedBy.js b/src/screens/Post/PostLikedBy.js new file mode 100644 index 0000000000..05f94eb35b --- /dev/null +++ b/src/screens/Post/PostLikedBy.js @@ -0,0 +1,24 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { makeRecordUri } from '#/lib/strings/url-helpers'; +import { usePostQuery } from '#/state/queries/post'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { PostLikedBy as PostLikedByComponent } from '#/view/com/post-thread/PostLikedBy'; +import * as Layout from '#/components/Layout'; +export var PostLikedByScreen = function (_a) { + var route = _a.route; + var setMinimalShellMode = useSetMinimalShellMode(); + var _b = route.params, name = _b.name, rkey = _b.rkey; + var uri = makeRecordUri(name, 'app.bsky.feed.post', rkey); + var post = usePostQuery(uri).data; + var likeCount; + if (post) { + likeCount = post.likeCount; + } + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: post && (_jsxs(_Fragment, { children: [_jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Liked By" }) }), _jsx(Layout.Header.SubtitleText, { children: _jsx(Plural, { value: likeCount !== null && likeCount !== void 0 ? likeCount : 0, one: "# like", other: "# likes" }) })] })) }), _jsx(Layout.Header.Slot, {})] }), _jsx(PostLikedByComponent, { uri: uri })] })); +}; diff --git a/src/screens/Post/PostQuotes.js b/src/screens/Post/PostQuotes.js new file mode 100644 index 0000000000..532a76844b --- /dev/null +++ b/src/screens/Post/PostQuotes.js @@ -0,0 +1,24 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { makeRecordUri } from '#/lib/strings/url-helpers'; +import { usePostQuery } from '#/state/queries/post'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { PostQuotes as PostQuotesComponent } from '#/view/com/post-thread/PostQuotes'; +import * as Layout from '#/components/Layout'; +export var PostQuotesScreen = function (_a) { + var route = _a.route; + var setMinimalShellMode = useSetMinimalShellMode(); + var _b = route.params, name = _b.name, rkey = _b.rkey; + var uri = makeRecordUri(name, 'app.bsky.feed.post', rkey); + var post = usePostQuery(uri).data; + var quoteCount; + if (post) { + quoteCount = post.quoteCount; + } + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: post && (_jsxs(_Fragment, { children: [_jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Quotes" }) }), _jsx(Layout.Header.SubtitleText, { children: _jsx(Plural, { value: quoteCount !== null && quoteCount !== void 0 ? quoteCount : 0, one: "# quote", other: "# quotes" }) })] })) }), _jsx(Layout.Header.Slot, {})] }), _jsx(PostQuotesComponent, { uri: uri })] })); +}; diff --git a/src/screens/Post/PostRepostedBy.js b/src/screens/Post/PostRepostedBy.js new file mode 100644 index 0000000000..1fc70e89ec --- /dev/null +++ b/src/screens/Post/PostRepostedBy.js @@ -0,0 +1,24 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { makeRecordUri } from '#/lib/strings/url-helpers'; +import { usePostQuery } from '#/state/queries/post'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { PostRepostedBy as PostRepostedByComponent } from '#/view/com/post-thread/PostRepostedBy'; +import * as Layout from '#/components/Layout'; +export var PostRepostedByScreen = function (_a) { + var route = _a.route; + var _b = route.params, name = _b.name, rkey = _b.rkey; + var uri = makeRecordUri(name, 'app.bsky.feed.post', rkey); + var setMinimalShellMode = useSetMinimalShellMode(); + var post = usePostQuery(uri).data; + var quoteCount; + if (post) { + quoteCount = post.repostCount; + } + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: post && (_jsxs(_Fragment, { children: [_jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Reposted By" }) }), _jsx(Layout.Header.SubtitleText, { children: _jsx(Plural, { value: quoteCount !== null && quoteCount !== void 0 ? quoteCount : 0, one: "# repost", other: "# reposts" }) })] })) }), _jsx(Layout.Header.Slot, {})] }), _jsx(PostRepostedByComponent, { uri: uri })] })); +}; diff --git a/src/screens/PostThread/components/GrowthHack.js b/src/screens/PostThread/components/GrowthHack.js new file mode 100644 index 0000000000..e5744837c1 --- /dev/null +++ b/src/screens/PostThread/components/GrowthHack.js @@ -0,0 +1,43 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { PrivacySensitive } from 'expo-privacy-sensitive'; +import { useAppState } from '#/lib/appState'; +import { atoms as a, useTheme } from '#/alf'; +import { sizes as iconSizes } from '#/components/icons/common'; +import { Mark as Logo } from '#/components/icons/Logo'; +import { IS_IOS } from '#/env'; +var ICON_SIZE = 'xl'; +export function GrowthHack(_a) { + var children = _a.children, _b = _a.align, align = _b === void 0 ? 'right' : _b; + var t = useTheme(); + // the button has a variable width and is absolutely positioned, so we need to manually + // set the minimum width of the underlying button + var _c = useState(undefined), width = _c[0], setWidth = _c[1]; + var appState = useAppState(); + if (!IS_IOS || appState !== 'active') + return children; + return (_jsxs(View, { style: [ + a.relative, + a.justify_center, + align === 'right' ? a.align_end : a.align_start, + { minWidth: width !== null && width !== void 0 ? width : iconSizes[ICON_SIZE] }, + ], children: [_jsx(PrivacySensitive, { style: [ + a.absolute, + a.z_10, + a.flex_col, + align === 'right' + ? [a.right_0, a.align_end] + : [a.left_0, a.align_start], + // when finding the size of the button, we need the containing + // element to have a concrete size otherwise the text will + // collapse to 0 width. so set it to a really big number + // and just use `pointer-events: box-none` so it doesn't interfere with the UI + { width: 1000 }, + a.pointer_events_box_none, + ], children: _jsx(View, { onLayout: function (evt) { return setWidth(evt.nativeEvent.layout.width); }, style: [ + t.atoms.bg, + // make sure it covers the icon! children might be undefined + { minWidth: iconSizes[ICON_SIZE], minHeight: iconSizes[ICON_SIZE] }, + ], children: children }) }), _jsx(Logo, { size: ICON_SIZE })] })); +} diff --git a/src/screens/PostThread/components/HeaderDropdown.js b/src/screens/PostThread/components/HeaderDropdown.js new file mode 100644 index 0000000000..c85a4f7768 --- /dev/null +++ b/src/screens/PostThread/components/HeaderDropdown.js @@ -0,0 +1,58 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { HITSLOP_10 } from '#/lib/constants'; +import { Button, ButtonIcon } from '#/components/Button'; +import { SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider } from '#/components/icons/SettingsSlider'; +import * as Menu from '#/components/Menu'; +import { useAnalytics } from '#/analytics'; +export function HeaderDropdown(_a) { + var sort = _a.sort, view = _a.view, setSort = _a.setSort, setView = _a.setView; + var ax = useAnalytics(); + var _ = useLingui()._; + return (_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Thread options"], ["Thread options"])))), children: function (_a) { + var _b = _a.props, onPress = _b.onPress, props = __rest(_b, ["onPress"]); + return (_jsx(Button, __assign({ label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Thread options"], ["Thread options"])))), size: "small", variant: "ghost", color: "secondary", shape: "round", hitSlop: HITSLOP_10, onPress: function () { + ax.metric('thread:click:headerMenuOpen', {}); + onPress(); + } }, props, { children: _jsx(ButtonIcon, { icon: SettingsSlider, size: "md" }) }))); + } }), _jsxs(Menu.Outer, { children: [_jsx(Menu.LabelText, { children: _jsx(Trans, { children: "Show replies as" }) }), _jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Linear"], ["Linear"])))), onPress: function () { + setView('linear'); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Linear" }) }), _jsx(Menu.ItemRadio, { selected: view === 'linear' })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Threaded"], ["Threaded"])))), onPress: function () { + setView('tree'); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Threaded" }) }), _jsx(Menu.ItemRadio, { selected: view === 'tree' })] })] }), _jsx(Menu.Divider, {}), _jsx(Menu.LabelText, { children: _jsx(Trans, { children: "Reply sorting" }) }), _jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Top replies first"], ["Top replies first"])))), onPress: function () { + setSort('top'); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Top replies first" }) }), _jsx(Menu.ItemRadio, { selected: sort === 'top' })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Oldest replies first"], ["Oldest replies first"])))), onPress: function () { + setSort('oldest'); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Oldest replies first" }) }), _jsx(Menu.ItemRadio, { selected: sort === 'oldest' })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Newest replies first"], ["Newest replies first"])))), onPress: function () { + setSort('newest'); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Newest replies first" }) }), _jsx(Menu.ItemRadio, { selected: sort === 'newest' })] })] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/screens/PostThread/components/ThreadComposePrompt.js b/src/screens/PostThread/components/ThreadComposePrompt.js new file mode 100644 index 0000000000..46e594b242 --- /dev/null +++ b/src/screens/PostThread/components/ThreadComposePrompt.js @@ -0,0 +1,58 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { PressableScale } from '#/lib/custom-animations/PressableScale'; +import { useHaptics } from '#/lib/haptics'; +import { useHideBottomBarBorderForScreen } from '#/lib/hooks/useHideBottomBarBorder'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, ios, native, useBreakpoints, useTheme } from '#/alf'; +import { transparentifyColor } from '#/alf/util/colorGeneration'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Text } from '#/components/Typography'; +export function ThreadComposePrompt(_a) { + var _b; + var onPressCompose = _a.onPressCompose, style = _a.style; + var currentAccount = useSession().currentAccount; + var profile = useProfileQuery({ did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }).data; + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var t = useTheme(); + var playHaptic = useHaptics(); + var _c = useInteractionState(), hovered = _c.state, onHoverIn = _c.onIn, onHoverOut = _c.onOut; + useHideBottomBarBorderForScreen(); + return (_jsxs(View, { style: [ + a.px_sm, + gtMobile + ? [a.py_xs, a.border_t, t.atoms.border_contrast_low, t.atoms.bg] + : [a.pb_2xs], + style, + ], children: [!gtMobile && (_jsx(LinearGradient, { start: [0.5, 0], end: [0.5, 1], colors: [ + transparentifyColor(t.atoms.bg.backgroundColor, 0), + t.atoms.bg.backgroundColor, + ], locations: [0.15, 0.4], style: [a.absolute, a.inset_0] }, t.name)), _jsxs(PressableScale, { accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Compose reply"], ["Compose reply"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Opens composer"], ["Opens composer"])))), onPress: function () { + onPressCompose(); + playHaptic('Light'); + }, onLongPress: ios(function () { + onPressCompose(); + playHaptic('Heavy'); + }), onHoverIn: onHoverIn, onHoverOut: onHoverOut, style: [ + a.flex_row, + a.align_center, + a.p_sm, + a.gap_sm, + a.rounded_full, + (!gtMobile || hovered) && t.atoms.bg_contrast_25, + native([a.border, t.atoms.border_contrast_low]), + a.transition_color, + ], children: [_jsx(UserAvatar, { size: 24, avatar: profile === null || profile === void 0 ? void 0 : profile.avatar, type: ((_b = profile === null || profile === void 0 ? void 0 : profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user' }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Write your reply" }) })] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/PostThread/components/ThreadError.js b/src/screens/PostThread/components/ThreadError.js new file mode 100644 index 0000000000..181b74f73f --- /dev/null +++ b/src/screens/PostThread/components/ThreadError.js @@ -0,0 +1,59 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { OUTER_SPACE } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon } from '#/components/icons/ArrowRotate'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +export function ThreadError(_a) { + var error = _a.error, onRetry = _a.onRetry; + var t = useTheme(); + var _ = useLingui()._; + var cleanError = useCleanError(); + var _b = useMemo(function () { + var title = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Error loading post"], ["Error loading post"])))); + var message = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Something went wrong. Please try again in a moment."], ["Something went wrong. Please try again in a moment."])))); + var _a = cleanError(error), raw = _a.raw, clean = _a.clean; + if (error.message.startsWith('Post not found')) { + title = _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Post not found"], ["Post not found"])))); + message = clean || raw || message; + } + return { title: title, message: message }; + }, [_, error, cleanError]), title = _b.title, message = _b.message; + return (_jsx(Layout.Center, { children: _jsx(View, { style: [ + a.w_full, + a.align_center, + { + padding: OUTER_SPACE, + paddingTop: OUTER_SPACE * 2, + }, + ], children: _jsxs(View, { style: [ + a.w_full, + a.align_center, + a.gap_xl, + { + maxWidth: 260, + }, + ], children: [_jsxs(View, { style: [a.gap_xs], children: [_jsx(Text, { style: [ + a.text_center, + a.text_lg, + a.font_semi_bold, + a.leading_snug, + ], children: title }), _jsx(Text, { style: [ + a.text_center, + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: message })] }), _jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Retry"], ["Retry"])))), size: "small", variant: "solid", color: "secondary_inverted", onPress: onRetry, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), _jsx(ButtonIcon, { icon: RetryIcon, position: "right" })] })] }) }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/PostThread/components/ThreadItemAnchor.js b/src/screens/PostThread/components/ThreadItemAnchor.js new file mode 100644 index 0000000000..9f5aa8a89c --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchor.js @@ -0,0 +1,360 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useCallback, useMemo } from 'react'; +import { Text as RNText, View } from 'react-native'; +import { AppBskyFeedDefs, AppBskyFeedPost, AtUri, RichText as RichTextAPI, } from '@atproto/api'; +import { Plural, Trans, useLingui } from '@lingui/react/macro'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { makeProfileLink } from '#/lib/routes/links'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { niceDate } from '#/lib/strings/time'; +import { getPostLanguage, getTranslatorLink, isPostInLanguage, } from '#/locale/helpers'; +import { POST_TOMBSTONE, usePostShadow, } from '#/state/cache/post-shadow'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { FeedFeedbackProvider, useFeedFeedback } from '#/state/feed-feedback'; +import { useLanguagePrefs } from '#/state/preferences'; +import { useSession } from '#/state/session'; +import { useMergedThreadgateHiddenReplies } from '#/state/threadgate-hidden-replies'; +import { PreviewableUserAvatar } from '#/view/com/util/UserAvatar'; +import { ThreadItemAnchorFollowButton } from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'; +import { LINEAR_AVI_WIDTH, OUTER_SPACE, REPLY_LINE_WIDTH, } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { DebugFieldDisplay } from '#/components/DebugFieldDisplay'; +import { CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon } from '#/components/icons/CalendarClock'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import { InlineLinkText, Link } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { ContentHider } from '#/components/moderation/ContentHider'; +import { LabelsOnMyPost } from '#/components/moderation/LabelsOnMe'; +import { PostAlerts } from '#/components/moderation/PostAlerts'; +import { Embed, PostEmbedViewContext } from '#/components/Post/Embed'; +import { TranslatedPost } from '#/components/Post/Translated'; +import { PostControls, PostControlsSkeleton } from '#/components/PostControls'; +import { useFormatPostStatCount } from '#/components/PostControls/util'; +import { ProfileHoverCard } from '#/components/ProfileHoverCard'; +import * as Prompt from '#/components/Prompt'; +import { RichText } from '#/components/RichText'; +import * as Skele from '#/components/Skeleton'; +import { Text } from '#/components/Typography'; +import { VerificationCheckButton } from '#/components/verification/VerificationCheckButton'; +import { WhoCanReply } from '#/components/WhoCanReply'; +import { useAnalytics } from '#/analytics'; +import { useActorStatus } from '#/features/liveNow'; +import { Provider as TranslateOnDeviceProvider, useTranslateOnDevice, } from '#/translation'; +import * as bsky from '#/types/bsky'; +export function ThreadItemAnchor(_a) { + var _b, _c; + var item = _a.item, onPostSuccess = _a.onPostSuccess, threadgateRecord = _a.threadgateRecord, postSource = _a.postSource; + var postShadow = usePostShadow(item.value.post); + var threadRootUri = ((_c = (_b = item.value.post.record.reply) === null || _b === void 0 ? void 0 : _b.root) === null || _c === void 0 ? void 0 : _c.uri) || item.uri; + var isRoot = threadRootUri === item.uri; + if (postShadow === POST_TOMBSTONE) { + return _jsx(ThreadItemAnchorDeleted, { isRoot: isRoot }); + } + return (_jsx(TranslateOnDeviceProvider, { children: _jsx(ThreadItemAnchorInner + // Safeguard from clobbering per-post state below: + , { item: item, isRoot: isRoot, postShadow: postShadow, onPostSuccess: onPostSuccess, threadgateRecord: threadgateRecord, postSource: postSource }, postShadow.uri) })); +} +function ThreadItemAnchorDeleted(_a) { + var isRoot = _a.isRoot; + var t = useTheme(); + return (_jsxs(_Fragment, { children: [_jsx(ThreadItemAnchorParentReplyLine, { isRoot: isRoot }), _jsx(View, { style: [ + { + paddingHorizontal: OUTER_SPACE, + paddingBottom: OUTER_SPACE, + }, + isRoot && [a.pt_lg], + ], children: _jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.py_md, + a.rounded_sm, + t.atoms.bg_contrast_25, + ], children: [_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.justify_center, + { + width: LINEAR_AVI_WIDTH, + }, + ], children: _jsx(TrashIcon, { style: [t.atoms.text_contrast_medium] }) }), _jsx(Text, { style: [a.text_md, a.font_semi_bold, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Post has been deleted" }) })] }) })] })); +} +function ThreadItemAnchorParentReplyLine(_a) { + var isRoot = _a.isRoot; + var t = useTheme(); + return !isRoot ? (_jsx(View, { style: [a.pl_lg, a.flex_row, a.pb_xs, { height: a.pt_lg.paddingTop }], children: _jsx(View, { style: { width: 42 }, children: _jsx(View, { style: [ + { + width: REPLY_LINE_WIDTH, + marginLeft: 'auto', + marginRight: 'auto', + flexGrow: 1, + backgroundColor: t.atoms.border_contrast_low.borderColor, + }, + ] }) }) })) : null; +} +var ThreadItemAnchorInner = memo(function ThreadItemAnchorInner(_a) { + var _b, _c, _d, _e, _f, _g, _h; + var item = _a.item, isRoot = _a.isRoot, postShadow = _a.postShadow, onPostSuccess = _a.onPostSuccess, threadgateRecord = _a.threadgateRecord, postSource = _a.postSource; + var t = useTheme(); + var ax = useAnalytics(); + var l = useLingui().t; + var openComposer = useOpenComposer().openComposer; + var _j = useSession(), currentAccount = _j.currentAccount, hasSession = _j.hasSession; + var feedFeedback = useFeedFeedback(postSource === null || postSource === void 0 ? void 0 : postSource.feedSourceInfo, hasSession); + var formatPostStatCount = useFormatPostStatCount(); + var post = postShadow; + var record = item.value.post.record; + var moderation = item.moderation; + var authorShadow = useProfileShadow(post.author); + var live = useActorStatus(post.author).isActive; + var richText = useMemo(function () { + return new RichTextAPI({ + text: record.text, + facets: record.facets, + }); + }, [record]); + var threadRootUri = ((_c = (_b = record.reply) === null || _b === void 0 ? void 0 : _b.root) === null || _c === void 0 ? void 0 : _c.uri) || post.uri; + var authorHref = makeProfileLink(post.author); + var isThreadAuthor = getThreadAuthor(post, record) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var likesHref = useMemo(function () { + var urip = new AtUri(post.uri); + return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by'); + }, [post.uri, post.author]); + var repostsHref = useMemo(function () { + var urip = new AtUri(post.uri); + return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by'); + }, [post.uri, post.author]); + var quotesHref = useMemo(function () { + var urip = new AtUri(post.uri); + return makeProfileLink(post.author, 'post', urip.rkey, 'quotes'); + }, [post.uri, post.author]); + var threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ + threadgateRecord: threadgateRecord, + }); + var additionalPostAlerts = useMemo(function () { + var isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri); + var isControlledByViewer = new AtUri(threadRootUri).host === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + return isControlledByViewer && isPostHiddenByThreadgate + ? [ + { + type: 'reply-hidden', + source: { type: 'user', did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }, + priority: 6, + }, + ] + : []; + }, [post, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, threadgateHiddenReplies, threadRootUri]); + var onlyFollowersCanReply = !!((_d = threadgateRecord === null || threadgateRecord === void 0 ? void 0 : threadgateRecord.allow) === null || _d === void 0 ? void 0 : _d.find(function (rule) { return rule.$type === 'app.bsky.feed.threadgate#followerRule'; })); + var showFollowButton = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) !== post.author.did && !onlyFollowersCanReply; + var viaRepost = useMemo(function () { + var reason = postSource === null || postSource === void 0 ? void 0 : postSource.post.reason; + if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { + return { + uri: reason.uri, + cid: reason.cid, + }; + } + }, [postSource]); + var onPressReply = useCallback(function () { + openComposer({ + replyTo: { + uri: post.uri, + cid: post.cid, + text: record.text, + author: post.author, + embed: post.embed, + moderation: moderation, + langs: record.langs, + }, + onPostSuccess: onPostSuccess, + logContext: 'PostReply', + }); + if (postSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: postSource.post.feedContext, + reqId: postSource.post.reqId, + }); + } + }, [ + openComposer, + post, + record, + onPostSuccess, + moderation, + postSource, + feedFeedback, + ]); + var onOpenAuthor = function () { + ax.metric('post:clickthroughAuthor', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }); + if (postSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#clickthroughAuthor', + feedContext: postSource.post.feedContext, + reqId: postSource.post.reqId, + }); + } + }; + var onOpenEmbed = function () { + ax.metric('post:clickthroughEmbed', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }); + if (postSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#clickthroughEmbed', + feedContext: postSource.post.feedContext, + reqId: postSource.post.reqId, + }); + } + }; + return (_jsxs(_Fragment, { children: [_jsx(ThreadItemAnchorParentReplyLine, { isRoot: isRoot }), _jsxs(View, { testID: "postThreadItem-by-".concat(post.author.handle), style: [ + { + paddingHorizontal: OUTER_SPACE, + }, + isRoot && [a.pt_lg], + ], children: [_jsxs(View, { style: [a.flex_row, a.gap_md, a.pb_md], children: [_jsx(View, { collapsable: false, children: _jsx(PreviewableUserAvatar, { size: 42, profile: post.author, moderation: moderation.ui('avatar'), type: ((_e = post.author.associated) === null || _e === void 0 ? void 0 : _e.labeler) ? 'labeler' : 'user', live: live, onBeforePress: onOpenAuthor }) }), _jsx(Link, { to: authorHref, style: [a.flex_1], label: sanitizeDisplayName(post.author.displayName || sanitizeHandle(post.author.handle), moderation.ui('displayName')), onPress: onOpenAuthor, children: _jsx(View, { style: [a.flex_1, a.align_start], children: _jsxs(ProfileHoverCard, { did: post.author.did, style: [a.w_full], children: [_jsxs(View, { style: [a.flex_row, a.align_center], children: [_jsx(Text, { emoji: true, style: [ + a.flex_shrink, + a.text_lg, + a.font_semi_bold, + a.leading_snug, + ], numberOfLines: 1, children: sanitizeDisplayName(post.author.displayName || + sanitizeHandle(post.author.handle), moderation.ui('displayName')) }), _jsx(View, { style: [a.pl_xs], children: _jsx(VerificationCheckButton, { profile: authorShadow, size: "md" }) })] }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: sanitizeHandle(post.author.handle, '@') })] }) }) }), _jsx(View, { collapsable: false, style: [a.self_center], children: _jsx(ThreadItemAnchorFollowButton, { did: post.author.did, enabled: showFollowButton }) })] }), _jsxs(View, { style: [a.pb_sm], children: [_jsx(LabelsOnMyPost, { post: post, style: [a.pb_sm] }), _jsxs(ContentHider, { modui: moderation.ui('contentView'), ignoreMute: true, childContainerStyle: [a.pt_sm], children: [_jsx(PostAlerts, { modui: moderation.ui('contentView'), size: "lg", includeMute: true, style: [a.pb_sm], additionalCauses: additionalPostAlerts }), (richText === null || richText === void 0 ? void 0 : richText.text) ? (_jsx(RichText, { enableTags: true, selectable: true, value: richText, style: [a.flex_1, a.text_lg], authorHandle: post.author.handle, shouldProxyLinks: true })) : undefined, _jsx(TranslatedPost, { postText: record.text, hideLoading: true }), _jsx(TranslateLink, { post: item.value.post }), post.embed && (_jsx(View, { style: [a.py_xs], children: _jsx(Embed, { embed: post.embed, moderation: moderation, viewContext: PostEmbedViewContext.ThreadHighlighted, onOpen: onOpenEmbed }) }))] }), _jsx(ExpandedPostDetails, { post: item.value.post, isThreadAuthor: isThreadAuthor }), post.repostCount !== 0 || + post.likeCount !== 0 || + post.quoteCount !== 0 || + post.bookmarkCount !== 0 ? ( + // Show this section unless we're *sure* it has no engagement. + _jsxs(View, { style: [ + a.flex_row, + a.flex_wrap, + a.align_center, + { + rowGap: a.gap_sm.gap, + columnGap: a.gap_lg.gap, + }, + a.border_t, + a.border_b, + a.mt_md, + a.py_md, + t.atoms.border_contrast_low, + ], children: [post.repostCount != null && post.repostCount !== 0 ? (_jsx(Link, { to: repostsHref, label: l(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Reposts of this post"], ["Reposts of this post"]))), children: _jsx(Text, { testID: "repostCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], children: _jsxs(Trans, { comment: "Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0)", children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold, t.atoms.text], children: formatPostStatCount(post.repostCount) }), ' ', _jsx(Plural, { value: post.repostCount, one: "repost", other: "reposts" })] }) }) })) : null, post.quoteCount != null && + post.quoteCount !== 0 && + !((_f = post.viewer) === null || _f === void 0 ? void 0 : _f.embeddingDisabled) ? (_jsx(Link, { to: quotesHref, label: l(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Quotes of this post"], ["Quotes of this post"]))), children: _jsx(Text, { testID: "quoteCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], children: _jsxs(Trans, { comment: "Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0)", children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold, t.atoms.text], children: formatPostStatCount(post.quoteCount) }), ' ', _jsx(Plural, { value: post.quoteCount, one: "quote", other: "quotes" })] }) }) })) : null, post.likeCount != null && post.likeCount !== 0 ? (_jsx(Link, { to: likesHref, label: l(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Likes on this post"], ["Likes on this post"]))), children: _jsx(Text, { testID: "likeCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], children: _jsxs(Trans, { comment: "Like count display, the <0> tags enclose the number of likes in bold (will never be 0)", children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold, t.atoms.text], children: formatPostStatCount(post.likeCount) }), ' ', _jsx(Plural, { value: post.likeCount, one: "like", other: "likes" })] }) }) })) : null, post.bookmarkCount != null && post.bookmarkCount !== 0 ? (_jsx(Text, { testID: "bookmarkCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], children: _jsxs(Trans, { comment: "Save count display, the <0> tags enclose the number of saves in bold (will never be 0)", children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold, t.atoms.text], children: formatPostStatCount(post.bookmarkCount) }), ' ', _jsx(Plural, { value: post.bookmarkCount, one: "save", other: "saves" })] }) })) : null] })) : null, _jsx(View, { style: [ + a.pt_sm, + a.pb_2xs, + { + marginLeft: -5, + }, + ], children: _jsx(FeedFeedbackProvider, { value: feedFeedback, children: _jsx(PostControls, { big: true, post: postShadow, record: record, richText: richText, onPressReply: onPressReply, logContext: "PostThreadItem", threadgateRecord: threadgateRecord, feedContext: (_g = postSource === null || postSource === void 0 ? void 0 : postSource.post) === null || _g === void 0 ? void 0 : _g.feedContext, reqId: (_h = postSource === null || postSource === void 0 ? void 0 : postSource.post) === null || _h === void 0 ? void 0 : _h.reqId, viaRepost: viaRepost }) }) }), _jsx(DebugFieldDisplay, { subject: post })] })] })] })); +}); +function TranslateLink(_a) { + var post = _a.post; + var t = useTheme(); + var ax = useAnalytics(); + var l = useLingui().t; + var langPrefs = useLanguagePrefs(); + var _b = useTranslateOnDevice(), translate = _b.translate, clearTranslation = _b.clearTranslation, translationState = _b.translationState; + var needsTranslation = useMemo(function () { + return Boolean(langPrefs.primaryLanguage && + !isPostInLanguage(post, [langPrefs.primaryLanguage])); + }, [post, langPrefs.primaryLanguage]); + var sourceLanguage = getPostLanguage(post); + var onTranslatePress = useCallback(function (e) { + var _a; + e.preventDefault(); + void translate(post.record.text || '', langPrefs.primaryLanguage, sourceLanguage); + if (bsky.dangerousIsType(post.record, AppBskyFeedPost.isRecord)) { + ax.metric('translate', { + sourceLanguages: (_a = post.record.langs) !== null && _a !== void 0 ? _a : [], + targetLanguage: langPrefs.primaryLanguage, + textLength: post.record.text.length, + }); + } + return false; + }, [ax, sourceLanguage, translate, langPrefs, post]); + var onHideTranslation = useCallback(function (e) { + e.preventDefault(); + clearTranslation(); + return false; + }, [clearTranslation]); + return (needsTranslation && (_jsx(View, { style: [a.gap_md, a.pt_md, a.align_start], children: translationState.status === 'loading' ? (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(Loader, { size: "xs" }), _jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Translating\u2026" }) })] })) : translationState.status === 'success' ? (_jsx(InlineLinkText, { to: "#", label: l(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Hide translation"], ["Hide translation"]))), style: [a.text_sm], onPress: onHideTranslation, children: _jsx(Trans, { children: "Hide translation" }) })) : (_jsx(InlineLinkText, { to: getTranslatorLink(post.record.text, langPrefs.primaryLanguage), label: l(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Translate"], ["Translate"]))), style: [a.text_sm], onPress: onTranslatePress, children: _jsx(Trans, { children: "Translate" }) })) }))); +} +function ExpandedPostDetails(_a) { + var post = _a.post, isThreadAuthor = _a.isThreadAuthor; + var t = useTheme(); + var i18n = useLingui().i18n; + var isRootPost = !('reply' in post.record); + return (_jsxs(View, { style: [a.gap_md, a.pt_md, a.align_start], children: [_jsx(BackdatedPostIndicator, { post: post }), _jsxs(View, { style: [a.flex_row, a.align_center, a.flex_wrap, a.gap_sm], children: [_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium], children: niceDate(i18n, post.indexedAt, 'dot separated') }), isRootPost && (_jsx(WhoCanReply, { post: post, isThreadAuthor: isThreadAuthor }))] })] })); +} +function BackdatedPostIndicator(_a) { + var post = _a.post; + var t = useTheme(); + var _b = useLingui(), l = _b.t, i18n = _b.i18n; + var control = Prompt.usePromptControl(); + var indexedAt = new Date(post.indexedAt); + var createdAt = bsky.dangerousIsType(post.record, AppBskyFeedPost.isRecord) + ? new Date(post.record.createdAt) + : new Date(post.indexedAt); + // backdated if createdAt is 24 hours or more before indexedAt + var isBackdated = indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000; + if (!isBackdated) + return null; + return (_jsxs(_Fragment, { children: [_jsx(Button, { label: l(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Archived post"], ["Archived post"]))), accessibilityHint: l(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Shows information about when this post was created"], ["Shows information about when this post was created"]))), onPress: function (e) { + e.preventDefault(); + e.stopPropagation(); + control.open(); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.rounded_full, + t.atoms.bg_contrast_25, + (hovered || pressed) && t.atoms.bg_contrast_50, + { + gap: 3, + paddingHorizontal: 6, + paddingVertical: 3, + }, + ], children: [_jsx(CalendarClockIcon, { fill: t.palette.yellow, size: "sm", "aria-hidden": true }), _jsx(Text, { style: [ + a.text_xs, + a.font_semi_bold, + a.leading_tight, + t.atoms.text_contrast_medium, + ], children: _jsxs(Trans, { children: ["Archived from ", niceDate(i18n, createdAt, 'medium')] }) })] })); + } }), _jsxs(Prompt.Outer, { control: control, children: [_jsxs(Prompt.Content, { children: [_jsx(Prompt.TitleText, { children: _jsx(Trans, { children: "Archived post" }) }), _jsx(Prompt.DescriptionText, { children: _jsxs(Trans, { children: ["This post claims to have been created on", ' ', _jsx(RNText, { style: [a.font_semi_bold], children: niceDate(i18n, createdAt) }), ", but was first seen by Bluesky on", ' ', _jsx(RNText, { style: [a.font_semi_bold], children: niceDate(i18n, indexedAt) }), "."] }) }), _jsx(Prompt.DescriptionText, { children: _jsx(Trans, { children: "Bluesky cannot confirm the authenticity of the claimed date." }) })] }), _jsx(Prompt.Actions, { children: _jsx(Prompt.Action, { cta: l(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Okay"], ["Okay"]))), onPress: function () { } }) })] })] })); +} +function getThreadAuthor(post, record) { + if (!record.reply) { + return post.author.did; + } + try { + return new AtUri(record.reply.root.uri).host; + } + catch (_a) { + return ''; + } +} +export function ThreadItemAnchorSkeleton() { + return (_jsxs(View, { style: [a.p_lg, a.gap_md], children: [_jsxs(Skele.Row, { style: [a.align_center, a.gap_md], children: [_jsx(Skele.Circle, { size: 42 }), _jsxs(Skele.Col, { children: [_jsx(Skele.Text, { style: [a.text_lg, { width: '20%' }] }), _jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '40%' }] })] })] }), _jsxs(View, { children: [_jsx(Skele.Text, { style: [a.text_xl, { width: '100%' }] }), _jsx(Skele.Text, { style: [a.text_xl, { width: '60%' }] })] }), _jsx(Skele.Text, { style: [a.text_sm, { width: '50%' }] }), _jsx(PostControlsSkeleton, { big: true })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/screens/PostThread/components/ThreadItemAnchorFollowButton.js b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.js new file mode 100644 index 0000000000..1f326c9062 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.js @@ -0,0 +1,165 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { logger } from '#/logger'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useProfileFollowMutationQueue, useProfileQuery, } from '#/state/queries/profile'; +import { useRequireAuth } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import { IS_IOS } from '#/env'; +import { GrowthHack } from './GrowthHack'; +export function ThreadItemAnchorFollowButton(_a) { + var did = _a.did, _b = _a.enabled, enabled = _b === void 0 ? true : _b; + if (IS_IOS) { + return (_jsx(GrowthHack, { children: _jsx(ThreadItemAnchorFollowButtonInner, { did: did, enabled: enabled }) })); + } + return _jsx(ThreadItemAnchorFollowButtonInner, { did: did, enabled: enabled }); +} +export function ThreadItemAnchorFollowButtonInner(_a) { + var did = _a.did, _b = _a.enabled, enabled = _b === void 0 ? true : _b; + var _c = useProfileQuery({ did: did }), profile = _c.data, isLoading = _c.isLoading; + // We will never hit this - the profile will always be cached or loaded above + // but it keeps the typechecker happy + if (!enabled || isLoading || !profile) + return null; + return _jsx(PostThreadFollowBtnLoaded, { profile: profile }); +} +function PostThreadFollowBtnLoaded(_a) { + var _this = this; + var _b, _c; + var profileUnshadowed = _a.profile; + var navigation = useNavigation(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var profile = useProfileShadow(profileUnshadowed); + var _d = useProfileFollowMutationQueue(profile, 'PostThreadItem'), queueFollow = _d[0], queueUnfollow = _d[1]; + var requireAuth = useRequireAuth(); + var isFollowing = !!((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.following); + var isFollowedBy = !!((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.followedBy); + var _e = React.useState(isFollowing), wasFollowing = _e[0], setWasFollowing = _e[1]; + // This prevents the button from disappearing as soon as we follow. + var showFollowBtn = React.useMemo(function () { return !isFollowing || !wasFollowing; }, [isFollowing, wasFollowing]); + /** + * We want this button to stay visible even after following, so that the user can unfollow if they want. + * However, we need it to disappear after we push to a screen and then come back. We also need it to + * show up if we view the post while following, go to the profile and unfollow, then come back to the + * post. + * + * We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native, + * we could do this only on focus because the transition animation gives us time to not notice the + * sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the + * button renders. So, we update the state in both cases. + */ + React.useEffect(function () { + var updateWasFollowing = function () { + if (wasFollowing !== isFollowing) { + setWasFollowing(isFollowing); + } + }; + var unsubscribeFocus = navigation.addListener('focus', updateWasFollowing); + var unsubscribeBlur = navigation.addListener('blur', updateWasFollowing); + return function () { + unsubscribeFocus(); + unsubscribeBlur(); + }; + }, [isFollowing, wasFollowing, navigation]); + var onPress = React.useCallback(function () { + if (!isFollowing) { + requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueFollow()]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + if ((e_1 === null || e_1 === void 0 ? void 0 : e_1.name) !== 'AbortError') { + logger.error('Failed to follow', { message: String(e_1) }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_1.toString())), 'xmark'); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }); + } + else { + requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + var e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueUnfollow()]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + e_2 = _a.sent(); + if ((e_2 === null || e_2 === void 0 ? void 0 : e_2.name) !== 'AbortError') { + logger.error('Failed to unfollow', { message: String(e_2) }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_2.toString())), 'xmark'); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }); + } + }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow]); + if (!showFollowBtn) + return null; + return (_jsxs(Button, { testID: "followBtn", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Follow ", ""], ["Follow ", ""])), profile.handle)), onPress: onPress, size: "small", color: isFollowing ? 'secondary' : 'secondary_inverted', style: [a.rounded_full], children: [gtMobile && (_jsx(ButtonIcon, { icon: isFollowing ? CheckIcon : PlusIcon, size: "sm" })), _jsx(ButtonText, { children: !isFollowing ? (isFollowedBy ? (_jsx(Trans, { children: "Follow back" })) : (_jsx(Trans, { children: "Follow" }))) : (_jsx(Trans, { children: "Following" })) })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.js b/src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.js new file mode 100644 index 0000000000..a1c21bad90 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.js @@ -0,0 +1,11 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme } from '#/alf'; +import { Lock_Stroke2_Corner0_Rounded as LockIcon } from '#/components/icons/Lock'; +import * as Skele from '#/components/Skeleton'; +import { Text } from '#/components/Typography'; +export function ThreadItemAnchorNoUnauthenticated() { + var t = useTheme(); + return (_jsxs(View, { style: [a.p_lg, a.gap_md], children: [_jsxs(Skele.Row, { style: [a.align_center, a.gap_md], children: [_jsx(Skele.Circle, { size: 42, children: _jsx(LockIcon, { size: "md", fill: t.atoms.text_contrast_medium.color }) }), _jsxs(Skele.Col, { children: [_jsx(Skele.Text, { style: [a.text_lg, { width: '20%' }] }), _jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '40%' }] })] })] }), _jsx(View, { style: [a.py_sm], children: _jsx(Text, { style: [a.text_xl, a.italic, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "This author has chosen to make their posts visible only to people who are signed in." }) }) })] })); +} diff --git a/src/screens/PostThread/components/ThreadItemPost.js b/src/screens/PostThread/components/ThreadItemPost.js new file mode 100644 index 0000000000..3cb5387c07 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemPost.js @@ -0,0 +1,166 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { AtUri, RichText as RichTextAPI, } from '@atproto/api'; +import { Trans } from '@lingui/react/macro'; +import { MAX_POST_LINES } from '#/lib/constants'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { makeProfileLink } from '#/lib/routes/links'; +import { countLines } from '#/lib/strings/helpers'; +import { POST_TOMBSTONE, usePostShadow, } from '#/state/cache/post-shadow'; +import { useSession } from '#/state/session'; +import { useMergedThreadgateHiddenReplies } from '#/state/threadgate-hidden-replies'; +import { PostMeta } from '#/view/com/util/PostMeta'; +import { PreviewableUserAvatar } from '#/view/com/util/UserAvatar'; +import { LINEAR_AVI_WIDTH, OUTER_SPACE, REPLY_LINE_WIDTH, } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { DebugFieldDisplay } from '#/components/DebugFieldDisplay'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import { LabelsOnMyPost } from '#/components/moderation/LabelsOnMe'; +import { PostAlerts } from '#/components/moderation/PostAlerts'; +import { PostHider } from '#/components/moderation/PostHider'; +import { Embed, PostEmbedViewContext } from '#/components/Post/Embed'; +import { ShowMoreTextButton } from '#/components/Post/ShowMoreTextButton'; +import { PostControls, PostControlsSkeleton } from '#/components/PostControls'; +import { RichText } from '#/components/RichText'; +import * as Skele from '#/components/Skeleton'; +import { SubtleHover } from '#/components/SubtleHover'; +import { Text } from '#/components/Typography'; +import { useActorStatus } from '#/features/liveNow'; +export function ThreadItemPost(_a) { + var item = _a.item, overrides = _a.overrides, onPostSuccess = _a.onPostSuccess, threadgateRecord = _a.threadgateRecord; + var postShadow = usePostShadow(item.value.post); + if (postShadow === POST_TOMBSTONE) { + return _jsx(ThreadItemPostDeleted, { item: item, overrides: overrides }); + } + return (_jsx(ThreadItemPostInner, { item: item, postShadow: postShadow, threadgateRecord: threadgateRecord, overrides: overrides, onPostSuccess: onPostSuccess })); +} +function ThreadItemPostDeleted(_a) { + var item = _a.item, overrides = _a.overrides; + var t = useTheme(); + return (_jsxs(ThreadItemPostOuterWrapper, { item: item, overrides: overrides, children: [_jsx(ThreadItemPostParentReplyLine, { item: item }), _jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.py_md, + a.rounded_sm, + t.atoms.bg_contrast_25, + ], children: [_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.justify_center, + { + width: LINEAR_AVI_WIDTH, + }, + ], children: _jsx(TrashIcon, { style: [t.atoms.text_contrast_medium] }) }), _jsx(Text, { style: [a.text_md, a.font_semi_bold, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Post has been deleted" }) })] }), _jsx(View, { style: [{ height: 4 }] })] })); +} +var ThreadItemPostOuterWrapper = memo(function ThreadItemPostOuterWrapper(_a) { + var item = _a.item, overrides = _a.overrides, children = _a.children; + var t = useTheme(); + var showTopBorder = !item.ui.showParentReplyLine && (overrides === null || overrides === void 0 ? void 0 : overrides.topBorder) !== true; + return (_jsx(View, { style: [ + showTopBorder && [a.border_t, t.atoms.border_contrast_low], + { paddingHorizontal: OUTER_SPACE }, + // If there's no next child, add a little padding to bottom + !item.ui.showChildReplyLine && + !item.ui.precedesChildReadMore && { + paddingBottom: OUTER_SPACE / 2, + }, + ], children: children })); +}); +/** + * Provides some space between posts as well as contains the reply line + */ +var ThreadItemPostParentReplyLine = memo(function ThreadItemPostParentReplyLine(_a) { + var item = _a.item; + var t = useTheme(); + return (_jsx(View, { style: [a.flex_row, { height: 12 }], children: _jsx(View, { style: { width: LINEAR_AVI_WIDTH }, children: item.ui.showParentReplyLine && (_jsx(View, { style: [ + a.mx_auto, + a.flex_1, + a.mb_xs, + { + width: REPLY_LINE_WIDTH, + backgroundColor: t.atoms.border_contrast_low.borderColor, + }, + ] })) }) })); +}); +var ThreadItemPostInner = memo(function ThreadItemPostInner(_a) { + var _b, _c, _d; + var item = _a.item, postShadow = _a.postShadow, overrides = _a.overrides, onPostSuccess = _a.onPostSuccess, threadgateRecord = _a.threadgateRecord; + var t = useTheme(); + var openComposer = useOpenComposer().openComposer; + var currentAccount = useSession().currentAccount; + var post = item.value.post; + var record = item.value.post.record; + var moderation = item.moderation; + var richText = useMemo(function () { + return new RichTextAPI({ + text: record.text, + facets: record.facets, + }); + }, [record]); + var _e = useState(function () { return countLines(richText === null || richText === void 0 ? void 0 : richText.text) >= MAX_POST_LINES; }), limitLines = _e[0], setLimitLines = _e[1]; + var threadRootUri = ((_c = (_b = record.reply) === null || _b === void 0 ? void 0 : _b.root) === null || _c === void 0 ? void 0 : _c.uri) || post.uri; + var postHref = useMemo(function () { + var urip = new AtUri(post.uri); + return makeProfileLink(post.author, 'post', urip.rkey); + }, [post.uri, post.author]); + var threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ + threadgateRecord: threadgateRecord, + }); + var additionalPostAlerts = useMemo(function () { + var isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri); + var isControlledByViewer = new AtUri(threadRootUri).host === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + return isControlledByViewer && isPostHiddenByThreadgate + ? [ + { + type: 'reply-hidden', + source: { type: 'user', did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }, + priority: 6, + }, + ] + : []; + }, [post, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, threadgateHiddenReplies, threadRootUri]); + var onPressReply = useCallback(function () { + openComposer({ + replyTo: { + uri: post.uri, + cid: post.cid, + text: record.text, + author: post.author, + embed: post.embed, + moderation: moderation, + langs: post.record.langs, + }, + onPostSuccess: onPostSuccess, + logContext: 'PostReply', + }); + }, [openComposer, post, record, onPostSuccess, moderation]); + var onPressShowMore = useCallback(function () { + setLimitLines(false); + }, [setLimitLines]); + var live = useActorStatus(post.author).isActive; + return (_jsx(SubtleHoverWrapper, { children: _jsx(ThreadItemPostOuterWrapper, { item: item, overrides: overrides, children: _jsxs(PostHider, { testID: "postThreadItem-by-".concat(post.author.handle), href: postHref, disabled: (overrides === null || overrides === void 0 ? void 0 : overrides.moderation) === true, modui: moderation.ui('contentList'), hiderStyle: [a.pl_0, a.pr_2xs, a.bg_transparent], iconSize: LINEAR_AVI_WIDTH, iconStyles: [a.mr_xs], profile: post.author, interpretFilterAsBlur: true, children: [_jsx(ThreadItemPostParentReplyLine, { item: item }), _jsxs(View, { style: [a.flex_row, a.gap_md], children: [_jsxs(View, { children: [_jsx(PreviewableUserAvatar, { size: LINEAR_AVI_WIDTH, profile: post.author, moderation: moderation.ui('avatar'), type: ((_d = post.author.associated) === null || _d === void 0 ? void 0 : _d.labeler) ? 'labeler' : 'user', live: live }), (item.ui.showChildReplyLine || + item.ui.precedesChildReadMore) && (_jsx(View, { style: [ + a.mx_auto, + a.mt_xs, + a.flex_1, + { + width: REPLY_LINE_WIDTH, + backgroundColor: t.atoms.border_contrast_low.borderColor, + }, + ] }))] }), _jsxs(View, { style: [a.flex_1], children: [_jsx(PostMeta, { author: post.author, moderation: moderation, timestamp: post.indexedAt, postHref: postHref, style: [a.pb_xs] }), _jsx(LabelsOnMyPost, { post: post, style: [a.pb_xs] }), _jsx(PostAlerts, { modui: moderation.ui('contentList'), style: [a.pb_2xs], additionalCauses: additionalPostAlerts }), (richText === null || richText === void 0 ? void 0 : richText.text) ? (_jsxs(View, { style: [a.mb_2xs], children: [_jsx(RichText, { enableTags: true, value: richText, style: [a.flex_1, a.text_md], numberOfLines: limitLines ? MAX_POST_LINES : undefined, authorHandle: post.author.handle, shouldProxyLinks: true }), limitLines && (_jsx(ShowMoreTextButton, { style: [a.text_md], onPress: onPressShowMore }))] })) : undefined, post.embed && (_jsx(View, { style: [a.pb_xs], children: _jsx(Embed, { embed: post.embed, moderation: moderation, viewContext: PostEmbedViewContext.Feed }) })), _jsx(PostControls, { post: postShadow, record: record, richText: richText, onPressReply: onPressReply, logContext: "PostThreadItem", threadgateRecord: threadgateRecord }), _jsx(DebugFieldDisplay, { subject: post })] })] })] }) }) })); +}); +function SubtleHoverWrapper(_a) { + var children = _a.children; + var _b = useInteractionState(), hover = _b.state, onHoverIn = _b.onIn, onHoverOut = _b.onOut; + return (_jsxs(View, { onPointerEnter: onHoverIn, onPointerLeave: onHoverOut, style: a.pointer, children: [_jsx(SubtleHover, { hover: hover }), children] })); +} +export function ThreadItemPostSkeleton(_a) { + var index = _a.index; + var even = index % 2 === 0; + return (_jsx(View, { style: [ + { paddingHorizontal: OUTER_SPACE, paddingVertical: OUTER_SPACE / 1.5 }, + a.gap_md, + ], children: _jsxs(Skele.Row, { style: [a.align_start, a.gap_md], children: [_jsx(Skele.Circle, { size: LINEAR_AVI_WIDTH }), _jsxs(Skele.Col, { style: [a.gap_xs], children: [_jsxs(Skele.Row, { style: [a.gap_sm], children: [_jsx(Skele.Text, { style: [a.text_md, { width: '20%' }] }), _jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '30%' }] })] }), _jsx(Skele.Col, { children: even ? (_jsxs(_Fragment, { children: [_jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '100%' }] }), _jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '60%' }] })] })) : (_jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '60%' }] })) }), _jsx(PostControlsSkeleton, {})] })] }) })); +} diff --git a/src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.js b/src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.js new file mode 100644 index 0000000000..a8f6baa4fd --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.js @@ -0,0 +1,35 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { LINEAR_AVI_WIDTH, OUTER_SPACE, REPLY_LINE_WIDTH, } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { Lock_Stroke2_Corner0_Rounded as LockIcon } from '#/components/icons/Lock'; +import * as Skele from '#/components/Skeleton'; +import { Text } from '#/components/Typography'; +export function ThreadItemPostNoUnauthenticated(_a) { + var item = _a.item; + var t = useTheme(); + return (_jsxs(View, { style: [{ paddingHorizontal: OUTER_SPACE }], children: [_jsx(View, { style: [a.flex_row, { height: 12 }], children: _jsx(View, { style: { width: LINEAR_AVI_WIDTH }, children: item.ui.showParentReplyLine && (_jsx(View, { style: [ + a.mx_auto, + a.flex_1, + a.mb_xs, + { + width: REPLY_LINE_WIDTH, + backgroundColor: t.atoms.border_contrast_low.borderColor, + }, + ] })) }) }), _jsxs(Skele.Row, { style: [a.align_center, a.gap_md], children: [_jsx(Skele.Circle, { size: LINEAR_AVI_WIDTH, children: _jsx(LockIcon, { size: "md", fill: t.atoms.text_contrast_medium.color }) }), _jsx(Text, { style: [a.text_md, a.italic, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "This author has chosen to make their posts visible only to people who are signed in." }) })] }), _jsx(View, { style: [ + a.flex_row, + a.justify_center, + { + height: OUTER_SPACE / 1.5, + width: LINEAR_AVI_WIDTH, + }, + ], children: item.ui.showChildReplyLine && (_jsx(View, { style: [ + a.mt_xs, + a.h_full, + { + width: REPLY_LINE_WIDTH, + backgroundColor: t.atoms.border_contrast_low.borderColor, + }, + ] })) })] })); +} diff --git a/src/screens/PostThread/components/ThreadItemPostTombstone.js b/src/screens/PostThread/components/ThreadItemPostTombstone.js new file mode 100644 index 0000000000..752c5157a4 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemPostTombstone.js @@ -0,0 +1,42 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { LINEAR_AVI_WIDTH, OUTER_SPACE } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { PersonX_Stroke2_Corner0_Rounded as PersonXIcon } from '#/components/icons/Person'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import { Text } from '#/components/Typography'; +export function ThreadItemPostTombstone(_a) { + var type = _a.type; + var t = useTheme(); + var _ = useLingui()._; + var _b = useMemo(function () { + switch (type) { + case 'blocked': + return { copy: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Post blocked"], ["Post blocked"])))), Icon: PersonXIcon }; + case 'not-found': + default: + return { copy: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Post not found"], ["Post not found"])))), Icon: TrashIcon }; + } + }, [_, type]), copy = _b.copy, Icon = _b.Icon; + return (_jsx(View, { style: [ + a.mb_xs, + { + paddingHorizontal: OUTER_SPACE, + paddingTop: OUTER_SPACE / 1.2, + }, + ], children: _jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.rounded_sm, + t.atoms.bg_contrast_25, + { paddingVertical: OUTER_SPACE / 1.2 }, + ], children: [_jsx(View, { style: [a.flex_row, a.justify_center, { width: LINEAR_AVI_WIDTH }], children: _jsx(Icon, { style: [t.atoms.text_contrast_medium] }) }), _jsx(Text, { style: [a.text_md, a.font_semi_bold, t.atoms.text_contrast_medium], children: copy })] }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/PostThread/components/ThreadItemReadMore.js b/src/screens/PostThread/components/ThreadItemReadMore.js new file mode 100644 index 0000000000..bf56815667 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReadMore.js @@ -0,0 +1,59 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { LINEAR_AVI_WIDTH, REPLY_LINE_WIDTH, TREE_AVI_WIDTH, TREE_INDENT, } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { CirclePlus_Stroke2_Corner0_Rounded as CirclePlus } from '#/components/icons/CirclePlus'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export var ThreadItemReadMore = memo(function ThreadItemReadMore(_a) { + var item = _a.item, view = _a.view; + var t = useTheme(); + var _ = useLingui()._; + var isTreeView = view === 'tree'; + var indent = Math.max(0, item.depth - 1); + var spacers = isTreeView + ? Array.from(Array(indent)).map(function (_, n) { + var isSkipped = item.skippedIndentIndices.has(n); + return (_jsx(View, { style: [ + t.atoms.border_contrast_low, + { + borderRightWidth: isSkipped ? 0 : REPLY_LINE_WIDTH, + width: TREE_INDENT + TREE_AVI_WIDTH / 2, + left: 1, + }, + ] }, "".concat(item.key, "-padding-").concat(n))); + }) + : null; + return (_jsxs(View, { style: [a.flex_row], children: [spacers, _jsx(View, { style: [ + t.atoms.border_contrast_low, + { + marginLeft: isTreeView + ? TREE_INDENT + TREE_AVI_WIDTH / 2 - 1 + : (LINEAR_AVI_WIDTH - REPLY_LINE_WIDTH) / 2 + 16, + borderLeftWidth: 2, + borderBottomWidth: 2, + borderBottomLeftRadius: a.rounded_sm.borderRadius, + height: 18, // magic, Link below is 38px tall + width: isTreeView ? TREE_INDENT : LINEAR_AVI_WIDTH / 2 + 10, + }, + ] }), _jsx(Link, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Read more replies"], ["Read more replies"])))), to: item.href, style: [a.pt_sm, a.pb_md, a.gap_xs], children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + var interacted = hovered || pressed; + return (_jsxs(_Fragment, { children: [_jsx(CirclePlus, { fill: interacted + ? t.atoms.text_contrast_high.color + : t.atoms.text_contrast_low.color, width: 18 }), _jsx(Text, { style: [ + a.text_sm, + t.atoms.text_contrast_medium, + interacted && a.underline, + ], children: _jsxs(Trans, { children: ["Read", ' ', _jsx(Plural, { one: "# more reply", other: "# more replies", value: item.moreReplies })] }) })] })); + } })] })); +}); +var templateObject_1; diff --git a/src/screens/PostThread/components/ThreadItemReadMoreUp.js b/src/screens/PostThread/components/ThreadItemReadMoreUp.js new file mode 100644 index 0000000000..2f4f2e6e56 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReadMoreUp.js @@ -0,0 +1,55 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { memo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { LINEAR_AVI_WIDTH, OUTER_SPACE, REPLY_LINE_WIDTH, } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { ArrowTopCircle_Stroke2_Corner0_Rounded as UpIcon } from '#/components/icons/ArrowTopCircle'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export var ThreadItemReadMoreUp = memo(function ThreadItemReadMoreUp(_a) { + var item = _a.item; + var t = useTheme(); + var _ = useLingui()._; + return (_jsx(Link, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Continue thread"], ["Continue thread"])))), to: item.href, style: [ + a.gap_xs, + { + paddingTop: OUTER_SPACE, + paddingHorizontal: OUTER_SPACE, + }, + ], children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + var interacted = hovered || pressed; + return (_jsxs(View, { children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_md], children: [_jsx(View, { style: [ + a.align_center, + { + width: LINEAR_AVI_WIDTH, + }, + ], children: _jsx(UpIcon, { fill: interacted + ? t.atoms.text_contrast_high.color + : t.atoms.text_contrast_low.color, width: 24 }) }), _jsx(Text, { style: [ + a.text_sm, + t.atoms.text_contrast_medium, + interacted && [a.underline], + ], children: _jsx(Trans, { children: "Continue thread..." }) })] }), _jsx(View, { style: [ + a.align_center, + { + width: LINEAR_AVI_WIDTH, + }, + ], children: _jsx(View, { style: [ + a.mt_xs, + { + height: OUTER_SPACE / 2, + width: REPLY_LINE_WIDTH, + backgroundColor: t.atoms.border_contrast_low.borderColor, + }, + ] }) })] })); + } })); +}); +var templateObject_1; diff --git a/src/screens/PostThread/components/ThreadItemReplyComposer.js b/src/screens/PostThread/components/ThreadItemReplyComposer.js new file mode 100644 index 0000000000..2b0756ce6a --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReplyComposer.js @@ -0,0 +1,11 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import * as Skele from '#/components/Skeleton'; +export function ThreadItemReplyComposerSkeleton() { + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + if (!gtMobile) + return null; + return (_jsx(View, { style: [a.px_sm, a.py_xs, a.border_t, t.atoms.border_contrast_low], children: _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm, a.px_sm, a.py_sm], children: [_jsx(Skele.Circle, { size: 24 }), _jsx(Skele.Text, { style: [a.text_md, { maxWidth: 119 }] })] }) })); +} diff --git a/src/screens/PostThread/components/ThreadItemShowOtherReplies.js b/src/screens/PostThread/components/ThreadItemShowOtherReplies.js new file mode 100644 index 0000000000..47b91739a5 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemShowOtherReplies.js @@ -0,0 +1,48 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { EyeSlash_Stroke2_Corner0_Rounded as EyeSlash } from '#/components/icons/EyeSlash'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +export function ThreadItemShowOtherReplies(_a) { + var onPress = _a.onPress; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var label = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Show more replies"], ["Show more replies"])))); + return (_jsx(Button, { onPress: function () { + onPress(); + ax.metric('thread:click:showOtherReplies', {}); + }, label: label, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.align_center, + a.gap_sm, + a.py_lg, + a.px_xl, + a.border_t, + t.atoms.border_contrast_low, + hovered || pressed ? t.atoms.bg_contrast_25 : t.atoms.bg, + ], children: [_jsx(View, { style: [ + t.atoms.bg_contrast_25, + a.align_center, + a.justify_center, + { + width: 26, + height: 26, + borderRadius: 13, + marginRight: 4, + }, + ], children: _jsx(EyeSlash, { size: "sm", fill: t.atoms.text_contrast_medium.color }) }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.flex_1, a.leading_snug], numberOfLines: 1, children: label })] })); + } })); +} +var templateObject_1; diff --git a/src/screens/PostThread/components/ThreadItemTreePost.js b/src/screens/PostThread/components/ThreadItemTreePost.js new file mode 100644 index 0000000000..e6c4b2bf70 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemTreePost.js @@ -0,0 +1,193 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { AtUri, RichText as RichTextAPI, } from '@atproto/api'; +import { Trans } from '@lingui/react/macro'; +import { MAX_POST_LINES } from '#/lib/constants'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { makeProfileLink } from '#/lib/routes/links'; +import { countLines } from '#/lib/strings/helpers'; +import { POST_TOMBSTONE, usePostShadow, } from '#/state/cache/post-shadow'; +import { useSession } from '#/state/session'; +import { useMergedThreadgateHiddenReplies } from '#/state/threadgate-hidden-replies'; +import { PostMeta } from '#/view/com/util/PostMeta'; +import { OUTER_SPACE, REPLY_LINE_WIDTH, TREE_AVI_WIDTH, TREE_INDENT, } from '#/screens/PostThread/const'; +import { atoms as a, useTheme } from '#/alf'; +import { DebugFieldDisplay } from '#/components/DebugFieldDisplay'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import { LabelsOnMyPost } from '#/components/moderation/LabelsOnMe'; +import { PostAlerts } from '#/components/moderation/PostAlerts'; +import { PostHider } from '#/components/moderation/PostHider'; +import { Embed, PostEmbedViewContext } from '#/components/Post/Embed'; +import { ShowMoreTextButton } from '#/components/Post/ShowMoreTextButton'; +import { PostControls, PostControlsSkeleton } from '#/components/PostControls'; +import { RichText } from '#/components/RichText'; +import * as Skele from '#/components/Skeleton'; +import { SubtleHover } from '#/components/SubtleHover'; +import { Text } from '#/components/Typography'; +/** + * Mimic the space in PostMeta + */ +var TREE_AVI_PLUS_SPACE = TREE_AVI_WIDTH + a.gap_xs.gap; +export function ThreadItemTreePost(_a) { + var item = _a.item, overrides = _a.overrides, onPostSuccess = _a.onPostSuccess, threadgateRecord = _a.threadgateRecord; + var postShadow = usePostShadow(item.value.post); + if (postShadow === POST_TOMBSTONE) { + return _jsx(ThreadItemTreePostDeleted, { item: item }); + } + return (_jsx(ThreadItemTreePostInner + // Safeguard from clobbering per-post state below: + , { item: item, postShadow: postShadow, threadgateRecord: threadgateRecord, overrides: overrides, onPostSuccess: onPostSuccess }, postShadow.uri)); +} +function ThreadItemTreePostDeleted(_a) { + var item = _a.item; + var t = useTheme(); + return (_jsx(ThreadItemTreePostOuterWrapper, { item: item, children: _jsxs(ThreadItemTreePostInnerWrapper, { item: item, children: [_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.rounded_sm, + t.atoms.bg_contrast_25, + { + gap: 6, + paddingHorizontal: OUTER_SPACE / 2, + height: TREE_AVI_WIDTH, + }, + ], children: [_jsx(TrashIcon, { style: [t.atoms.text], width: 14 }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.mt_2xs], children: _jsx(Trans, { children: "Post has been deleted" }) })] }), item.ui.isLastChild && !item.ui.precedesChildReadMore && (_jsx(View, { style: { height: OUTER_SPACE / 2 } }))] }) })); +} +var ThreadItemTreePostOuterWrapper = memo(function ThreadItemTreePostOuterWrapper(_a) { + var item = _a.item, children = _a.children; + var t = useTheme(); + var indents = Math.max(0, item.ui.indent - 1); + return (_jsxs(View, { style: [ + a.flex_row, + item.ui.indent === 1 && + !item.ui.showParentReplyLine && [ + a.border_t, + t.atoms.border_contrast_low, + ], + ], children: [Array.from(Array(indents)).map(function (_, n) { + var isSkipped = item.ui.skippedIndentIndices.has(n); + return (_jsx(View, { style: [ + t.atoms.border_contrast_low, + { + borderRightWidth: isSkipped ? 0 : REPLY_LINE_WIDTH, + width: TREE_INDENT + TREE_AVI_WIDTH / 2, + left: 1, + }, + ] }, "".concat(item.value.post.uri, "-padding-").concat(n))); + }), children] })); +}); +var ThreadItemTreePostInnerWrapper = memo(function ThreadItemTreePostInnerWrapper(_a) { + var item = _a.item, children = _a.children; + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_1, // TODO check on ios + { + paddingHorizontal: OUTER_SPACE, + paddingTop: OUTER_SPACE / 2, + }, + item.ui.indent === 1 && [ + !item.ui.showParentReplyLine && { paddingTop: OUTER_SPACE / 1.5 }, + !item.ui.showChildReplyLine && a.pb_sm, + ], + item.ui.isLastChild && + !item.ui.precedesChildReadMore && [ + { + paddingBottom: OUTER_SPACE / 2, + }, + ], + ], children: [item.ui.indent > 1 && (_jsx(View, { style: [ + a.absolute, + t.atoms.border_contrast_low, + { + left: -1, + top: 0, + height: TREE_AVI_WIDTH / 2 + REPLY_LINE_WIDTH / 2 + OUTER_SPACE / 2, + width: OUTER_SPACE, + borderLeftWidth: REPLY_LINE_WIDTH, + borderBottomWidth: REPLY_LINE_WIDTH, + borderBottomLeftRadius: a.rounded_sm.borderRadius, + }, + ] })), children] })); +}); +var ThreadItemTreeReplyChildReplyLine = memo(function ThreadItemTreeReplyChildReplyLine(_a) { + var item = _a.item; + var t = useTheme(); + return (_jsx(View, { style: [a.relative, a.pt_2xs, { width: TREE_AVI_PLUS_SPACE }], children: item.ui.showChildReplyLine && (_jsx(View, { style: [ + a.flex_1, + t.atoms.border_contrast_low, + { borderRightWidth: 2, width: '50%', left: -1 }, + ] })) })); +}); +var ThreadItemTreePostInner = memo(function ThreadItemTreePostInner(_a) { + var _b, _c; + var item = _a.item, postShadow = _a.postShadow, overrides = _a.overrides, onPostSuccess = _a.onPostSuccess, threadgateRecord = _a.threadgateRecord; + var openComposer = useOpenComposer().openComposer; + var currentAccount = useSession().currentAccount; + var post = item.value.post; + var record = item.value.post.record; + var moderation = item.moderation; + var richText = useMemo(function () { + return new RichTextAPI({ + text: record.text, + facets: record.facets, + }); + }, [record]); + var _d = useState(function () { return countLines(richText === null || richText === void 0 ? void 0 : richText.text) >= MAX_POST_LINES; }), limitLines = _d[0], setLimitLines = _d[1]; + var threadRootUri = ((_c = (_b = record.reply) === null || _b === void 0 ? void 0 : _b.root) === null || _c === void 0 ? void 0 : _c.uri) || post.uri; + var postHref = useMemo(function () { + var urip = new AtUri(post.uri); + return makeProfileLink(post.author, 'post', urip.rkey); + }, [post.uri, post.author]); + var threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ + threadgateRecord: threadgateRecord, + }); + var additionalPostAlerts = useMemo(function () { + var isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri); + var isControlledByViewer = new AtUri(threadRootUri).host === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + return isControlledByViewer && isPostHiddenByThreadgate + ? [ + { + type: 'reply-hidden', + source: { type: 'user', did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }, + priority: 6, + }, + ] + : []; + }, [post, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, threadgateHiddenReplies, threadRootUri]); + var onPressReply = useCallback(function () { + openComposer({ + replyTo: { + uri: post.uri, + cid: post.cid, + text: record.text, + author: post.author, + embed: post.embed, + moderation: moderation, + langs: post.record.langs, + }, + onPostSuccess: onPostSuccess, + logContext: 'PostReply', + }); + }, [openComposer, post, record, onPostSuccess, moderation]); + var onPressShowMore = useCallback(function () { + setLimitLines(false); + }, [setLimitLines]); + return (_jsx(ThreadItemTreePostOuterWrapper, { item: item, children: _jsx(SubtleHoverWrapper, { children: _jsx(PostHider, { testID: "postThreadItem-by-".concat(post.author.handle), href: postHref, disabled: (overrides === null || overrides === void 0 ? void 0 : overrides.moderation) === true, modui: moderation.ui('contentList'), iconSize: 42, iconStyles: { marginLeft: 2, marginRight: 2 }, profile: post.author, interpretFilterAsBlur: true, children: _jsx(ThreadItemTreePostInnerWrapper, { item: item, children: _jsxs(View, { style: [a.flex_1], children: [_jsx(PostMeta, { author: post.author, moderation: moderation, timestamp: post.indexedAt, postHref: postHref, avatarSize: TREE_AVI_WIDTH, style: [a.pb_0], showAvatar: true }), _jsxs(View, { style: [a.flex_row], children: [_jsx(ThreadItemTreeReplyChildReplyLine, { item: item }), _jsxs(View, { style: [a.flex_1, a.pl_2xs], children: [_jsx(LabelsOnMyPost, { post: post, style: [a.pb_2xs] }), _jsx(PostAlerts, { modui: moderation.ui('contentList'), style: [a.pb_2xs], additionalCauses: additionalPostAlerts }), (richText === null || richText === void 0 ? void 0 : richText.text) ? (_jsxs(View, { style: [a.mb_2xs], children: [_jsx(RichText, { enableTags: true, value: richText, style: [a.flex_1, a.text_md], numberOfLines: limitLines ? MAX_POST_LINES : undefined, authorHandle: post.author.handle, shouldProxyLinks: true }), limitLines && (_jsx(ShowMoreTextButton, { style: [a.text_md], onPress: onPressShowMore }))] })) : null, post.embed && (_jsx(View, { style: [a.pb_xs], children: _jsx(Embed, { embed: post.embed, moderation: moderation, viewContext: PostEmbedViewContext.Feed }) })), _jsx(PostControls, { variant: "compact", post: postShadow, record: record, richText: richText, onPressReply: onPressReply, logContext: "PostThreadItem", threadgateRecord: threadgateRecord }), _jsx(DebugFieldDisplay, { subject: post })] })] })] }) }) }) }) })); +}); +function SubtleHoverWrapper(_a) { + var children = _a.children; + var _b = useInteractionState(), hover = _b.state, onHoverIn = _b.onIn, onHoverOut = _b.onOut; + return (_jsxs(View, { onPointerEnter: onHoverIn, onPointerLeave: onHoverOut, style: [a.flex_1, a.pointer], children: [_jsx(SubtleHover, { hover: hover }), children] })); +} +export function ThreadItemTreePostSkeleton(_a) { + var index = _a.index; + var t = useTheme(); + var even = index % 2 === 0; + return (_jsx(View, { style: [ + { paddingHorizontal: OUTER_SPACE, paddingVertical: OUTER_SPACE / 1.5 }, + a.border_t, + t.atoms.border_contrast_low, + ], children: _jsxs(Skele.Row, { style: [a.align_start, a.gap_xs], children: [_jsx(Skele.Circle, { size: TREE_AVI_WIDTH }), _jsxs(Skele.Col, { style: [a.gap_xs], children: [_jsxs(Skele.Row, { style: [a.gap_sm], children: [_jsx(Skele.Text, { style: [a.text_md, { width: '20%' }] }), _jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '30%' }] })] }), _jsx(Skele.Col, { children: even ? (_jsxs(_Fragment, { children: [_jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '100%' }] }), _jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '60%' }] })] })) : (_jsx(Skele.Text, { blend: true, style: [a.text_md, { width: '60%' }] })) }), _jsx(PostControlsSkeleton, {})] })] }) })); +} diff --git a/src/screens/PostThread/const.js b/src/screens/PostThread/const.js new file mode 100644 index 0000000000..c0d3c41036 --- /dev/null +++ b/src/screens/PostThread/const.js @@ -0,0 +1,6 @@ +import { tokens } from '#/alf'; +export var TREE_INDENT = tokens.space.lg; +export var TREE_AVI_WIDTH = 24; +export var LINEAR_AVI_WIDTH = 42; +export var REPLY_LINE_WIDTH = 2; +export var OUTER_SPACE = tokens.space.lg; diff --git a/src/screens/PostThread/index.js b/src/screens/PostThread/index.js new file mode 100644 index 0000000000..9a79f5dcd2 --- /dev/null +++ b/src/screens/PostThread/index.js @@ -0,0 +1,507 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import Animated, { useAnimatedStyle } from 'react-native-reanimated'; +import { Trans } from '@lingui/react/macro'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { usePostViewTracking } from '#/lib/hooks/usePostViewTracking'; +import { useFeedFeedback } from '#/state/feed-feedback'; +import { PostThreadContextProvider, usePostThread, } from '#/state/queries/usePostThread'; +import { useSession } from '#/state/session'; +import { useShellLayout } from '#/state/shell/shell-layout'; +import { useUnstablePostSource } from '#/state/unstable-post-source'; +import { List } from '#/view/com/util/List'; +import { HeaderDropdown } from '#/screens/PostThread/components/HeaderDropdown'; +import { ThreadComposePrompt } from '#/screens/PostThread/components/ThreadComposePrompt'; +import { ThreadError } from '#/screens/PostThread/components/ThreadError'; +import { ThreadItemAnchor, ThreadItemAnchorSkeleton, } from '#/screens/PostThread/components/ThreadItemAnchor'; +import { ThreadItemAnchorNoUnauthenticated } from '#/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated'; +import { ThreadItemPost, ThreadItemPostSkeleton, } from '#/screens/PostThread/components/ThreadItemPost'; +import { ThreadItemPostNoUnauthenticated } from '#/screens/PostThread/components/ThreadItemPostNoUnauthenticated'; +import { ThreadItemPostTombstone } from '#/screens/PostThread/components/ThreadItemPostTombstone'; +import { ThreadItemReadMore } from '#/screens/PostThread/components/ThreadItemReadMore'; +import { ThreadItemReadMoreUp } from '#/screens/PostThread/components/ThreadItemReadMoreUp'; +import { ThreadItemReplyComposerSkeleton } from '#/screens/PostThread/components/ThreadItemReplyComposer'; +import { ThreadItemShowOtherReplies } from '#/screens/PostThread/components/ThreadItemShowOtherReplies'; +import { ThreadItemTreePost, ThreadItemTreePostSkeleton, } from '#/screens/PostThread/components/ThreadItemTreePost'; +import { atoms as a, native, platform, useBreakpoints, web } from '#/alf'; +import * as Layout from '#/components/Layout'; +import { ListFooter } from '#/components/Lists'; +import { useAnalytics } from '#/analytics'; +var PARENT_CHUNK_SIZE = 5; +var CHILDREN_CHUNK_SIZE = 50; +export function PostThread(_a) { + var _b, _c; + var uri = _a.uri; + var ax = useAnalytics(); + var gtMobile = useBreakpoints().gtMobile; + var hasSession = useSession().hasSession; + var initialNumToRender = useInitialNumToRender(); + var windowHeight = useWindowDimensions().height; + var anchorPostSource = useUnstablePostSource(uri); + var feedFeedback = useFeedFeedback(anchorPostSource === null || anchorPostSource === void 0 ? void 0 : anchorPostSource.feedSourceInfo, hasSession); + /* + * One query to rule them all + */ + var thread = usePostThread({ anchor: uri }); + var _d = useMemo(function () { + var hasParents = false; + for (var _i = 0, _a = thread.data.items; _i < _a.length; _i++) { + var item = _a[_i]; + if (item.type === 'threadPost' && item.depth === 0) { + return { anchor: item, hasParents: hasParents }; + } + hasParents = true; + } + return { hasParents: hasParents }; + }, [thread.data.items]), anchor = _d.anchor, hasParents = _d.hasParents; + // Track post:view event when anchor post is viewed + var seenPostUriRef = useRef(null); + useEffect(function () { + if ((anchor === null || anchor === void 0 ? void 0 : anchor.type) === 'threadPost' && + anchor.value.post.uri !== seenPostUriRef.current) { + var post = anchor.value.post; + seenPostUriRef.current = post.uri; + ax.metric('post:view', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'Post', + feedDescriptor: feedFeedback.feedDescriptor, + }); + } + }, [ax, anchor, feedFeedback.feedDescriptor]); + // Track post:view events for parent posts and replies (non-anchor posts) + var trackThreadItemView = usePostViewTracking('PostThreadItem'); + var openComposer = useOpenComposer().openComposer; + var optimisticOnPostReply = useCallback(function (payload) { + if (payload) { + var replyToUri = payload.replyToUri, posts = payload.posts; + if (replyToUri && posts.length) { + thread.actions.insertReplies(replyToUri, posts); + } + } + }, [thread]); + var onReplyToAnchor = useCallback(function () { + if ((anchor === null || anchor === void 0 ? void 0 : anchor.type) !== 'threadPost') { + return; + } + var post = anchor.value.post; + openComposer({ + replyTo: { + uri: anchor.uri, + cid: post.cid, + text: post.record.text, + author: post.author, + embed: post.embed, + moderation: anchor.moderation, + langs: post.record.langs, + }, + onPostSuccess: optimisticOnPostReply, + logContext: 'PostReply', + }); + if (anchorPostSource) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, + }); + } + }, [ + anchor, + openComposer, + optimisticOnPostReply, + anchorPostSource, + feedFeedback, + ]); + var isRoot = !!anchor && anchor.value.post.record.reply === undefined; + var canReply = !((_c = (_b = anchor === null || anchor === void 0 ? void 0 : anchor.value.post) === null || _b === void 0 ? void 0 : _b.viewer) === null || _c === void 0 ? void 0 : _c.replyDisabled); + var _e = useState(PARENT_CHUNK_SIZE), maxParentCount = _e[0], setMaxParentCount = _e[1]; + var _f = useState(CHILDREN_CHUNK_SIZE), maxChildrenCount = _f[0], setMaxChildrenCount = _f[1]; + var totalParentCount = useRef(0); // recomputed below + var totalChildrenCount = useRef(thread.data.items.length); // recomputed below + var listRef = useRef(null); + var anchorRef = useRef(null); + var headerRef = useRef(null); + /* + * On a cold load, parents are not prepended until the anchor post has + * rendered as the first item in the list. This gives us a consistent + * reference point for which to pin the anchor post to the top of the screen. + * + * We simulate a cold load any time the user changes the view or sort params + * so that this handling is consistent. + * + * On native, `maintainVisibleContentPosition={{minIndexForVisible: 0}}` gives + * us this for free, since the anchor post is the first item in the list. + * + * On web, `onContentSizeChange` is used to get ahead of next paint and handle + * this scrolling. + */ + var _g = useState(true), deferParents = _g[0], setDeferParents = _g[1]; + /** + * Used to flag whether we should scroll to the anchor post. On a cold load, + * this is always true. And when a user changes thread parameters, we also + * manually set this to true. + */ + var shouldHandleScroll = useRef(true); + /** + * Called any time the content size of the list changes. Could be a fresh + * render, items being added to the list, or any resize that changes the + * scrollable size of the content. + * + * We want this to fire every time we change params (which will reset + * `deferParents` via `onLayout` on the anchor post, due to the key change), + * or click into a new post (which will result in a fresh `deferParents` + * hook). + * + * The result being: any intentional change in view by the user will result + * in the anchor being pinned as the first item. + */ + var onContentSizeChangeWebOnly = web(function () { + var list = listRef.current; + var anchor = anchorRef.current; + var header = headerRef.current; + if (list && anchor && header && shouldHandleScroll.current) { + var anchorOffsetTop = anchor.getBoundingClientRect().top; + var headerHeight = header.getBoundingClientRect().height; + /* + * `deferParents` is `true` on a cold load, and always reset to + * `true` when params change via `prepareForParamsUpdate`. + * + * On a cold load or a push to a new post, on the first pass of this + * logic, the anchor post is the first item in the list. Therefore + * `anchorOffsetTop - headerHeight` will be 0. + * + * When a user changes thread params, on the first pass of this logic, + * the anchor post may not move (if there are no parents above it), or it + * may have gone off the screen above, because of the sudden lack of + * parents due to `deferParents === true`. This negative value (minus + * `headerHeight`) will result in a _negative_ `offset` value, which will + * scroll the anchor post _down_ to the top of the screen. + * + * However, `prepareForParamsUpdate` also resets scroll to `0`, so when a user + * changes params, the anchor post's offset will actually be equivalent + * to the `headerHeight` because of how the DOM is stacked on web. + * Therefore, `anchorOffsetTop - headerHeight` will once again be 0, + * which means the first pass in this case will result in no scroll. + * + * Then, once parents are prepended, this will fire again. Now, the + * `anchorOffsetTop` will be positive, which minus the header height, + * will give us a _positive_ offset, which will scroll the anchor post + * back _up_ to the top of the screen. + */ + var offset = anchorOffsetTop - headerHeight; + list.scrollToOffset({ offset: offset }); + /* + * After we manage to do a positive adjustment, we need to ensure this + * doesn't run again until scroll handling is requested again via + * `shouldHandleScroll.current === true` and a params change via + * `prepareForParamsUpdate`. + * + * The `isRoot` here is needed because if we're looking at the anchor + * post, this handler will not fire after `deferParents` is set to + * `false`, since there are no parents to render above it. In this case, + * we want to make sure `shouldHandleScroll` is set to `false` right away + * so that subsequent size changes unrelated to a params change (like + * pagination) do not affect scroll. + */ + if (offset > 0 || isRoot) + shouldHandleScroll.current = false; + } + }); + /** + * Ditto the above, but for native. + */ + var onContentSizeChangeNativeOnly = native(function () { + var list = listRef.current; + var anchor = anchorRef.current; + if (list && anchor && shouldHandleScroll.current) { + /* + * `prepareForParamsUpdate` is called any time the user changes thread params like + * `view` or `sort`, which sets `deferParents(true)` and resets the + * scroll to the top of the list. However, there is a split second + * where the top of the list is wherever the parents _just were_. So if + * there were parents, the anchor is not at the top of the list just + * prior to this handler being called. + * + * Once this handler is called, the anchor post is the first item in + * the list (because of `deferParents` being `true`), and so we can + * synchronously scroll the list back to the top of the list (which is + * 0 on native, no need to handle `headerHeight`). + */ + list.scrollToOffset({ + animated: false, + offset: 0, + }); + /* + * After this first pass, `deferParents` will be `false`, and those + * will render in. However, the anchor post will retain its position + * because of `maintainVisibleContentPosition` handling on native. So we + * don't need to let this handler run again, like we do on web. + */ + shouldHandleScroll.current = false; + } + }); + /** + * Called any time the user changes thread params, such as `view` or `sort`. + * Prepares the UI for repositioning of the scroll so that the anchor post is + * always at the top after a params change. + * + * No need to handle max parents here, deferParents will handle that and we + * want it to re-render with the same items above the anchor. + */ + var prepareForParamsUpdate = useCallback(function () { + /** + * Truncate list so that anchor post is the first item in the list. Manual + * scroll handling on web is predicated on this, and on native, this allows + * `maintainVisibleContentPosition` to do its thing. + */ + setDeferParents(true); + // reset this to a lower value for faster re-render + setMaxChildrenCount(CHILDREN_CHUNK_SIZE); + // set flag + shouldHandleScroll.current = true; + }, [setDeferParents, setMaxChildrenCount]); + var setSortWrapped = useCallback(function (sort) { + prepareForParamsUpdate(); + thread.actions.setSort(sort); + }, [thread, prepareForParamsUpdate]); + var setViewWrapped = useCallback(function (view) { + prepareForParamsUpdate(); + thread.actions.setView(view); + }, [thread, prepareForParamsUpdate]); + var onStartReached = function () { + if (thread.state.isFetching) + return; + // can be true after `prepareForParamsUpdate` is called + if (deferParents) + return; + // prevent any state mutations if we know we're done + if (maxParentCount >= totalParentCount.current) + return; + setMaxParentCount(function (n) { return n + PARENT_CHUNK_SIZE; }); + }; + var onEndReached = function () { + if (thread.state.isFetching) + return; + // can be true after `prepareForParamsUpdate` is called + if (deferParents) + return; + // prevent any state mutations if we know we're done + if (maxChildrenCount >= totalChildrenCount.current) + return; + setMaxChildrenCount(function (prev) { return prev + CHILDREN_CHUNK_SIZE; }); + }; + var slices = useMemo(function () { + var results = []; + if (!thread.data.items.length) + return results; + /* + * Pagination hack, tracks the # of items below the anchor post. + */ + var childrenCount = 0; + for (var i = 0; i < thread.data.items.length; i++) { + var item = thread.data.items[i]; + /* + * Need to check `depth`, since not found or blocked posts are not + * `threadPost`s, but still have `depth`. + */ + var hasDepth = 'depth' in item; + /* + * Handle anchor post. + */ + if (hasDepth && item.depth === 0) { + results.push(item); + // Recalculate total parents current index. + totalParentCount.current = i; + // Recalculate total children using (length - 1) - current index. + totalChildrenCount.current = thread.data.items.length - 1 - i; + /* + * Walk up the parents, limiting by `maxParentCount` + */ + if (!deferParents) { + var start = i - 1; + if (start >= 0) { + var limit = Math.max(0, start - maxParentCount); + for (var pi = start; pi >= limit; pi--) { + results.unshift(thread.data.items[pi]); + } + } + } + } + else { + // ignore any parent items + if (item.type === 'readMoreUp' || (hasDepth && item.depth < 0)) + continue; + // can exit early if we've reached the max children count + if (childrenCount > maxChildrenCount) + break; + results.push(item); + childrenCount++; + } + } + return results; + }, [thread, deferParents, maxParentCount, maxChildrenCount]); + var isTombstoneView = useMemo(function () { + if (slices.length > 1) + return false; + return slices.every(function (s) { return s.type === 'threadPostBlocked' || s.type === 'threadPostNotFound'; }); + }, [slices]); + var renderItem = useCallback(function (_a) { + var _b, _c, _d, _e, _f, _g, _h, _j; + var item = _a.item, index = _a.index; + if (item.type === 'threadPost') { + if (item.depth < 0) { + return (_jsx(ThreadItemPost, { item: item, threadgateRecord: (_c = (_b = thread.data.threadgate) === null || _b === void 0 ? void 0 : _b.record) !== null && _c !== void 0 ? _c : undefined, overrides: { + topBorder: index === 0, + }, onPostSuccess: optimisticOnPostReply })); + } + else if (item.depth === 0) { + return ( + /* + * Keep this view wrapped so that the anchor post is always index 0 + * in the list and `maintainVisibleContentPosition` can do its + * thing. + */ + _jsxs(View, { collapsable: false, children: [_jsx(View + /* + * IMPORTANT: this is a load-bearing key on all platforms. We + * want to force `onLayout` to fire any time the thread params + * change so that `deferParents` is always reset to `false` once + * the anchor post is rendered. + * + * If we ever add additional thread params to this screen, they + * will need to be added here. + */ + , { ref: anchorRef, onLayout: function () { return setDeferParents(false); } }, item.uri + thread.state.view + thread.state.sort), _jsx(ThreadItemAnchor, { item: item, threadgateRecord: (_e = (_d = thread.data.threadgate) === null || _d === void 0 ? void 0 : _d.record) !== null && _e !== void 0 ? _e : undefined, onPostSuccess: optimisticOnPostReply, postSource: anchorPostSource })] })); + } + else { + if (thread.state.view === 'tree') { + return (_jsx(ThreadItemTreePost, { item: item, threadgateRecord: (_g = (_f = thread.data.threadgate) === null || _f === void 0 ? void 0 : _f.record) !== null && _g !== void 0 ? _g : undefined, overrides: { + moderation: thread.state.otherItemsVisible && item.depth > 0, + }, onPostSuccess: optimisticOnPostReply })); + } + else { + return (_jsx(ThreadItemPost, { item: item, threadgateRecord: (_j = (_h = thread.data.threadgate) === null || _h === void 0 ? void 0 : _h.record) !== null && _j !== void 0 ? _j : undefined, overrides: { + moderation: thread.state.otherItemsVisible && item.depth > 0, + }, onPostSuccess: optimisticOnPostReply })); + } + } + } + else if (item.type === 'threadPostNoUnauthenticated') { + if (item.depth < 0) { + return _jsx(ThreadItemPostNoUnauthenticated, { item: item }); + } + else if (item.depth === 0) { + return _jsx(ThreadItemAnchorNoUnauthenticated, {}); + } + } + else if (item.type === 'readMore') { + return (_jsx(ThreadItemReadMore, { item: item, view: thread.state.view === 'tree' ? 'tree' : 'linear' })); + } + else if (item.type === 'readMoreUp') { + return _jsx(ThreadItemReadMoreUp, { item: item }); + } + else if (item.type === 'threadPostBlocked') { + return _jsx(ThreadItemPostTombstone, { type: "blocked" }); + } + else if (item.type === 'threadPostNotFound') { + return _jsx(ThreadItemPostTombstone, { type: "not-found" }); + } + else if (item.type === 'replyComposer') { + return (_jsx(View, { children: gtMobile && (_jsx(ThreadComposePrompt, { onPressCompose: onReplyToAnchor })) })); + } + else if (item.type === 'showOtherReplies') { + return _jsx(ThreadItemShowOtherReplies, { onPress: item.onPress }); + } + else if (item.type === 'skeleton') { + if (item.item === 'anchor') { + return _jsx(ThreadItemAnchorSkeleton, {}); + } + else if (item.item === 'reply') { + if (thread.state.view === 'linear') { + return _jsx(ThreadItemPostSkeleton, { index: index }); + } + else { + return _jsx(ThreadItemTreePostSkeleton, { index: index }); + } + } + else if (item.item === 'replyComposer') { + return _jsx(ThreadItemReplyComposerSkeleton, {}); + } + } + return null; + }, [ + thread, + optimisticOnPostReply, + onReplyToAnchor, + gtMobile, + anchorPostSource, + ]); + var defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined; + return (_jsxs(PostThreadContextProvider, { context: thread.context, children: [_jsxs(Layout.Header.Outer, { headerRef: headerRef, children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { context: "description", children: "Post" }) }) }), _jsx(Layout.Header.Slot, { children: _jsx(HeaderDropdown, { sort: thread.state.sort, setSort: setSortWrapped, view: thread.state.view, setView: setViewWrapped }) })] }), thread.state.error ? (_jsx(ThreadError, { error: thread.state.error, onRetry: thread.actions.refetch })) : (_jsx(List, { ref: listRef, data: slices, renderItem: renderItem, keyExtractor: keyExtractor, onContentSizeChange: platform({ + web: onContentSizeChangeWebOnly, + default: onContentSizeChangeNativeOnly, + }), onStartReached: onStartReached, onEndReached: onEndReached, onEndReachedThreshold: 4, onStartReachedThreshold: 1, onItemSeen: function (item) { + // Track post:view for parent posts and replies (non-anchor posts) + if (item.type === 'threadPost' && item.depth !== 0) { + trackThreadItemView(item.value.post); + } + }, + /** + * NATIVE ONLY + * {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition} + */ + maintainVisibleContentPosition: { minIndexForVisible: 0 }, desktopFixedHeight: true, sideBorders: false, ListFooterComponent: _jsx(ListFooter + /* + * On native, if `deferParents` is true, we need some extra buffer to + * account for the `on*ReachedThreshold` values. + * + * Otherwise, and on web, this value needs to be the height of + * the viewport _minus_ a sensible min-post height e.g. 200, so + * that there's enough scroll remaining to get the anchor post + * back to the top of the screen when handling scroll. + */ + , { + /* + * On native, if `deferParents` is true, we need some extra buffer to + * account for the `on*ReachedThreshold` values. + * + * Otherwise, and on web, this value needs to be the height of + * the viewport _minus_ a sensible min-post height e.g. 200, so + * that there's enough scroll remaining to get the anchor post + * back to the top of the screen when handling scroll. + */ + height: platform({ + web: defaultListFooterHeight, + default: deferParents + ? windowHeight * 2 + : defaultListFooterHeight, + }), style: isTombstoneView ? { borderTopWidth: 0 } : undefined }), initialNumToRender: initialNumToRender, + /** + * Default: 21 + */ + windowSize: 7, + /** + * Default: 10 + */ + maxToRenderPerBatch: 5, + /** + * Default: 50 + */ + updateCellsBatchingPeriod: 100 })), !gtMobile && canReply && hasSession && (_jsx(MobileComposePrompt, { onPressReply: onReplyToAnchor }))] })); +} +function MobileComposePrompt(_a) { + var onPressReply = _a.onPressReply; + var footerHeight = useShellLayout().footerHeight; + var animatedStyle = useAnimatedStyle(function () { + return { + bottom: footerHeight.get(), + }; + }); + return (_jsx(Animated.View, { style: [a.fixed, a.left_0, a.right_0, animatedStyle], children: _jsx(ThreadComposePrompt, { onPressCompose: onPressReply }) })); +} +var keyExtractor = function (item) { + return item.key; +}; diff --git a/src/screens/Profile/ErrorState.js b/src/screens/Profile/ErrorState.js new file mode 100644 index 0000000000..f21a1f4212 --- /dev/null +++ b/src/screens/Profile/ErrorState.js @@ -0,0 +1,43 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { Text } from '#/components/Typography'; +export function ErrorState(_a) { + var error = _a.error; + var t = useTheme(); + var _ = useLingui()._; + var navigation = useNavigation(); + var onPressBack = React.useCallback(function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }, [navigation]); + return (_jsxs(View, { style: [a.px_xl], children: [_jsx(CircleInfo, { width: 48, style: [t.atoms.text_contrast_low] }), _jsx(Text, { style: [a.text_xl, a.font_semi_bold, a.pb_md, a.pt_xl], children: _jsx(Trans, { children: "Hmmmm, we couldn't load that moderation service." }) }), _jsx(Text, { style: [ + a.text_md, + a.leading_normal, + a.pb_md, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "This moderation service is unavailable. See below for more details. If this issue persists, contact us." }) }), _jsx(View, { style: [ + a.relative, + a.py_md, + a.px_lg, + a.rounded_md, + a.mb_2xl, + t.atoms.bg_contrast_25, + ], children: _jsx(Text, { style: [a.text_md, a.leading_normal], children: error }) }), _jsx(View, { style: { flexDirection: 'row' }, children: _jsx(Button, { size: "small", color: "secondary", variant: "solid", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go Back"], ["Go Back"])))), accessibilityHint: "Returns to previous page", onPress: onPressBack, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go Back" }) }) }) })] })); +} +var templateObject_1; diff --git a/src/screens/Profile/Header/DisplayName.js b/src/screens/Profile/Header/DisplayName.js new file mode 100644 index 0000000000..5d4b1a02a1 --- /dev/null +++ b/src/screens/Profile/Header/DisplayName.js @@ -0,0 +1,17 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Text } from '#/components/Typography'; +export function ProfileHeaderDisplayName(_a) { + var profile = _a.profile, moderation = _a.moderation; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + return (_jsx(View, { pointerEvents: "none", children: _jsx(Text, { emoji: true, testID: "profileHeaderDisplayName", style: [ + t.atoms.text, + gtMobile ? a.text_4xl : a.text_3xl, + a.self_start, + a.font_bold, + ], children: sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')) }) })); +} diff --git a/src/screens/Profile/Header/EditProfileDialog.js b/src/screens/Profile/Header/EditProfileDialog.js new file mode 100644 index 0000000000..ea374ac216 --- /dev/null +++ b/src/screens/Profile/Header/EditProfileDialog.js @@ -0,0 +1,243 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { urls } from '#/lib/constants'; +import { cleanError } from '#/lib/strings/errors'; +import { isOverMaxGraphemeCount } from '#/lib/strings/helpers'; +import { logger } from '#/logger'; +import { useProfileUpdateMutation } from '#/state/queries/profile'; +import { ErrorMessage } from '#/view/com/util/error/ErrorMessage'; +import * as Toast from '#/view/com/util/Toast'; +import { EditableUserAvatar } from '#/view/com/util/UserAvatar'; +import { UserBanner } from '#/view/com/util/UserBanner'; +import { atoms as a, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextField from '#/components/forms/TextField'; +import { InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +var DISPLAY_NAME_MAX_GRAPHEMES = 64; +var DESCRIPTION_MAX_GRAPHEMES = 256; +export function EditProfileDialog(_a) { + var profile = _a.profile, control = _a.control, onUpdate = _a.onUpdate; + var _ = useLingui()._; + var cancelControl = Dialog.useDialogControl(); + var _b = useState(false), dirty = _b[0], setDirty = _b[1]; + var height = useWindowDimensions().height; + var onPressCancel = useCallback(function () { + if (dirty) { + cancelControl.open(); + } + else { + control.close(); + } + }, [dirty, control, cancelControl]); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { + preventDismiss: dirty, + minHeight: height, + }, webOptions: { + onBackgroundPress: function () { + if (dirty) { + cancelControl.open(); + } + else { + control.close(); + } + }, + }, testID: "editProfileModal", children: [_jsx(DialogInner, { profile: profile, onUpdate: onUpdate, setDirty: setDirty, onPressCancel: onPressCancel }), _jsx(Prompt.Basic, { control: cancelControl, title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Discard changes?"], ["Discard changes?"])))), description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Are you sure you want to discard your changes?"], ["Are you sure you want to discard your changes?"])))), onConfirm: function () { return control.close(); }, confirmButtonCta: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Discard"], ["Discard"])))), confirmButtonColor: "negative" })] })); +} +function DialogInner(_a) { + var _this = this; + var profile = _a.profile, onUpdate = _a.onUpdate, setDirty = _a.setDirty, onPressCancel = _a.onPressCancel; + var _ = useLingui()._; + var t = useTheme(); + var control = Dialog.useDialogContext(); + var verification = useSimpleVerificationState({ + profile: profile, + }); + var _b = useProfileUpdateMutation(), updateProfileMutation = _b.mutateAsync, updateProfileError = _b.error, isUpdateProfileError = _b.isError, isUpdatingProfile = _b.isPending; + var _c = useState(''), imageError = _c[0], setImageError = _c[1]; + var initialDisplayName = profile.displayName || ''; + var _d = useState(initialDisplayName), displayName = _d[0], setDisplayName = _d[1]; + var initialDescription = profile.description || ''; + var _e = useState(initialDescription), description = _e[0], setDescription = _e[1]; + var _f = useState(profile.banner), userBanner = _f[0], setUserBanner = _f[1]; + var _g = useState(profile.avatar), userAvatar = _g[0], setUserAvatar = _g[1]; + var _h = useState(), newUserBanner = _h[0], setNewUserBanner = _h[1]; + var _j = useState(), newUserAvatar = _j[0], setNewUserAvatar = _j[1]; + var dirty = displayName !== initialDisplayName || + description !== initialDescription || + userAvatar !== profile.avatar || + userBanner !== profile.banner; + useEffect(function () { + setDirty(dirty); + }, [dirty, setDirty]); + var onSelectNewAvatar = useCallback(function (img) { + setImageError(''); + if (img === null) { + setNewUserAvatar(null); + setUserAvatar(null); + return; + } + try { + setNewUserAvatar(img); + setUserAvatar(img.path); + } + catch (e) { + setImageError(cleanError(e)); + } + }, [setNewUserAvatar, setUserAvatar, setImageError]); + var onSelectNewBanner = useCallback(function (img) { + setImageError(''); + if (!img) { + setNewUserBanner(null); + setUserBanner(null); + return; + } + try { + setNewUserBanner(img); + setUserBanner(img.path); + } + catch (e) { + setImageError(cleanError(e)); + } + }, [setNewUserBanner, setUserBanner, setImageError]); + var onPressSave = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setImageError(''); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, updateProfileMutation({ + profile: profile, + updates: { + displayName: displayName.trimEnd(), + description: description.trimEnd(), + }, + newUserAvatar: newUserAvatar, + newUserBanner: newUserBanner, + })]; + case 2: + _a.sent(); + control.close(function () { return onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate(); }); + Toast.show(_(msg({ message: 'Profile updated', context: 'toast' }))); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + logger.error('Failed to update user profile', { message: String(e_1) }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [ + updateProfileMutation, + profile, + onUpdate, + control, + displayName, + description, + newUserAvatar, + newUserBanner, + setImageError, + _, + ]); + var displayNameTooLong = isOverMaxGraphemeCount({ + text: displayName, + maxCount: DISPLAY_NAME_MAX_GRAPHEMES, + }); + var descriptionTooLong = isOverMaxGraphemeCount({ + text: description, + maxCount: DESCRIPTION_MAX_GRAPHEMES, + }); + var cancelButton = useCallback(function () { return (_jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: onPressCancel, size: "small", color: "primary", variant: "ghost", style: [a.rounded_full], testID: "editProfileCancelBtn", children: _jsx(ButtonText, { style: [a.text_md], children: _jsx(Trans, { children: "Cancel" }) }) })); }, [onPressCancel, _]); + var saveButton = useCallback(function () { return (_jsxs(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Save"], ["Save"])))), onPress: onPressSave, disabled: !dirty || + isUpdatingProfile || + displayNameTooLong || + descriptionTooLong, size: "small", color: "primary", variant: "ghost", style: [a.rounded_full], testID: "editProfileSaveBtn", children: [_jsx(ButtonText, { style: [a.text_md, !dirty && t.atoms.text_contrast_low], children: _jsx(Trans, { children: "Save" }) }), isUpdatingProfile && _jsx(ButtonIcon, { icon: Loader })] })); }, [ + _, + t, + dirty, + onPressSave, + isUpdatingProfile, + displayNameTooLong, + descriptionTooLong, + ]); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Edit profile"], ["Edit profile"])))), style: [a.overflow_hidden], contentContainerStyle: [a.px_0, a.pt_0], header: _jsx(Dialog.Header, { renderLeft: cancelButton, renderRight: saveButton, children: _jsx(Dialog.HeaderText, { children: _jsx(Trans, { children: "Edit profile" }) }) }), children: [_jsxs(View, { style: [a.relative], children: [_jsx(UserBanner, { banner: userBanner, onSelectNewBanner: onSelectNewBanner }), _jsx(View, { style: [ + a.absolute, + { + top: 80, + left: 20, + width: 84, + height: 84, + borderWidth: 2, + borderRadius: 42, + borderColor: t.atoms.bg.backgroundColor, + }, + ], children: _jsx(EditableUserAvatar, { size: 80, avatar: userAvatar, onSelectNewAvatar: onSelectNewAvatar }) })] }), isUpdateProfileError && (_jsx(View, { style: [a.mt_xl], children: _jsx(ErrorMessage, { message: cleanError(updateProfileError) }) })), imageError !== '' && (_jsx(View, { style: [a.mt_xl], children: _jsx(ErrorMessage, { message: imageError }) })), _jsxs(View, { style: [a.mt_4xl, a.px_xl, a.gap_xl], children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Display name" }) }), _jsx(TextField.Root, { isInvalid: displayNameTooLong, children: _jsx(Dialog.Input, { defaultValue: displayName, onChangeText: setDisplayName, label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Display name"], ["Display name"])))), placeholder: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["e.g. Alice Lastname"], ["e.g. Alice Lastname"])))), testID: "editProfileDisplayNameInput" }) }), displayNameTooLong && (_jsx(Text, { style: [ + a.text_sm, + a.mt_xs, + a.font_semi_bold, + { color: t.palette.negative_400 }, + ], children: _jsx(Plural, { value: DISPLAY_NAME_MAX_GRAPHEMES, other: "Display name is too long. The maximum number of characters is #." }) }))] }), verification.isVerified && + verification.role === 'default' && + displayName !== initialDisplayName && (_jsx(Admonition, { type: "error", children: _jsxs(Trans, { children: ["You are verified. You will lose your verification status if you change your display name.", ' ', _jsx(InlineLinkText, { label: _(msg({ + message: "Learn more", + context: "english-only-resource", + })), to: urls.website.blog.initialVerificationAnnouncement, children: _jsx(Trans, { context: "english-only-resource", children: "Learn more." }) })] }) })), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Description" }) }), _jsx(TextField.Root, { isInvalid: descriptionTooLong, children: _jsx(Dialog.Input, { defaultValue: description, onChangeText: setDescription, multiline: true, label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Description"], ["Description"])))), placeholder: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Tell us a bit about yourself"], ["Tell us a bit about yourself"])))), testID: "editProfileDescriptionInput" }) }), descriptionTooLong && (_jsx(Text, { style: [ + a.text_sm, + a.mt_xs, + a.font_semi_bold, + { color: t.palette.negative_400 }, + ], children: _jsx(Plural, { value: DESCRIPTION_MAX_GRAPHEMES, other: "Description is too long. The maximum number of characters is #." }) }))] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10; diff --git a/src/screens/Profile/Header/GrowableAvatar.js b/src/screens/Profile/Header/GrowableAvatar.js new file mode 100644 index 0000000000..07d2c73099 --- /dev/null +++ b/src/screens/Profile/Header/GrowableAvatar.js @@ -0,0 +1,28 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import Animated, { Extrapolation, interpolate, useAnimatedStyle, } from 'react-native-reanimated'; +import { usePagerHeaderContext } from '#/view/com/pager/PagerHeaderContext'; +import { IS_IOS } from '#/env'; +export function GrowableAvatar(_a) { + var children = _a.children, style = _a.style; + var pagerContext = usePagerHeaderContext(); + // pagerContext should only be present on iOS, but better safe than sorry + if (!pagerContext || !IS_IOS) { + return _jsx(View, { style: style, children: children }); + } + var scrollY = pagerContext.scrollY; + return (_jsx(GrowableAvatarInner, { scrollY: scrollY, style: style, children: children })); +} +function GrowableAvatarInner(_a) { + var scrollY = _a.scrollY, children = _a.children, style = _a.style; + var animatedStyle = useAnimatedStyle(function () { return ({ + transform: [ + { + scale: interpolate(scrollY.get(), [-150, 0], [1.2, 1], { + extrapolateRight: Extrapolation.CLAMP, + }), + }, + ], + }); }); + return (_jsx(Animated.View, { style: [style, { transformOrigin: 'bottom left' }, animatedStyle], children: children })); +} diff --git a/src/screens/Profile/Header/GrowableBanner.js b/src/screens/Profile/Header/GrowableBanner.js new file mode 100644 index 0000000000..29409d39aa --- /dev/null +++ b/src/screens/Profile/Header/GrowableBanner.js @@ -0,0 +1,124 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useEffect, useState } from 'react'; +import { ActivityIndicator, Pressable, View } from 'react-native'; +import Animated, { Extrapolation, interpolate, runOnJS, useAnimatedProps, useAnimatedReaction, useAnimatedStyle, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { BlurView } from 'expo-blur'; +import { useIsFetching } from '@tanstack/react-query'; +import { RQKEY_ROOT as STARTERPACK_RQKEY_ROOT } from '#/state/queries/actor-starter-packs'; +import { RQKEY_ROOT as FEED_RQKEY_ROOT } from '#/state/queries/post-feed'; +import { RQKEY_ROOT as FEEDGEN_RQKEY_ROOT } from '#/state/queries/profile-feedgens'; +import { RQKEY_ROOT as LIST_RQKEY_ROOT } from '#/state/queries/profile-lists'; +import { usePagerHeaderContext } from '#/view/com/pager/PagerHeaderContext'; +import { atoms as a } from '#/alf'; +import { IS_IOS } from '#/env'; +var AnimatedBlurView = Animated.createAnimatedComponent(BlurView); +export function GrowableBanner(_a) { + var backButton = _a.backButton, children = _a.children, onPress = _a.onPress, bannerRef = _a.bannerRef; + var pagerContext = usePagerHeaderContext(); + // plain non-growable mode for Android/Web + if (!pagerContext || !IS_IOS) { + return (_jsxs(Pressable, { onPress: onPress, accessibilityRole: "image", style: [a.w_full, a.h_full], children: [_jsx(Animated.View, { ref: bannerRef, style: [a.w_full, a.h_full], children: children }), backButton] })); + } + var scrollY = pagerContext.scrollY; + return (_jsx(GrowableBannerInner, { scrollY: scrollY, backButton: backButton, onPress: onPress, bannerRef: bannerRef, children: children })); +} +function GrowableBannerInner(_a) { + var scrollY = _a.scrollY, backButton = _a.backButton, children = _a.children, onPress = _a.onPress, bannerRef = _a.bannerRef; + var topInset = useSafeAreaInsets().top; + var isFetching = useIsProfileFetching(); + var animateSpinner = useShouldAnimateSpinner({ isFetching: isFetching, scrollY: scrollY }); + var animatedStyle = useAnimatedStyle(function () { return ({ + transform: [ + { + scale: interpolate(scrollY.get(), [-150, 0], [2, 1], { + extrapolateRight: Extrapolation.CLAMP, + }), + }, + ], + }); }); + var animatedBlurViewProps = useAnimatedProps(function () { + return { + intensity: interpolate(scrollY.get(), [-300, -65, -15], [50, 40, 0], Extrapolation.CLAMP), + }; + }); + var animatedSpinnerStyle = useAnimatedStyle(function () { + var scrollYValue = scrollY.get(); + return { + display: scrollYValue < 0 ? 'flex' : 'none', + opacity: interpolate(scrollYValue, [-60, -15], [1, 0], Extrapolation.CLAMP), + transform: [ + { translateY: interpolate(scrollYValue, [-150, 0], [-75, 0]) }, + { rotate: '90deg' }, + ], + }; + }); + var animatedBackButtonStyle = useAnimatedStyle(function () { return ({ + transform: [ + { + translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], { + extrapolateRight: Extrapolation.CLAMP, + }), + }, + ], + }); }); + return (_jsxs(_Fragment, { children: [_jsxs(Animated.View, { style: [ + a.absolute, + { left: 0, right: 0, bottom: 0 }, + { height: 150 }, + { transformOrigin: 'bottom' }, + animatedStyle, + ], children: [_jsx(Pressable, { onPress: onPress, accessibilityRole: "image", style: [a.w_full, a.h_full], children: _jsx(Animated.View, { ref: bannerRef, collapsable: false, style: [a.w_full, a.h_full], children: children }) }), _jsx(AnimatedBlurView, { pointerEvents: "none", style: [a.absolute, a.inset_0], tint: "dark", animatedProps: animatedBlurViewProps })] }), _jsx(View, { pointerEvents: "none", style: [ + a.absolute, + a.inset_0, + { top: topInset - (IS_IOS ? 15 : 0) }, + a.justify_center, + a.align_center, + ], children: _jsx(Animated.View, { style: [animatedSpinnerStyle], children: _jsx(ActivityIndicator, { size: "large", color: "white", animating: animateSpinner, hidesWhenStopped: false }, animateSpinner ? 'spin' : 'stop') }) }), _jsx(Animated.View, { style: [animatedBackButtonStyle], children: backButton })] })); +} +function useIsProfileFetching() { + // are any of the profile-related queries fetching? + return [ + useIsFetching({ queryKey: [FEED_RQKEY_ROOT] }), + useIsFetching({ queryKey: [FEEDGEN_RQKEY_ROOT] }), + useIsFetching({ queryKey: [LIST_RQKEY_ROOT] }), + useIsFetching({ queryKey: [STARTERPACK_RQKEY_ROOT] }), + ].some(function (isFetching) { return isFetching; }); +} +function useShouldAnimateSpinner(_a) { + var isFetching = _a.isFetching, scrollY = _a.scrollY; + var _b = useState(false), isOverscrolled = _b[0], setIsOverscrolled = _b[1]; + // HACK: it reports a scroll pos of 0 for a tick when fetching finishes + // so paper over that by keeping it true for a bit -sfn + var stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10); + useAnimatedReaction(function () { return scrollY.get() < -5; }, function (value, prevValue) { + if (value !== prevValue) { + runOnJS(setIsOverscrolled)(value); + } + }, [scrollY]); + var _c = useState(isFetching), isAnimating = _c[0], setIsAnimating = _c[1]; + if (isFetching && !isAnimating) { + setIsAnimating(true); + } + if (!isFetching && isAnimating && !stickyIsOverscrolled) { + setIsAnimating(false); + } + return isAnimating; +} +// stayed true for at least `delay` ms before returning to false +function useStickyToggle(value, delay) { + var _a = useState(value), prevValue = _a[0], setPrevValue = _a[1]; + var _b = useState(false), isSticking = _b[0], setIsSticking = _b[1]; + useEffect(function () { + if (isSticking) { + var timeout_1 = setTimeout(function () { return setIsSticking(false); }, delay); + return function () { return clearTimeout(timeout_1); }; + } + }, [isSticking, delay]); + if (value !== prevValue) { + setIsSticking(prevValue); // Going true -> false should stick. + setPrevValue(value); + return prevValue ? true : value; + } + return isSticking ? true : value; +} diff --git a/src/screens/Profile/Header/Handle.js b/src/screens/Profile/Header/Handle.js new file mode 100644 index 0000000000..f72f23c7f0 --- /dev/null +++ b/src/screens/Profile/Header/Handle.js @@ -0,0 +1,44 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { isInvalidHandle, sanitizeHandle } from '#/lib/strings/handles'; +import { atoms as a, useTheme, web } from '#/alf'; +import { NewskieDialog } from '#/components/NewskieDialog'; +import { Text } from '#/components/Typography'; +import { IS_IOS, IS_NATIVE } from '#/env'; +export function ProfileHeaderHandle(_a) { + var _b, _c, _d; + var profile = _a.profile, disableTaps = _a.disableTaps; + var t = useTheme(); + var _ = useLingui()._; + var invalidHandle = isInvalidHandle(profile.handle); + var blockHide = ((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.blocking) || ((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.blockedBy); + return (_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center, { maxWidth: '100%' }], pointerEvents: disableTaps ? 'none' : IS_IOS ? 'auto' : 'box-none', children: [_jsx(NewskieDialog, { profile: profile, disabled: disableTaps }), ((_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.followedBy) && !blockHide ? (_jsx(View, { style: [t.atoms.bg_contrast_50, a.rounded_xs, a.px_sm, a.py_xs], children: _jsx(Text, { style: [t.atoms.text, a.text_sm], children: _jsx(Trans, { children: "Follows you" }) }) })) : undefined, _jsx(Text, { emoji: true, numberOfLines: 1, style: [ + invalidHandle + ? [ + a.border, + a.text_xs, + a.px_sm, + a.py_xs, + a.rounded_xs, + { borderColor: t.palette.contrast_200 }, + ] + : [a.text_md, a.leading_snug, t.atoms.text_contrast_medium], + web({ + wordBreak: 'break-all', + direction: 'ltr', + unicodeBidi: 'isolate', + }), + ], children: invalidHandle + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\u26A0Invalid Handle"], ["\u26A0Invalid Handle"])))) + : sanitizeHandle(profile.handle, '@', + // forceLTR handled by CSS above on web + IS_NATIVE) })] })); +} +var templateObject_1; diff --git a/src/screens/Profile/Header/Metrics.js b/src/screens/Profile/Header/Metrics.js new file mode 100644 index 0000000000..f21468fa96 --- /dev/null +++ b/src/screens/Profile/Header/Metrics.js @@ -0,0 +1,30 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg, plural } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { makeProfileLink } from '#/lib/routes/links'; +import { formatCount } from '#/view/com/util/numeric/format'; +import { atoms as a, useTheme } from '#/alf'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export function ProfileHeaderMetrics(_a) { + var profile = _a.profile; + var t = useTheme(); + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var following = formatCount(i18n, profile.followsCount || 0); + var followers = formatCount(i18n, profile.followersCount || 0); + var pluralizedFollowers = plural(profile.followersCount || 0, { + one: 'follower', + other: 'followers', + }); + var pluralizedFollowings = plural(profile.followsCount || 0, { + one: 'following', + other: 'following', + }); + return (_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center], pointerEvents: "box-none", children: [_jsxs(InlineLinkText, { testID: "profileHeaderFollowersButton", style: [a.flex_row, t.atoms.text], to: makeProfileLink(profile, 'followers'), label: "".concat(profile.followersCount || 0, " ").concat(pluralizedFollowers), children: [_jsxs(Text, { style: [a.font_semi_bold, a.text_md], children: [followers, " "] }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.text_md], children: pluralizedFollowers })] }), _jsxs(InlineLinkText, { testID: "profileHeaderFollowsButton", style: [a.flex_row, t.atoms.text], to: makeProfileLink(profile, 'follows'), label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["", " following"], ["", " following"])), profile.followsCount || 0)), children: [_jsxs(Text, { style: [a.font_semi_bold, a.text_md], children: [following, " "] }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.text_md], children: pluralizedFollowings })] }), _jsxs(Text, { style: [a.font_semi_bold, t.atoms.text, a.text_md], children: [formatCount(i18n, profile.postsCount || 0), ' ', _jsx(Text, { style: [t.atoms.text_contrast_medium, a.font_normal, a.text_md], children: plural(profile.postsCount || 0, { one: 'post', other: 'posts' }) })] })] })); +} +var templateObject_1; diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.js b/src/screens/Profile/Header/ProfileHeaderLabeler.js new file mode 100644 index 0000000000..c00149407c --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.js @@ -0,0 +1,233 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { moderateProfile, } from '@atproto/api'; +import { msg, plural } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useHaptics } from '#/lib/haptics'; +import { isAppLabeler } from '#/lib/moderation'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useLabelerSubscriptionMutation } from '#/state/queries/labeler'; +import { useLikeMutation, useUnlikeMutation } from '#/state/queries/like'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useRequireAuth, useSession } from '#/state/session'; +import { ProfileMenu } from '#/view/com/profile/ProfileMenu'; +import { atoms as a, tokens, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, Heart2_Stroke2_Corner0_Rounded as Heart, } from '#/components/icons/Heart2'; +import { Link } from '#/components/Link'; +import * as Prompt from '#/components/Prompt'; +import { RichText } from '#/components/RichText'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS } from '#/env'; +import { ProfileHeaderDisplayName } from './DisplayName'; +import { EditProfileDialog } from './EditProfileDialog'; +import { ProfileHeaderHandle } from './Handle'; +import { ProfileHeaderMetrics } from './Metrics'; +import { ProfileHeaderShell } from './Shell'; +var ProfileHeaderLabeler = function (_a) { + var _b; + var profileUnshadowed = _a.profile, labeler = _a.labeler, descriptionRT = _a.descriptionRT, moderationOpts = _a.moderationOpts, _c = _a.hideBackButton, hideBackButton = _c === void 0 ? false : _c, isPlaceholderProfile = _a.isPlaceholderProfile; + var profile = useProfileShadow(profileUnshadowed); + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var _d = useSession(), currentAccount = _d.currentAccount, hasSession = _d.hasSession; + var playHaptic = useHaptics(); + var isSelf = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did; + var moderation = useMemo(function () { return moderateProfile(profile, moderationOpts); }, [profile, moderationOpts]); + var _e = useLikeMutation(), likeMod = _e.mutateAsync, isLikePending = _e.isPending; + var _f = useUnlikeMutation(), unlikeMod = _f.mutateAsync, isUnlikePending = _f.isPending; + var _g = useState(((_b = labeler.viewer) === null || _b === void 0 ? void 0 : _b.like) || ''), likeUri = _g[0], setLikeUri = _g[1]; + var _h = useState(labeler.likeCount || 0), likeCount = _h[0], setLikeCount = _h[1]; + var onToggleLiked = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { + var res, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!labeler) { + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 6, , 7]); + playHaptic(); + if (!likeUri) return [3 /*break*/, 3]; + return [4 /*yield*/, unlikeMod({ uri: likeUri })]; + case 2: + _a.sent(); + setLikeCount(function (c) { return c - 1; }); + setLikeUri(''); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, likeMod({ uri: labeler.uri, cid: labeler.cid })]; + case 4: + res = _a.sent(); + setLikeCount(function (c) { return c + 1; }); + setLikeUri(res.uri); + _a.label = 5; + case 5: return [3 /*break*/, 7]; + case 6: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["There was an issue contacting the server, please check your internet connection and try again."], ["There was an issue contacting the server, please check your internet connection and try again."])))), { type: 'error' }); + ax.logger.error("Failed to toggle labeler like", { message: e_1.message }); + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); }, [ax, labeler, playHaptic, likeUri, unlikeMod, likeMod, _]); + return (_jsx(ProfileHeaderShell, { profile: profile, moderation: moderation, hideBackButton: hideBackButton, isPlaceholderProfile: isPlaceholderProfile, children: _jsxs(View, { style: [a.px_lg, a.pt_md, a.pb_sm], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: [_jsx(View, { style: [a.flex_row, a.justify_end, a.align_center, a.gap_xs, a.pb_lg], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: _jsx(HeaderLabelerButtons, { profile: profile }) }), _jsxs(View, { style: [a.flex_col, a.gap_2xs, a.pt_2xs, a.pb_md], children: [_jsx(ProfileHeaderDisplayName, { profile: profile, moderation: moderation }), _jsx(ProfileHeaderHandle, { profile: profile })] }), !isPlaceholderProfile && (_jsxs(_Fragment, { children: [isSelf && _jsx(ProfileHeaderMetrics, { profile: profile }), descriptionRT && !moderation.ui('profileView').blur ? (_jsx(View, { pointerEvents: "auto", children: _jsx(RichText, { testID: "profileHeaderDescription", style: [a.text_md], numberOfLines: 15, value: descriptionRT, enableTags: true, authorHandle: profile.handle }) })) : undefined, !isAppLabeler(profile.did) && (_jsxs(View, { style: [a.flex_row, a.gap_xs, a.align_center, a.pt_lg], children: [_jsx(Button, { testID: "toggleLikeBtn", size: "small", color: "secondary", shape: "round", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Like this labeler"], ["Like this labeler"])))), disabled: !hasSession || isLikePending || isUnlikePending, onPress: onToggleLiked, children: likeUri ? (_jsx(HeartFilled, { fill: t.palette.negative_400 })) : (_jsx(Heart, { fill: t.atoms.text_contrast_medium.color })) }), typeof likeCount === 'number' && (_jsx(Link, { to: { + screen: 'ProfileLabelerLikedBy', + params: { + name: labeler.creator.handle || labeler.creator.did, + }, + }, size: "tiny", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Liked by ", ""], ["Liked by ", ""])), plural(likeCount, { + one: '# user', + other: '# users', + }))), children: function (_a) { + var hovered = _a.hovered, focused = _a.focused, pressed = _a.pressed; + return (_jsx(Text, { style: [ + a.font_semi_bold, + a.text_sm, + t.atoms.text_contrast_medium, + (hovered || focused || pressed) && + t.atoms.text_contrast_high, + ], children: _jsxs(Trans, { children: ["Liked by", ' ', _jsx(Plural, { value: likeCount, one: "# user", other: "# users" })] }) })); + } }))] }))] }))] }) })); +}; +ProfileHeaderLabeler = memo(ProfileHeaderLabeler); +export { ProfileHeaderLabeler }; +/** + * Keep this in sync with the value of {@link MAX_LABELERS} + */ +function CantSubscribePrompt(_a) { + var control = _a.control; + var _ = useLingui()._; + return (_jsxs(Prompt.Outer, { control: control, children: [_jsxs(Prompt.Content, { children: [_jsx(Prompt.TitleText, { children: "Unable to subscribe" }), _jsx(Prompt.DescriptionText, { children: _jsx(Trans, { children: "We're sorry! You can only subscribe to twenty labelers, and you've reached your limit of twenty." }) })] }), _jsx(Prompt.Actions, { children: _jsx(Prompt.Action, { onPress: function () { return control.close(); }, cta: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["OK"], ["OK"])))) }) })] })); +} +export function HeaderLabelerButtons(_a) { + var _this = this; + var _b; + var profile = _a.profile, _c = _a.minimal, minimal = _c === void 0 ? false : _c; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var requireAuth = useRequireAuth(); + var playHaptic = useHaptics(); + var editProfileControl = useDialogControl(); + var preferences = usePreferencesQuery().data; + var _d = useLabelerSubscriptionMutation(), toggleSubscription = _d.mutateAsync, variables = _d.variables, reset = _d.reset; + var isSubscribed = (_b = variables === null || variables === void 0 ? void 0 : variables.subscribe) !== null && _b !== void 0 ? _b : preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs.labelers.find(function (l) { return l.did === profile.did; }); + var cantSubscribePrompt = Prompt.usePromptControl(); + var isMe = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did; + var onPressSubscribe = function () { + return requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + var subscribe, e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + playHaptic(); + subscribe = !isSubscribed; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, toggleSubscription({ + did: profile.did, + subscribe: subscribe, + })]; + case 2: + _a.sent(); + ax.metric(subscribe + ? 'moderation:subscribedToLabeler' + : 'moderation:unsubscribedFromLabeler', {}); + return [3 /*break*/, 4]; + case 3: + e_2 = _a.sent(); + reset(); + if (e_2.message === 'MAX_LABELERS') { + cantSubscribePrompt.open(); + return [2 /*return*/]; + } + ax.logger.error("Failed to subscribe to labeler", { message: e_2.message }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }); + }; + return (_jsxs(_Fragment, { children: [isMe ? (_jsxs(_Fragment, { children: [_jsx(Button, { testID: "profileHeaderEditProfileButton", size: "small", color: "secondary", onPress: editProfileControl.open, label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Edit profile"], ["Edit profile"])))), style: a.rounded_full, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Edit Profile" }) }) }), _jsx(EditProfileDialog, { profile: profile, control: editProfileControl })] })) : !isAppLabeler(profile.did) && !minimal ? ( + // hidden in the minimal header, because it's not shadowed so the two buttons + // can get out of sync. if you want to reenable, you'll need to add shadowing + // to the subscribed state -sfn + _jsx(Button, { testID: "toggleSubscribeBtn", label: isSubscribed + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Unsubscribe from this labeler"], ["Unsubscribe from this labeler"])))) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Subscribe to this labeler"], ["Subscribe to this labeler"])))), onPress: onPressSubscribe, children: function (state) { return (_jsx(View, { style: [ + { + paddingVertical: 9, + paddingHorizontal: 12, + borderRadius: 6, + gap: 6, + backgroundColor: isSubscribed + ? state.hovered || state.pressed + ? t.palette.contrast_50 + : t.palette.contrast_25 + : state.hovered || state.pressed + ? tokens.color.temp_purple_dark + : tokens.color.temp_purple, + }, + ], children: _jsx(Text, { style: [ + { + color: isSubscribed + ? t.palette.contrast_700 + : t.palette.white, + }, + a.font_semi_bold, + a.text_center, + a.leading_tight, + ], children: isSubscribed ? (_jsx(Trans, { children: "Unsubscribe" })) : (_jsx(Trans, { children: "Subscribe to Labeler" })) }) })); } })) : null, _jsx(ProfileMenu, { profile: profile }), _jsx(CantSubscribePrompt, { control: cantSubscribePrompt })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.js b/src/screens/Profile/Header/ProfileHeaderStandard.js new file mode 100644 index 0000000000..220c61bb7e --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderStandard.js @@ -0,0 +1,247 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { moderateProfile, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useHaptics } from '#/lib/haptics'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useProfileBlockMutationQueue, useProfileFollowMutationQueue, } from '#/state/queries/profile'; +import { useRequireAuth, useSession } from '#/state/session'; +import { ProfileMenu } from '#/view/com/profile/ProfileMenu'; +import { atoms as a, platform, useBreakpoints, useTheme } from '#/alf'; +import { SubscribeProfileButton } from '#/components/activity-notifications/SubscribeProfileButton'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { DebugFieldDisplay } from '#/components/DebugFieldDisplay'; +import { useDialogControl } from '#/components/Dialog'; +import { MessageProfileButton } from '#/components/dms/MessageProfileButton'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { KnownFollowers, shouldShowKnownFollowers, } from '#/components/KnownFollowers'; +import * as Prompt from '#/components/Prompt'; +import { RichText } from '#/components/RichText'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { VerificationCheckButton } from '#/components/verification/VerificationCheckButton'; +import { IS_IOS } from '#/env'; +import { useActorStatus } from '#/features/liveNow'; +import { GermButton } from '../components/GermButton'; +import { EditProfileDialog } from './EditProfileDialog'; +import { ProfileHeaderHandle } from './Handle'; +import { ProfileHeaderMetrics } from './Metrics'; +import { ProfileHeaderShell } from './Shell'; +import { ProfileHeaderSuggestedFollows } from './SuggestedFollows'; +var ProfileHeaderStandard = function (_a) { + var _b, _c, _d, _e, _f, _g; + var profileUnshadowed = _a.profile, descriptionRT = _a.descriptionRT, moderationOpts = _a.moderationOpts, _h = _a.hideBackButton, hideBackButton = _h === void 0 ? false : _h, isPlaceholderProfile = _a.isPlaceholderProfile; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var profile = useProfileShadow(profileUnshadowed); + var currentAccount = useSession().currentAccount; + var _ = useLingui()._; + var moderation = useMemo(function () { return moderateProfile(profile, moderationOpts); }, [profile, moderationOpts]); + var _j = useProfileBlockMutationQueue(profile), queueUnblock = _j[1]; + var unblockPromptControl = Prompt.usePromptControl(); + var _k = useState(false), showSuggestedFollows = _k[0], setShowSuggestedFollows = _k[1]; + var isBlockedUser = ((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.blocking) || + ((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.blockedBy) || + ((_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.blockingByList); + var unblockAccount = function () { return __awaiter(void 0, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueUnblock()]; + case 1: + _a.sent(); + Toast.show(_(msg({ message: 'Account unblocked', context: 'toast' }))); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + if ((e_1 === null || e_1 === void 0 ? void 0 : e_1.name) !== 'AbortError') { + logger.error('Failed to unblock account', { message: e_1 }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_1.toString())), { type: 'error' }); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var isMe = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did; + var live = useActorStatus(profile).isActive; + return (_jsxs(_Fragment, { children: [_jsxs(ProfileHeaderShell, { profile: profile, moderation: moderation, hideBackButton: hideBackButton, isPlaceholderProfile: isPlaceholderProfile, children: [_jsxs(View, { style: [a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: [_jsx(View, { style: [ + { paddingLeft: 90 }, + a.flex_row, + a.align_center, + a.justify_end, + a.gap_xs, + a.pb_sm, + a.flex_wrap, + ], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: _jsx(HeaderStandardButtons, { profile: profile, moderation: moderation, moderationOpts: moderationOpts, onFollow: function () { return setShowSuggestedFollows(true); }, onUnfollow: function () { return setShowSuggestedFollows(false); } }) }), _jsxs(View, { style: [a.flex_col, a.gap_xs, a.pb_sm, live ? a.pt_sm : a.pt_2xs], children: [_jsx(View, { style: [a.flex_row, a.align_center, a.gap_xs, a.flex_1], children: _jsxs(Text, { emoji: true, testID: "profileHeaderDisplayName", style: [ + t.atoms.text, + gtMobile ? a.text_4xl : a.text_3xl, + a.self_start, + a.font_bold, + a.leading_tight, + ], children: [sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')), _jsx(View, { style: [a.pl_xs, { marginTop: platform({ ios: 2 }) }], children: _jsx(VerificationCheckButton, { profile: profile, size: "lg" }) })] }) }), _jsx(ProfileHeaderHandle, { profile: profile })] }), !isPlaceholderProfile && !isBlockedUser && (_jsxs(View, { style: a.gap_md, children: [_jsx(ProfileHeaderMetrics, { profile: profile }), descriptionRT && !moderation.ui('profileView').blur ? (_jsx(View, { pointerEvents: "auto", children: _jsx(RichText, { testID: "profileHeaderDescription", style: [a.text_md], numberOfLines: 15, selectable: true, value: descriptionRT, enableTags: true, authorHandle: profile.handle }) })) : undefined, ((_e = profile.associated) === null || _e === void 0 ? void 0 : _e.germ) && (_jsx(GermButton, { germ: profile.associated.germ, profile: profile })), !isMe && + !isBlockedUser && + shouldShowKnownFollowers((_f = profile.viewer) === null || _f === void 0 ? void 0 : _f.knownFollowers) && (_jsx(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: _jsx(KnownFollowers, { profile: profile, moderationOpts: moderationOpts }) }))] })), _jsx(DebugFieldDisplay, { subject: profile })] }), _jsx(Prompt.Basic, { control: unblockPromptControl, title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Unblock Account?"], ["Unblock Account?"])))), description: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["The account will be able to interact with you after unblocking."], ["The account will be able to interact with you after unblocking."])))), onConfirm: unblockAccount, confirmButtonCta: ((_g = profile.viewer) === null || _g === void 0 ? void 0 : _g.blocking) ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Unblock"], ["Unblock"])))) : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Block"], ["Block"])))), confirmButtonColor: "negative" })] }), _jsx(ProfileHeaderSuggestedFollows, { isExpanded: showSuggestedFollows, actorDid: profile.did })] })); +}; +ProfileHeaderStandard = memo(ProfileHeaderStandard); +export { ProfileHeaderStandard }; +export function HeaderStandardButtons(_a) { + var _this = this; + var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; + var profile = _a.profile, moderation = _a.moderation, moderationOpts = _a.moderationOpts, onFollow = _a.onFollow, onUnfollow = _a.onUnfollow, minimal = _a.minimal; + var _ = useLingui()._; + var _p = useSession(), hasSession = _p.hasSession, currentAccount = _p.currentAccount; + var playHaptic = useHaptics(); + var requireAuth = useRequireAuth(); + var _q = useProfileFollowMutationQueue(profile, 'ProfileHeader'), queueFollow = _q[0], queueUnfollow = _q[1]; + var _r = useProfileBlockMutationQueue(profile), queueUnblock = _r[1]; + var editProfileControl = useDialogControl(); + var unblockPromptControl = Prompt.usePromptControl(); + var isMe = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did; + var onPressFollow = function () { + playHaptic(); + requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + var e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueFollow()]; + case 1: + _a.sent(); + onFollow === null || onFollow === void 0 ? void 0 : onFollow(); + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Following ", ""], ["Following ", ""])), sanitizeDisplayName(profile.displayName || profile.handle, moderation.ui('displayName'))))); + return [3 /*break*/, 3]; + case 2: + e_2 = _a.sent(); + if ((e_2 === null || e_2 === void 0 ? void 0 : e_2.name) !== 'AbortError') { + logger.error('Failed to follow', { message: String(e_2) }); + Toast.show(_(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_2.toString())), { + type: 'error', + }); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }); + }; + var onPressUnfollow = function () { + playHaptic(); + requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + var e_3; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueUnfollow()]; + case 1: + _a.sent(); + onUnfollow === null || onUnfollow === void 0 ? void 0 : onUnfollow(); + Toast.show(_(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["No longer following ", ""], ["No longer following ", ""])), sanitizeDisplayName(profile.displayName || profile.handle, moderation.ui('displayName')))), { type: 'default' }); + return [3 /*break*/, 3]; + case 2: + e_3 = _a.sent(); + if ((e_3 === null || e_3 === void 0 ? void 0 : e_3.name) !== 'AbortError') { + logger.error('Failed to unfollow', { message: String(e_3) }); + Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_3.toString())), { + type: 'error', + }); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }); + }; + var unblockAccount = function () { return __awaiter(_this, void 0, void 0, function () { + var e_4; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, queueUnblock()]; + case 1: + _a.sent(); + Toast.show(_(msg({ message: 'Account unblocked', context: 'toast' }))); + return [3 /*break*/, 3]; + case 2: + e_4 = _a.sent(); + if ((e_4 === null || e_4 === void 0 ? void 0 : e_4.name) !== 'AbortError') { + logger.error('Failed to unblock account', { message: e_4 }); + Toast.show(_(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_4.toString())), { type: 'error' }); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var subscriptionsAllowed = useMemo(function () { + var _a, _b, _c, _d; + switch ((_b = (_a = profile.associated) === null || _a === void 0 ? void 0 : _a.activitySubscription) === null || _b === void 0 ? void 0 : _b.allowSubscriptions) { + case 'followers': + case undefined: + return !!((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.following); + case 'mutuals': + return !!((_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.following) && !!profile.viewer.followedBy; + case 'none': + default: + return false; + } + }, [profile]); + return (_jsxs(_Fragment, { children: [isMe ? (_jsxs(_Fragment, { children: [_jsx(Button, { testID: "profileHeaderEditProfileButton", size: "small", color: "secondary", onPress: function () { + playHaptic('Light'); + editProfileControl.open(); + }, label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Edit profile"], ["Edit profile"])))), children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Edit Profile" }) }) }), _jsx(EditProfileDialog, { profile: profile, control: editProfileControl })] })) : ((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.blocking) ? (((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.blockingByList) ? null : (_jsx(Button, { testID: "unblockBtn", size: "small", color: "secondary", label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Unblock"], ["Unblock"])))), disabled: !hasSession, onPress: function () { return unblockPromptControl.open(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { context: "action", children: "Unblock" }) }) }))) : !((_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.blockedBy) ? (_jsxs(_Fragment, { children: [hasSession && (!minimal || ((_e = profile.viewer) === null || _e === void 0 ? void 0 : _e.following)) && (_jsxs(_Fragment, { children: [subscriptionsAllowed && (_jsx(SubscribeProfileButton, { profile: profile, moderationOpts: moderationOpts, disableHint: minimal })), _jsx(MessageProfileButton, { profile: profile })] })), (!minimal || !((_f = profile.viewer) === null || _f === void 0 ? void 0 : _f.following)) && (_jsxs(Button, { testID: ((_g = profile.viewer) === null || _g === void 0 ? void 0 : _g.following) ? 'unfollowBtn' : 'followBtn', size: "small", color: ((_h = profile.viewer) === null || _h === void 0 ? void 0 : _h.following) ? 'secondary' : 'primary', label: ((_j = profile.viewer) === null || _j === void 0 ? void 0 : _j.following) + ? _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Unfollow ", ""], ["Unfollow ", ""])), profile.handle)) + : _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Follow ", ""], ["Follow ", ""])), profile.handle)), onPress: ((_k = profile.viewer) === null || _k === void 0 ? void 0 : _k.following) ? onPressUnfollow : onPressFollow, children: [!((_l = profile.viewer) === null || _l === void 0 ? void 0 : _l.following) && _jsx(ButtonIcon, { icon: Plus }), _jsx(ButtonText, { children: ((_m = profile.viewer) === null || _m === void 0 ? void 0 : _m.following) ? (_jsx(Trans, { children: "Following" })) : ((_o = profile.viewer) === null || _o === void 0 ? void 0 : _o.followedBy) ? (_jsx(Trans, { children: "Follow back" })) : (_jsx(Trans, { children: "Follow" })) })] }))] })) : null, _jsx(ProfileMenu, { profile: profile }), _jsx(Prompt.Basic, { control: unblockPromptControl, title: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Unblock Account?"], ["Unblock Account?"])))), description: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["The account will be able to interact with you after unblocking."], ["The account will be able to interact with you after unblocking."])))), onConfirm: unblockAccount, confirmButtonCta: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Unblock"], ["Unblock"])))), confirmButtonColor: "negative" })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17; diff --git a/src/screens/Profile/Header/Shell.js b/src/screens/Profile/Header/Shell.js new file mode 100644 index 0000000000..2b52d222c9 --- /dev/null +++ b/src/screens/Profile/Header/Shell.js @@ -0,0 +1,187 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { memo, useCallback, useEffect, useMemo } from 'react'; +import { Pressable, View } from 'react-native'; +import Animated, { measure, runOnJS, runOnUI, useAnimatedRef, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { utils } from '@bsky.app/alf'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { BACK_HITSLOP } from '#/lib/constants'; +import { useHaptics } from '#/lib/haptics'; +import { useLightboxControls } from '#/state/lightbox'; +import { useSession } from '#/state/session'; +import { LoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { UserBanner } from '#/view/com/util/UserBanner'; +import { atoms as a, platform, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon } from '#/components/icons/Arrow'; +import { LabelsOnMe } from '#/components/moderation/LabelsOnMe'; +import { ProfileHeaderAlerts } from '#/components/moderation/ProfileHeaderAlerts'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS } from '#/env'; +import { useActorStatus } from '#/features/liveNow'; +import { EditLiveDialog } from '#/features/liveNow/components/EditLiveDialog'; +import { LiveIndicator } from '#/features/liveNow/components/LiveIndicator'; +import { LiveStatusDialog } from '#/features/liveNow/components/LiveStatusDialog'; +import { GrowableAvatar } from './GrowableAvatar'; +import { GrowableBanner } from './GrowableBanner'; +import { StatusBarShadow } from './StatusBarShadow'; +var ProfileHeaderShell = function (_a) { + var _b, _c, _d; + var children = _a.children, profile = _a.profile, moderation = _a.moderation, _e = _a.hideBackButton, hideBackButton = _e === void 0 ? false : _e, isPlaceholderProfile = _a.isPlaceholderProfile; + var t = useTheme(); + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var _ = useLingui()._; + var openLightbox = useLightboxControls().openLightbox; + var navigation = useNavigation(); + var topInset = useSafeAreaInsets().top; + var playHaptic = useHaptics(); + var liveStatusControl = useDialogControl(); + var aviRef = useAnimatedRef(); + var bannerRef = useAnimatedRef(); + var onPressBack = useCallback(function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }, [navigation]); + var _openLightbox = useCallback(function (uri, thumbRect, type) { + if (type === void 0) { type = 'circle-avi'; } + openLightbox({ + images: [ + { + uri: uri, + thumbUri: uri, + thumbRect: thumbRect, + dimensions: type === 'circle-avi' || type === 'rect-avi' + ? { + // It's fine if it's actually smaller but we know it's 1:1. + height: 1000, + width: 1000, + } + : { + // Banner aspect ratio is 3:1 + width: 3000, + height: 1000, + }, + thumbDimensions: null, + type: type, + }, + ], + index: 0, + }); + }, [openLightbox]); + var isMe = useMemo(function () { return (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did; }, [currentAccount, profile]); + var live = useActorStatus(profile); + useEffect(function () { + if (live.isActive) { + ax.metric('live:view:profile', { subject: profile.did }); + } + }, [ax, live.isActive, profile.did]); + var onPressAvi = useCallback(function () { + var _a; + if (live.isActive) { + playHaptic('Light'); + ax.metric('live:card:open', { subject: profile.did, from: 'profile' }); + liveStatusControl.open(); + } + else { + var modui = moderation.ui('avatar'); + var avatar_1 = profile.avatar; + var type_1 = ((_a = profile.associated) === null || _a === void 0 ? void 0 : _a.labeler) ? 'rect-avi' : 'circle-avi'; + if (avatar_1 && !(modui.blur && modui.noOverride)) { + runOnUI(function () { + 'worklet'; + var rect = measure(aviRef); + runOnJS(_openLightbox)(avatar_1, rect, type_1); + })(); + } + } + }, [ + ax, + profile, + moderation, + _openLightbox, + aviRef, + liveStatusControl, + live, + playHaptic, + ]); + var onPressBanner = useCallback(function () { + var modui = moderation.ui('banner'); + var banner = profile.banner; + if (banner && !(modui.blur && modui.noOverride)) { + runOnUI(function () { + 'worklet'; + var rect = measure(bannerRef); + runOnJS(_openLightbox)(banner, rect, 'image'); + })(); + } + }, [profile.banner, moderation, _openLightbox, bannerRef]); + return (_jsxs(View, { style: t.atoms.bg, pointerEvents: IS_IOS ? 'auto' : 'box-none', children: [_jsxs(View, { pointerEvents: IS_IOS ? 'auto' : 'box-none', style: [a.relative, { height: 150 }], children: [_jsx(StatusBarShadow, {}), _jsx(GrowableBanner, { onPress: isPlaceholderProfile ? undefined : onPressBanner, bannerRef: bannerRef, backButton: !hideBackButton && (_jsx(Button, { testID: "profileHeaderBackBtn", onPress: onPressBack, hitSlop: BACK_HITSLOP, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Back"], ["Back"])))), style: [ + a.absolute, + a.pointer, + { + top: platform({ + web: 10, + default: topInset, + }), + left: platform({ + web: 18, + default: 12, + }), + }, + ], children: function (_a) { + var hovered = _a.hovered; + return (_jsx(View, { style: [ + a.align_center, + a.justify_center, + a.rounded_full, + { + width: 31, + height: 31, + backgroundColor: utils.alpha('#000', 0.5), + }, + hovered && { + backgroundColor: utils.alpha('#000', 0.75), + }, + ], children: _jsx(ArrowLeftIcon, { size: "lg", fill: "white" }) })); + } })), children: isPlaceholderProfile ? (_jsx(LoadingPlaceholder, { width: "100%", height: "100%", style: { borderRadius: 0 } })) : (_jsx(UserBanner, { type: ((_b = profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'default', banner: profile.banner, moderation: moderation.ui('banner') })) })] }), children, !isPlaceholderProfile && + (isMe ? (_jsx(LabelsOnMe, { type: "account", labels: profile.labels, style: [ + a.px_lg, + a.pt_xs, + a.pb_sm, + IS_IOS ? a.pointer_events_auto : { pointerEvents: 'box-none' }, + ] })) : (_jsx(ProfileHeaderAlerts, { moderation: moderation, style: [ + a.px_lg, + a.pt_xs, + a.pb_sm, + IS_IOS ? a.pointer_events_auto : { pointerEvents: 'box-none' }, + ] }))), _jsx(GrowableAvatar, { style: [a.absolute, { top: 104, left: 10 }], children: _jsx(Pressable, { testID: "profileHeaderAviButton", onPress: onPressAvi, accessibilityRole: "image", accessibilityLabel: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["View ", "'s avatar"], ["View ", "'s avatar"])), profile.handle)), accessibilityHint: "", children: _jsx(View, { style: [ + t.atoms.bg, + a.rounded_full, + { + width: 94, + height: 94, + borderWidth: live.isActive ? 3 : 2, + borderColor: live.isActive + ? t.palette.negative_500 + : t.atoms.bg.backgroundColor, + }, + ((_c = profile.associated) === null || _c === void 0 ? void 0 : _c.labeler) && a.rounded_md, + ], children: _jsxs(Animated.View, { ref: aviRef, collapsable: false, children: [_jsx(UserAvatar, { type: ((_d = profile.associated) === null || _d === void 0 ? void 0 : _d.labeler) ? 'labeler' : 'user', size: live.isActive ? 88 : 90, avatar: profile.avatar, moderation: moderation.ui('avatar'), noBorder: true }), live.isActive && _jsx(LiveIndicator, { size: "large" })] }) }) }) }), live.isActive && + (isMe ? (_jsx(EditLiveDialog, { control: liveStatusControl, status: live, embed: live.embed })) : (_jsx(LiveStatusDialog, { control: liveStatusControl, status: live, embed: live.embed, profile: profile })))] })); +}; +ProfileHeaderShell = memo(ProfileHeaderShell); +export { ProfileHeaderShell }; +var templateObject_1, templateObject_2; diff --git a/src/screens/Profile/Header/StatusBarShadow.js b/src/screens/Profile/Header/StatusBarShadow.js new file mode 100644 index 0000000000..0aca537b02 --- /dev/null +++ b/src/screens/Profile/Header/StatusBarShadow.js @@ -0,0 +1,40 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import Animated, { useAnimatedStyle, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { LinearGradient } from 'expo-linear-gradient'; +import { usePagerHeaderContext } from '#/view/com/pager/PagerHeaderContext'; +import { atoms as a } from '#/alf'; +import { IS_IOS } from '#/env'; +var AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient); +export function StatusBarShadow() { + var topInset = useSafeAreaInsets().top; + var pagerContext = usePagerHeaderContext(); + if (IS_IOS && pagerContext) { + var scrollY_1 = pagerContext.scrollY; + return _jsx(StatusBarShadowInnner, { scrollY: scrollY_1 }); + } + return (_jsx(LinearGradient, { colors: ['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)'], style: [ + a.absolute, + a.z_10, + { height: topInset, top: 0, left: 0, right: 0 }, + ] })); +} +function StatusBarShadowInnner(_a) { + var scrollY = _a.scrollY; + var topInset = useSafeAreaInsets().top; + var animatedStyle = useAnimatedStyle(function () { + return { + transform: [ + { + translateY: Math.min(0, scrollY.get()), + }, + ], + }; + }); + return (_jsx(AnimatedLinearGradient, { colors: ['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)'], style: [ + animatedStyle, + a.absolute, + a.z_10, + { height: topInset, top: 0, left: 0, right: 0 }, + ] })); +} diff --git a/src/screens/Profile/Header/StatusBarShadow.web.js b/src/screens/Profile/Header/StatusBarShadow.web.js new file mode 100644 index 0000000000..d755d5ea21 --- /dev/null +++ b/src/screens/Profile/Header/StatusBarShadow.web.js @@ -0,0 +1,3 @@ +export function StatusBarShadow() { + return null; +} diff --git a/src/screens/Profile/Header/SuggestedFollows.js b/src/screens/Profile/Header/SuggestedFollows.js new file mode 100644 index 0000000000..2dd47df18a --- /dev/null +++ b/src/screens/Profile/Header/SuggestedFollows.js @@ -0,0 +1,87 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { AccordionAnimation } from '#/lib/custom-animations/AccordionAnimation'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useSuggestedFollowsByActorQuery, useSuggestedFollowsQuery, } from '#/state/queries/suggested-follows'; +import { useBreakpoints } from '#/alf'; +import { ProfileGrid } from '#/components/FeedInterstitials'; +import { IS_ANDROID } from '#/env'; +export function ProfileHeaderSuggestedFollows(_a) { + var isExpanded = _a.isExpanded, actorDid = _a.actorDid; + var _b = useProfileHeaderSuggestions(actorDid), allProfiles = _b.allProfiles, filteredProfiles = _b.filteredProfiles, onDismiss = _b.onDismiss, isLoading = _b.isLoading, error = _b.error; + if (!allProfiles.length && !isLoading) + return null; + /* NOTE (caidanw): + * Android does not work well with this feature yet. + * This issue stems from Android not allowing dragging on clickable elements in the profile header. + * Blocking the ability to scroll on Android is too much of a trade-off for now. + **/ + if (IS_ANDROID) + return null; + return (_jsx(AccordionAnimation, { isExpanded: isExpanded, children: _jsx(ProfileGrid, { isSuggestionsLoading: isLoading, profiles: filteredProfiles, totalProfileCount: allProfiles.length, error: error, viewContext: "profileHeader", onDismiss: onDismiss, isVisible: isExpanded }) })); +} +function useProfileHeaderSuggestions(actorDid) { + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var maxLength = gtMobile ? 4 : 12; + var _a = useSuggestedFollowsByActorQuery({ + did: actorDid, + }), isLoading = _a.isLoading, data = _a.data, error = _a.error; + var _b = useSuggestedFollowsQuery({ limit: 25 }), moreSuggestions = _b.data, fetchNextPage = _b.fetchNextPage, hasNextPage = _b.hasNextPage, isFetchingNextPage = _b.isFetchingNextPage; + var _c = useState(new Set()), dismissedDids = _c[0], setDismissedDids = _c[1]; + var onDismiss = useCallback(function (did) { + setDismissedDids(function (prev) { return new Set(prev).add(did); }); + }, []); + // Combine profiles from the actor-specific query with fallback suggestions + var allProfiles = useMemo(function () { + var _a, _b; + var actorProfiles = (_a = data === null || data === void 0 ? void 0 : data.suggestions) !== null && _a !== void 0 ? _a : []; + var fallbackProfiles = (_b = moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages.flatMap(function (page) { + return page.actors.map(function (actor) { return ({ actor: actor, recId: page.recId }); }); + })) !== null && _b !== void 0 ? _b : []; + // Dedupe by did, preferring actor-specific profiles + var seen = new Set(); + var combined = []; + for (var _i = 0, actorProfiles_1 = actorProfiles; _i < actorProfiles_1.length; _i++) { + var profile = actorProfiles_1[_i]; + if (!seen.has(profile.did)) { + seen.add(profile.did); + combined.push({ actor: profile, recId: data === null || data === void 0 ? void 0 : data.recId }); + } + } + for (var _c = 0, fallbackProfiles_1 = fallbackProfiles; _c < fallbackProfiles_1.length; _c++) { + var profile = fallbackProfiles_1[_c]; + if (!seen.has(profile.actor.did) && profile.actor.did !== actorDid) { + seen.add(profile.actor.did); + combined.push(profile); + } + } + return combined; + }, [data === null || data === void 0 ? void 0 : data.suggestions, moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages, actorDid, data === null || data === void 0 ? void 0 : data.recId]); + var filteredProfiles = useMemo(function () { + return allProfiles.filter(function (p) { return !dismissedDids.has(p.actor.did); }); + }, [allProfiles, dismissedDids]); + // Fetch more when running low + useEffect(function () { + if (moderationOpts && + filteredProfiles.length < maxLength && + hasNextPage && + !isFetchingNextPage) { + void fetchNextPage(); + } + }, [ + filteredProfiles.length, + maxLength, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + moderationOpts, + ]); + return { + allProfiles: allProfiles, + filteredProfiles: filteredProfiles, + onDismiss: onDismiss, + isLoading: isLoading, + error: error, + }; +} diff --git a/src/screens/Profile/Header/index.js b/src/screens/Profile/Header/index.js new file mode 100644 index 0000000000..f5c2c8b51a --- /dev/null +++ b/src/screens/Profile/Header/index.js @@ -0,0 +1,152 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useMemo, useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, withTiming, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { moderateProfile, } from '@atproto/api'; +import { useIsFocused } from '@react-navigation/native'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useSetLightStatusBar } from '#/state/shell/light-status-bar'; +import { usePagerHeaderContext } from '#/view/com/pager/PagerHeaderContext'; +import { LoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { atoms as a, useTheme } from '#/alf'; +import { Header } from '#/components/Layout'; +import * as ProfileCard from '#/components/ProfileCard'; +import { IS_NATIVE } from '#/env'; +import { HeaderLabelerButtons, ProfileHeaderLabeler, } from './ProfileHeaderLabeler'; +import { HeaderStandardButtons, ProfileHeaderStandard, } from './ProfileHeaderStandard'; +var ProfileHeaderLoading = function (_props) { + var t = useTheme(); + return (_jsxs(View, { style: t.atoms.bg, children: [_jsx(LoadingPlaceholder, { width: "100%", height: 150, style: { borderRadius: 0 } }), _jsx(View, { style: [ + t.atoms.bg, + { borderColor: t.atoms.bg.backgroundColor }, + styles.avi, + ], children: _jsx(LoadingPlaceholder, { width: 90, height: 90, style: styles.br45 }) }), _jsx(View, { style: styles.content, children: _jsx(View, { style: [styles.buttonsLine], children: _jsx(LoadingPlaceholder, { width: 140, height: 34, style: styles.br50 }) }) })] })); +}; +ProfileHeaderLoading = memo(ProfileHeaderLoading); +export { ProfileHeaderLoading }; +var ProfileHeader = function (_a) { + var _b; + var setMinimumHeight = _a.setMinimumHeight, props = __rest(_a, ["setMinimumHeight"]); + var content; + if ((_b = props.profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) { + if (!props.labeler) { + content = _jsx(ProfileHeaderLoading, {}); + } + else { + content = _jsx(ProfileHeaderLabeler, __assign({}, props, { labeler: props.labeler })); + } + } + else { + content = _jsx(ProfileHeaderStandard, __assign({}, props)); + } + return (_jsxs(_Fragment, { children: [IS_NATIVE && (_jsx(MinimalHeader, { onLayout: function (evt) { return setMinimumHeight(evt.nativeEvent.layout.height); }, profile: props.profile, labeler: props.labeler, hideBackButton: props.hideBackButton })), content] })); +}; +ProfileHeader = memo(ProfileHeader); +export { ProfileHeader }; +var MinimalHeader = memo(function MinimalHeader(_a) { + var _b; + var onLayout = _a.onLayout, profileUnshadowed = _a.profile, labeler = _a.labeler, _c = _a.hideBackButton, hideBackButton = _c === void 0 ? false : _c; + var t = useTheme(); + var insets = useSafeAreaInsets(); + var ctx = usePagerHeaderContext(); + var profile = useProfileShadow(profileUnshadowed); + var moderationOpts = useModerationOpts(); + var moderation = useMemo(function () { return (moderationOpts ? moderateProfile(profile, moderationOpts) : null); }, [moderationOpts, profile]); + var _d = useState(false), visible = _d[0], setVisible = _d[1]; + var _e = useState(insets.top), minimalHeaderHeight = _e[0], setMinimalHeaderHeight = _e[1]; + var isScreenFocused = useIsFocused(); + if (!ctx) + throw new Error('MinimalHeader cannot be used on web'); + var scrollY = ctx.scrollY, headerHeight = ctx.headerHeight; + var animatedStyle = useAnimatedStyle(function () { + // if we don't yet have the min header height in JS, hide + if (!_WORKLET || minimalHeaderHeight === 0) { + return { + opacity: 0, + }; + } + var pastThreshold = scrollY.get() > 100; + return { + opacity: pastThreshold + ? withTiming(1, { duration: 75 }) + : withTiming(0, { duration: 75 }), + transform: [ + { + translateY: Math.min(scrollY.get(), headerHeight - minimalHeaderHeight), + }, + ], + }; + }); + useAnimatedReaction(function () { return scrollY.get() > 100; }, function (value, prev) { + if (prev !== value) { + runOnJS(setVisible)(value); + } + }); + useSetLightStatusBar(isScreenFocused && !visible); + return (_jsx(Animated.View, { pointerEvents: visible ? 'auto' : 'none', "aria-hidden": !visible, accessibilityElementsHidden: !visible, importantForAccessibility: visible ? 'auto' : 'no-hide-descendants', onLayout: function (evt) { + setMinimalHeaderHeight(evt.nativeEvent.layout.height); + onLayout(evt); + }, style: [ + a.absolute, + a.z_50, + t.atoms.bg, + { + top: 0, + left: 0, + right: 0, + paddingTop: insets.top, + }, + animatedStyle, + ], children: _jsxs(Header.Outer, { noBottomBorder: true, children: [hideBackButton ? _jsx(Header.MenuButton, {}) : _jsx(Header.BackButton, {}), _jsxs(Header.Content, { align: "left", children: [moderationOpts ? (_jsx(ProfileCard.Name, { profile: profile, moderationOpts: moderationOpts, textStyle: [a.font_bold] })) : (_jsx(ProfileCard.NamePlaceholder, {})), _jsx(Header.SubtitleText, { children: sanitizeHandle(profile.handle, '@') })] }), !((_b = profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) + ? moderationOpts && + moderation && (_jsx(View, { style: [a.flex_row, a.justify_end, a.gap_xs], children: _jsx(HeaderStandardButtons, { profile: profile, moderation: moderation, moderationOpts: moderationOpts, minimal: true }) })) + : labeler && (_jsx(View, { style: [a.flex_row, a.justify_end, a.gap_xs], children: _jsx(HeaderLabelerButtons, { profile: profile, minimal: true }) }))] }) })); +}); +MinimalHeader.displayName = 'MinimalHeader'; +var styles = StyleSheet.create({ + avi: { + position: 'absolute', + top: 110, + left: 10, + width: 94, + height: 94, + borderRadius: 47, + borderWidth: 2, + }, + content: { + paddingTop: 12, + paddingHorizontal: 16, + paddingBottom: 8, + }, + buttonsLine: { + flexDirection: 'row', + marginLeft: 'auto', + }, + br45: { borderRadius: 45 }, + br50: { borderRadius: 50 }, +}); diff --git a/src/screens/Profile/KnownFollowers.js b/src/screens/Profile/KnownFollowers.js new file mode 100644 index 0000000000..b45f3bffb7 --- /dev/null +++ b/src/screens/Profile/KnownFollowers.js @@ -0,0 +1,135 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect } from '@react-navigation/native'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { cleanError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { useProfileKnownFollowersQuery } from '#/state/queries/known-followers'; +import { useResolveDidQuery } from '#/state/queries/resolve-uri'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { ProfileCardWithFollowBtn } from '#/view/com/profile/ProfileCard'; +import { List } from '#/view/com/util/List'; +import { ViewHeader } from '#/view/com/util/ViewHeader'; +import * as Layout from '#/components/Layout'; +import { ListFooter, ListMaybePlaceholder } from '#/components/Lists'; +function renderItem(_a) { + var item = _a.item, index = _a.index; + return (_jsx(ProfileCardWithFollowBtn, { profile: item, noBorder: index === 0 }, item.did)); +} +function keyExtractor(item) { + return item.did; +} +export var ProfileKnownFollowersScreen = function (_a) { + var route = _a.route; + var _ = useLingui()._; + var setMinimalShellMode = useSetMinimalShellMode(); + var initialNumToRender = useInitialNumToRender(); + var name = route.params.name; + var _b = React.useState(false), isPTRing = _b[0], setIsPTRing = _b[1]; + var _c = useResolveDidQuery(route.params.name), resolvedDid = _c.data, isDidLoading = _c.isLoading, resolveError = _c.error; + var _d = useProfileKnownFollowersQuery(resolvedDid), data = _d.data, isFollowersLoading = _d.isLoading, isFetchingNextPage = _d.isFetchingNextPage, hasNextPage = _d.hasNextPage, fetchNextPage = _d.fetchNextPage, error = _d.error, refetch = _d.refetch; + var onRefresh = React.useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTRing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, refetch()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + logger.error('Failed to refresh followers', { message: err_1 }); + return [3 /*break*/, 4]; + case 4: + setIsPTRing(false); + return [2 /*return*/]; + } + }); + }); }, [refetch, setIsPTRing]); + var onEndReached = React.useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { + var err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isFetchingNextPage || !hasNextPage || !!error) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPage()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_2 = _a.sent(); + logger.error('Failed to load more followers', { message: err_2 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]); + var followers = React.useMemo(function () { + if (data === null || data === void 0 ? void 0 : data.pages) { + return data.pages.flatMap(function (page) { return page.followers; }); + } + return []; + }, [data]); + var isError = Boolean(resolveError || error); + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + if (followers.length < 1) { + return (_jsxs(Layout.Screen, { children: [_jsx(ViewHeader, { title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Followers you know"], ["Followers you know"])))) }), _jsx(ListMaybePlaceholder, { isLoading: isDidLoading || isFollowersLoading, isError: isError, emptyType: "results", emptyMessage: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You don't follow any users who follow @", "."], ["You don't follow any users who follow @", "."])), name)), errorMessage: cleanError(resolveError || error), onRetry: isError ? refetch : undefined, topBorder: false, sideBorders: false })] })); + } + return (_jsxs(Layout.Screen, { children: [_jsx(ViewHeader, { title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Followers you know"], ["Followers you know"])))) }), _jsx(List, { data: followers, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTRing, onRefresh: onRefresh, onEndReached: onEndReached, onEndReachedThreshold: 4, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage }), + // @ts-ignore our .web version only -prf + desktopFixedHeight: true, initialNumToRender: initialNumToRender, windowSize: 11, sideBorders: false })] })); +}; +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Profile/ProfileFeed/index.js b/src/screens/Profile/ProfileFeed/index.js new file mode 100644 index 0000000000..cdbb20c629 --- /dev/null +++ b/src/screens/Profile/ProfileFeed/index.js @@ -0,0 +1,98 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useAnimatedRef } from 'react-native-reanimated'; +import { AppBskyFeedDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useIsFocused } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { VIDEO_FEED_URIS } from '#/lib/constants'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { useSetTitle } from '#/lib/hooks/useSetTitle'; +import { ComposeIcon2 } from '#/lib/icons'; +import { cleanError } from '#/lib/strings/errors'; +import { makeRecordUri } from '#/lib/strings/url-helpers'; +import { listenSoftReset } from '#/state/events'; +import { FeedFeedbackProvider, useFeedFeedback } from '#/state/feed-feedback'; +import { useFeedSourceInfoQuery, } from '#/state/queries/feed'; +import { RQKEY as FEED_RQKEY } from '#/state/queries/post-feed'; +import { usePreferencesQuery, } from '#/state/queries/preferences'; +import { useResolveUriQuery } from '#/state/queries/resolve-uri'; +import { truncateAndInvalidate } from '#/state/queries/util'; +import { useSession } from '#/state/session'; +import { PostFeed } from '#/view/com/posts/PostFeed'; +import { EmptyState } from '#/view/com/util/EmptyState'; +import { ErrorScreen } from '#/view/com/util/error/ErrorScreen'; +import { FAB } from '#/view/com/util/fab/FAB'; +import { LoadLatestBtn } from '#/view/com/util/load-latest/LoadLatestBtn'; +import { PostFeedLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { ProfileFeedHeader, ProfileFeedHeaderSkeleton, } from '#/screens/Profile/components/ProfileFeedHeader'; +import { HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon } from '#/components/icons/Hashtag'; +import * as Layout from '#/components/Layout'; +import { IS_NATIVE } from '#/env'; +export function ProfileFeedScreen(props) { + var _a = props.route.params, rkey = _a.rkey, handleOrDid = _a.name; + var feedParams = props.route.params.feedCacheKey + ? { feedCacheKey: props.route.params.feedCacheKey } + : undefined; + var _ = useLingui()._; + var uri = useMemo(function () { return makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey); }, [rkey, handleOrDid]); + var _b = useResolveUriQuery(uri), error = _b.error, resolvedUri = _b.data, refetch = _b.refetch, isRefetching = _b.isRefetching; + if (error && !isRefetching) { + return (_jsx(Layout.Screen, { testID: "profileFeedScreenError", children: _jsx(ErrorScreen, { showHeader: true, title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Could not load feed"], ["Could not load feed"])))), message: cleanError(error), onPressTryAgain: function () { return void refetch(); } }) })); + } + return resolvedUri ? (_jsx(Layout.Screen, { testID: "profileFeedScreen", children: _jsx(ProfileFeedScreenIntermediate, { feedUri: resolvedUri.uri, feedParams: feedParams }) })) : (_jsxs(Layout.Screen, { testID: "profileFeedScreen", children: [_jsx(ProfileFeedHeaderSkeleton, {}), _jsx(Layout.Content, { children: _jsx(PostFeedLoadingPlaceholder, {}) })] })); +} +function ProfileFeedScreenIntermediate(_a) { + var feedUri = _a.feedUri, feedParams = _a.feedParams; + var preferences = usePreferencesQuery().data; + var info = useFeedSourceInfoQuery({ uri: feedUri }).data; + if (!preferences || !info) { + return (_jsxs(Layout.Content, { children: [_jsx(ProfileFeedHeaderSkeleton, {}), _jsx(PostFeedLoadingPlaceholder, {})] })); + } + return (_jsx(ProfileFeedScreenInner, { preferences: preferences, feedInfo: info, feedParams: feedParams })); +} +export function ProfileFeedScreenInner(_a) { + var feedInfo = _a.feedInfo, feedParams = _a.feedParams; + var _ = useLingui()._; + var hasSession = useSession().hasSession; + var openComposer = useOpenComposer().openComposer; + var isScreenFocused = useIsFocused(); + useSetTitle(feedInfo === null || feedInfo === void 0 ? void 0 : feedInfo.displayName); + var feed = "feedgen|".concat(feedInfo.uri); + var _b = useState(false), hasNew = _b[0], setHasNew = _b[1]; + var _c = useState(false), isScrolledDown = _c[0], setIsScrolledDown = _c[1]; + var queryClient = useQueryClient(); + var feedFeedback = useFeedFeedback(feedInfo, hasSession); + var scrollElRef = useAnimatedRef(); + var onScrollToTop = useCallback(function () { + var _a; + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: 0, // -headerHeight, + }); + void truncateAndInvalidate(queryClient, FEED_RQKEY(feed)); + setHasNew(false); + }, [scrollElRef, queryClient, feed, setHasNew]); + useEffect(function () { + if (!isScreenFocused) { + return; + } + return listenSoftReset(onScrollToTop); + }, [onScrollToTop, isScreenFocused]); + var renderPostsEmpty = useCallback(function () { + return (_jsx(EmptyState, { icon: HashtagWideIcon, iconSize: "2xl", message: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["This feed is empty."], ["This feed is empty."])))) })); + }, [_]); + var isVideoFeed = useMemo(function () { + var isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri); + var feedIsVideoMode = feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO; + var _isVideoFeed = isBskyVideoFeed || feedIsVideoMode; + return IS_NATIVE && _isVideoFeed; + }, [feedInfo]); + return (_jsxs(_Fragment, { children: [_jsx(ProfileFeedHeader, { info: feedInfo }), _jsx(FeedFeedbackProvider, { value: feedFeedback, children: _jsx(PostFeed, { feed: feed, feedParams: feedParams, pollInterval: 60e3, disablePoll: hasNew, onHasNew: setHasNew, scrollElRef: scrollElRef, onScrolledDownChange: setIsScrolledDown, renderEmptyState: renderPostsEmpty, isVideoFeed: isVideoFeed }) }), (isScrolledDown || hasNew) && (_jsx(LoadLatestBtn, { onPress: onScrollToTop, label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Load new posts"], ["Load new posts"])))), showIndicator: hasNew })), hasSession && (_jsx(FAB, { testID: "composeFAB", onPress: function () { return openComposer({ logContext: 'Fab' }); }, icon: _jsx(ComposeIcon2, { strokeWidth: 1.5, size: 29, style: { color: 'white' } }), accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["New post"], ["New post"])))), accessibilityHint: "" }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Profile/ProfileFollowers.js b/src/screens/Profile/ProfileFollowers.js new file mode 100644 index 0000000000..7e3ba518dd --- /dev/null +++ b/src/screens/Profile/ProfileFollowers.js @@ -0,0 +1,24 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Plural } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useResolveDidQuery } from '#/state/queries/resolve-uri'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { ProfileFollowers as ProfileFollowersComponent } from '#/view/com/profile/ProfileFollowers'; +import * as Layout from '#/components/Layout'; +export var ProfileFollowersScreen = function (_a) { + var _b; + var route = _a.route; + var name = route.params.name; + var setMinimalShellMode = useSetMinimalShellMode(); + var resolvedDid = useResolveDidQuery(name).data; + var profile = useProfileQuery({ + did: resolvedDid, + }).data; + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + return (_jsxs(Layout.Screen, { testID: "profileFollowersScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: profile && (_jsxs(_Fragment, { children: [_jsx(Layout.Header.TitleText, { children: sanitizeDisplayName(profile.displayName || profile.handle) }), _jsx(Layout.Header.SubtitleText, { children: _jsx(Plural, { value: (_b = profile.followersCount) !== null && _b !== void 0 ? _b : 0, one: "# follower", other: "# followers" }) })] })) }), _jsx(Layout.Header.Slot, {})] }), _jsx(ProfileFollowersComponent, { name: name })] })); +}; diff --git a/src/screens/Profile/ProfileFollows.js b/src/screens/Profile/ProfileFollows.js new file mode 100644 index 0000000000..29986d6d5b --- /dev/null +++ b/src/screens/Profile/ProfileFollows.js @@ -0,0 +1,24 @@ +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Plural } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useResolveDidQuery } from '#/state/queries/resolve-uri'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { ProfileFollows as ProfileFollowsComponent } from '#/view/com/profile/ProfileFollows'; +import * as Layout from '#/components/Layout'; +export var ProfileFollowsScreen = function (_a) { + var _b; + var route = _a.route; + var name = route.params.name; + var setMinimalShellMode = useSetMinimalShellMode(); + var resolvedDid = useResolveDidQuery(name).data; + var profile = useProfileQuery({ + did: resolvedDid, + }).data; + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + return (_jsxs(Layout.Screen, { testID: "profileFollowsScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: profile && (_jsxs(_Fragment, { children: [_jsx(Layout.Header.TitleText, { children: sanitizeDisplayName(profile.displayName || profile.handle) }), _jsx(Layout.Header.SubtitleText, { children: _jsx(Plural, { value: (_b = profile.followsCount) !== null && _b !== void 0 ? _b : 0, one: "# following", other: "# following" }) })] })) }), _jsx(Layout.Header.Slot, {})] }), _jsx(ProfileFollowsComponent, { name: name })] })); +}; diff --git a/src/screens/Profile/ProfileLabelerLikedBy.js b/src/screens/Profile/ProfileLabelerLikedBy.js new file mode 100644 index 0000000000..2477cf4aa1 --- /dev/null +++ b/src/screens/Profile/ProfileLabelerLikedBy.js @@ -0,0 +1,26 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect } from '@react-navigation/native'; +import { makeRecordUri } from '#/lib/strings/url-helpers'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { ViewHeader } from '#/view/com/util/ViewHeader'; +import * as Layout from '#/components/Layout'; +import { LikedByList } from '#/components/LikedByList'; +export function ProfileLabelerLikedByScreen(_a) { + var route = _a.route; + var setMinimalShellMode = useSetMinimalShellMode(); + var handleOrDid = route.params.name; + var uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self'); + var _ = useLingui()._; + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + return (_jsxs(Layout.Screen, { children: [_jsx(ViewHeader, { title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Liked By"], ["Liked By"])))) }), _jsx(LikedByList, { uri: uri })] })); +} +var templateObject_1; diff --git a/src/screens/Profile/ProfileSearch.js b/src/screens/Profile/ProfileSearch.js new file mode 100644 index 0000000000..1edb364e81 --- /dev/null +++ b/src/screens/Profile/ProfileSearch.js @@ -0,0 +1,32 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useResolveDidQuery } from '#/state/queries/resolve-uri'; +import { useSession } from '#/state/session'; +import { SearchScreenShell } from '#/screens/Search/Shell'; +export var ProfileSearchScreen = function (_a) { + var route = _a.route; + var _b = route.params, name = _b.name, _c = _b.q, queryParam = _c === void 0 ? '' : _c; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var resolvedDid = useResolveDidQuery(name).data; + var profile = useProfileQuery({ did: resolvedDid }).data; + var fixedParams = useMemo(function () { + var _a; + return ({ + from: (_a = profile === null || profile === void 0 ? void 0 : profile.handle) !== null && _a !== void 0 ? _a : name, + }); + }, [profile === null || profile === void 0 ? void 0 : profile.handle, name]); + return (_jsx(SearchScreenShell, { navButton: "back", inputPlaceholder: profile + ? (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Search my posts"], ["Search my posts"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Search @", "'s posts"], ["Search @", "'s posts"])), profile.handle)) + : _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Search..."], ["Search..."])))), fixedParams: fixedParams, queryParam: queryParam, testID: "searchPostsScreen" })); +}; +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Profile/Sections/Feed.js b/src/screens/Profile/Sections/Feed.js new file mode 100644 index 0000000000..4e38055c6d --- /dev/null +++ b/src/screens/Profile/Sections/Feed.js @@ -0,0 +1,60 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useImperativeHandle, useState } from 'react'; +import { findNodeHandle, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { RQKEY as FEED_RQKEY, } from '#/state/queries/post-feed'; +import { truncateAndInvalidate } from '#/state/queries/util'; +import { PostFeed } from '#/view/com/posts/PostFeed'; +import { EmptyState, } from '#/view/com/util/EmptyState'; +import { LoadLatestBtn } from '#/view/com/util/load-latest/LoadLatestBtn'; +import { atoms as a, ios, useTheme } from '#/alf'; +import { EditBig_Stroke1_Corner0_Rounded as EditIcon } from '#/components/icons/EditBig'; +import { Text } from '#/components/Typography'; +import { IS_IOS, IS_NATIVE } from '#/env'; +export function ProfileFeedSection(_a) { + var ref = _a.ref, feed = _a.feed, headerHeight = _a.headerHeight, isFocused = _a.isFocused, scrollElRef = _a.scrollElRef, ignoreFilterFor = _a.ignoreFilterFor, setScrollViewTag = _a.setScrollViewTag, emptyStateMessage = _a.emptyStateMessage, emptyStateButton = _a.emptyStateButton, emptyStateIcon = _a.emptyStateIcon; + var _ = useLingui()._; + var queryClient = useQueryClient(); + var _b = useState(false), hasNew = _b[0], setHasNew = _b[1]; + var _c = useState(false), isScrolledDown = _c[0], setIsScrolledDown = _c[1]; + var shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads'); + var isVideoFeed = IS_NATIVE && feed.endsWith('posts_with_video'); + var adjustedInitialNumToRender = useInitialNumToRender({ + screenHeightOffset: headerHeight, + }); + var onScrollToTop = useCallback(function () { + var _a; + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: -headerHeight, + }); + truncateAndInvalidate(queryClient, FEED_RQKEY(feed)); + setHasNew(false); + }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]); + useImperativeHandle(ref, function () { return ({ + scrollToTop: onScrollToTop, + }); }); + var renderPostsEmpty = useCallback(function () { + return (_jsx(View, { style: [a.flex_1, a.justify_center, a.align_center], children: _jsx(EmptyState, { style: { width: '100%' }, icon: emptyStateIcon || EditIcon, iconSize: "3xl", message: emptyStateMessage || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["No posts yet"], ["No posts yet"])))), button: emptyStateButton }) })); + }, [_, emptyStateButton, emptyStateIcon, emptyStateMessage]); + useEffect(function () { + if (IS_IOS && isFocused && scrollElRef.current) { + var nativeTag = findNodeHandle(scrollElRef.current); + setScrollViewTag(nativeTag); + } + }, [isFocused, scrollElRef, setScrollViewTag]); + return (_jsxs(View, { children: [_jsx(PostFeed, { testID: "postsFeed", enabled: isFocused, feed: feed, scrollElRef: scrollElRef, onHasNew: setHasNew, onScrolledDownChange: setIsScrolledDown, renderEmptyState: renderPostsEmpty, headerOffset: headerHeight, progressViewOffset: ios(0), renderEndOfFeed: isVideoFeed ? undefined : ProfileEndOfFeed, ignoreFilterFor: ignoreFilterFor, initialNumToRender: shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined, isVideoFeed: isVideoFeed }), (isScrolledDown || hasNew) && (_jsx(LoadLatestBtn, { onPress: onScrollToTop, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Load new posts"], ["Load new posts"])))), showIndicator: hasNew }))] })); +} +function ProfileEndOfFeed() { + var t = useTheme(); + return (_jsx(View, { style: [a.w_full, a.py_5xl, a.border_t, t.atoms.border_contrast_medium], children: _jsx(Text, { style: [t.atoms.text_contrast_medium, a.text_center], children: _jsx(Trans, { children: "End of feed" }) }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Profile/Sections/Labels.js b/src/screens/Profile/Sections/Labels.js new file mode 100644 index 0000000000..218a452d07 --- /dev/null +++ b/src/screens/Profile/Sections/Labels.js @@ -0,0 +1,106 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useImperativeHandle, useMemo } from 'react'; +import { findNodeHandle, View } from 'react-native'; +import { interpretLabelValueDefinitions, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { isLabelerSubscribed, lookupLabelValueDefinition } from '#/lib/moderation'; +import { List } from '#/view/com/util/List'; +import { atoms as a, ios, tokens, useTheme } from '#/alf'; +import { Divider } from '#/components/Divider'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { ListFooter } from '#/components/Lists'; +import { Loader } from '#/components/Loader'; +import { LabelerLabelPreference } from '#/components/moderation/LabelPreference'; +import { Text } from '#/components/Typography'; +import { IS_IOS, IS_NATIVE } from '#/env'; +import { ErrorState } from '../ErrorState'; +export function ProfileLabelsSection(_a) { + var ref = _a.ref, isLabelerLoading = _a.isLabelerLoading, labelerInfo = _a.labelerInfo, labelerError = _a.labelerError, moderationOpts = _a.moderationOpts, scrollElRef = _a.scrollElRef, headerHeight = _a.headerHeight, isFocused = _a.isFocused, setScrollViewTag = _a.setScrollViewTag; + var t = useTheme(); + var onScrollToTop = useCallback(function () { + var _a; + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: -headerHeight, + }); + }, [scrollElRef, headerHeight]); + useImperativeHandle(ref, function () { return ({ + scrollToTop: onScrollToTop, + }); }); + useEffect(function () { + if (IS_IOS && isFocused && scrollElRef.current) { + var nativeTag = findNodeHandle(scrollElRef.current); + setScrollViewTag(nativeTag); + } + }, [isFocused, scrollElRef, setScrollViewTag]); + var isSubscribed = labelerInfo + ? !!isLabelerSubscribed(labelerInfo, moderationOpts) + : false; + var labelValues = useMemo(function () { + if (isLabelerLoading || !labelerInfo || labelerError) + return []; + var customDefs = interpretLabelValueDefinitions(labelerInfo); + return labelerInfo.policies.labelValues + .filter(function (val, i, arr) { return arr.indexOf(val) === i; }) // dedupe + .map(function (val) { return lookupLabelValueDefinition(val, customDefs); }) + .filter(function (def) { return def && (def === null || def === void 0 ? void 0 : def.configurable); }); + }, [labelerInfo, labelerError, isLabelerLoading]); + var numItems = labelValues.length; + var renderItem = useCallback(function (_a) { + var item = _a.item, index = _a.index; + if (!labelerInfo) + return null; + return (_jsxs(View, { style: [ + t.atoms.bg_contrast_25, + index === 0 && [ + a.overflow_hidden, + { + borderTopLeftRadius: tokens.borderRadius.md, + borderTopRightRadius: tokens.borderRadius.md, + }, + ], + index === numItems - 1 && [ + a.overflow_hidden, + { + borderBottomLeftRadius: tokens.borderRadius.md, + borderBottomRightRadius: tokens.borderRadius.md, + }, + ], + ], children: [index !== 0 && _jsx(Divider, {}), _jsx(LabelerLabelPreference, { disabled: isSubscribed ? undefined : true, labelDefinition: item, labelerDid: labelerInfo.creator.did })] })); + }, [labelerInfo, isSubscribed, numItems, t]); + return (_jsx(View, { children: _jsx(List, { ref: scrollElRef, data: labelValues, renderItem: renderItem, keyExtractor: keyExtractor, contentContainerStyle: a.px_xl, headerOffset: headerHeight, progressViewOffset: ios(0), ListHeaderComponent: _jsx(LabelerListHeader, { isLabelerLoading: isLabelerLoading, labelerInfo: labelerInfo, labelerError: labelerError, hasValues: labelValues.length !== 0, isSubscribed: isSubscribed }), ListFooterComponent: _jsx(ListFooter, { height: headerHeight + 180, style: a.border_transparent }) }) })); +} +function keyExtractor(item) { + return item.identifier; +} +export function LabelerListHeader(_a) { + var _b; + var isLabelerLoading = _a.isLabelerLoading, labelerError = _a.labelerError, labelerInfo = _a.labelerInfo, hasValues = _a.hasValues, isSubscribed = _a.isSubscribed; + var t = useTheme(); + var _ = useLingui()._; + if (isLabelerLoading) { + return (_jsx(View, { style: [a.w_full, a.align_center, a.py_4xl], children: _jsx(Loader, { size: "xl" }) })); + } + if (labelerError || !labelerInfo) { + return (_jsx(View, { style: [a.w_full, a.align_center, a.py_4xl], children: _jsx(ErrorState, { error: (labelerError === null || labelerError === void 0 ? void 0 : labelerError.toString()) || + _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Something went wrong, please try again."], ["Something went wrong, please try again."])))) }) })); + } + return (_jsxs(View, { style: [a.py_xl], children: [_jsx(Text, { style: [t.atoms.text_contrast_high, a.leading_snug, a.text_sm], children: _jsx(Trans, { children: "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." }) }), ((_b = labelerInfo === null || labelerInfo === void 0 ? void 0 : labelerInfo.creator.viewer) === null || _b === void 0 ? void 0 : _b.blocking) ? (_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center, a.mt_md], children: [_jsx(CircleInfo, { size: "sm", fill: t.atoms.text_contrast_medium.color }), _jsx(Text, { style: [t.atoms.text_contrast_high, a.leading_snug, a.text_sm], children: _jsx(Trans, { children: "Blocking does not prevent this labeler from placing labels on your account." }) })] })) : null, !hasValues ? (_jsx(Text, { style: [ + a.pt_xl, + t.atoms.text_contrast_high, + a.leading_snug, + a.text_sm, + ], children: _jsx(Trans, { children: "This labeler hasn't declared what labels it publishes, and may not be active." }) })) : !isSubscribed ? (_jsx(Text, { style: [ + a.pt_xl, + t.atoms.text_contrast_high, + a.leading_snug, + a.text_sm, + ], children: _jsxs(Trans, { children: ["Subscribe to @", labelerInfo.creator.handle, " to use these labels:"] }) })) : null] })); +} +var templateObject_1; diff --git a/src/screens/Profile/Sections/types.js b/src/screens/Profile/Sections/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/screens/Profile/Sections/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/screens/Profile/components/GermButton.js b/src/screens/Profile/components/GermButton.js new file mode 100644 index 0000000000..e940c18766 --- /dev/null +++ b/src/screens/Profile/components/GermButton.js @@ -0,0 +1,267 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { Platform, View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { until } from '#/lib/async/until'; +import { isNetworkError } from '#/lib/strings/errors'; +import { RQKEY } from '#/state/queries/profile'; +import { useAgent, useSession } from '#/state/session'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { CustomLinkWarningDialog } from '#/components/dialogs/LinkWarning'; +import { ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRightIcon } from '#/components/icons/Arrow'; +import { Link } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +export function GermButton(_a) { + var _b; + var germ = _a.germ, profile = _a.profile; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var linkWarningControl = Dialog.useDialogControl(); + // exclude `none` and all unknown values + if (!(germ.showButtonTo === 'everyone' || germ.showButtonTo === 'usersIFollow')) { + return null; + } + if ((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did) { + return _jsx(GermSelfButton, { did: currentAccount.did }); + } + if (germ.showButtonTo === 'usersIFollow' && !((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.followedBy)) { + return null; + } + var url = constructGermUrl(germ, profile, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + if (!url) { + return null; + } + return (_jsxs(_Fragment, { children: [_jsxs(Link, { to: url, onPress: function (evt) { + ax.metric('profile:associated:germ:click-to-chat', {}); + if (isCustomGermDomain(url)) { + evt.preventDefault(); + linkWarningControl.open(); + return false; + } + }, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Open Germ DM"], ["Open Germ DM"])))), overridePresentation: false, shouldProxy: false, style: [ + t.atoms.bg_contrast_50, + a.rounded_full, + a.self_start, + { padding: 6 }, + ], children: [_jsx(GermLogo, { size: "small" }), _jsx(Text, { style: [a.text_sm, a.font_medium, a.ml_xs], children: _jsx(Trans, { children: "Germ DM" }) }), _jsx(ArrowTopRightIcon, { style: [t.atoms.text, a.mx_2xs], width: 14 })] }), _jsx(CustomLinkWarningDialog, { control: linkWarningControl, link: { + href: url, + displayText: '', + share: false, + } })] })); +} +function GermLogo(_a) { + var size = _a.size; + return (_jsx(Image, { source: require('../../../../assets/images/germ_logo.webp'), accessibilityIgnoresInvertColors: false, contentFit: "cover", style: [ + a.rounded_full, + size === 'large' ? { width: 32, height: 32 } : { width: 16, height: 16 }, + ] })); +} +function GermSelfButton(_a) { + var _this = this; + var did = _a.did; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var selfExplanationDialogControl = Dialog.useDialogControl(); + var agent = useAgent(); + var queryClient = useQueryClient(); + var _b = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var previousRecord; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.com.germnetwork.declaration + .get({ + repo: did, + rkey: 'self', + }) + .then(function (res) { return res.value; }) + .catch(function () { return null; })]; + case 1: + previousRecord = _a.sent(); + return [4 /*yield*/, agent.com.germnetwork.declaration.delete({ + repo: did, + rkey: 'self', + })]; + case 2: + _a.sent(); + return [4 /*yield*/, whenAppViewReady(agent, did, function (res) { var _a; return !((_a = res.data.associated) === null || _a === void 0 ? void 0 : _a.germ); })]; + case 3: + _a.sent(); + return [2 /*return*/, previousRecord]; + } + }); + }); }, + onSuccess: function (previousRecord) { + ax.metric('profile:associated:germ:self-disconnect', {}); + function undo() { + return __awaiter(this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!previousRecord) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 5, , 6]); + return [4 /*yield*/, agent.com.germnetwork.declaration.put({ + repo: did, + rkey: 'self', + }, previousRecord)]; + case 2: + _a.sent(); + return [4 /*yield*/, whenAppViewReady(agent, did, function (res) { var _a; return !!((_a = res.data.associated) === null || _a === void 0 ? void 0 : _a.germ); })]; + case 3: + _a.sent(); + return [4 /*yield*/, queryClient.refetchQueries({ queryKey: RQKEY(did) })]; + case 4: + _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Germ DM reconnected"], ["Germ DM reconnected"]))))); + ax.metric('profile:associated:germ:self-reconnect', {}); + return [3 /*break*/, 6]; + case 5: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to reconnect Germ DM. Error: ", ""], ["Failed to reconnect Germ DM. Error: ", ""])), e_1 === null || e_1 === void 0 ? void 0 : e_1.message)), { + type: 'error', + }); + if (!isNetworkError(e_1)) { + ax.logger.error('Failed to reconnect Germ DM link', { + safeMessage: e_1, + }); + } + return [3 /*break*/, 6]; + case 6: return [2 /*return*/]; + } + }); + }); + } + selfExplanationDialogControl.close(function () { + void queryClient.refetchQueries({ queryKey: RQKEY(did) }); + Toast.show(_jsxs(Toast.Outer, { children: [_jsx(Toast.Icon, {}), _jsx(Toast.Text, { children: _jsx(Trans, { children: "Germ DM disconnected" }) }), previousRecord && (_jsx(Toast.Action, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Undo"], ["Undo"])))), onPress: function () { return void undo(); }, children: _jsx(Trans, { children: "Undo" }) }))] })); + }); + }, + onError: function (error) { + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Failed to disconnect Germ DM. Error: ", ""], ["Failed to disconnect Germ DM. Error: ", ""])), error === null || error === void 0 ? void 0 : error.message)), { + type: 'error', + }); + if (!isNetworkError(error)) { + ax.logger.error('Failed to disconnect Germ DM link', { + safeMessage: error, + }); + } + }, + }), deleteDeclaration = _b.mutate, isPending = _b.isPending; + return (_jsxs(_Fragment, { children: [_jsxs(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Learn more about your Germ DM link"], ["Learn more about your Germ DM link"])))), onPress: function () { + ax.metric('profile:associated:germ:click-self-info', {}); + selfExplanationDialogControl.open(); + }, style: [ + t.atoms.bg_contrast_50, + a.rounded_full, + a.self_start, + { padding: 6, paddingRight: 10 }, + ], children: [_jsx(GermLogo, { size: "small" }), _jsx(Text, { style: [a.text_sm, a.font_medium, a.ml_xs], children: _jsx(Trans, { children: "Germ DM" }) })] }), _jsxs(Dialog.Outer, { control: selfExplanationDialogControl, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Germ DM Link"], ["Germ DM Link"])))), style: web([{ maxWidth: 400, borderRadius: 36 }]), children: [_jsxs(View, { style: [a.flex_row, a.align_center, { gap: 6 }], children: [_jsx(GermLogo, { size: "large" }), _jsx(Text, { style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "Germ DM Link" }) })] }), _jsx(Text, { style: [a.text_md, a.leading_snug, a.mt_sm], children: _jsx(Trans, { children: "This button lets others open the Germ DM app to send you a message. You can manage its visibility from the Germ DM app, or you can disconnect your Bluesky account from Germ DM altogether by clicking the button below." }) }), _jsxs(View, { style: [a.mt_2xl, a.gap_md], children: [_jsx(Button, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Got it"], ["Got it"])))), size: "large", color: "primary", onPress: function () { return selfExplanationDialogControl.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Got it" }) }) }), _jsxs(Button, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Disconnect Germ DM"], ["Disconnect Germ DM"])))), size: "large", color: "secondary", onPress: function () { return deleteDeclaration(); }, disabled: isPending, children: [isPending && _jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Disconnect Germ DM" }) })] })] })] })] })] })); +} +function constructGermUrl(declaration, profile, viewerDid) { + try { + var urlp = new URL(declaration.messageMeUrl); + if (urlp.pathname.endsWith('/')) { + urlp.pathname = urlp.pathname.slice(0, -1); + } + urlp.pathname += "/".concat(platform()); + if (viewerDid) { + urlp.hash = "#".concat(profile.did, "+").concat(viewerDid); + } + else { + urlp.hash = "#".concat(profile.did); + } + return urlp.toString(); + } + catch (_a) { + return null; + } +} +function isCustomGermDomain(url) { + try { + var urlp = new URL(url); + return urlp.hostname !== 'landing.ger.mx'; + } + catch (_a) { + return false; + } +} +function platform() { + switch (Platform.OS) { + case 'ios': + return 'iOS'; + case 'android': + return 'android'; + default: + return 'web'; + } +} +function whenAppViewReady(agent, actor, fn) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, until(5, // 5 tries + 1e3, // 1s delay between tries + fn, function () { return agent.app.bsky.actor.getProfile({ actor: actor }); })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Profile/components/ProfileFeedHeader.js b/src/screens/Profile/components/ProfileFeedHeader.js new file mode 100644 index 0000000000..213c03004f --- /dev/null +++ b/src/screens/Profile/components/ProfileFeedHeader.js @@ -0,0 +1,322 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { AtUri } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useHaptics } from '#/lib/haptics'; +import { makeCustomFeedLink, makeProfileLink } from '#/lib/routes/links'; +import { shareUrl } from '#/lib/sharing'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { toShareUrl } from '#/lib/strings/url-helpers'; +import { logger } from '#/logger'; +import { useLikeMutation, useUnlikeMutation } from '#/state/queries/like'; +import { useAddSavedFeedsMutation, usePreferencesQuery, useRemoveFeedMutation, useUpdateSavedFeedsMutation, } from '#/state/queries/preferences'; +import { useSession } from '#/state/session'; +import { formatCount } from '#/view/com/util/numeric/format'; +import * as Toast from '#/view/com/util/Toast'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Divider } from '#/components/Divider'; +import { useRichText } from '#/components/hooks/useRichText'; +import { ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share } from '#/components/icons/ArrowOutOfBox'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis } from '#/components/icons/DotGrid'; +import { Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, Heart2_Stroke2_Corner0_Rounded as Heart, } from '#/components/icons/Heart2'; +import { Pin_Filled_Corner0_Rounded as PinFilled, Pin_Stroke2_Corner0_Rounded as Pin, } from '#/components/icons/Pin'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Trash_Stroke2_Corner0_Rounded as Trash } from '#/components/icons/Trash'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import * as Menu from '#/components/Menu'; +import { ReportDialog, useReportDialogControl, } from '#/components/moderation/ReportDialog'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +export function ProfileFeedHeaderSkeleton() { + var t = useTheme(); + return (_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(View, { style: [a.w_full, a.rounded_sm, t.atoms.bg_contrast_25, { height: 40 }] }) }), _jsx(Layout.Header.Slot, { children: _jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + t.atoms.bg_contrast_25, + { + height: 34, + width: 34, + }, + ], children: _jsx(Pin, { size: "lg", fill: t.atoms.text_contrast_low.color }) }) })] })); +} +export function ProfileFeedHeader(_a) { + var _this = this; + var _b; + var info = _a.info; + var t = useTheme(); + var _c = useLingui(), _ = _c._, i18n = _c.i18n; + var ax = useAnalytics(); + var hasSession = useSession().hasSession; + var gtMobile = useBreakpoints().gtMobile; + var infoControl = Dialog.useDialogControl(); + var playHaptic = useHaptics(); + var preferences = usePreferencesQuery().data; + var _d = React.useState(info.likeUri || ''), likeUri = _d[0], setLikeUri = _d[1]; + var likeCount = (info.likeCount || 0) + + (likeUri && !info.likeUri ? 1 : !likeUri && info.likeUri ? -1 : 0); + var _e = useAddSavedFeedsMutation(), addSavedFeeds = _e.mutateAsync, isAddSavedFeedPending = _e.isPending; + var _f = useRemoveFeedMutation(), removeFeed = _f.mutateAsync, isRemovePending = _f.isPending; + var _g = useUpdateSavedFeedsMutation(), updateSavedFeeds = _g.mutateAsync, isUpdateFeedPending = _g.isPending; + var isFeedStateChangePending = isAddSavedFeedPending || isRemovePending || isUpdateFeedPending; + var savedFeedConfig = (_b = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds) === null || _b === void 0 ? void 0 : _b.find(function (f) { return f.value === info.uri; }); + var isSaved = Boolean(savedFeedConfig); + var isPinned = Boolean(savedFeedConfig === null || savedFeedConfig === void 0 ? void 0 : savedFeedConfig.pinned); + var onToggleSaved = function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 5, , 6]); + playHaptic(); + if (!savedFeedConfig) return [3 /*break*/, 2]; + return [4 /*yield*/, removeFeed(savedFeedConfig)]; + case 1: + _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Removed from your feeds"], ["Removed from your feeds"]))))); + ax.metric('feed:unsave', { feedUrl: info.uri }); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, addSavedFeeds([ + { + type: 'feed', + value: info.uri, + pinned: false, + }, + ])]; + case 3: + _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Saved to your feeds"], ["Saved to your feeds"]))))); + ax.metric('feed:save', { feedUrl: info.uri }); + _a.label = 4; + case 4: return [3 /*break*/, 6]; + case 5: + err_1 = _a.sent(); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["There was an issue updating your feeds, please check your internet connection and try again."], ["There was an issue updating your feeds, please check your internet connection and try again."])))), 'xmark'); + logger.error('Failed to update feeds', { message: err_1 }); + return [3 /*break*/, 6]; + case 6: return [2 /*return*/]; + } + }); + }); }; + var onTogglePinned = function () { return __awaiter(_this, void 0, void 0, function () { + var pinned, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 5, , 6]); + playHaptic(); + if (!savedFeedConfig) return [3 /*break*/, 2]; + pinned = !savedFeedConfig.pinned; + return [4 /*yield*/, updateSavedFeeds([ + __assign(__assign({}, savedFeedConfig), { pinned: pinned }), + ])]; + case 1: + _a.sent(); + if (pinned) { + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Pinned ", " to Home"], ["Pinned ", " to Home"])), info.displayName))); + ax.metric('feed:pin', { feedUrl: info.uri }); + } + else { + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Unpinned ", " from Home"], ["Unpinned ", " from Home"])), info.displayName))); + ax.metric('feed:unpin', { feedUrl: info.uri }); + } + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, addSavedFeeds([ + { + type: 'feed', + value: info.uri, + pinned: true, + }, + ])]; + case 3: + _a.sent(); + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Pinned ", " to Home"], ["Pinned ", " to Home"])), info.displayName))); + ax.metric('feed:pin', { feedUrl: info.uri }); + _a.label = 4; + case 4: return [3 /*break*/, 6]; + case 5: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["There was an issue contacting the server"], ["There was an issue contacting the server"])))), 'xmark'); + logger.error('Failed to toggle pinned feed', { message: e_1 }); + return [3 /*break*/, 6]; + case 6: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(_Fragment, { children: [_jsx(Layout.Center, { style: [t.atoms.bg, a.z_10, web([a.sticky, a.z_10, { top: 0 }])], children: _jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { align: "left", children: _jsx(Button, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Open feed info screen"], ["Open feed info screen"])))), style: [ + a.justify_start, + { + paddingVertical: IS_WEB ? 2 : 4, + paddingRight: 8, + }, + ], onPress: function () { + playHaptic(); + infoControl.open(); + }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.rounded_sm, + a.transition_all, + t.atoms.bg_contrast_25, + { + opacity: 0, + left: IS_WEB ? -2 : -4, + right: 0, + }, + pressed && { + opacity: 1, + }, + hovered && { + opacity: 1, + transform: [{ scaleX: 1.01 }, { scaleY: 1.1 }], + }, + ] }), _jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [info.avatar && (_jsx(UserAvatar, { size: 36, type: "algo", avatar: info.avatar })), _jsxs(View, { style: [a.flex_1], children: [_jsx(Text, { style: [ + a.text_md, + a.font_bold, + a.leading_snug, + gtMobile && a.text_lg, + ], numberOfLines: 2, emoji: true, children: info.displayName }), _jsxs(View, { style: [a.flex_row, { gap: 6 }], children: [_jsx(Text, { style: [ + a.flex_shrink, + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: sanitizeHandle(info.creatorHandle, '@') }), _jsxs(View, { style: [a.flex_row, a.align_center, { gap: 2 }], children: [_jsx(HeartFilled, { size: "xs", fill: likeUri + ? t.palette.pink + : t.atoms.text_contrast_low.color }), _jsx(Text, { style: [ + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: formatCount(i18n, likeCount) })] })] })] }), _jsx(Ellipsis, { size: "md", fill: t.atoms.text_contrast_low.color })] })] })); + } }) }), hasSession && (_jsx(Layout.Header.Slot, { children: isPinned ? (_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Open feed options menu"], ["Open feed options menu"])))), children: function (_a) { + var props = _a.props; + return (_jsx(Button, __assign({}, props, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Open feed options menu"], ["Open feed options menu"])))), size: "small", variant: "ghost", shape: "square", color: "secondary", children: _jsx(PinFilled, { size: "lg", fill: t.palette.primary_500 }) }))); + } }), _jsxs(Menu.Outer, { children: [_jsxs(Menu.Item, { disabled: isFeedStateChangePending, label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Unpin from home"], ["Unpin from home"])))), onPress: onTogglePinned, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Unpin from home"], ["Unpin from home"])))) }), _jsx(Menu.ItemIcon, { icon: X, position: "right" })] }), _jsxs(Menu.Item, { disabled: isFeedStateChangePending, label: isSaved + ? _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Remove from my feeds"], ["Remove from my feeds"])))) + : _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Save to my feeds"], ["Save to my feeds"])))), onPress: onToggleSaved, children: [_jsx(Menu.ItemText, { children: isSaved + ? _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Remove from my feeds"], ["Remove from my feeds"])))) + : _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Save to my feeds"], ["Save to my feeds"])))) }), _jsx(Menu.ItemIcon, { icon: isSaved ? Trash : Plus, position: "right" })] })] })] })) : (_jsx(Button, { label: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Pin to Home"], ["Pin to Home"])))), size: "small", variant: "ghost", shape: "square", color: "secondary", onPress: onTogglePinned, children: _jsx(ButtonIcon, { icon: Pin, size: "lg" }) })) }))] }) }), _jsxs(Dialog.Outer, { control: infoControl, children: [_jsx(Dialog.Handle, {}), _jsx(Dialog.ScrollableInner, { label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Feed menu"], ["Feed menu"])))), style: [gtMobile ? { width: 'auto', minWidth: 450 } : a.w_full], children: _jsx(DialogInner, { info: info, likeUri: likeUri, setLikeUri: setLikeUri, likeCount: likeCount, isPinned: isPinned, onTogglePinned: onTogglePinned, isFeedStateChangePending: isFeedStateChangePending }) })] })] })); +} +function DialogInner(_a) { + var _this = this; + var info = _a.info, likeUri = _a.likeUri, setLikeUri = _a.setLikeUri, likeCount = _a.likeCount, isPinned = _a.isPinned, onTogglePinned = _a.onTogglePinned, isFeedStateChangePending = _a.isFeedStateChangePending; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var hasSession = useSession().hasSession; + var playHaptic = useHaptics(); + var control = Dialog.useDialogContext(); + var reportDialogControl = useReportDialogControl(); + var rt = useRichText(info.description.text)[0]; + var _b = useLikeMutation(), likeFeed = _b.mutateAsync, isLikePending = _b.isPending; + var _c = useUnlikeMutation(), unlikeFeed = _c.mutateAsync, isUnlikePending = _c.isPending; + var isLiked = !!likeUri; + var feedRkey = React.useMemo(function () { return new AtUri(info.uri).rkey; }, [info.uri]); + var onToggleLiked = function () { return __awaiter(_this, void 0, void 0, function () { + var res, err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 5, , 6]); + playHaptic(); + if (!(isLiked && likeUri)) return [3 /*break*/, 2]; + return [4 /*yield*/, unlikeFeed({ uri: likeUri })]; + case 1: + _a.sent(); + setLikeUri(''); + ax.metric('feed:unlike', { feedUrl: info.uri }); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, likeFeed({ uri: info.uri, cid: info.cid })]; + case 3: + res = _a.sent(); + setLikeUri(res.uri); + ax.metric('feed:like', { feedUrl: info.uri }); + _a.label = 4; + case 4: return [3 /*break*/, 6]; + case 5: + err_2 = _a.sent(); + Toast.show(_(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["There was an issue contacting the server, please check your internet connection and try again."], ["There was an issue contacting the server, please check your internet connection and try again."])))), 'xmark'); + logger.error('Failed to toggle like', { message: err_2 }); + return [3 /*break*/, 6]; + case 6: return [2 /*return*/]; + } + }); + }); }; + var onPressShare = React.useCallback(function () { + playHaptic(); + var url = toShareUrl(info.route.href); + shareUrl(url); + ax.metric('feed:share', { feedUrl: info.uri }); + }, [info, playHaptic]); + var onPressReport = React.useCallback(function () { + reportDialogControl.open(); + }, [reportDialogControl]); + return (_jsxs(View, { style: [a.gap_md], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_md], children: [_jsx(UserAvatar, { type: "algo", size: 48, avatar: info.avatar }), _jsxs(View, { style: [a.flex_1, a.gap_2xs], children: [_jsx(Text, { style: [a.text_2xl, a.font_bold, a.leading_tight], numberOfLines: 2, emoji: true, children: info.displayName }), _jsx(Text, { style: [a.text_sm, a.leading_relaxed, t.atoms.text_contrast_medium], numberOfLines: 1, children: _jsxs(Trans, { children: ["By", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["View ", "'s profile"], ["View ", "'s profile"])), info.creatorHandle)), to: makeProfileLink({ + did: info.creatorDid, + handle: info.creatorHandle, + }), style: [a.text_sm, a.underline, t.atoms.text_contrast_medium], numberOfLines: 1, onPress: function () { return control.close(); }, children: sanitizeHandle(info.creatorHandle, '@') })] }) })] }), _jsx(Button, { label: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Share this feed"], ["Share this feed"])))), size: "small", variant: "ghost", color: "secondary", shape: "round", onPress: onPressShare, children: _jsx(ButtonIcon, { icon: Share, size: "lg" }) })] }), _jsx(RichText, { value: rt, style: [a.text_md] }), _jsx(View, { style: [a.flex_row, a.gap_sm, a.align_center], children: typeof likeCount === 'number' && (_jsx(InlineLinkText, { label: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["View users who like this feed"], ["View users who like this feed"])))), to: makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by'), style: [a.underline, t.atoms.text_contrast_medium], onPress: function () { return control.close(); }, children: _jsxs(Trans, { children: ["Liked by ", _jsx(Plural, { value: likeCount, one: "# user", other: "# users" })] }) })) }), hasSession && (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center, a.pt_sm], children: [_jsxs(Button, { disabled: isLikePending || isUnlikePending, label: _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Like this feed"], ["Like this feed"])))), size: "small", color: "secondary", onPress: onToggleLiked, style: [a.flex_1], children: [isLiked ? (_jsx(HeartFilled, { size: "sm", fill: t.palette.pink })) : (_jsx(ButtonIcon, { icon: Heart })), _jsx(ButtonText, { children: isLiked ? _jsx(Trans, { children: "Unlike" }) : _jsx(Trans, { children: "Like" }) })] }), _jsxs(Button, { disabled: isFeedStateChangePending, label: isPinned ? _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Unpin feed"], ["Unpin feed"])))) : _(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Pin feed"], ["Pin feed"])))), size: "small", color: isPinned ? 'secondary' : 'primary', onPress: onTogglePinned, style: [a.flex_1], children: [_jsx(ButtonText, { children: isPinned ? _jsx(Trans, { children: "Unpin feed" }) : _jsx(Trans, { children: "Pin feed" }) }), _jsx(ButtonIcon, { icon: Pin, position: "right" })] })] }), _jsxs(View, { style: [a.pt_xs, a.gap_lg], children: [_jsx(Divider, {}), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm, a.justify_between], children: [_jsx(Text, { style: [a.italic, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Something wrong? Let us know." }) }), _jsxs(Button, { label: _(msg(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Report feed"], ["Report feed"])))), size: "small", variant: "solid", color: "secondary", onPress: onPressReport, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Report feed" }) }), _jsx(ButtonIcon, { icon: CircleInfo, position: "right" })] })] }), info.view && (_jsx(ReportDialog, { control: reportDialogControl, subject: __assign(__assign({}, info.view), { $type: 'app.bsky.feed.defs#generatorView' }) }))] })] }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26; diff --git a/src/screens/ProfileList/AboutSection.js b/src/screens/ProfileList/AboutSection.js new file mode 100644 index 0000000000..23083cc3a4 --- /dev/null +++ b/src/screens/ProfileList/AboutSection.js @@ -0,0 +1,51 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useImperativeHandle, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useSession } from '#/state/session'; +import { ListMembers } from '#/view/com/lists/ListMembers'; +import { EmptyState } from '#/view/com/util/EmptyState'; +import { LoadLatestBtn } from '#/view/com/util/load-latest/LoadLatestBtn'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { BulletList_Stroke1_Corner0_Rounded as ListIcon } from '#/components/icons/BulletList'; +import { PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon } from '#/components/icons/Person'; +import { IS_NATIVE } from '#/env'; +export function AboutSection(_a) { + var ref = _a.ref, list = _a.list, onPressAddUser = _a.onPressAddUser, headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var gtMobile = useBreakpoints().gtMobile; + var _b = useState(false), isScrolledDown = _b[0], setIsScrolledDown = _b[1]; + var isOwner = list.creator.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var onScrollToTop = useCallback(function () { + var _a; + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: -headerHeight, + }); + }, [scrollElRef, headerHeight]); + useImperativeHandle(ref, function () { return ({ + scrollToTop: onScrollToTop, + }); }); + var renderHeader = useCallback(function () { + if (!isOwner) { + return _jsx(View, {}); + } + if (!gtMobile) { + return (_jsx(View, { style: [a.px_sm, a.py_sm], children: _jsxs(Button, { testID: "addUserBtn", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Add a user to this list"], ["Add a user to this list"])))), onPress: onPressAddUser, color: "primary", size: "small", variant: "outline", style: [a.py_md], children: [_jsx(ButtonIcon, { icon: PersonPlusIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Add people" }) })] }) })); + } + return (_jsx(View, { style: [a.px_lg, a.py_md, a.flex_row_reverse], children: _jsxs(Button, { testID: "addUserBtn", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Add a user to this list"], ["Add a user to this list"])))), onPress: onPressAddUser, color: "primary", size: "small", variant: "ghost", style: [a.py_sm], children: [_jsx(ButtonIcon, { icon: PersonPlusIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Add people" }) })] }) })); + }, [isOwner, _, onPressAddUser, gtMobile]); + var renderEmptyState = useCallback(function () { + return (_jsxs(View, { style: [a.gap_xl, a.align_center], children: [_jsx(EmptyState, { icon: ListIcon, message: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["This list is empty."], ["This list is empty."])))) }), isOwner && (_jsxs(Button, { testID: "emptyStateAddUserBtn", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Start adding people"], ["Start adding people"])))), onPress: onPressAddUser, color: "primary", size: "small", children: [_jsx(ButtonIcon, { icon: PersonPlusIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Start adding people!" }) })] }))] })); + }, [_, isOwner, onPressAddUser]); + return (_jsxs(View, { children: [_jsx(ListMembers, { testID: "listItems", list: list.uri, scrollElRef: scrollElRef, renderHeader: renderHeader, renderEmptyState: renderEmptyState, headerOffset: headerHeight, onScrolledDownChange: setIsScrolledDown }), isScrolledDown && (_jsx(LoadLatestBtn, { onPress: onScrollToTop, label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Scroll to top"], ["Scroll to top"])))), showIndicator: false }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/screens/ProfileList/FeedSection.js b/src/screens/ProfileList/FeedSection.js new file mode 100644 index 0000000000..460b65904f --- /dev/null +++ b/src/screens/ProfileList/FeedSection.js @@ -0,0 +1,53 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useImperativeHandle, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useIsFocused } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { listenSoftReset } from '#/state/events'; +import { RQKEY as FEED_RQKEY, } from '#/state/queries/post-feed'; +import { PostFeed } from '#/view/com/posts/PostFeed'; +import { EmptyState } from '#/view/com/util/EmptyState'; +import { LoadLatestBtn } from '#/view/com/util/load-latest/LoadLatestBtn'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon } from '#/components/icons/Hashtag'; +import { PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon } from '#/components/icons/Person'; +import { IS_NATIVE } from '#/env'; +export function FeedSection(_a) { + var ref = _a.ref, feed = _a.feed, scrollElRef = _a.scrollElRef, headerHeight = _a.headerHeight, isFocused = _a.isFocused, isOwner = _a.isOwner, onPressAddUser = _a.onPressAddUser; + var queryClient = useQueryClient(); + var _b = useState(false), hasNew = _b[0], setHasNew = _b[1]; + var _c = useState(false), isScrolledDown = _c[0], setIsScrolledDown = _c[1]; + var isScreenFocused = useIsFocused(); + var _ = useLingui()._; + var onScrollToTop = useCallback(function () { + var _a; + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: -headerHeight, + }); + queryClient.resetQueries({ queryKey: FEED_RQKEY(feed) }); + setHasNew(false); + }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]); + useImperativeHandle(ref, function () { return ({ + scrollToTop: onScrollToTop, + }); }); + useEffect(function () { + if (!isScreenFocused) { + return; + } + return listenSoftReset(onScrollToTop); + }, [onScrollToTop, isScreenFocused]); + var renderPostsEmpty = useCallback(function () { + return (_jsxs(View, { style: [a.gap_xl, a.align_center], children: [_jsx(EmptyState, { icon: HashtagWideIcon, iconSize: "2xl", message: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["This feed is empty."], ["This feed is empty."])))) }), isOwner && (_jsxs(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Start adding people"], ["Start adding people"])))), onPress: onPressAddUser, color: "primary", size: "small", children: [_jsx(ButtonIcon, { icon: PersonPlusIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Start adding people!" }) })] }))] })); + }, [_, onPressAddUser, isOwner]); + return (_jsxs(View, { children: [_jsx(PostFeed, { testID: "listFeed", enabled: isFocused, feed: feed, pollInterval: 60e3, disablePoll: hasNew, scrollElRef: scrollElRef, onHasNew: setHasNew, onScrolledDownChange: setIsScrolledDown, renderEmptyState: renderPostsEmpty, headerOffset: headerHeight }), (isScrolledDown || hasNew) && (_jsx(LoadLatestBtn, { onPress: onScrollToTop, label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Load new posts"], ["Load new posts"])))), showIndicator: hasNew }))] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/ProfileList/components/ErrorScreen.js b/src/screens/ProfileList/components/ErrorScreen.js new file mode 100644 index 0000000000..be80db5057 --- /dev/null +++ b/src/screens/ProfileList/components/ErrorScreen.js @@ -0,0 +1,29 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { Text } from '#/components/Typography'; +export function ErrorScreen(_a) { + var error = _a.error; + var t = useTheme(); + var navigation = useNavigation(); + var _ = useLingui()._; + var onPressBack = function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }; + return (_jsxs(View, { style: [a.px_xl, a.py_md, a.gap_md], children: [_jsx(Text, { style: [a.text_4xl, a.font_bold], children: _jsx(Trans, { children: "Could not load list" }) }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_high, a.leading_snug], children: error }), _jsx(View, { style: [a.flex_row, a.mt_lg], children: _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go back"], ["Go back"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Returns to previous page"], ["Returns to previous page"])))), onPress: onPressBack, size: "small", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go back" }) }) }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/ProfileList/components/Header.js b/src/screens/ProfileList/components/Header.js new file mode 100644 index 0000000000..dc40adda32 --- /dev/null +++ b/src/screens/ProfileList/components/Header.js @@ -0,0 +1,187 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { AppBskyGraphDefs, RichText as RichTextAPI } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useHaptics } from '#/lib/haptics'; +import { makeListLink } from '#/lib/routes/links'; +import { logger } from '#/logger'; +import { useListBlockMutation, useListMuteMutation } from '#/state/queries/list'; +import { useAddSavedFeedsMutation, useUpdateSavedFeedsMutation, } from '#/state/queries/preferences'; +import { useSession } from '#/state/session'; +import { ProfileSubpageHeader } from '#/view/com/profile/ProfileSubpageHeader'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Pin_Stroke2_Corner0_Rounded as PinIcon } from '#/components/icons/Pin'; +import { Loader } from '#/components/Loader'; +import { RichText } from '#/components/RichText'; +import * as Toast from '#/components/Toast'; +import { useAnalytics } from '#/analytics'; +import { MoreOptionsMenu } from './MoreOptionsMenu'; +import { SubscribeMenu } from './SubscribeMenu'; +export function Header(_a) { + var _this = this; + var _b, _c, _d; + var rkey = _a.rkey, list = _a.list, preferences = _a.preferences; + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST; + var isModList = list.purpose === AppBskyGraphDefs.MODLIST; + var isBlocking = !!((_b = list.viewer) === null || _b === void 0 ? void 0 : _b.blocked); + var isMuting = !!((_c = list.viewer) === null || _c === void 0 ? void 0 : _c.muted); + var playHaptic = useHaptics(); + var _e = useListMuteMutation(), muteList = _e.mutateAsync, isMutePending = _e.isPending; + var _f = useListBlockMutation(), blockList = _f.mutateAsync, isBlockPending = _f.isPending; + var _g = useAddSavedFeedsMutation(), addSavedFeeds = _g.mutateAsync, isAddSavedFeedPending = _g.isPending; + var _h = useUpdateSavedFeedsMutation(), updateSavedFeeds = _h.mutateAsync, isUpdatingSavedFeeds = _h.isPending; + var isPending = isAddSavedFeedPending || isUpdatingSavedFeeds; + var savedFeedConfig = (_d = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds) === null || _d === void 0 ? void 0 : _d.find(function (f) { return f.value === list.uri; }); + var isPinned = Boolean(savedFeedConfig === null || savedFeedConfig === void 0 ? void 0 : savedFeedConfig.pinned); + var onTogglePinned = function () { return __awaiter(_this, void 0, void 0, function () { + var pinned, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + playHaptic(); + _a.label = 1; + case 1: + _a.trys.push([1, 6, , 7]); + if (!savedFeedConfig) return [3 /*break*/, 3]; + pinned = !savedFeedConfig.pinned; + return [4 /*yield*/, updateSavedFeeds([ + __assign(__assign({}, savedFeedConfig), { pinned: pinned }), + ])]; + case 2: + _a.sent(); + Toast.show(pinned + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Pinned to your feeds"], ["Pinned to your feeds"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Unpinned from your feeds"], ["Unpinned from your feeds"]))))); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, addSavedFeeds([ + { + type: 'list', + value: list.uri, + pinned: true, + }, + ])]; + case 4: + _a.sent(); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Saved to your feeds"], ["Saved to your feeds"]))))); + _a.label = 5; + case 5: return [3 /*break*/, 7]; + case 6: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["There was an issue contacting the server"], ["There was an issue contacting the server"])))), { + type: 'error', + }); + logger.error('Failed to toggle pinned feed', { message: e_1 }); + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); }; + var onUnsubscribeMute = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, muteList({ uri: list.uri, mute: false })]; + case 1: + _b.sent(); + Toast.show(_(msg({ message: 'List unmuted', context: 'toast' }))); + ax.metric('moderation:unsubscribedFromList', { listType: 'mute' }); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."]))))); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var onUnsubscribeBlock = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, blockList({ uri: list.uri, block: false })]; + case 1: + _b.sent(); + Toast.show(_(msg({ message: 'List unblocked', context: 'toast' }))); + ax.metric('moderation:unsubscribedFromList', { listType: 'block' }); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."]))))); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var descriptionRT = useMemo(function () { + return list.description + ? new RichTextAPI({ + text: list.description, + facets: list.descriptionFacets, + }) + : undefined; + }, [list]); + return (_jsxs(_Fragment, { children: [_jsxs(ProfileSubpageHeader, { href: makeListLink(list.creator.handle || list.creator.did || '', rkey), title: list.name, avatar: list.avatar, isOwner: list.creator.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did), creator: list.creator, purpose: list.purpose, avatarType: "list", children: [isCurateList ? (_jsxs(Button, { testID: isPinned ? 'unpinBtn' : 'pinBtn', color: isPinned ? 'secondary' : 'primary_subtle', label: isPinned ? _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Unpin"], ["Unpin"])))) : _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Pin to home"], ["Pin to home"])))), onPress: onTogglePinned, disabled: isPending, size: "small", style: [a.rounded_full], children: [!isPinned && _jsx(ButtonIcon, { icon: isPending ? Loader : PinIcon }), _jsx(ButtonText, { children: isPinned ? _jsx(Trans, { children: "Unpin" }) : _jsx(Trans, { children: "Pin to home" }) })] })) : isModList ? (isBlocking ? (_jsxs(Button, { testID: "unblockBtn", color: "secondary", label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Unblock"], ["Unblock"])))), onPress: onUnsubscribeBlock, size: "small", style: [a.rounded_full], disabled: isBlockPending, children: [isBlockPending && _jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Unblock" }) })] })) : isMuting ? (_jsxs(Button, { testID: "unmuteBtn", color: "secondary", label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Unmute"], ["Unmute"])))), onPress: onUnsubscribeMute, size: "small", style: [a.rounded_full], disabled: isMutePending, children: [isMutePending && _jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Unmute" }) })] })) : (_jsx(SubscribeMenu, { list: list }))) : null, _jsx(MoreOptionsMenu, { list: list })] }), descriptionRT ? (_jsx(View, { style: [a.px_lg, a.pt_sm, a.pb_sm, a.gap_md], children: _jsx(RichText, { value: descriptionRT, style: [a.text_md] }) })) : null] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10; diff --git a/src/screens/ProfileList/components/MoreOptionsMenu.js b/src/screens/ProfileList/components/MoreOptionsMenu.js new file mode 100644 index 0000000000..670e10e119 --- /dev/null +++ b/src/screens/ProfileList/components/MoreOptionsMenu.js @@ -0,0 +1,225 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { AppBskyGraphDefs, AtUri } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { shareUrl } from '#/lib/sharing'; +import { toShareUrl } from '#/lib/strings/url-helpers'; +import { logger } from '#/logger'; +import { useListBlockMutation, useListDeleteMutation, useListMuteMutation, } from '#/state/queries/list'; +import { useRemoveFeedMutation } from '#/state/queries/preferences'; +import { useSession } from '#/state/session'; +import { Button, ButtonIcon } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { CreateOrEditListDialog } from '#/components/dialogs/lists/CreateOrEditListDialog'; +import { ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon } from '#/components/icons/ArrowOutOfBox'; +import { ChainLink_Stroke2_Corner0_Rounded as ChainLink } from '#/components/icons/ChainLink'; +import { DotGrid3x1_Stroke2_Corner0_Rounded as DotGridIcon } from '#/components/icons/DotGrid'; +import { PencilLine_Stroke2_Corner0_Rounded as PencilLineIcon } from '#/components/icons/Pencil'; +import { PersonCheck_Stroke2_Corner0_Rounded as PersonCheckIcon } from '#/components/icons/Person'; +import { Pin_Stroke2_Corner0_Rounded as PinIcon } from '#/components/icons/Pin'; +import { SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon } from '#/components/icons/Speaker'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import { Warning_Stroke2_Corner0_Rounded as WarningIcon } from '#/components/icons/Warning'; +import * as Menu from '#/components/Menu'; +import { ReportDialog, useReportDialogControl, } from '#/components/moderation/ReportDialog'; +import * as Prompt from '#/components/Prompt'; +import * as Toast from '#/components/Toast'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +export function MoreOptionsMenu(_a) { + var _this = this; + var _b, _c; + var list = _a.list, savedFeedConfig = _a.savedFeedConfig; + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var editListDialogControl = useDialogControl(); + var deleteListPromptControl = useDialogControl(); + var reportDialogControl = useReportDialogControl(); + var navigation = useNavigation(); + var removeSavedFeed = useRemoveFeedMutation().mutateAsync; + var deleteList = useListDeleteMutation().mutateAsync; + var muteList = useListMuteMutation().mutateAsync; + var blockList = useListBlockMutation().mutateAsync; + var isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST; + var isModList = list.purpose === AppBskyGraphDefs.MODLIST; + var isBlocking = !!((_b = list.viewer) === null || _b === void 0 ? void 0 : _b.blocked); + var isMuting = !!((_c = list.viewer) === null || _c === void 0 ? void 0 : _c.muted); + var isPinned = Boolean(savedFeedConfig === null || savedFeedConfig === void 0 ? void 0 : savedFeedConfig.pinned); + var isOwner = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === list.creator.did; + var onPressShare = function () { + var rkey = new AtUri(list.uri).rkey; + var url = toShareUrl("/profile/".concat(list.creator.did, "/lists/").concat(rkey)); + shareUrl(url); + }; + var onRemoveFromSavedFeeds = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!savedFeedConfig) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, removeSavedFeed(savedFeedConfig)]; + case 2: + _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Removed from your feeds"], ["Removed from your feeds"]))))); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["There was an issue contacting the server"], ["There was an issue contacting the server"])))), { + type: 'error', + }); + logger.error('Failed to remove pinned list', { message: e_1 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var onPressDelete = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, deleteList({ uri: list.uri })]; + case 1: + _a.sent(); + if (!savedFeedConfig) return [3 /*break*/, 3]; + return [4 /*yield*/, removeSavedFeed(savedFeedConfig)]; + case 2: + _a.sent(); + _a.label = 3; + case 3: + Toast.show(_(msg({ message: 'List deleted', context: 'toast' }))); + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + return [2 /*return*/]; + } + }); + }); }; + var onUnpinModList = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + if (!savedFeedConfig) + return [2 /*return*/]; + return [4 /*yield*/, removeSavedFeed(savedFeedConfig)]; + case 1: + _b.sent(); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Unpinned list"], ["Unpinned list"]))))); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Failed to unpin list"], ["Failed to unpin list"])))), { + type: 'error', + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var onUnsubscribeMute = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, muteList({ uri: list.uri, mute: false })]; + case 1: + _b.sent(); + Toast.show(_(msg({ message: 'List unmuted', context: 'toast' }))); + ax.metric('moderation:unsubscribedFromList', { listType: 'mute' }); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."]))))); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var onUnsubscribeBlock = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, blockList({ uri: list.uri, block: false })]; + case 1: + _b.sent(); + Toast.show(_(msg({ message: 'List unblocked', context: 'toast' }))); + ax.metric('moderation:unsubscribedFromList', { listType: 'block' }); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."]))))); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["More options"], ["More options"])))), children: function (_a) { + var props = _a.props; + return (_jsx(Button, __assign({ label: props.accessibilityLabel, testID: "moreOptionsBtn", size: "small", color: "secondary", shape: "round" }, props, { children: _jsx(ButtonIcon, { icon: DotGridIcon }) }))); + } }), _jsxs(Menu.Outer, { children: [_jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { label: IS_WEB ? _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Copy link to list"], ["Copy link to list"])))) : _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Share via..."], ["Share via..."])))), onPress: onPressShare, children: [_jsx(Menu.ItemText, { children: IS_WEB ? (_jsx(Trans, { children: "Copy link to list" })) : (_jsx(Trans, { children: "Share via..." })) }), _jsx(Menu.ItemIcon, { position: "right", icon: IS_WEB ? ChainLink : ShareIcon })] }), savedFeedConfig && (_jsxs(Menu.Item, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Remove from my feeds"], ["Remove from my feeds"])))), onPress: onRemoveFromSavedFeeds, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Remove from my feeds" }) }), _jsx(Menu.ItemIcon, { position: "right", icon: TrashIcon })] }))] }), _jsx(Menu.Divider, {}), isOwner ? (_jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Edit list details"], ["Edit list details"])))), onPress: editListDialogControl.open, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Edit list details" }) }), _jsx(Menu.ItemIcon, { position: "right", icon: PencilLineIcon })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Delete list"], ["Delete list"])))), onPress: deleteListPromptControl.open, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Delete list" }) }), _jsx(Menu.ItemIcon, { position: "right", icon: TrashIcon })] })] })) : (_jsx(Menu.Group, { children: _jsxs(Menu.Item, { label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Report list"], ["Report list"])))), onPress: reportDialogControl.open, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Report list" }) }), _jsx(Menu.ItemIcon, { position: "right", icon: WarningIcon })] }) })), isModList && isPinned && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsx(Menu.Group, { children: _jsxs(Menu.Item, { label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Unpin moderation list"], ["Unpin moderation list"])))), onPress: onUnpinModList, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Unpin moderation list" }) }), _jsx(Menu.ItemIcon, { icon: PinIcon })] }) })] })), isCurateList && (isBlocking || isMuting) && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsxs(Menu.Group, { children: [isBlocking && (_jsxs(Menu.Item, { label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Unblock list"], ["Unblock list"])))), onPress: onUnsubscribeBlock, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Unblock list" }) }), _jsx(Menu.ItemIcon, { icon: PersonCheckIcon })] })), isMuting && (_jsxs(Menu.Item, { label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Unmute list"], ["Unmute list"])))), onPress: onUnsubscribeMute, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Unmute list" }) }), _jsx(Menu.ItemIcon, { icon: UnmuteIcon })] }))] })] }))] })] }), _jsx(CreateOrEditListDialog, { control: editListDialogControl, list: list }), _jsx(Prompt.Basic, { control: deleteListPromptControl, title: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Delete this list?"], ["Delete this list?"])))), description: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["If you delete this list, you won't be able to recover it."], ["If you delete this list, you won't be able to recover it."])))), onConfirm: onPressDelete, confirmButtonCta: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Delete"], ["Delete"])))), confirmButtonColor: "negative" }), _jsx(ReportDialog, { control: reportDialogControl, subject: __assign(__assign({}, list), { $type: 'app.bsky.graph.defs#listView' }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19; diff --git a/src/screens/ProfileList/components/SubscribeMenu.js b/src/screens/ProfileList/components/SubscribeMenu.js new file mode 100644 index 0000000000..d366a6a20b --- /dev/null +++ b/src/screens/ProfileList/components/SubscribeMenu.js @@ -0,0 +1,121 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useListBlockMutation, useListMuteMutation } from '#/state/queries/list'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Mute_Stroke2_Corner0_Rounded as MuteIcon } from '#/components/icons/Mute'; +import { PersonX_Stroke2_Corner0_Rounded as PersonXIcon } from '#/components/icons/Person'; +import { Loader } from '#/components/Loader'; +import * as Menu from '#/components/Menu'; +import * as Prompt from '#/components/Prompt'; +import * as Toast from '#/components/Toast'; +import { useAnalytics } from '#/analytics'; +export function SubscribeMenu(_a) { + var _this = this; + var list = _a.list; + var _ = useLingui()._; + var ax = useAnalytics(); + var subscribeMutePromptControl = Prompt.usePromptControl(); + var subscribeBlockPromptControl = Prompt.usePromptControl(); + var _b = useListMuteMutation(), muteList = _b.mutateAsync, isMutePending = _b.isPending; + var _c = useListBlockMutation(), blockList = _c.mutateAsync, isBlockPending = _c.isPending; + var isPending = isMutePending || isBlockPending; + var onSubscribeMute = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, muteList({ uri: list.uri, mute: true })]; + case 1: + _b.sent(); + Toast.show(_(msg({ message: 'List muted', context: 'toast' }))); + ax.metric('moderation:subscribedToList', { listType: 'mute' }); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."])))), { type: 'error' }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var onSubscribeBlock = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, blockList({ uri: list.uri, block: true })]; + case 1: + _b.sent(); + Toast.show(_(msg({ message: 'List blocked', context: 'toast' }))); + ax.metric('moderation:subscribedToList', { listType: 'block' }); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["There was an issue. Please check your internet connection and try again."], ["There was an issue. Please check your internet connection and try again."])))), { type: 'error' }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Subscribe to this list"], ["Subscribe to this list"])))), children: function (_a) { + var props = _a.props; + return (_jsxs(Button, __assign({ label: props.accessibilityLabel, testID: "subscribeBtn", size: "small", color: "primary_subtle", style: [a.rounded_full], disabled: isPending }, props, { children: [isPending && _jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Subscribe" }) })] }))); + } }), _jsx(Menu.Outer, { showCancel: true, children: _jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Mute accounts"], ["Mute accounts"])))), onPress: subscribeMutePromptControl.open, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Mute accounts" }) }), _jsx(Menu.ItemIcon, { position: "right", icon: MuteIcon })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Block accounts"], ["Block accounts"])))), onPress: subscribeBlockPromptControl.open, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Block accounts" }) }), _jsx(Menu.ItemIcon, { position: "right", icon: PersonXIcon })] })] }) })] }), _jsx(Prompt.Basic, { control: subscribeMutePromptControl, title: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Mute these accounts?"], ["Mute these accounts?"])))), description: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them."], ["Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them."])))), onConfirm: onSubscribeMute, confirmButtonCta: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Mute list"], ["Mute list"])))) }), _jsx(Prompt.Basic, { control: subscribeBlockPromptControl, title: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Block these accounts?"], ["Block these accounts?"])))), description: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."], ["Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."])))), onConfirm: onSubscribeBlock, confirmButtonCta: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Block list"], ["Block list"])))), confirmButtonColor: "negative" })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11; diff --git a/src/screens/ProfileList/index.js b/src/screens/ProfileList/index.js new file mode 100644 index 0000000000..6cd8b8fa47 --- /dev/null +++ b/src/screens/ProfileList/index.js @@ -0,0 +1,133 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useMemo, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { useAnimatedRef } from 'react-native-reanimated'; +import { AppBskyGraphDefs, AtUri, moderateUserList, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect, useIsFocused } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { useSetTitle } from '#/lib/hooks/useSetTitle'; +import { ComposeIcon2 } from '#/lib/icons'; +import { cleanError } from '#/lib/strings/errors'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useListQuery } from '#/state/queries/list'; +import { RQKEY as FEED_RQKEY } from '#/state/queries/post-feed'; +import { usePreferencesQuery, } from '#/state/queries/preferences'; +import { useResolveUriQuery } from '#/state/queries/resolve-uri'; +import { truncateAndInvalidate } from '#/state/queries/util'; +import { useSession } from '#/state/session'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { PagerWithHeader } from '#/view/com/pager/PagerWithHeader'; +import { FAB } from '#/view/com/util/fab/FAB'; +import { ListHiddenScreen } from '#/screens/List/ListHiddenScreen'; +import { atoms as a, native, platform, useTheme } from '#/alf'; +import { useDialogControl } from '#/components/Dialog'; +import { ListAddRemoveUsersDialog } from '#/components/dialogs/lists/ListAddRemoveUsersDialog'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import * as Hider from '#/components/moderation/Hider'; +import { IS_WEB } from '#/env'; +import { AboutSection } from './AboutSection'; +import { ErrorScreen } from './components/ErrorScreen'; +import { Header } from './components/Header'; +import { FeedSection } from './FeedSection'; +export function ProfileListScreen(props) { + return (_jsx(Layout.Screen, { testID: "profileListScreen", children: _jsx(ProfileListScreenInner, __assign({}, props)) })); +} +function ProfileListScreenInner(props) { + var _ = useLingui()._; + var _a = props.route.params, handleOrDid = _a.name, rkey = _a.rkey; + var _b = useResolveUriQuery(AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString()), resolvedUri = _b.data, resolveError = _b.error; + var preferences = usePreferencesQuery().data; + var _c = useListQuery(resolvedUri === null || resolvedUri === void 0 ? void 0 : resolvedUri.uri), list = _c.data, listError = _c.error; + var moderationOpts = useModerationOpts(); + if (resolveError) { + return (_jsxs(_Fragment, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Could not load list" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { centerContent: true, children: _jsx(ErrorScreen, { error: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @", "."], ["We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @", "."])), handleOrDid)) }) })] })); + } + if (listError) { + return (_jsxs(_Fragment, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Could not load list" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { centerContent: true, children: _jsx(ErrorScreen, { error: cleanError(listError) }) })] })); + } + return resolvedUri && list && moderationOpts && preferences ? (_jsx(ProfileListScreenLoaded, __assign({}, props, { uri: resolvedUri.uri, list: list, moderationOpts: moderationOpts, preferences: preferences }))) : (_jsxs(_Fragment, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, {}), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { centerContent: true, contentContainerStyle: platform({ + web: [a.mx_auto], + native: [a.align_center], + }), children: _jsx(Loader, { size: "2xl" }) })] })); +} +function ProfileListScreenLoaded(_a) { + var _b; + var route = _a.route, uri = _a.uri, list = _a.list, moderationOpts = _a.moderationOpts, preferences = _a.preferences; + var t = useTheme(); + var _ = useLingui()._; + var queryClient = useQueryClient(); + var openComposer = useOpenComposer().openComposer; + var setMinimalShellMode = useSetMinimalShellMode(); + var currentAccount = useSession().currentAccount; + var rkey = route.params.rkey; + var feedSectionRef = useRef(null); + var aboutSectionRef = useRef(null); + var isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST; + var isScreenFocused = useIsFocused(); + var isHidden = ((_b = list.labels) === null || _b === void 0 ? void 0 : _b.findIndex(function (l) { return l.val === '!hide'; })) !== -1; + var isOwner = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === list.creator.did; + var scrollElRef = useAnimatedRef(); + var addUserDialogControl = useDialogControl(); + var sectionTitlesCurate = [_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Posts"], ["Posts"])))), _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["People"], ["People"]))))]; + // modlist only + var _c = useState(null), headerHeight = _c[0], setHeaderHeight = _c[1]; + var moderation = useMemo(function () { + return moderateUserList(list, moderationOpts); + }, [list, moderationOpts]); + useSetTitle(isHidden ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["List Hidden"], ["List Hidden"])))) : list.name); + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var onChangeMembers = function () { + if (isCurateList) { + truncateAndInvalidate(queryClient, FEED_RQKEY("list|".concat(list.uri))); + } + }; + var onCurrentPageSelected = useCallback(function (index) { + var _a, _b; + if (index === 0) { + (_a = feedSectionRef.current) === null || _a === void 0 ? void 0 : _a.scrollToTop(); + } + else if (index === 1) { + (_b = aboutSectionRef.current) === null || _b === void 0 ? void 0 : _b.scrollToTop(); + } + }, [feedSectionRef]); + var renderHeader = useCallback(function () { + return _jsx(Header, { rkey: rkey, list: list, preferences: preferences }); + }, [rkey, list, preferences]); + if (isCurateList) { + return (_jsxs(Hider.Outer, { modui: moderation.ui('contentView'), allowOverride: isOwner, children: [_jsx(Hider.Mask, { children: _jsx(ListHiddenScreen, { list: list, preferences: preferences }) }), _jsxs(Hider.Content, { children: [_jsxs(View, { style: [a.util_screen_outer], children: [_jsxs(PagerWithHeader, { items: sectionTitlesCurate, isHeaderReady: true, renderHeader: renderHeader, onCurrentPageSelected: onCurrentPageSelected, children: [function (_a) { + var headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef, isFocused = _a.isFocused; + return (_jsx(FeedSection, { ref: feedSectionRef, feed: "list|".concat(uri), scrollElRef: scrollElRef, headerHeight: headerHeight, isFocused: isScreenFocused && isFocused, isOwner: isOwner, onPressAddUser: addUserDialogControl.open })); + }, function (_a) { + var headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + return (_jsx(AboutSection, { ref: aboutSectionRef, scrollElRef: scrollElRef, list: list, onPressAddUser: addUserDialogControl.open, headerHeight: headerHeight })); + }] }), _jsx(FAB, { testID: "composeFAB", onPress: function () { return openComposer({ logContext: 'Fab' }); }, icon: _jsx(ComposeIcon2, { strokeWidth: 1.5, size: 29, style: { color: 'white' } }), accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["New post"], ["New post"])))), accessibilityHint: "" })] }), _jsx(ListAddRemoveUsersDialog, { control: addUserDialogControl, list: list, onChange: onChangeMembers })] })] })); + } + return (_jsxs(Hider.Outer, { modui: moderation.ui('contentView'), allowOverride: isOwner, children: [_jsx(Hider.Mask, { children: _jsx(ListHiddenScreen, { list: list, preferences: preferences }) }), _jsxs(Hider.Content, { children: [_jsxs(View, { style: [a.util_screen_outer], children: [_jsx(Layout.Center, { onLayout: function (evt) { return setHeaderHeight(evt.nativeEvent.layout.height); }, style: [ + native([a.absolute, a.z_10, t.atoms.bg]), + a.border_b, + t.atoms.border_contrast_low, + ], children: renderHeader() }), headerHeight !== null && (_jsx(AboutSection, { list: list, scrollElRef: scrollElRef, onPressAddUser: addUserDialogControl.open, headerHeight: IS_WEB ? 0 : headerHeight })), _jsx(FAB, { testID: "composeFAB", onPress: function () { return openComposer({ logContext: 'Fab' }); }, icon: _jsx(ComposeIcon2, { strokeWidth: 1.5, size: 29, style: { color: 'white' } }), accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["New post"], ["New post"])))), accessibilityHint: "" })] }), _jsx(ListAddRemoveUsersDialog, { control: addUserDialogControl, list: list, onChange: onChangeMembers })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/SavedFeeds.js b/src/screens/SavedFeeds.js new file mode 100644 index 0000000000..047062d1d5 --- /dev/null +++ b/src/screens/SavedFeeds.js @@ -0,0 +1,294 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { useAnimatedRef, useScrollViewOffset } from 'react-native-reanimated'; +import { TID } from '@atproto/common-web'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; +import { RECOMMENDED_SAVED_FEEDS, TIMELINE_SAVED_FEED } from '#/lib/constants'; +import { useHaptics } from '#/lib/haptics'; +import { logger } from '#/logger'; +import { useA11y } from '#/state/a11y'; +import { useOverwriteSavedFeedsMutation, usePreferencesQuery, } from '#/state/queries/preferences'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { FeedSourceCard } from '#/view/com/feeds/FeedSourceCard'; +import * as Toast from '#/view/com/util/Toast'; +import { NoFollowingFeed } from '#/screens/Feeds/NoFollowingFeed'; +import { NoSavedFeedsOfAnyType } from '#/screens/Feeds/NoSavedFeedsOfAnyType'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { SortableList } from '#/components/DraggableList'; +import { ArrowBottom_Stroke2_Corner0_Rounded as ArrowDownIcon, ArrowTop_Stroke2_Corner0_Rounded as ArrowUpIcon, } from '#/components/icons/Arrow'; +import { FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline } from '#/components/icons/FilterTimeline'; +import { FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon } from '#/components/icons/FloppyDisk'; +import { Pin_Filled_Corner0_Rounded as PinIcon } from '#/components/icons/Pin'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +export function SavedFeeds(_a) { + var preferences = usePreferencesQuery().data; + var screenReaderEnabled = useA11y().screenReaderEnabled; + if (!preferences) { + return _jsx(View, {}); + } + if (screenReaderEnabled) { + return _jsx(SavedFeedsA11y, { preferences: preferences }); + } + return _jsx(SavedFeedsInner, { preferences: preferences }); +} +function SavedFeedsInner(_a) { + var _this = this; + var preferences = _a.preferences; + var t = useTheme(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var setMinimalShellMode = useSetMinimalShellMode(); + var _b = useOverwriteSavedFeedsMutation(), overwriteSavedFeeds = _b.mutateAsync, isOverwritePending = _b.isPending; + var navigation = useNavigation(); + var scrollRef = useAnimatedRef(); + var scrollOffset = useScrollViewOffset(scrollRef); + /* + * Use optimistic data if exists and no error, otherwise fallback to remote + * data + */ + var _c = useState(function () { return preferences.savedFeeds || []; }), currentFeeds = _c[0], setCurrentFeeds = _c[1]; + var hasUnsavedChanges = currentFeeds !== preferences.savedFeeds; + var pinnedFeeds = currentFeeds.filter(function (f) { return f.pinned; }); + var unpinnedFeeds = currentFeeds.filter(function (f) { return !f.pinned; }); + var noSavedFeedsOfAnyType = pinnedFeeds.length + unpinnedFeeds.length === 0; + var noFollowingFeed = currentFeeds.every(function (f) { return f.type !== 'timeline'; }) && !noSavedFeedsOfAnyType; + var _d = useState(false), isDragging = _d[0], setIsDragging = _d[1]; + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var onSaveChanges = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, overwriteSavedFeeds(currentFeeds)]; + case 1: + _a.sent(); + Toast.show(_(msg({ message: 'Feeds updated!', context: 'toast' }))); + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Feeds'); + } + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["There was an issue contacting the server"], ["There was an issue contacting the server"])))), 'xmark'); + logger.error('Failed to toggle pinned feed', { message: e_1 }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { align: "left", children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Feeds" }) }) }), _jsxs(Button, { testID: "saveChangesBtn", size: "small", color: hasUnsavedChanges ? 'primary' : 'secondary', onPress: onSaveChanges, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Save changes"], ["Save changes"])))), disabled: isOverwritePending || !hasUnsavedChanges, children: [_jsx(ButtonIcon, { icon: isOverwritePending ? Loader : SaveIcon }), _jsx(ButtonText, { children: gtMobile ? _jsx(Trans, { children: "Save changes" }) : _jsx(Trans, { children: "Save" }) })] })] }), _jsxs(Layout.Content, { ref: scrollRef, scrollEnabled: !isDragging, children: [noSavedFeedsOfAnyType && (_jsx(View, { style: [t.atoms.border_contrast_low, a.border_b], children: _jsx(NoSavedFeedsOfAnyType, { onAddRecommendedFeeds: function () { + return setCurrentFeeds(RECOMMENDED_SAVED_FEEDS.map(function (f) { return (__assign(__assign({}, f), { id: TID.nextStr() })); })); + } }) })), _jsx(SectionHeaderText, { children: _jsx(Trans, { children: "Pinned Feeds" }) }), preferences ? (!pinnedFeeds.length ? (_jsx(View, { style: [a.flex_1, a.p_lg], children: _jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "You don't have any pinned feeds." }) }) })) : (_jsx(SortableList, { data: pinnedFeeds, keyExtractor: function (f) { return f.id; }, itemHeight: 68, scrollRef: scrollRef, scrollOffset: scrollOffset, onDragStart: function () { return setIsDragging(true); }, onDragEnd: function () { return setIsDragging(false); }, onReorder: function (reordered) { + setCurrentFeeds(__spreadArray(__spreadArray([], reordered, true), unpinnedFeeds, true)); + }, renderItem: function (feed, dragHandle) { return (_jsx(PinnedFeedItem, { feed: feed, currentFeeds: currentFeeds, setCurrentFeeds: setCurrentFeeds, dragHandle: dragHandle })); } }))) : (_jsx(View, { style: [a.w_full, a.py_2xl, a.align_center], children: _jsx(Loader, { size: "xl" }) })), noFollowingFeed && (_jsx(View, { style: [t.atoms.border_contrast_low, a.border_b], children: _jsx(NoFollowingFeed, { onAddFeed: function () { + return setCurrentFeeds(function (feeds) { return __spreadArray(__spreadArray([], feeds, true), [ + __assign(__assign({}, TIMELINE_SAVED_FEED), { id: TID.next().toString() }), + ], false); }); + } }) })), _jsx(SectionHeaderText, { children: _jsx(Trans, { children: "Saved Feeds" }) }), preferences ? (!unpinnedFeeds.length ? (_jsx(View, { style: [a.flex_1, a.p_lg], children: _jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "You don't have any saved feeds." }) }) })) : (unpinnedFeeds.map(function (f) { return (_jsx(UnpinnedFeedItem, { feed: f, currentFeeds: currentFeeds, setCurrentFeeds: setCurrentFeeds }, f.id)); }))) : (_jsx(View, { style: [a.w_full, a.py_2xl, a.align_center], children: _jsx(Loader, { size: "xl" }) })), _jsx(View, { style: [a.px_lg, a.py_xl], children: _jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug], children: _jsxs(Trans, { children: ["Feeds are custom algorithms that users build with a little coding expertise.", ' ', _jsx(InlineLinkText, { to: "https://github.com/bluesky-social/feed-generator", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["See this guide"], ["See this guide"])))), disableMismatchWarning: true, style: [a.leading_snug], children: "See this guide" }), ' ', "for more information."] }) }) })] })] })); +} +function SavedFeedsA11y(_a) { + var _this = this; + var preferences = _a.preferences; + var t = useTheme(); + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var setMinimalShellMode = useSetMinimalShellMode(); + var _b = useOverwriteSavedFeedsMutation(), overwriteSavedFeeds = _b.mutateAsync, isOverwritePending = _b.isPending; + var navigation = useNavigation(); + var _c = useState(function () { return preferences.savedFeeds || []; }), currentFeeds = _c[0], setCurrentFeeds = _c[1]; + var hasUnsavedChanges = currentFeeds !== preferences.savedFeeds; + var pinnedFeeds = currentFeeds.filter(function (f) { return f.pinned; }); + var unpinnedFeeds = currentFeeds.filter(function (f) { return !f.pinned; }); + var noSavedFeedsOfAnyType = pinnedFeeds.length + unpinnedFeeds.length === 0; + var noFollowingFeed = currentFeeds.every(function (f) { return f.type !== 'timeline'; }) && !noSavedFeedsOfAnyType; + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var onSaveChanges = function () { return __awaiter(_this, void 0, void 0, function () { + var e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, overwriteSavedFeeds(currentFeeds)]; + case 1: + _a.sent(); + Toast.show(_(msg({ message: 'Feeds updated!', context: 'toast' }))); + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Feeds'); + } + return [3 /*break*/, 3]; + case 2: + e_2 = _a.sent(); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["There was an issue contacting the server"], ["There was an issue contacting the server"])))), 'xmark'); + logger.error('Failed to toggle pinned feed', { message: e_2 }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var onMoveUp = function (index) { + var _a; + var pinned = __spreadArray([], pinnedFeeds, true); + _a = [pinned[index], pinned[index - 1]], pinned[index - 1] = _a[0], pinned[index] = _a[1]; + setCurrentFeeds(__spreadArray(__spreadArray([], pinned, true), unpinnedFeeds, true)); + }; + var onMoveDown = function (index) { + var _a; + var pinned = __spreadArray([], pinnedFeeds, true); + _a = [pinned[index + 1], pinned[index]], pinned[index] = _a[0], pinned[index + 1] = _a[1]; + setCurrentFeeds(__spreadArray(__spreadArray([], pinned, true), unpinnedFeeds, true)); + }; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { align: "left", children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Feeds" }) }) }), _jsxs(Button, { testID: "saveChangesBtn", size: "small", color: hasUnsavedChanges ? 'primary' : 'secondary', onPress: onSaveChanges, label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Save changes"], ["Save changes"])))), disabled: isOverwritePending || !hasUnsavedChanges, children: [_jsx(ButtonIcon, { icon: isOverwritePending ? Loader : SaveIcon }), _jsx(ButtonText, { children: gtMobile ? _jsx(Trans, { children: "Save changes" }) : _jsx(Trans, { children: "Save" }) })] })] }), _jsxs(Layout.Content, { children: [noSavedFeedsOfAnyType && (_jsx(View, { style: [t.atoms.border_contrast_low, a.border_b], children: _jsx(NoSavedFeedsOfAnyType, { onAddRecommendedFeeds: function () { + return setCurrentFeeds(RECOMMENDED_SAVED_FEEDS.map(function (f) { return (__assign(__assign({}, f), { id: TID.nextStr() })); })); + } }) })), _jsx(SectionHeaderText, { children: _jsx(Trans, { children: "Pinned Feeds" }) }), !pinnedFeeds.length ? (_jsx(View, { style: [a.flex_1, a.p_lg], children: _jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "You don't have any pinned feeds." }) }) })) : (pinnedFeeds.map(function (feed, i) { return (_jsx(PinnedFeedItem, { feed: feed, currentFeeds: currentFeeds, setCurrentFeeds: setCurrentFeeds, index: i, total: pinnedFeeds.length, onMoveUp: function () { return onMoveUp(i); }, onMoveDown: function () { return onMoveDown(i); } }, feed.id)); })), noFollowingFeed && (_jsx(View, { style: [t.atoms.border_contrast_low, a.border_b], children: _jsx(NoFollowingFeed, { onAddFeed: function () { + return setCurrentFeeds(function (feeds) { return __spreadArray(__spreadArray([], feeds, true), [ + __assign(__assign({}, TIMELINE_SAVED_FEED), { id: TID.next().toString() }), + ], false); }); + } }) })), _jsx(SectionHeaderText, { children: _jsx(Trans, { children: "Saved Feeds" }) }), !unpinnedFeeds.length ? (_jsx(View, { style: [a.flex_1, a.p_lg], children: _jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "You don't have any saved feeds." }) }) })) : (unpinnedFeeds.map(function (f) { return (_jsx(UnpinnedFeedItem, { feed: f, currentFeeds: currentFeeds, setCurrentFeeds: setCurrentFeeds }, f.id)); })), _jsx(View, { style: [a.px_lg, a.py_xl], children: _jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug], children: _jsxs(Trans, { children: ["Feeds are custom algorithms that users build with a little coding expertise.", ' ', _jsx(InlineLinkText, { to: "https://github.com/bluesky-social/feed-generator", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["See this guide"], ["See this guide"])))), disableMismatchWarning: true, style: [a.leading_snug], children: "See this guide" }), ' ', "for more information."] }) }) })] })] })); +} +function PinnedFeedItem(_a) { + var feed = _a.feed, currentFeeds = _a.currentFeeds, setCurrentFeeds = _a.setCurrentFeeds, dragHandle = _a.dragHandle, index = _a.index, total = _a.total, onMoveUp = _a.onMoveUp, onMoveDown = _a.onMoveDown; + var _ = useLingui()._; + var t = useTheme(); + var playHaptic = useHaptics(); + var feedUri = feed.value; + var onTogglePinned = function () { + playHaptic(); + setCurrentFeeds(currentFeeds.map(function (f) { + return f.id === feed.id ? __assign(__assign({}, feed), { pinned: !feed.pinned }) : f; + })); + }; + return (_jsxs(View, { style: [a.flex_row, t.atoms.bg], children: [feed.type === 'timeline' ? (_jsx(FollowingFeedCard, {})) : (_jsx(FeedSourceCard, { feedUri: feedUri, style: [a.pr_sm], showMinimalPlaceholder: true, hideTopBorder: true })), _jsxs(View, { style: [a.pr_sm, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Button, { testID: "feed-".concat(feed.type, "-togglePin"), label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Unpin feed"], ["Unpin feed"])))), onPress: onTogglePinned, size: "small", color: "primary_subtle", shape: "square", children: _jsx(ButtonIcon, { icon: PinIcon }) }), onMoveUp !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Button, { testID: "feed-".concat(feed.type, "-moveUp"), label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Move feed up"], ["Move feed up"])))), onPress: onMoveUp, disabled: index === 0, size: "small", color: "secondary", shape: "square", children: _jsx(ButtonIcon, { icon: ArrowUpIcon }) }), _jsx(Button, { testID: "feed-".concat(feed.type, "-moveDown"), label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Move feed down"], ["Move feed down"])))), onPress: onMoveDown, disabled: index === total - 1, size: "small", color: "secondary", shape: "square", children: _jsx(ButtonIcon, { icon: ArrowDownIcon }) })] })) : (dragHandle)] })] })); +} +function UnpinnedFeedItem(_a) { + var feed = _a.feed, currentFeeds = _a.currentFeeds, setCurrentFeeds = _a.setCurrentFeeds; + var _ = useLingui()._; + var t = useTheme(); + var playHaptic = useHaptics(); + var feedUri = feed.value; + var onTogglePinned = function () { + playHaptic(); + setCurrentFeeds(currentFeeds.map(function (f) { + return f.id === feed.id ? __assign(__assign({}, feed), { pinned: !feed.pinned }) : f; + })); + }; + var onPressRemove = function () { + playHaptic(); + setCurrentFeeds(currentFeeds.filter(function (f) { return f.id !== feed.id; })); + }; + return (_jsxs(View, { style: [a.flex_row, a.border_b, t.atoms.border_contrast_low], children: [feed.type === 'timeline' ? (_jsx(FollowingFeedCard, {})) : (_jsx(FeedSourceCard, { feedUri: feedUri, showMinimalPlaceholder: true, hideTopBorder: true })), _jsxs(View, { style: [a.pr_lg, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Button, { testID: "feed-".concat(feedUri, "-toggleSave"), label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Remove from my feeds"], ["Remove from my feeds"])))), onPress: onPressRemove, size: "small", color: "secondary", variant: "ghost", shape: "square", children: _jsx(ButtonIcon, { icon: TrashIcon }) }), _jsx(Button, { testID: "feed-".concat(feed.type, "-togglePin"), label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Pin feed"], ["Pin feed"])))), onPress: onTogglePinned, size: "small", color: "secondary", shape: "square", children: _jsx(ButtonIcon, { icon: PinIcon }) })] })] })); +} +function SectionHeaderText(_a) { + var children = _a.children; + var t = useTheme(); + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text + return (_jsx(View, { style: [ + a.flex_row, + a.flex_1, + a.px_lg, + a.pt_2xl, + a.pb_md, + a.border_b, + t.atoms.border_contrast_low, + ], children: _jsx(Text, { style: [a.text_xl, a.font_bold, a.leading_snug], children: children }) })); +} +function FollowingFeedCard() { + var t = useTheme(); + return (_jsxs(View, { style: [a.flex_row, a.align_center, a.flex_1, a.p_lg], children: [_jsx(View, { style: [ + a.align_center, + a.justify_center, + a.rounded_sm, + a.mr_md, + { + width: 36, + height: 36, + backgroundColor: t.palette.primary_500, + }, + ], children: _jsx(FilterTimeline, { style: [ + { + width: 22, + height: 22, + }, + ], fill: t.palette.white }) }), _jsx(View, { style: [a.flex_1, a.flex_row, a.gap_sm, a.align_center], children: _jsx(Text, { style: [a.text_sm, a.font_semi_bold, a.leading_snug], children: _jsx(Trans, { context: "feed-name", children: "Following" }) }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11; diff --git a/src/screens/Search/Explore.js b/src/screens/Search/Explore.js new file mode 100644 index 0000000000..867b478288 --- /dev/null +++ b/src/screens/Search/Explore.js @@ -0,0 +1,903 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useMemo, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import * as bcp47Match from 'bcp-47-match'; +import { popularInterests, useInterestsDisplayNames } from '#/lib/interests'; +import { cleanError } from '#/lib/strings/errors'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useLanguagePrefs } from '#/state/preferences/languages'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { RQKEY_ROOT as useActorSearchQueryKeyRoot } from '#/state/queries/actor-search'; +import { useFeedPreviews, } from '#/state/queries/explore-feed-previews'; +import { useGetPopularFeedsQuery } from '#/state/queries/feed'; +import { Nux, useNux } from '#/state/queries/nuxs'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { createGetSuggestedFeedsQueryKey, useGetSuggestedFeedsQuery, } from '#/state/queries/trending/useGetSuggestedFeedsQuery'; +import { getSuggestedUsersQueryKeyRoot } from '#/state/queries/trending/useGetSuggestedUsersQuery'; +import { createGetTrendsQueryKey } from '#/state/queries/trending/useGetTrendsQuery'; +import { createSuggestedStarterPacksQueryKey, useSuggestedStarterPacksQuery, } from '#/state/queries/useSuggestedStarterPacksQuery'; +import { isThreadChildAt, isThreadParentAt } from '#/view/com/posts/PostFeed'; +import { PostFeedItem } from '#/view/com/posts/PostFeedItem'; +import { ViewFullThread } from '#/view/com/posts/ViewFullThread'; +import { List } from '#/view/com/util/List'; +import { FeedFeedLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { LoadMoreRetryBtn } from '#/view/com/util/LoadMoreRetryBtn'; +import { StarterPackCard, StarterPackCardSkeleton, } from '#/screens/Search/components/StarterPackCard'; +import { ExploreInterestsCard } from '#/screens/Search/modules/ExploreInterestsCard'; +import { ExploreRecommendations } from '#/screens/Search/modules/ExploreRecommendations'; +import { ExploreTrendingTopics } from '#/screens/Search/modules/ExploreTrendingTopics'; +import { ExploreTrendingVideos } from '#/screens/Search/modules/ExploreTrendingVideos'; +import { useSuggestedUsers } from '#/screens/Search/util/useSuggestedUsers'; +import { atoms as a, native, platform, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button } from '#/components/Button'; +import * as FeedCard from '#/components/FeedCard'; +import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon } from '#/components/icons/Chevron'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { ListSparkle_Stroke2_Corner0_Rounded as ListSparkle } from '#/components/icons/ListSparkle'; +import { StarterPack } from '#/components/icons/StarterPack'; +import { UserCircle_Stroke2_Corner0_Rounded as Person } from '#/components/icons/UserCircle'; +import { boostInterests } from '#/components/InterestTabs'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import { SubtleHover } from '#/components/SubtleHover'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { ExploreScreenLiveEventFeedsBanner } from '#/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner'; +import * as ModuleHeader from './components/ModuleHeader'; +import { SuggestedAccountsTabBar, SuggestedProfileCard, } from './modules/ExploreSuggestedAccounts'; +function LoadMore(_a) { + var item = _a.item; + var t = useTheme(); + var _ = useLingui()._; + var handleOnPress = function () { + void item.onLoadMore(); + }; + return (_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Load more"], ["Load more"])))), onPress: handleOnPress, style: [a.relative, a.w_full], children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(_Fragment, { children: [_jsx(SubtleHover, { hover: hovered || pressed }), _jsxs(View, { style: [ + a.flex_1, + a.flex_row, + a.align_center, + a.justify_center, + a.px_lg, + a.py_md, + a.gap_sm, + ], children: [_jsx(Text, { style: [a.leading_snug], children: item.message }), item.isLoadingMore ? (_jsx(Loader, { size: "sm" })) : (_jsx(ChevronDownIcon, { size: "sm", style: t.atoms.text_contrast_medium }))] })] })); + } })); +} +export function Explore(_a) { + var _this = this; + var _b, _c, _d; + var focusSearchInput = _a.focusSearchInput; + var ax = useAnalytics(); + var _ = useLingui()._; + var t = useTheme(); + var _e = usePreferencesQuery(), preferences = _e.data, preferencesError = _e.error; + var moderationOpts = useModerationOpts(); + var _f = useState(null), selectedInterest = _f[0], setSelectedInterest = _f[1]; + /* + * Begin special language handling + */ + var contentLanguages = useLanguagePrefs().contentLanguages; + var useFullExperience = useMemo(function () { + if (contentLanguages.length === 0) + return true; + return bcp47Match.basicFilter('en', contentLanguages).length > 0; + }, [contentLanguages]); + var personalizedInterests = (_b = preferences === null || preferences === void 0 ? void 0 : preferences.interests) === null || _b === void 0 ? void 0 : _b.tags; + var interestsDisplayNames = useInterestsDisplayNames(); + var interests = Object.keys(interestsDisplayNames) + .sort(boostInterests(popularInterests)) + .sort(boostInterests(personalizedInterests)); + var _g = useSuggestedUsers({ + category: selectedInterest || (useFullExperience ? null : interests[0]), + search: !useFullExperience, + }), suggestedUsers = _g.data, suggestedUsersIsLoading = _g.isLoading, suggestedUsersError = _g.error, suggestedUsersIsRefetching = _g.isRefetching; + /* End special language handling */ + var _h = useGetPopularFeedsQuery({ limit: 10, enabled: useFullExperience }), feeds = _h.data, hasNextFeedsPage = _h.hasNextPage, isLoadingFeeds = _h.isLoading, isFetchingNextFeedsPage = _h.isFetchingNextPage, feedsError = _h.error, fetchNextFeedsPage = _h.fetchNextPage; + var interestsNux = useNux(Nux.ExploreInterestsCard); + var showInterestsNux = interestsNux.status === 'ready' && !((_c = interestsNux.nux) === null || _c === void 0 ? void 0 : _c.completed); + var _j = useSuggestedStarterPacksQuery({ enabled: useFullExperience }), suggestedSPs = _j.data, isLoadingSuggestedSPs = _j.isLoading, suggestedSPsError = _j.error, isRefetchingSuggestedSPs = _j.isRefetching; + var isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds; + var _k = useState(false), hasPressedLoadMoreFeeds = _k[0], setHasPressedLoadMoreFeeds = _k[1]; + var onLoadMoreFeeds = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isFetchingNextFeedsPage || !hasNextFeedsPage || feedsError) + return [2 /*return*/]; + if (!hasPressedLoadMoreFeeds) { + setHasPressedLoadMoreFeeds(true); + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextFeedsPage()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + ax.logger.error('Failed to load more suggested follows', { message: err_1 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [ + ax, + isFetchingNextFeedsPage, + hasNextFeedsPage, + feedsError, + fetchNextFeedsPage, + hasPressedLoadMoreFeeds, + ]); + var _l = useGetSuggestedFeedsQuery({ + enabled: useFullExperience, + }), suggestedFeeds = _l.data, suggestedFeedsError = _l.error; + var _m = useFeedPreviews((_d = suggestedFeeds === null || suggestedFeeds === void 0 ? void 0 : suggestedFeeds.feeds) !== null && _d !== void 0 ? _d : [], useFullExperience), feedPreviewSlices = _m.data, _o = _m.query, isPendingFeedPreviews = _o.isPending, isFetchingNextPageFeedPreviews = _o.isFetchingNextPage, fetchNextPageFeedPreviews = _o.fetchNextPage, hasNextPageFeedPreviews = _o.hasNextPage, feedPreviewSlicesError = _o.error; + var qc = useQueryClient(); + var _p = useState(false), isPTR = _p[0], setIsPTR = _p[1]; + var onPTR = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTR(true); + return [4 /*yield*/, Promise.all([ + qc.resetQueries({ + queryKey: createGetTrendsQueryKey(), + }), + qc.resetQueries({ + queryKey: createSuggestedStarterPacksQueryKey(), + }), + qc.resetQueries({ + queryKey: [getSuggestedUsersQueryKeyRoot], + }), + qc.resetQueries({ + queryKey: [useActorSearchQueryKeyRoot], + }), + qc.resetQueries({ + queryKey: createGetSuggestedFeedsQueryKey(), + }), + ])]; + case 1: + _a.sent(); + setIsPTR(false); + return [2 /*return*/]; + } + }); + }); }, [qc, setIsPTR]); + var onLoadMoreFeedPreviews = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isPendingFeedPreviews || + isFetchingNextPageFeedPreviews || + !hasNextPageFeedPreviews || + feedPreviewSlicesError) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPageFeedPreviews()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_2 = _a.sent(); + ax.logger.error('Failed to load more feed previews', { message: err_2 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [ + ax, + isPendingFeedPreviews, + isFetchingNextPageFeedPreviews, + hasNextPageFeedPreviews, + feedPreviewSlicesError, + fetchNextPageFeedPreviews, + ]); + var topBorder = useMemo(function () { + return ({ + type: 'topBorder', + key: 'top-border', + }); + }, []); + var trendingTopicsModule = useMemo(function () { + return ({ + type: 'trendingTopics', + key: 'trending-topics', + }); + }, []); + var suggestedFollowsModule = useMemo(function () { + var _a; + var i = []; + i.push({ + type: 'tabbedHeader', + key: 'suggested-accounts-header', + title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Suggested accounts"], ["Suggested accounts"])))), + icon: Person, + searchButton: { + label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Search for more accounts"], ["Search for more accounts"])))), + metricsTag: 'suggestedAccounts', + tab: 'user', + }, + hideDefaultTab: !useFullExperience, + }); + if (suggestedUsersIsLoading || suggestedUsersIsRefetching) { + i.push({ type: 'profilePlaceholder', key: 'profilePlaceholder' }); + } + else if (suggestedUsersError) { + i.push({ + type: 'error', + key: 'suggestedUsersError', + message: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Failed to load suggested follows"], ["Failed to load suggested follows"])))), + error: cleanError(suggestedUsersError), + }); + } + else { + if (suggestedUsers !== undefined) { + if (suggestedUsers.actors.length > 0 && moderationOpts) { + // Currently the responses contain duplicate items. + // Needs to be fixed on backend, but let's dedupe to be safe. + var seen = new Set(); + var profileItems = []; + for (var _i = 0, _b = suggestedUsers.actors; _i < _b.length; _i++) { + var actor = _b[_i]; + // checking for following still necessary if search data is used + if (!seen.has(actor.did) && !((_a = actor.viewer) === null || _a === void 0 ? void 0 : _a.following)) { + seen.add(actor.did); + profileItems.push({ + type: 'profile', + key: actor.did, + profile: actor, + }); + } + } + if (profileItems.length === 0) { + i.push({ + type: 'profileEmpty', + key: 'profileEmpty', + }); + } + else { + if (selectedInterest === null && useFullExperience) { + // First "For You" tab, only show 5 to keep screen short + i.push.apply(i, profileItems.slice(0, 5)); + } + else { + i.push.apply(i, profileItems); + } + } + } + else { + i.push({ + type: 'profileEmpty', + key: 'profileEmpty', + }); + } + } + else { + i.push({ type: 'profilePlaceholder', key: 'profilePlaceholder' }); + } + } + return i; + }, [ + _, + moderationOpts, + suggestedUsers, + suggestedUsersIsLoading, + suggestedUsersIsRefetching, + suggestedUsersError, + selectedInterest, + useFullExperience, + ]); + var suggestedFeedsModule = useMemo(function () { + var i = []; + i.push({ + type: 'header', + key: 'suggested-feeds-header', + title: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Discover new feeds"], ["Discover new feeds"])))), + icon: ListSparkle, + searchButton: { + label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Search for more feeds"], ["Search for more feeds"])))), + metricsTag: 'suggestedFeeds', + tab: 'feed', + }, + }); + if (useFullExperience) { + if (suggestedFeeds && preferences) { + var seen = new Set(); + var feedItems = []; + for (var _i = 0, _a = suggestedFeeds.feeds; _i < _a.length; _i++) { + var feed = _a[_i]; + if (!seen.has(feed.uri)) { + seen.add(feed.uri); + feedItems.push({ + type: 'feed', + key: feed.uri, + feed: feed, + }); + } + } + // feeds errors can occur during pagination, so feeds is truthy + if (suggestedFeedsError) { + i.push({ + type: 'error', + key: 'suggestedFeedsError', + message: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Failed to load suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(suggestedFeedsError), + }); + } + else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Failed to load feeds preferences"], ["Failed to load feeds preferences"])))), + error: cleanError(preferencesError), + }); + } + else { + if (feedItems.length === 0) { + i.pop(); + } + else { + // This query doesn't follow the limit very well, so the first press of the + // load more button just unslices the array back to ~10 items + if (!hasPressedLoadMoreFeeds) { + i.push.apply(i, feedItems.slice(0, 6)); + } + else { + i.push.apply(i, feedItems); + } + for (var _b = 0, _c = feedItems.entries(); _b < _c.length; _b++) { + var _d = _c[_b], index = _d[0], item = _d[1]; + if (item.type !== 'feed') { + continue; + } + // don't log the ones we've already sent + if (hasPressedLoadMoreFeeds && index < 6) { + continue; + } + ax.metric('feed:suggestion:seen', { feedUrl: item.feed.uri }); + } + } + if (!hasPressedLoadMoreFeeds) { + i.push({ + type: 'loadMore', + key: 'loadMoreFeeds', + message: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Load more suggested feeds"], ["Load more suggested feeds"])))), + isLoadingMore: isLoadingMoreFeeds, + onLoadMore: onLoadMoreFeeds, + }); + } + } + } + else { + if (feedsError) { + i.push({ + type: 'error', + key: 'feedsError', + message: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Failed to load feeds"], ["Failed to load feeds"])))), + error: cleanError(feedsError), + }); + } + else if (suggestedFeedsError) { + i.push({ + type: 'error', + key: 'suggestedFeedsError', + message: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Failed to load suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(suggestedFeedsError), + }); + } + else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Failed to load feeds preferences"], ["Failed to load feeds preferences"])))), + error: cleanError(preferencesError), + }); + } + else { + i.push({ type: 'feedPlaceholder', key: 'feedPlaceholder' }); + } + } + } + else { + if (feeds && preferences) { + // Currently the responses contain duplicate items. + // Needs to be fixed on backend, but let's dedupe to be safe. + var seen = new Set(); + var feedItems = []; + for (var _e = 0, _f = feeds.pages; _e < _f.length; _e++) { + var page = _f[_e]; + for (var _g = 0, _h = page.feeds; _g < _h.length; _g++) { + var feed = _h[_g]; + if (!seen.has(feed.uri)) { + seen.add(feed.uri); + feedItems.push({ + type: 'feed', + key: feed.uri, + feed: feed, + }); + } + } + } + // feeds errors can occur during pagination, so feeds is truthy + if (feedsError) { + i.push({ + type: 'error', + key: 'feedsError', + message: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Failed to load feeds"], ["Failed to load feeds"])))), + error: cleanError(feedsError), + }); + } + else if (suggestedFeedsError) { + i.push({ + type: 'error', + key: 'suggestedFeedsError', + message: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Failed to load suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(suggestedFeedsError), + }); + } + else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Failed to load feeds preferences"], ["Failed to load feeds preferences"])))), + error: cleanError(preferencesError), + }); + } + else { + if (feedItems.length === 0) { + if (!hasNextFeedsPage) { + i.pop(); + } + } + else { + // This query doesn't follow the limit very well, so the first press of the + // load more button just unslices the array back to ~10 items + if (!hasPressedLoadMoreFeeds) { + i.push.apply(i, feedItems.slice(0, 3)); + } + else { + i.push.apply(i, feedItems); + } + } + if (hasNextFeedsPage) { + i.push({ + type: 'loadMore', + key: 'loadMoreFeeds', + message: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Load more suggested feeds"], ["Load more suggested feeds"])))), + isLoadingMore: isLoadingMoreFeeds, + onLoadMore: onLoadMoreFeeds, + }); + } + } + } + else { + if (feedsError) { + i.push({ + type: 'error', + key: 'feedsError', + message: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Failed to load feeds"], ["Failed to load feeds"])))), + error: cleanError(feedsError), + }); + } + else if (suggestedFeedsError) { + i.push({ + type: 'error', + key: 'feedsError', + message: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Failed to load suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(suggestedFeedsError), + }); + } + else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Failed to load feeds preferences"], ["Failed to load feeds preferences"])))), + error: cleanError(preferencesError), + }); + } + else { + i.push({ type: 'feedPlaceholder', key: 'feedPlaceholder' }); + } + } + } + return i; + }, [ + _, + ax, + useFullExperience, + suggestedFeeds, + preferences, + suggestedFeedsError, + preferencesError, + feedsError, + hasNextFeedsPage, + hasPressedLoadMoreFeeds, + isLoadingMoreFeeds, + onLoadMoreFeeds, + feeds, + ]); + var suggestedStarterPacksModule = useMemo(function () { + var i = []; + i.push({ + type: 'header', + key: 'suggested-starterPacks-header', + title: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Starter Packs"], ["Starter Packs"])))), + icon: StarterPack, + iconSize: 'xl', + }); + if (isLoadingSuggestedSPs || isRefetchingSuggestedSPs) { + Array.from({ length: 3 }).forEach(function (__, index) { + return i.push({ + type: 'starterPackSkeleton', + key: "starterPackSkeleton-".concat(index), + }); + }); + } + else if (suggestedSPsError || !suggestedSPs) { + // just get rid of the section + i.pop(); + } + else { + suggestedSPs.starterPacks.map(function (s) { + i.push({ + type: 'starterPack', + key: s.uri, + view: s, + }); + }); + } + return i; + }, [ + suggestedSPs, + _, + isLoadingSuggestedSPs, + suggestedSPsError, + isRefetchingSuggestedSPs, + ]); + var feedPreviewsModule = useMemo(function () { + var i = []; + i.push.apply(i, feedPreviewSlices); + if (isFetchingNextPageFeedPreviews) { + i.push({ + type: 'preview:loading', + key: 'preview-loading-more', + }); + } + return i; + }, [feedPreviewSlices, isFetchingNextPageFeedPreviews]); + var interestsNuxModule = useMemo(function () { + if (!showInterestsNux) + return []; + return [ + { + type: 'interests-card', + key: 'interests-card', + }, + ]; + }, [showInterestsNux]); + var items = useMemo(function () { + var i = []; + // Dynamic module ordering + i.push(topBorder); + i.push.apply(i, interestsNuxModule); + i.push({ type: 'liveEventFeedsBanner', key: 'liveEventFeedsBanner' }); + if (useFullExperience) { + i.push(trendingTopicsModule); + i.push.apply(i, suggestedFeedsModule); + i.push.apply(i, suggestedFollowsModule); + i.push.apply(i, suggestedStarterPacksModule); + i.push.apply(i, feedPreviewsModule); + } + else { + i.push.apply(i, suggestedFollowsModule); + } + return i; + }, [ + topBorder, + suggestedFollowsModule, + suggestedStarterPacksModule, + suggestedFeedsModule, + trendingTopicsModule, + feedPreviewsModule, + interestsNuxModule, + useFullExperience, + ]); + var renderItem = useCallback(function (_a) { + var item = _a.item, index = _a.index; + var handleOnPressRetry = function () { + void fetchNextPageFeedPreviews(); + }; + switch (item.type) { + case 'topBorder': + return (_jsx(View, { style: [a.w_full, t.atoms.border_contrast_low, a.border_t] })); + case 'header': { + return (_jsxs(ModuleHeader.Container, { bottomBorder: item.bottomBorder, children: [_jsx(ModuleHeader.Icon, { icon: item.icon, size: item.iconSize }), _jsx(ModuleHeader.TitleText, { children: item.title }), item.searchButton && (_jsx(ModuleHeader.SearchButton, __assign({}, item.searchButton, { onPress: function () { var _a; return focusSearchInput(((_a = item.searchButton) === null || _a === void 0 ? void 0 : _a.tab) || 'user'); } })))] })); + } + case 'tabbedHeader': { + return (_jsxs(View, { style: [a.pb_md], children: [_jsxs(ModuleHeader.Container, { style: [a.pb_xs], children: [_jsx(ModuleHeader.Icon, { icon: item.icon }), _jsx(ModuleHeader.TitleText, { children: item.title }), item.searchButton && (_jsx(ModuleHeader.SearchButton, __assign({}, item.searchButton, { onPress: function () { var _a; return focusSearchInput(((_a = item.searchButton) === null || _a === void 0 ? void 0 : _a.tab) || 'user'); } })))] }), _jsx(SuggestedAccountsTabBar, { selectedInterest: selectedInterest, onSelectInterest: setSelectedInterest, hideDefaultTab: item.hideDefaultTab })] })); + } + case 'trendingTopics': { + return (_jsx(View, { style: [a.pb_md], children: _jsx(ExploreTrendingTopics, {}) })); + } + case 'trendingVideos': { + return _jsx(ExploreTrendingVideos, {}); + } + case 'recommendations': { + return _jsx(ExploreRecommendations, {}); + } + case 'profile': { + return (_jsx(SuggestedProfileCard, { profile: item.profile, moderationOpts: moderationOpts, recId: item.recId, position: index })); + } + case 'profileEmpty': { + return (_jsx(View, { style: [a.px_lg, a.pb_lg], children: _jsx(Admonition, { children: selectedInterest ? (_jsxs(Trans, { children: ["No results for \"", interestsDisplayNames[selectedInterest], "\"."] })) : (_jsx(Trans, { children: "No results." })) }) })); + } + case 'feed': { + return (_jsx(View, { style: [ + a.border_t, + t.atoms.border_contrast_low, + a.px_lg, + a.py_lg, + ], children: _jsx(FeedCard.Default, { view: item.feed, onPress: function () { + if (!useFullExperience) { + return; + } + ax.metric('feed:suggestion:press', { + feedUrl: item.feed.uri, + }); + } }) })); + } + case 'starterPack': { + return (_jsx(View, { style: [a.px_lg, a.pb_lg], children: _jsx(StarterPackCard, { view: item.view }) })); + } + case 'starterPackSkeleton': { + return (_jsx(View, { style: [a.px_lg, a.pb_lg], children: _jsx(StarterPackCardSkeleton, {}) })); + } + case 'loadMore': { + return (_jsx(View, { style: [a.border_t, t.atoms.border_contrast_low], children: _jsx(LoadMore, { item: item }) })); + } + case 'profilePlaceholder': { + return (_jsx(_Fragment, { children: Array.from({ length: 3 }).map(function (__, i) { return (_jsx(View, { style: [ + a.px_lg, + a.py_lg, + a.border_t, + t.atoms.border_contrast_low, + ], children: _jsxs(ProfileCard.Outer, { children: [_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.AvatarPlaceholder, {}), _jsx(ProfileCard.NameAndHandlePlaceholder, {})] }), _jsx(ProfileCard.DescriptionPlaceholder, { numberOfLines: 2 })] }) }, i)); }) })); + } + case 'feedPlaceholder': { + return _jsx(FeedFeedLoadingPlaceholder, {}); + } + case 'error': + case 'preview:error': { + return (_jsx(View, { style: [ + a.border_t, + a.pt_md, + a.px_md, + t.atoms.border_contrast_low, + ], children: _jsxs(View, { style: [ + a.flex_row, + a.gap_md, + a.p_lg, + a.rounded_sm, + t.atoms.bg_contrast_25, + ], children: [_jsx(CircleInfo, { size: "md", fill: t.palette.negative_400 }), _jsxs(View, { style: [a.flex_1, a.gap_sm], children: [_jsx(Text, { style: [a.font_semi_bold, a.leading_snug], children: item.message }), _jsx(Text, { style: [ + a.italic, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: item.error })] })] }) })); + } + // feed previews + case 'preview:spacer': { + return _jsx(View, { style: [a.w_full, a.pt_4xl] }); + } + case 'preview:empty': { + return null; // what should we do here? + } + case 'preview:loading': { + return (_jsx(View, { style: [a.py_2xl, a.flex_1, a.align_center], children: _jsx(Loader, { size: "lg" }) })); + } + case 'preview:header': { + return (_jsxs(ModuleHeader.Container, { style: [a.pt_xs], bottomBorder: true, children: [_jsx(View, { style: [a.absolute, a.inset_0, t.atoms.bg, { top: -2 }] }), _jsxs(ModuleHeader.FeedLink, { feed: item.feed, children: [_jsx(ModuleHeader.FeedAvatar, { feed: item.feed }), _jsxs(View, { style: [a.flex_1, a.gap_2xs], children: [_jsx(ModuleHeader.TitleText, { style: [a.text_lg], children: item.feed.displayName }), _jsx(ModuleHeader.SubtitleText, { children: _jsxs(Trans, { children: ["By ", sanitizeHandle(item.feed.creator.handle, '@')] }) })] })] }), _jsx(ModuleHeader.PinButton, { feed: item.feed })] })); + } + case 'preview:footer': { + return (_jsx(View, { style: [ + a.border_t, + t.atoms.border_contrast_low, + a.w_full, + a.pt_4xl, + ] })); + } + case 'preview:sliceItem': { + var slice = item.slice; + var indexInSlice = item.indexInSlice; + var subItem = slice.items[indexInSlice]; + return (_jsx(PostFeedItem, { post: subItem.post, record: subItem.record, reason: indexInSlice === 0 ? slice.reason : undefined, feedContext: slice.feedContext, reqId: slice.reqId, moderation: subItem.moderation, parentAuthor: subItem.parentAuthor, showReplyTo: item.showReplyTo, isThreadParent: isThreadParentAt(slice.items, indexInSlice), isThreadChild: isThreadChildAt(slice.items, indexInSlice), isThreadLastChild: isThreadChildAt(slice.items, indexInSlice) && + slice.items.length === indexInSlice + 1, isParentBlocked: subItem.isParentBlocked, isParentNotFound: subItem.isParentNotFound, hideTopBorder: item.hideTopBorder, rootPost: slice.items[0].post })); + } + case 'preview:sliceViewFullThread': { + return _jsx(ViewFullThread, { uri: item.uri }); + } + case 'preview:loadMoreError': { + return (_jsx(LoadMoreRetryBtn, { label: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["There was an issue fetching posts. Tap here to try again."], ["There was an issue fetching posts. Tap here to try again."])))), onPress: handleOnPressRetry })); + } + case 'interests-card': { + return _jsx(ExploreInterestsCard, {}); + } + case 'liveEventFeedsBanner': { + return _jsx(ExploreScreenLiveEventFeedsBanner, {}); + } + } + }, [ + ax, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + t.atoms.text_contrast_medium, + t.atoms.bg, + t.palette.negative_400, + focusSearchInput, + selectedInterest, + moderationOpts, + interestsDisplayNames, + useFullExperience, + _, + fetchNextPageFeedPreviews, + ]); + var stickyHeaderIndices = useMemo(function () { + return items.reduce(function (acc, curr) { + return ['topBorder', 'preview:header'].includes(curr.type) + ? acc.concat(items.indexOf(curr)) + : acc; + }, []); + }, [items]); + // track headers and report module viewability + var alreadyReportedRef = useRef(new Map()); + var seenProfilesRef = useRef(new Set()); + var onItemSeen = useCallback(function (item) { + var module; + if (item.type === 'trendingTopics' || item.type === 'trendingVideos') { + module = item.type; + } + else if (item.type === 'profile') { + module = 'suggestedAccounts'; + // Track individual profile seen events + if (!seenProfilesRef.current.has(item.profile.did)) { + seenProfilesRef.current.add(item.profile.did); + var position = suggestedFollowsModule.findIndex(function (i) { return i.type === 'profile' && i.profile.did === item.profile.did; }); + ax.metric('suggestedUser:seen', { + logContext: 'Explore', + recId: item.recId, + position: position !== -1 ? position - 1 : 0, // -1 to account for header + suggestedDid: item.profile.did, + category: null, + }); + } + } + else if (item.type === 'feed') { + module = 'suggestedFeeds'; + } + else if (item.type === 'starterPack') { + module = 'suggestedStarterPacks'; + } + else if (item.type === 'preview:sliceItem') { + module = "feed:feedgen|".concat(item.feed.uri); + } + else { + return; + } + if (!alreadyReportedRef.current.has(module)) { + alreadyReportedRef.current.set(module, module); + ax.metric('explore:module:seen', { module: module }); + } + }, [ax, suggestedFollowsModule]); + var handleOnEndReached = function () { + void onLoadMoreFeedPreviews(); + }; + var handleOnRefresh = function () { + void onPTR(); + }; + return (_jsx(List, { data: items, renderItem: renderItem, keyExtractor: keyExtractor, desktopFixedHeight: true, contentContainerStyle: { paddingBottom: 100 }, keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", stickyHeaderIndices: native(stickyHeaderIndices), viewabilityConfig: viewabilityConfig, onItemSeen: onItemSeen, onEndReached: handleOnEndReached, + /** + * Default: 2 + */ + onEndReachedThreshold: 4, + /** + * Default: 10 + */ + initialNumToRender: 10, + /** + * Default: 21 + */ + windowSize: platform({ android: 11 }), + /** + * Default: 10 + * + * NOTE: This was 1 on Android. Unfortunately this leads to the list totally freaking out + * when the sticky headers changed. I made a minimal reproduction and yeah, it's this prop. + * Totally fine when the sticky headers are static, but when they're dynamic, it's a mess. + * + * Repro: https://github.com/mozzius/stickyindices-repro + * + * I then found doubling this prop on iOS also reduced it freaking out there as well. + * + * Trades off seeing more blank space due to it having to render more items before it can show anything. + * -sfn + */ + maxToRenderPerBatch: platform({ android: 10, ios: 20 }), + /** + * Default: 50 + * + * NOTE: This was 25 on Android. However, due to maxToRenderPerBatch being set to 10, + * the lower batching period is no longer necessary (?) + */ + updateCellsBatchingPeriod: 50, refreshing: isPTR, onRefresh: handleOnRefresh })); +} +function keyExtractor(item) { + return item.key; +} +var viewabilityConfig = { + itemVisiblePercentThreshold: 100, +}; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21; diff --git a/src/screens/Search/SearchResults.js b/src/screens/Search/SearchResults.js new file mode 100644 index 0000000000..ebfd6ce7e2 --- /dev/null +++ b/src/screens/Search/SearchResults.js @@ -0,0 +1,279 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useCallback, useMemo, useState } from 'react'; +import { ActivityIndicator, View } from 'react-native'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { urls } from '#/lib/constants'; +import { usePostViewTracking } from '#/lib/hooks/usePostViewTracking'; +import { cleanError } from '#/lib/strings/errors'; +import { augmentSearchQuery } from '#/lib/strings/helpers'; +import { useActorSearch } from '#/state/queries/actor-search'; +import { usePopularFeedsSearch } from '#/state/queries/feed'; +import { useSearchPostsQuery } from '#/state/queries/search-posts'; +import { useSession } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { useCloseAllActiveElements } from '#/state/util'; +import { Pager } from '#/view/com/pager/Pager'; +import { TabBar } from '#/view/com/pager/TabBar'; +import { Post } from '#/view/com/post/Post'; +import { ProfileCardWithFollowBtn } from '#/view/com/profile/ProfileCard'; +import { List } from '#/view/com/util/List'; +import { atoms as a, useTheme, web } from '#/alf'; +import * as FeedCard from '#/components/FeedCard'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import { ListFooter } from '#/components/Lists'; +import { SearchError } from '#/components/SearchError'; +import { Text } from '#/components/Typography'; +var SearchResults = function (_a) { + var query = _a.query, queryWithParams = _a.queryWithParams, activeTab = _a.activeTab, onPageSelected = _a.onPageSelected, headerHeight = _a.headerHeight, _b = _a.initialPage, initialPage = _b === void 0 ? 0 : _b; + var l = useLingui().t; + var sections = useMemo(function () { + if (!queryWithParams) + return []; + var noParams = queryWithParams === query; + return [ + { + title: l(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Top"], ["Top"]))), + component: (_jsx(SearchScreenPostResults, { query: queryWithParams, sort: "top", active: activeTab === 0 })), + }, + { + title: l(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Latest"], ["Latest"]))), + component: (_jsx(SearchScreenPostResults, { query: queryWithParams, sort: "latest", active: activeTab === 1 })), + }, + noParams && { + title: l(templateObject_3 || (templateObject_3 = __makeTemplateObject(["People"], ["People"]))), + component: (_jsx(SearchScreenUserResults, { query: query, active: activeTab === 2 })), + }, + noParams && { + title: l(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Feeds"], ["Feeds"]))), + component: (_jsx(SearchScreenFeedsResults, { query: query, active: activeTab === 3 })), + }, + ].filter(Boolean); + }, [l, query, queryWithParams, activeTab]); + // There may be fewer tabs after changing the search options. + var selectedPage = initialPage > sections.length - 1 ? 0 : initialPage; + return (_jsx(Pager, { onPageSelected: onPageSelected, renderTabBar: function (props) { return (_jsx(Layout.Center, { style: [a.z_10, web([a.sticky, { top: headerHeight }])], children: _jsx(TabBar, __assign({ items: sections.map(function (section) { return section.title; }) }, props)) })); }, initialPage: selectedPage, children: sections.map(function (section, i) { return (_jsx(View, { children: section.component }, i)); }) })); +}; +SearchResults = memo(SearchResults); +export { SearchResults }; +function Loader() { + return (_jsx(Layout.Content, { children: _jsx(View, { style: [a.py_xl], children: _jsx(ActivityIndicator, {}) }) })); +} +function EmptyState(_a) { + var messageText = _a.messageText, error = _a.error, children = _a.children; + var t = useTheme(); + return (_jsx(Layout.Content, { children: _jsx(View, { style: [a.p_xl], children: _jsxs(View, { style: [t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg], children: [_jsx(Text, { style: [a.text_md], children: messageText }), error && (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + { + marginVertical: 12, + height: 1, + width: '100%', + backgroundColor: t.atoms.text.color, + opacity: 0.2, + }, + ] }), _jsx(Text, { style: [t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Error: ", error] }) })] })), children] }) }) })); +} +function NoResultsText(_a) { + var query = _a.query; + var t = useTheme(); + var l = useLingui().t; + return (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_lg, t.atoms.text_contrast_high], children: _jsxs(Trans, { children: ["No results found for \u201C", _jsx(Text, { style: [a.text_lg, t.atoms.text, a.font_medium], children: query }), "\u201D."] }) }), '\n\n', _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_high], children: _jsxs(Trans, { context: "english-only-resource", children: ["Try a different search term, or", ' ', _jsx(InlineLinkText, { label: l({ + message: 'read about how to use search filters', + context: 'english-only-resource', + }), to: urls.website.blog.searchTipsAndTricks, style: [a.text_md, a.leading_snug], children: "read about how to use search filters" }), "."] }) })] })); +} +var SearchScreenPostResults = function (_a) { + var query = _a.query, sort = _a.sort, active = _a.active; + var l = useLingui().t; + var _b = useSession(), currentAccount = _b.currentAccount, hasSession = _b.hasSession; + var _c = useState(false), isPTR = _c[0], setIsPTR = _c[1]; + var trackPostView = usePostViewTracking('SearchResults'); + var augmentedQuery = useMemo(function () { + return augmentSearchQuery(query || '', { did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }); + }, [query, currentAccount]); + var _d = useSearchPostsQuery({ query: augmentedQuery, sort: sort, enabled: active }), isFetched = _d.isFetched, results = _d.data, isFetching = _d.isFetching, error = _d.error, refetch = _d.refetch, fetchNextPage = _d.fetchNextPage, isFetchingNextPage = _d.isFetchingNextPage, hasNextPage = _d.hasNextPage; + var t = useTheme(); + var onPullToRefresh = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTR(true); + return [4 /*yield*/, refetch()]; + case 1: + _a.sent(); + setIsPTR(false); + return [2 /*return*/]; + } + }); + }); }, [setIsPTR, refetch]); + var onEndReached = useCallback(function () { + if (isFetching || !hasNextPage || error) + return; + void fetchNextPage(); + }, [isFetching, error, hasNextPage, fetchNextPage]); + var posts = useMemo(function () { + return (results === null || results === void 0 ? void 0 : results.pages.flatMap(function (page) { return page.posts; })) || []; + }, [results]); + var items = useMemo(function () { + var temp = []; + var seenUris = new Set(); + for (var _i = 0, posts_1 = posts; _i < posts_1.length; _i++) { + var post = posts_1[_i]; + if (seenUris.has(post.uri)) { + continue; + } + temp.push({ + type: 'post', + key: post.uri, + post: post, + }); + seenUris.add(post.uri); + } + if (isFetchingNextPage) { + temp.push({ + type: 'loadingMore', + key: 'loadingMore', + }); + } + return temp; + }, [posts, isFetchingNextPage]); + var closeAllActiveElements = useCloseAllActiveElements(); + var requestSwitchToAccount = useLoggedOutViewControls().requestSwitchToAccount; + var showSignIn = function () { + closeAllActiveElements(); + requestSwitchToAccount({ requestedAccount: 'none' }); + }; + var showCreateAccount = function () { + closeAllActiveElements(); + requestSwitchToAccount({ requestedAccount: 'new' }); + }; + if (!hasSession) { + return (_jsx(SearchError, { title: l(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Search is currently unavailable when logged out"], ["Search is currently unavailable when logged out"]))), children: _jsx(Text, { style: [a.text_md, a.text_center, a.leading_snug], children: _jsxs(Trans, { children: [_jsx(InlineLinkText, { label: l(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Sign in"], ["Sign in"]))), to: '#', onPress: showSignIn, children: "Sign in" }), _jsx(Text, { style: t.atoms.text_contrast_medium, children: " or " }), _jsx(InlineLinkText, { label: l(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Create an account"], ["Create an account"]))), to: '#', onPress: showCreateAccount, children: "create an account" }), _jsx(Text, { children: " " }), _jsx(Text, { style: t.atoms.text_contrast_medium, children: "to search for news, sports, politics, and everything else happening on Bluesky." })] }) }) })); + } + return error ? (_jsx(EmptyState, { messageText: l(templateObject_8 || (templateObject_8 = __makeTemplateObject(["We're sorry, but your search could not be completed. Please try again in a few minutes."], ["We're sorry, but your search could not be completed. Please try again in a few minutes."]))), error: cleanError(error) })) : (_jsx(_Fragment, { children: isFetched ? (_jsx(_Fragment, { children: posts.length ? (_jsx(List, { data: items, renderItem: function (_a) { + var item = _a.item; + if (item.type === 'post') { + return _jsx(Post, { post: item.post }); + } + else { + return null; + } + }, keyExtractor: function (item) { return item.key; }, refreshing: isPTR, onRefresh: function () { + void onPullToRefresh(); + }, onEndReached: onEndReached, onItemSeen: function (item) { + if (item.type === 'post') { + trackPostView(item.post); + } + }, desktopFixedHeight: true, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, hasNextPage: hasNextPage }) })) : (_jsx(EmptyState, { messageText: _jsx(NoResultsText, { query: query }) })) })) : (_jsx(Loader, {})) })); +}; +SearchScreenPostResults = memo(SearchScreenPostResults); +var SearchScreenUserResults = function (_a) { + var query = _a.query, active = _a.active; + var l = useLingui().t; + var hasSession = useSession().hasSession; + var _b = useState(false), isPTR = _b[0], setIsPTR = _b[1]; + var _c = useActorSearch({ + query: query, + enabled: active, + }), isFetched = _c.isFetched, results = _c.data, isFetching = _c.isFetching, error = _c.error, refetch = _c.refetch, fetchNextPage = _c.fetchNextPage, isFetchingNextPage = _c.isFetchingNextPage, hasNextPage = _c.hasNextPage; + var onPullToRefresh = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTR(true); + return [4 /*yield*/, refetch()]; + case 1: + _a.sent(); + setIsPTR(false); + return [2 /*return*/]; + } + }); + }); }, [setIsPTR, refetch]); + var onEndReached = useCallback(function () { + if (!hasSession) + return; + if (isFetching || !hasNextPage || error) + return; + void fetchNextPage(); + }, [isFetching, error, hasNextPage, fetchNextPage, hasSession]); + var profiles = useMemo(function () { + return (results === null || results === void 0 ? void 0 : results.pages.flatMap(function (page) { return page.actors; })) || []; + }, [results]); + if (error) { + return (_jsx(EmptyState, { messageText: l(templateObject_9 || (templateObject_9 = __makeTemplateObject(["We\u2019re sorry, but your search could not be completed. Please try again in a few minutes."], ["We\u2019re sorry, but your search could not be completed. Please try again in a few minutes."]))), error: error.toString() })); + } + return isFetched && profiles ? (_jsx(_Fragment, { children: profiles.length ? (_jsx(List, { data: profiles, renderItem: function (_a) { + var item = _a.item; + return (_jsx(ProfileCardWithFollowBtn, { profile: item })); + }, keyExtractor: function (item) { return item.did; }, refreshing: isPTR, onRefresh: function () { return void onPullToRefresh(); }, onEndReached: onEndReached, desktopFixedHeight: true, ListFooterComponent: _jsx(ListFooter, { hasNextPage: hasNextPage && hasSession, isFetchingNextPage: isFetchingNextPage }) })) : (_jsx(EmptyState, { messageText: _jsx(NoResultsText, { query: query }) })) })) : (_jsx(Loader, {})); +}; +SearchScreenUserResults = memo(SearchScreenUserResults); +var SearchScreenFeedsResults = function (_a) { + var query = _a.query, active = _a.active; + var t = useTheme(); + var _b = usePopularFeedsSearch({ + query: query, + enabled: active, + }), results = _b.data, isFetched = _b.isFetched; + return isFetched && results ? (_jsx(_Fragment, { children: results.length ? (_jsx(List, { data: results, renderItem: function (_a) { + var item = _a.item; + return (_jsx(View, { style: [ + a.border_t, + t.atoms.border_contrast_low, + a.px_lg, + a.py_lg, + ], children: _jsx(FeedCard.Default, { view: item }) })); + }, keyExtractor: function (item) { return item.uri; }, desktopFixedHeight: true, ListFooterComponent: _jsx(ListFooter, {}) })) : (_jsx(EmptyState, { messageText: _jsx(NoResultsText, { query: query }) })) })) : (_jsx(Loader, {})); +}; +SearchScreenFeedsResults = memo(SearchScreenFeedsResults); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Search/Shell.js b/src/screens/Search/Shell.js new file mode 100644 index 0000000000..63c6fc0ef3 --- /dev/null +++ b/src/screens/Search/Shell.js @@ -0,0 +1,339 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react'; +import { View, } from 'react-native'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { HITSLOP_10, HITSLOP_20 } from '#/lib/constants'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { MagnifyingGlassIcon } from '#/lib/icons'; +import { listenSoftReset } from '#/state/events'; +import { useActorAutocompleteQuery } from '#/state/queries/actor-autocomplete'; +import { unstableCacheProfileView, useProfilesQuery, } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { makeSearchQuery, parseSearchQuery, } from '#/screens/Search/utils'; +import { atoms as a, tokens, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { SearchInput } from '#/components/forms/SearchInput'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +import { account, useStorage } from '#/storage'; +import { AutocompleteResults } from './components/AutocompleteResults'; +import { SearchHistory } from './components/SearchHistory'; +import { SearchLanguageDropdown } from './components/SearchLanguageDropdown'; +import { Explore } from './Explore'; +import { SearchResults } from './SearchResults'; +// Map tab parameter to tab index +function getTabIndex(tabParam) { + switch (tabParam) { + case 'feed': + return 3; // Feeds tab + case 'user': + case 'profile': + return 2; // People tab + case 'latest': + return 1; // Latest tab + default: + return 0; // Top tab + } +} +export function SearchScreenShell(_a) { + var _b, _c, _d; + var queryParam = _a.queryParam, testID = _a.testID, fixedParams = _a.fixedParams, _e = _a.navButton, navButton = _e === void 0 ? 'menu' : _e, inputPlaceholder = _a.inputPlaceholder, isExplore = _a.isExplore; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var navigation = useNavigation(); + var route = useRoute(); + var textInput = useRef(null); + var l = useLingui().t; + var setMinimalShellMode = useSetMinimalShellMode(); + var currentAccount = useSession().currentAccount; + var queryClient = useQueryClient(); + // Get tab parameter from route params + var tabParam = (_b = route.params) === null || _b === void 0 ? void 0 : _b.tab; + var _f = useState(function () { return getTabIndex(tabParam); }), activeTab = _f[0], setActiveTab = _f[1]; + // Query terms + var _g = useState(queryParam), searchText = _g[0], setSearchText = _g[1]; + var _h = useActorAutocompleteQuery(searchText, true), autocompleteData = _h.data, isAutocompleteFetching = _h.isFetching; + var _j = useState(false), showAutocomplete = _j[0], setShowAutocomplete = _j[1]; + var _k = useStorage(account, [ + (_c = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) !== null && _c !== void 0 ? _c : 'pwi', + 'searchTermHistory', + ]), _l = _k[0], termHistory = _l === void 0 ? [] : _l, setTermHistory = _k[1]; + var _m = useStorage(account, [ + (_d = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) !== null && _d !== void 0 ? _d : 'pwi', + 'searchAccountHistory', + ]), _o = _m[0], accountHistory = _o === void 0 ? [] : _o, setAccountHistory = _m[1]; + var accountHistoryProfiles = useProfilesQuery({ + handles: accountHistory, + maintainData: true, + }).data; + var updateSearchHistory = useCallback(function (item) { + if (!item) + return; + var newSearchHistory = __spreadArray([ + item + ], termHistory.filter(function (search) { return search !== item; }), true).slice(0, 6); + setTermHistory(newSearchHistory); + }, [termHistory, setTermHistory]); + var updateProfileHistory = useCallback(function (item) { + var newAccountHistory = __spreadArray([ + item.did + ], accountHistory.filter(function (p) { return p !== item.did; }), true).slice(0, 10); + setAccountHistory(newAccountHistory); + }, [accountHistory, setAccountHistory]); + var deleteSearchHistoryItem = useCallback(function (item) { + setTermHistory(termHistory.filter(function (search) { return search !== item; })); + }, [termHistory, setTermHistory]); + var deleteProfileHistoryItem = useCallback(function (item) { + setAccountHistory(accountHistory.filter(function (p) { return p !== item.did; })); + }, [accountHistory, setAccountHistory]); + var _p = useQueryManager({ + initialQuery: queryParam, + fixedParams: fixedParams, + }), params = _p.params, query = _p.query, queryWithParams = _p.queryWithParams; + var showFilters = Boolean(queryWithParams && !showAutocomplete); + // web only - measure header height for sticky positioning + var _r = useState(0), headerHeight = _r[0], setHeaderHeight = _r[1]; + var headerRef = useRef(null); + useLayoutEffect(function () { + if (IS_WEB) { + if (!headerRef.current) + return; + var measurement = headerRef.current.getBoundingClientRect(); + setHeaderHeight(measurement.height); + } + }, []); + useFocusEffect(useNonReactiveCallback(function () { + if (IS_WEB) { + setSearchText(queryParam); + } + })); + var onPressClearQuery = useCallback(function () { + var _a; + scrollToTopWeb(); + setSearchText(''); + (_a = textInput.current) === null || _a === void 0 ? void 0 : _a.focus(); + }, []); + var onChangeText = useCallback(function (text) { + scrollToTopWeb(); + setSearchText(text); + }, []); + var navigateToItem = useCallback(function (item) { + var _a; + scrollToTopWeb(); + setShowAutocomplete(false); + updateSearchHistory(item); + if (IS_WEB) { + // @ts-expect-error route is not typesafe + navigation.push(route.name, __assign(__assign({}, route.params), { q: item })); + } + else { + (_a = textInput.current) === null || _a === void 0 ? void 0 : _a.blur(); + navigation.setParams({ q: item }); + } + }, [updateSearchHistory, navigation, route]); + var onPressCancelSearch = useCallback(function () { + var _a, _b; + scrollToTopWeb(); + (_a = textInput.current) === null || _a === void 0 ? void 0 : _a.blur(); + setShowAutocomplete(false); + if (IS_WEB) { + // Empty params resets the URL to be /search rather than /search?q= + // Also clear the tab parameter + var _c = ((_b = route.params) !== null && _b !== void 0 ? _b : {}), _q = _c.q, _tab = _c.tab, parameters = __rest(_c, ["q", "tab"]); + // @ts-expect-error route is not typesafe + navigation.replace(route.name, parameters); + } + else { + setSearchText(''); + navigation.setParams({ q: '', tab: undefined }); + } + }, [setShowAutocomplete, setSearchText, navigation, route.params, route.name]); + var onSubmit = useCallback(function () { + navigateToItem(searchText); + }, [navigateToItem, searchText]); + var onAutocompleteResultPress = useCallback(function () { + var _a; + if (IS_WEB) { + setShowAutocomplete(false); + } + else { + (_a = textInput.current) === null || _a === void 0 ? void 0 : _a.blur(); + } + }, []); + var handleHistoryItemClick = useCallback(function (item) { + setSearchText(item); + navigateToItem(item); + }, [navigateToItem]); + var handleProfileClick = useCallback(function (profile) { + unstableCacheProfileView(queryClient, profile); + // Slight delay to avoid updating during push nav animation. + setTimeout(function () { + updateProfileHistory(profile); + }, 400); + }, [updateProfileHistory, queryClient]); + var onSoftReset = useCallback(function () { + var _a, _b; + if (IS_WEB) { + // Empty params resets the URL to be /search rather than /search?q= + // Also clear the tab parameter when soft resetting + var _c = ((_a = route.params) !== null && _a !== void 0 ? _a : {}), _q = _c.q, _tab = _c.tab, parameters = __rest(_c, ["q", "tab"]); + // @ts-expect-error route is not typesafe + navigation.replace(route.name, parameters); + } + else { + setSearchText(''); + navigation.setParams({ q: '', tab: undefined }); + (_b = textInput.current) === null || _b === void 0 ? void 0 : _b.focus(); + } + }, [navigation, route]); + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + return listenSoftReset(onSoftReset); + }, [onSoftReset, setMinimalShellMode])); + var onSearchInputFocus = useCallback(function () { + if (IS_WEB) { + // Prevent a jump on iPad by ensuring that + // the initial focused render has no result list. + requestAnimationFrame(function () { + setShowAutocomplete(true); + }); + } + else { + setShowAutocomplete(true); + } + }, [setShowAutocomplete]); + var focusSearchInput = useCallback(function (tab) { + var _a; + (_a = textInput.current) === null || _a === void 0 ? void 0 : _a.focus(); + // If a tab is specified, set the tab parameter + if (tab) { + if (IS_WEB) { + navigation.setParams(__assign(__assign({}, route.params), { tab: tab })); + } + else { + navigation.setParams({ tab: tab }); + } + } + }, [navigation, route]); + var showHeader = !gtMobile || navButton !== 'menu'; + return (_jsxs(Layout.Screen, { testID: testID, children: [_jsx(View, { ref: headerRef, onLayout: function (evt) { + if (IS_WEB) + setHeaderHeight(evt.nativeEvent.layout.height); + }, style: [ + a.relative, + a.z_10, + web({ + position: 'sticky', + top: 0, + }), + ], children: _jsxs(Layout.Center, { style: t.atoms.bg, children: [showHeader && (_jsx(View + // HACK: shift up search input. we can't remove the top padding + // on the search input because it messes up the layout animation + // if we add it only when the header is hidden + , { + // HACK: shift up search input. we can't remove the top padding + // on the search input because it messes up the layout animation + // if we add it only when the header is hidden + style: { marginBottom: tokens.space.xs * -1 }, children: _jsxs(Layout.Header.Outer, { noBottomBorder: true, children: [navButton === 'menu' ? (_jsx(Layout.Header.MenuButton, {})) : (_jsx(Layout.Header.BackButton, {})), _jsx(Layout.Header.Content, { align: "left", children: _jsx(Layout.Header.TitleText, { children: isExplore ? _jsx(Trans, { children: "Explore" }) : _jsx(Trans, { children: "Search" }) }) }), showFilters ? (_jsx(SearchLanguageDropdown, { value: params.lang, onChange: params.setLang })) : (_jsx(Layout.Header.Slot, {}))] }) })), _jsx(View, { style: [a.px_lg, a.pt_sm, a.pb_sm, a.overflow_hidden], children: _jsxs(View, { style: [a.gap_sm], children: [_jsxs(View, { style: [a.w_full, a.flex_row, a.align_stretch, a.gap_xs], children: [_jsx(View, { style: [a.flex_1], children: _jsx(SearchInput, { ref: textInput, value: searchText, onFocus: onSearchInputFocus, onChangeText: onChangeText, onClearText: onPressClearQuery, onSubmitEditing: onSubmit, placeholder: inputPlaceholder !== null && inputPlaceholder !== void 0 ? inputPlaceholder : l(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Search for posts, users, or feeds"], ["Search for posts, users, or feeds"]))), hitSlop: __assign(__assign({}, HITSLOP_20), { top: 0 }) }) }), showAutocomplete && (_jsx(Button, { label: l(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Cancel search"], ["Cancel search"]))), size: "large", variant: "ghost", color: "secondary", shape: "rectangular", style: [a.px_sm], onPress: onPressCancelSearch, hitSlop: HITSLOP_10, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) }))] }), showFilters && !showHeader && (_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.justify_between, + a.gap_sm, + ], children: _jsx(SearchLanguageDropdown, { value: params.lang, onChange: params.setLang }) }))] }) })] }) }), _jsx(View, { style: { + display: showAutocomplete && !fixedParams ? 'flex' : 'none', + flex: 1, + }, children: searchText.length > 0 ? (_jsx(AutocompleteResults, { isAutocompleteFetching: isAutocompleteFetching, autocompleteData: autocompleteData, searchText: searchText, onSubmit: onSubmit, onResultPress: onAutocompleteResultPress, onProfileClick: handleProfileClick })) : (_jsx(SearchHistory, { searchHistory: termHistory, selectedProfiles: (accountHistoryProfiles === null || accountHistoryProfiles === void 0 ? void 0 : accountHistoryProfiles.profiles) || [], onItemClick: handleHistoryItemClick, onProfileClick: handleProfileClick, onRemoveItemClick: deleteSearchHistoryItem, onRemoveProfileClick: deleteProfileHistoryItem })) }), _jsx(View, { style: { + display: showAutocomplete ? 'none' : 'flex', + flex: 1, + }, children: _jsx(SearchScreenInner, { activeTab: activeTab, setActiveTab: setActiveTab, query: query, queryWithParams: queryWithParams, headerHeight: headerHeight, focusSearchInput: focusSearchInput }, params.lang) })] })); +} +var SearchScreenInner = function (_a) { + var activeTab = _a.activeTab, setActiveTab = _a.setActiveTab, query = _a.query, queryWithParams = _a.queryWithParams, headerHeight = _a.headerHeight, focusSearchInput = _a.focusSearchInput; + var t = useTheme(); + var setMinimalShellMode = useSetMinimalShellMode(); + var hasSession = useSession().hasSession; + var gtTablet = useBreakpoints().gtTablet; + var onPageSelected = useCallback(function (index) { + setMinimalShellMode(false); + setActiveTab(index); + }, [setActiveTab, setMinimalShellMode]); + return queryWithParams ? (_jsx(SearchResults, { query: query, queryWithParams: queryWithParams, activeTab: activeTab, headerHeight: headerHeight, onPageSelected: onPageSelected, initialPage: activeTab })) : hasSession ? (_jsx(Explore, { focusSearchInput: focusSearchInput, headerHeight: headerHeight })) : (_jsx(Layout.Center, { children: _jsxs(View, { style: a.flex_1, children: [gtTablet && (_jsx(View, { style: [ + a.border_b, + t.atoms.border_contrast_low, + a.px_lg, + a.pt_sm, + a.pb_lg, + ], children: _jsx(Text, { style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "Search" }) }) })), _jsxs(View, { style: [a.align_center, a.justify_center, a.py_4xl, a.gap_lg], children: [_jsx(MagnifyingGlassIcon, { strokeWidth: 3, size: 60, style: t.atoms.text_contrast_medium }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.text_md], children: _jsx(Trans, { children: "Find posts, users, and feeds on Bluesky" }) })] })] }) })); +}; +SearchScreenInner = memo(SearchScreenInner); +function useQueryManager(_a) { + var initialQuery = _a.initialQuery, fixedParams = _a.fixedParams; + var _b = useMemo(function () { + return parseSearchQuery(initialQuery || ''); + }, [initialQuery]), query = _b.query, initialParams = _b.params; + var _c = useState(initialQuery), prevInitialQuery = _c[0], setPrevInitialQuery = _c[1]; + var _d = useState(initialParams.lang || ''), lang = _d[0], setLang = _d[1]; + if (initialQuery !== prevInitialQuery) { + // handle new queryParam change (from manual search entry) + setPrevInitialQuery(initialQuery); + setLang(initialParams.lang || ''); + } + var params = useMemo(function () { return (__assign(__assign(__assign({}, initialParams), { + // managed stuff + lang: lang }), fixedParams)); }, [lang, initialParams, fixedParams]); + var handlers = useMemo(function () { return ({ + setLang: setLang, + }); }, [setLang]); + return useMemo(function () { + return { + query: query, + queryWithParams: makeSearchQuery(query, params), + params: __assign(__assign({}, params), handlers), + }; + }, [query, params, handlers]); +} +function scrollToTopWeb() { + if (IS_WEB) { + window.scrollTo(0, 0); + } +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Search/__tests__/utils.test.js b/src/screens/Search/__tests__/utils.test.js new file mode 100644 index 0000000000..3bfeb1f7be --- /dev/null +++ b/src/screens/Search/__tests__/utils.test.js @@ -0,0 +1,38 @@ +import { describe, expect, it } from '@jest/globals'; +import { parseSearchQuery } from '#/screens/Search/utils'; +describe("parseSearchQuery", function () { + var tests = [ + { + input: "bluesky", + output: { query: "bluesky", params: {} }, + }, + { + input: "bluesky from:esb.lol", + output: { query: "bluesky", params: { from: "esb.lol" } }, + }, + { + input: "bluesky \"from:esb.lol\"", + output: { query: "bluesky \"from:esb.lol\"", params: {} }, + }, + { + input: "bluesky mentions:@esb.lol", + output: { query: "bluesky", params: { mentions: "@esb.lol" } }, + }, + { + input: "bluesky since:2021-01-01:00:00:00", + output: { query: "bluesky", params: { since: "2021-01-01:00:00:00" } }, + }, + { + input: "bluesky lang:\"en\"", + output: { query: "bluesky", params: { lang: "en" } }, + }, + { + input: "bluesky \"literal\" lang:en \"from:invalid\"", + output: { query: "bluesky \"literal\" \"from:invalid\"", params: { lang: "en" } }, + }, + ]; + it.each(tests)("$input -> $output.query $output.params", function (_a) { + var input = _a.input, output = _a.output; + expect(parseSearchQuery(input)).toEqual(output); + }); +}); diff --git a/src/screens/Search/components/AutocompleteResults.js b/src/screens/Search/components/AutocompleteResults.js new file mode 100644 index 0000000000..4c92e2dc58 --- /dev/null +++ b/src/screens/Search/components/AutocompleteResults.js @@ -0,0 +1,30 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo } from 'react'; +import { ActivityIndicator, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { SearchLinkCard } from '#/view/shell/desktop/Search'; +import { SearchProfileCard } from '#/screens/Search/components/SearchProfileCard'; +import { atoms as a, native } from '#/alf'; +import * as Layout from '#/components/Layout'; +import { IS_NATIVE } from '#/env'; +var AutocompleteResults = function (_a) { + var isAutocompleteFetching = _a.isAutocompleteFetching, autocompleteData = _a.autocompleteData, searchText = _a.searchText, onSubmit = _a.onSubmit, onResultPress = _a.onResultPress, onProfileClick = _a.onProfileClick; + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + return (_jsx(_Fragment, { children: (isAutocompleteFetching && !(autocompleteData === null || autocompleteData === void 0 ? void 0 : autocompleteData.length)) || + !moderationOpts ? (_jsx(Layout.Content, { children: _jsx(View, { style: [a.py_xl], children: _jsx(ActivityIndicator, {}) }) })) : (_jsxs(Layout.Content, { keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", children: [_jsx(SearchLinkCard, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Search for \"", "\""], ["Search for \"", "\""])), searchText)), onPress: native(onSubmit), to: IS_NATIVE + ? undefined + : "/search?q=".concat(encodeURIComponent(searchText)), style: a.border_b }), autocompleteData === null || autocompleteData === void 0 ? void 0 : autocompleteData.map(function (item) { return (_jsx(SearchProfileCard, { profile: item, moderationOpts: moderationOpts, onPress: function () { + onProfileClick(item); + onResultPress(); + } }, item.did)); }), _jsx(View, { style: { height: 200 } })] })) })); +}; +AutocompleteResults = memo(AutocompleteResults); +export { AutocompleteResults }; +var templateObject_1; diff --git a/src/screens/Search/components/ModuleHeader.js b/src/screens/Search/components/ModuleHeader.js new file mode 100644 index 0000000000..6fb76d4911 --- /dev/null +++ b/src/screens/Search/components/ModuleHeader.js @@ -0,0 +1,110 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { AtUri } from '@atproto/api'; +import { PressableScale } from '#/lib/custom-animations/PressableScale'; +import { makeCustomFeedLink } from '#/lib/routes/links'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, native, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import * as FeedCard from '#/components/FeedCard'; +import { sizes as iconSizes } from '#/components/icons/common'; +import { MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon } from '#/components/icons/MagnifyingGlass'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +export function Container(_a) { + var style = _a.style, children = _a.children, bottomBorder = _a.bottomBorder; + var t = useTheme(); + return (_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.px_lg, + a.pt_2xl, + a.pb_md, + a.gap_sm, + t.atoms.bg, + bottomBorder && [a.border_b, t.atoms.border_contrast_low], + style, + ], children: children })); +} +export function FeedLink(_a) { + var feed = _a.feed, children = _a.children; + var t = useTheme(); + var _b = useMemo(function () { return new AtUri(feed.uri); }, [feed.uri]), did = _b.host, rkey = _b.rkey; + return (_jsx(Link, { to: makeCustomFeedLink(did, rkey), label: feed.displayName, style: [a.flex_1], children: function (_a) { + var focused = _a.focused, hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(View, { style: [ + a.flex_1, + a.flex_row, + a.align_center, + { gap: 10 }, + a.rounded_md, + a.p_xs, + { marginLeft: -6 }, + (focused || hovered || pressed) && t.atoms.bg_contrast_25, + ], children: children })); + } })); +} +export function FeedAvatar(_a) { + var feed = _a.feed; + return _jsx(UserAvatar, { type: "algo", size: 38, avatar: feed.avatar }); +} +export function Icon(_a) { + var Comp = _a.icon, _b = _a.size, size = _b === void 0 ? 'lg' : _b; + var iconSize = iconSizes[size]; + return (_jsx(View, { style: [a.z_20, { width: iconSize, height: iconSize, marginLeft: -2 }], children: _jsx(Comp, { width: iconSize }) })); +} +export function TitleText(_a) { + var style = _a.style, props = __rest(_a, ["style"]); + return (_jsx(Text, __assign({ style: [a.font_semi_bold, a.flex_1, a.text_xl, style], emoji: true }, props))); +} +export function SubtitleText(_a) { + var style = _a.style, props = __rest(_a, ["style"]); + var t = useTheme(); + return (_jsx(Text, __assign({ style: [ + t.atoms.text_contrast_medium, + a.leading_tight, + a.flex_1, + a.text_sm, + style, + ] }, props))); +} +export function SearchButton(_a) { + var label = _a.label, metricsTag = _a.metricsTag, onPress = _a.onPress; + var ax = useAnalytics(); + return (_jsx(Button, { label: label, size: "small", variant: "ghost", color: "secondary", shape: "round", PressableComponent: native(PressableScale), onPress: function () { + ax.metric('explore:module:searchButtonPress', { module: metricsTag }); + onPress === null || onPress === void 0 ? void 0 : onPress(); + }, style: [ + { + right: -4, + }, + ], children: _jsx(ButtonIcon, { icon: SearchIcon, size: "lg" }) })); +} +export function PinButton(_a) { + var feed = _a.feed; + return (_jsx(View, { style: [a.z_20, { marginRight: -6 }], children: _jsx(FeedCard.SaveButton, { pin: true, view: feed, size: "large", color: "secondary", variant: "ghost", shape: "square", text: false }) })); +} diff --git a/src/screens/Search/components/SearchHistory.js b/src/screens/Search/components/SearchHistory.js new file mode 100644 index 0000000000..1cf7edfdf8 --- /dev/null +++ b/src/screens/Search/components/SearchHistory.js @@ -0,0 +1,63 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Pressable, ScrollView, View } from 'react-native'; +import { moderateProfile } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { createHitslop, HITSLOP_10 } from '#/lib/constants'; +import { makeProfileLink } from '#/lib/routes/links'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { BlockDrawerGesture } from '#/view/shell/BlockDrawerGesture'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import * as Layout from '#/components/Layout'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +export function SearchHistory(_a) { + var searchHistory = _a.searchHistory, selectedProfiles = _a.selectedProfiles, onItemClick = _a.onItemClick, onProfileClick = _a.onProfileClick, onRemoveItemClick = _a.onRemoveItemClick, onRemoveProfileClick = _a.onRemoveProfileClick; + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + return (_jsx(Layout.Content, { keyboardDismissMode: "interactive", keyboardShouldPersistTaps: "handled", children: _jsxs(View, { style: [a.w_full, a.gap_md], children: [(searchHistory.length > 0 || selectedProfiles.length > 0) && (_jsx(View, { style: [a.px_lg, a.pt_sm], children: _jsx(Text, { style: [a.text_md, a.font_semi_bold], children: _jsx(Trans, { children: "Recent Searches" }) }) })), selectedProfiles.length > 0 && (_jsx(View, { children: _jsx(BlockDrawerGesture, { children: _jsx(ScrollView, { horizontal: true, keyboardShouldPersistTaps: "handled", showsHorizontalScrollIndicator: false, contentContainerStyle: [ + a.px_lg, + a.flex_row, + a.flex_nowrap, + a.gap_xl, + ], children: moderationOpts && + selectedProfiles.map(function (profile) { return (_jsx(RecentProfileItem, { profile: profile, moderationOpts: moderationOpts, onPress: function () { return onProfileClick(profile); }, onRemove: function () { return onRemoveProfileClick(profile); } }, profile.did)); }) }) }) })), searchHistory.length > 0 && (_jsx(View, { style: [a.px_lg, a.pt_sm], children: searchHistory.slice(0, 5).map(function (historyItem, index) { return (_jsxs(View, { style: [a.flex_row, a.align_center], children: [_jsx(Pressable, { accessibilityRole: "button", onPress: function () { return onItemClick(historyItem); }, hitSlop: HITSLOP_10, style: [a.flex_1, a.py_sm], children: _jsx(Text, { style: [a.text_md], children: historyItem }) }), _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Remove ", ""], ["Remove ", ""])), historyItem)), onPress: function () { return onRemoveItemClick(historyItem); }, size: "small", variant: "ghost", color: "secondary", shape: "round", children: _jsx(ButtonIcon, { icon: XIcon }) })] }, index)); }) }))] }) })); +} +function RecentProfileItem(_a) { + var _b; + var profile = _a.profile, moderationOpts = _a.moderationOpts, onPress = _a.onPress, onRemove = _a.onRemove; + var _ = useLingui()._; + var width = 80; + var moderation = moderateProfile(profile, moderationOpts); + var name = sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')); + var verification = useSimpleVerificationState({ profile: profile }); + return (_jsxs(View, { style: [a.relative], children: [_jsxs(Link, { to: makeProfileLink(profile), label: profile.handle, onPress: onPress, style: [ + a.flex_col, + a.align_center, + a.gap_xs, + { + width: width, + }, + ], children: [_jsx(UserAvatar, { avatar: profile.avatar, type: ((_b = profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', size: width - 8, moderation: moderation.ui('avatar') }), _jsxs(View, { style: [a.flex_row, a.align_center, a.justify_center, a.w_full], children: [_jsx(Text, { emoji: true, style: [a.text_xs, a.leading_snug], numberOfLines: 1, children: name }), verification.showBadge && (_jsx(View, { style: [a.pl_2xs], children: _jsx(VerificationCheck, { width: 10, verifier: verification.role === 'verifier' }) }))] })] }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Remove profile"], ["Remove profile"])))), hitSlop: createHitslop(6), size: "tiny", variant: "outline", color: "secondary", shape: "round", onPress: onRemove, style: [ + a.absolute, + { + top: 0, + right: 0, + height: 18, + width: 18, + }, + ], children: _jsx(ButtonIcon, { icon: XIcon }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Search/components/SearchLanguageDropdown.js b/src/screens/Search/components/SearchLanguageDropdown.js new file mode 100644 index 0000000000..77559235b5 --- /dev/null +++ b/src/screens/Search/components/SearchLanguageDropdown.js @@ -0,0 +1,83 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { languageName } from '#/locale/helpers'; +import { APP_LANGUAGES, LANGUAGES } from '#/locale/languages'; +import { useLanguagePrefs } from '#/state/preferences'; +import { atoms as a, native, platform, tokens } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon, } from '#/components/icons/Chevron'; +import { Earth_Stroke2_Corner0_Rounded as EarthIcon } from '#/components/icons/Globe'; +import * as Menu from '#/components/Menu'; +export function SearchLanguageDropdown(_a) { + var _b, _c; + var value = _a.value, onChange = _a.onChange; + var _ = useLingui()._; + var _d = useLanguagePrefs(), appLanguage = _d.appLanguage, contentLanguages = _d.contentLanguages, primaryLanguage = _d.primaryLanguage; + var languages = useMemo(function () { + return LANGUAGES.filter(function (lang, index, self) { + return Boolean(lang.code2) && // reduce to the code2 varieties + index === self.findIndex(function (t) { return t.code2 === lang.code2; }); + }) + .map(function (l) { return ({ + label: languageName(l, appLanguage), + value: l.code2, + key: l.code2 + l.code3, + }); }) + .sort(function (a, b) { + // prioritize user's languages + var aIsUser = contentLanguages.includes(a.value); + var bIsUser = contentLanguages.includes(b.value); + if (aIsUser && !bIsUser) + return -1; + if (bIsUser && !aIsUser) + return 1; + // prioritize "common" langs in the network + var aIsCommon = !!APP_LANGUAGES.find(function (al) { + // skip `ast`, because it uses a 3-letter code which conflicts with `as` + // it begins with `a` anyway so still is top of the list + return (al.code2) !== 'ast' && al.code2.startsWith(a.value); + }); + var bIsCommon = !!APP_LANGUAGES.find(function (al) { + // ditto + return (al.code2) !== 'ast' && al.code2.startsWith(b.value); + }); + if (aIsCommon && !bIsCommon) + return -1; + if (bIsCommon && !aIsCommon) + return 1; + // fall back to alphabetical + return a.label.localeCompare(b.label, primaryLanguage); + }); + }, [appLanguage, contentLanguages, primaryLanguage]); + var currentLanguageLabel = (_c = (_b = languages.find(function (lang) { return lang.value === value; })) === null || _b === void 0 ? void 0 : _b.label) !== null && _c !== void 0 ? _c : _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["All languages"], ["All languages"])))); + return (_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Filter search by language (currently: ", ")"], ["Filter search by language (currently: ", ")"])), currentLanguageLabel)), children: function (_a) { + var props = _a.props; + return (_jsxs(Button, __assign({}, props, { label: props.accessibilityLabel, size: "small", color: platform({ native: 'primary', default: 'secondary' }), variant: platform({ native: 'ghost', default: 'solid' }), style: native([ + a.py_sm, + a.px_sm, + { marginRight: tokens.space.sm * -1 }, + ]), children: [_jsx(ButtonIcon, { icon: EarthIcon }), _jsx(ButtonText, { children: currentLanguageLabel }), _jsx(ButtonIcon, { icon: platform({ + native: ChevronUpDownIcon, + default: ChevronDownIcon, + }) })] }))); + } }), _jsxs(Menu.Outer, { children: [_jsx(Menu.LabelText, { children: _jsx(Trans, { children: "Filter search by language" }) }), _jsxs(Menu.Item, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["All languages"], ["All languages"])))), onPress: function () { return onChange(''); }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "All languages" }) }), _jsx(Menu.ItemRadio, { selected: value === '' })] }), _jsx(Menu.Divider, {}), _jsx(Menu.Group, { children: languages.map(function (lang) { return (_jsxs(Menu.Item, { label: lang.label, onPress: function () { return onChange(lang.value); }, children: [_jsx(Menu.ItemText, { children: lang.label }), _jsx(Menu.ItemRadio, { selected: value === lang.value })] }, lang.key)); }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Search/components/SearchProfileCard.js b/src/screens/Search/components/SearchProfileCard.js new file mode 100644 index 0000000000..5796f6f0d3 --- /dev/null +++ b/src/screens/Search/components/SearchProfileCard.js @@ -0,0 +1,35 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { makeProfileLink } from '#/lib/routes/links'; +import { unstableCacheProfileView } from '#/state/queries/unstable-profile-cache'; +import { atoms as a, useTheme } from '#/alf'; +import { Link } from '#/components/Link'; +import * as ProfileCard from '#/components/ProfileCard'; +export function SearchProfileCard(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, onPressInner = _a.onPress; + var t = useTheme(); + var _ = useLingui()._; + var qc = useQueryClient(); + var onPress = useCallback(function () { + unstableCacheProfileView(qc, profile); + onPressInner === null || onPressInner === void 0 ? void 0 : onPressInner(); + }, [qc, profile, onPressInner]); + return (_jsx(Link, { testID: "searchAutoCompleteResult-".concat(profile.handle), to: makeProfileLink(profile), label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["View ", "'s profile"], ["View ", "'s profile"])), profile.handle)), onPress: onPress, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(View, { style: [ + a.flex_1, + a.px_md, + a.py_sm, + (hovered || pressed) && t.atoms.bg_contrast_25, + ], children: _jsx(ProfileCard.Outer, { children: _jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts })] }) }) })); + } })); +} +var templateObject_1; diff --git a/src/screens/Search/components/StarterPackCard.js b/src/screens/Search/components/StarterPackCard.js new file mode 100644 index 0000000000..08d257a33b --- /dev/null +++ b/src/screens/Search/components/StarterPackCard.js @@ -0,0 +1,167 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { AppBskyGraphStarterpack, moderateProfile, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useSession } from '#/state/session'; +import { LoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useBreakpoints, useTheme, web } from '#/alf'; +import { ButtonText } from '#/components/Button'; +import { PlusSmall_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { Link } from '#/components/Link'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { useStarterPackLink } from '#/components/StarterPack/StarterPackCard'; +import { SubtleHover } from '#/components/SubtleHover'; +import { Text } from '#/components/Typography'; +import * as bsky from '#/types/bsky'; +export function StarterPackCard(_a) { + var _b; + var view = _a.view; + var t = useTheme(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var gtPhone = useBreakpoints().gtPhone; + var link = useStarterPackLink({ view: view }); + var record = view.record; + if (!bsky.dangerousIsType(record, AppBskyGraphStarterpack.isRecord)) { + return null; + } + var profileCount = gtPhone ? 11 : 8; + var profiles = (_b = view.listItemsSample) === null || _b === void 0 ? void 0 : _b.slice(0, profileCount).map(function (item) { return item.subject; }); + return (_jsx(Link, { to: link.to, label: link.label, onHoverIn: link.precache, onPress: link.precache, children: function (s) { + var _a, _b; + return (_jsxs(_Fragment, { children: [_jsx(SubtleHover, { hover: s.hovered || s.pressed }), _jsxs(View, { style: [ + a.w_full, + a.p_lg, + a.gap_md, + a.border, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border_contrast_low, + ], children: [_jsx(AvatarStack, { profiles: profiles !== null && profiles !== void 0 ? profiles : [], numPending: profileCount, total: (_a = view.list) === null || _a === void 0 ? void 0 : _a.listItemCount }), _jsxs(View, { style: [ + a.w_full, + a.flex_row, + a.align_start, + a.gap_lg, + web({ + position: 'static', + zIndex: 'unset', + }), + ], children: [_jsxs(View, { style: [a.flex_1], children: [_jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_snug], numberOfLines: 1, children: record.name }), _jsx(Text, { emoji: true, style: [ + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: ((_b = view.creator) === null || _b === void 0 ? void 0 : _b.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["By you"], ["By you"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["By ", ""], ["By ", ""])), sanitizeHandle(view.creator.handle, '@'))) })] }), _jsx(Link, { to: link.to, label: link.label, onHoverIn: link.precache, onPress: link.precache, variant: "solid", color: "secondary", size: "small", style: [a.z_50], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Open pack" }) }) })] })] })] })); + } })); +} +export function AvatarStack(_a) { + var profiles = _a.profiles, numPending = _a.numPending, total = _a.total; + var t = useTheme(); + var gtPhone = useBreakpoints().gtPhone; + var moderationOpts = useModerationOpts(); + var computedTotal = (total !== null && total !== void 0 ? total : numPending) - numPending; + var circlesCount = numPending + 1; // add total at end + var widthPerc = 100 / circlesCount; + var _b = React.useState(null), size = _b[0], setSize = _b[1]; + var isPending = (numPending && profiles.length === 0) || !moderationOpts; + var items = isPending + ? Array.from({ length: numPending !== null && numPending !== void 0 ? numPending : circlesCount }).map(function (_, i) { return ({ + key: i, + profile: null, + moderation: null, + }); }) + : profiles.map(function (item) { return ({ + key: item.did, + profile: item, + moderation: moderateProfile(item, moderationOpts), + }); }); + return (_jsxs(View, { style: [ + a.w_full, + a.flex_row, + a.align_center, + a.relative, + { width: "".concat(100 - widthPerc * 0.2, "%") }, + ], children: [items.map(function (item, i) { + var _a; + return (_jsx(View, { style: [ + { + width: "".concat(widthPerc, "%"), + zIndex: 100 - i, + }, + ], children: _jsx(View, { style: [ + a.relative, + { + width: '120%', + }, + ], children: _jsx(View, { onLayout: function (e) { return setSize(e.nativeEvent.layout.width); }, style: [ + a.rounded_full, + t.atoms.bg_contrast_25, + { + paddingTop: '100%', + }, + ], children: size && item.profile ? (_jsx(UserAvatar, { size: size, avatar: item.profile.avatar, type: ((_a = item.profile.associated) === null || _a === void 0 ? void 0 : _a.labeler) ? 'labeler' : 'user', moderation: item.moderation.ui('avatar'), style: [a.absolute, a.inset_0] })) : (_jsx(MediaInsetBorder, { style: [a.rounded_full] })) }) }) }, item.key)); + }), _jsx(View, { style: [ + { + width: "".concat(widthPerc, "%"), + zIndex: 1, + }, + ], children: _jsx(View, { style: [ + a.relative, + { + width: '120%', + }, + ], children: _jsx(View, { style: [ + { + paddingTop: '100%', + }, + ], children: _jsx(View, { style: [ + a.absolute, + a.inset_0, + a.rounded_full, + a.align_center, + a.justify_center, + { + backgroundColor: t.atoms.text_contrast_low.color, + }, + ], children: computedTotal > 0 ? (_jsx(Text, { style: [ + gtPhone ? a.text_md : a.text_xs, + a.font_semi_bold, + a.leading_snug, + { color: 'white' }, + ], children: _jsxs(Trans, { comment: "Indicates the number of additional profiles are in the Starter Pack e.g. +12", children: ["+", computedTotal] }) })) : (_jsx(Plus, { fill: "white" })) }) }) }) })] })); +} +export function StarterPackCardSkeleton() { + var t = useTheme(); + var gtPhone = useBreakpoints().gtPhone; + var profileCount = gtPhone ? 11 : 8; + return (_jsxs(View, { style: [ + a.w_full, + a.p_lg, + a.gap_md, + a.border, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border_contrast_low, + ], children: [_jsx(AvatarStack, { profiles: [], numPending: profileCount }), _jsxs(View, { style: [ + a.w_full, + a.flex_row, + a.align_start, + a.gap_lg, + web({ + position: 'static', + zIndex: 'unset', + }), + ], children: [_jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsx(LoadingPlaceholder, { width: 180, height: 18 }), _jsx(LoadingPlaceholder, { width: 120, height: 14 })] }), _jsx(LoadingPlaceholder, { width: 100, height: 33 })] })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Search/index.js b/src/screens/Search/index.js new file mode 100644 index 0000000000..b5c52012bb --- /dev/null +++ b/src/screens/Search/index.js @@ -0,0 +1,7 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { SearchScreenShell } from './Shell'; +export function SearchScreen(props) { + var _a, _b, _c; + var queryParam = (_c = (_b = (_a = props.route) === null || _a === void 0 ? void 0 : _a.params) === null || _b === void 0 ? void 0 : _b.q) !== null && _c !== void 0 ? _c : ''; + return (_jsx(SearchScreenShell, { queryParam: queryParam, testID: "searchScreen", isExplore: true })); +} diff --git a/src/screens/Search/modules/ExploreInterestsCard.js b/src/screens/Search/modules/ExploreInterestsCard.js new file mode 100644 index 0000000000..2b62b8dce7 --- /dev/null +++ b/src/screens/Search/modules/ExploreInterestsCard.js @@ -0,0 +1,63 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useInterestsDisplayNames } from '#/lib/interests'; +import { Nux, useSaveNux } from '#/state/queries/nuxs'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Shapes_Stroke2_Corner0_Rounded as Shapes } from '#/components/icons/Shapes'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Link } from '#/components/Link'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +export function ExploreInterestsCard() { + var _a; + var t = useTheme(); + var _ = useLingui()._; + var preferences = usePreferencesQuery().data; + var interestsDisplayNames = useInterestsDisplayNames(); + var saveNux = useSaveNux().mutateAsync; + var trendingPrompt = Prompt.usePromptControl(); + var _b = useState(false), closing = _b[0], setClosing = _b[1]; + var onClose = function () { + trendingPrompt.open(); + }; + var onConfirmClose = function () { + setClosing(true); + // if this fails, they can try again later + saveNux({ + id: Nux.ExploreInterestsCard, + completed: true, + data: undefined, + }).catch(function () { }); + }; + return closing ? null : (_jsxs(_Fragment, { children: [_jsx(Prompt.Basic, { control: trendingPrompt, title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Dismiss interests"], ["Dismiss interests"])))), description: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You can adjust your interests at any time from \"Content and media\" settings."], ["You can adjust your interests at any time from \"Content and media\" settings."])))), confirmButtonCta: _(msg({ + message: "OK", + comment: "Confirm button text.", + })), onConfirm: onConfirmClose }), _jsx(View, { style: [a.pb_2xs], children: _jsxs(View, { style: [ + a.p_lg, + a.border_b, + a.gap_md, + t.atoms.border_contrast_medium, + ], children: [_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center], children: [_jsx(Shapes, {}), _jsx(Text, { style: [a.text_xl, a.font_semi_bold, a.leading_tight], children: _jsx(Trans, { children: "Your interests" }) })] }), ((_a = preferences === null || preferences === void 0 ? void 0 : preferences.interests) === null || _a === void 0 ? void 0 : _a.tags) && + preferences.interests.tags.length > 0 ? (_jsx(View, { style: [a.flex_row, a.flex_wrap, { gap: 6 }], children: preferences.interests.tags.map(function (tag) { return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + t.atoms.bg_contrast_25, + a.px_lg, + { height: 32 }, + ], children: _jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_high], children: interestsDisplayNames[tag] }) }, tag)); }) })) : null, _jsx(Text, { style: [a.text_sm, a.leading_snug], children: _jsx(Trans, { children: "Your interests help us find what you like!" }) }), _jsx(Link, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Edit interests"], ["Edit interests"])))), to: "/settings/interests", size: "small", variant: "solid", color: "primary", style: [a.justify_center], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Edit interests" }) }) }), _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Hide this card"], ["Hide this card"])))), size: "small", variant: "ghost", color: "secondary", shape: "round", onPress: onClose, style: [ + a.absolute, + { top: a.pt_sm.paddingTop, right: a.pr_sm.paddingRight }, + ], children: _jsx(ButtonIcon, { icon: X, size: "md" }) })] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Search/modules/ExploreRecommendations.js b/src/screens/Search/modules/ExploreRecommendations.js new file mode 100644 index 0000000000..90b78f5401 --- /dev/null +++ b/src/screens/Search/modules/ExploreRecommendations.js @@ -0,0 +1,57 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { DEFAULT_LIMIT as RECOMMENDATIONS_COUNT, useTrendingTopics, } from '#/state/queries/trending/useTrendingTopics'; +import { useTrendingConfig } from '#/state/service-config'; +import { atoms as a, useGutters, useTheme } from '#/alf'; +import { Hashtag_Stroke2_Corner0_Rounded } from '#/components/icons/Hashtag'; +import { TrendingTopic, TrendingTopicLink, TrendingTopicSkeleton, } from '#/components/TrendingTopics'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +// Note: This module is not currently used and may be removed in the future. +export function ExploreRecommendations() { + var enabled = useTrendingConfig().enabled; + return enabled ? _jsx(Inner, {}) : null; +} +function Inner() { + var _a; + var t = useTheme(); + var ax = useAnalytics(); + var gutters = useGutters([0, 'compact']); + var _b = useTrendingTopics(), trending = _b.data, error = _b.error, isLoading = _b.isLoading; + var noRecs = !isLoading && !error && !((_a = trending === null || trending === void 0 ? void 0 : trending.suggested) === null || _a === void 0 ? void 0 : _a.length); + var allFeeds = (trending === null || trending === void 0 ? void 0 : trending.suggested) && isAllFeeds(trending.suggested); + return error || noRecs ? null : (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.flex_row, + IS_WEB + ? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md] + : [a.p_lg, a.pt_2xl, a.gap_md], + a.border_b, + t.atoms.border_contrast_low, + ], children: _jsxs(View, { style: [a.flex_1, a.gap_sm], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: [_jsx(Hashtag_Stroke2_Corner0_Rounded, { size: "lg", fill: t.palette.primary_500, style: { marginLeft: -2 } }), _jsx(Text, { style: [a.text_2xl, a.font_bold, t.atoms.text], children: _jsx(Trans, { children: "Recommended" }) })] }), !allFeeds ? (_jsx(Text, { style: [t.atoms.text_contrast_high, a.leading_snug], children: _jsx(Trans, { children: "Content from across the network we think you might like." }) })) : (_jsx(Text, { style: [t.atoms.text_contrast_high, a.leading_snug], children: _jsx(Trans, { children: "Feeds we think you might like." }) }))] }) }), _jsx(View, { style: [a.pt_md, a.pb_lg], children: _jsx(View, { style: [ + a.flex_row, + a.justify_start, + a.flex_wrap, + { rowGap: 8, columnGap: 6 }, + gutters, + ], children: isLoading ? (Array(RECOMMENDATIONS_COUNT) + .fill(0) + .map(function (_, i) { return _jsx(TrendingTopicSkeleton, { index: i }, i); })) : !(trending === null || trending === void 0 ? void 0 : trending.suggested) ? null : (_jsx(_Fragment, { children: trending.suggested.map(function (topic) { return (_jsx(TrendingTopicLink, { topic: topic, onPress: function () { + ax.metric('recommendedTopic:click', { context: 'explore' }); + }, children: function (_a) { + var hovered = _a.hovered; + return (_jsx(TrendingTopic, { topic: topic, style: [ + hovered && [ + t.atoms.border_contrast_high, + t.atoms.bg_contrast_25, + ], + ] })); + } }, topic.link)); }) })) }) })] })); +} +function isAllFeeds(topics) { + return topics.every(function (topic) { + var segments = topic.link.split('/').slice(1); + return segments[0] === 'profile' && segments[2] === 'feed'; + }); +} diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.js b/src/screens/Search/modules/ExploreSuggestedAccounts.js new file mode 100644 index 0000000000..aa8241c47f --- /dev/null +++ b/src/screens/Search/modules/ExploreSuggestedAccounts.js @@ -0,0 +1,110 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useEffect } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { popularInterests, useInterestsDisplayNames } from '#/lib/interests'; +import { logger } from '#/logger'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { BlockDrawerGesture } from '#/view/shell/BlockDrawerGesture'; +import { atoms as a, useTheme } from '#/alf'; +import { boostInterests, InterestTabs } from '#/components/InterestTabs'; +import * as ProfileCard from '#/components/ProfileCard'; +import { SubtleHover } from '#/components/SubtleHover'; +import { useAnalytics } from '#/analytics'; +export function useLoadEnoughProfiles(_a) { + var interest = _a.interest, data = _a.data, isLoading = _a.isLoading, isFetchingNextPage = _a.isFetchingNextPage, hasNextPage = _a.hasNextPage, fetchNextPage = _a.fetchNextPage; + var profileCount = (data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { + return page.actors.filter(function (actor) { var _a; return !((_a = actor.viewer) === null || _a === void 0 ? void 0 : _a.following); }); + }).length) || 0; + var isAnyLoading = isLoading || isFetchingNextPage; + var isEnoughProfiles = profileCount > 3; + var shouldFetchMore = !isEnoughProfiles && hasNextPage && !!interest; + useEffect(function () { + if (shouldFetchMore && !isAnyLoading) { + logger.info('Not enough suggested accounts - fetching more'); + fetchNextPage(); + } + }, [shouldFetchMore, fetchNextPage, isAnyLoading, interest]); + return { + isReady: !shouldFetchMore, + }; +} +export function SuggestedAccountsTabBar(_a) { + var _b; + var selectedInterest = _a.selectedInterest, onSelectInterest = _a.onSelectInterest, hideDefaultTab = _a.hideDefaultTab, defaultTabLabel = _a.defaultTabLabel; + var _ = useLingui()._; + var ax = useAnalytics(); + var interestsDisplayNames = useInterestsDisplayNames(); + var preferences = usePreferencesQuery().data; + var personalizedInterests = (_b = preferences === null || preferences === void 0 ? void 0 : preferences.interests) === null || _b === void 0 ? void 0 : _b.tags; + var interests = Object.keys(interestsDisplayNames) + .sort(boostInterests(popularInterests)) + .sort(boostInterests(personalizedInterests)); + return (_jsx(BlockDrawerGesture, { children: _jsx(InterestTabs, { interests: hideDefaultTab ? interests : __spreadArray(['all'], interests, true), selectedInterest: selectedInterest || (hideDefaultTab ? interests[0] : 'all'), onSelectTab: function (tab) { + ax.metric('explore:suggestedAccounts:tabPressed', { tab: tab }); + onSelectInterest(tab === 'all' ? null : tab); + }, interestsDisplayNames: hideDefaultTab + ? interestsDisplayNames + : __assign({ all: defaultTabLabel || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["For You"], ["For You"])))) }, interestsDisplayNames) }) })); +} +/** + * Profile card for suggested accounts. Note: border is on the bottom edge + */ +var SuggestedProfileCard = function (_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, recId = _a.recId, position = _a.position; + var t = useTheme(); + var ax = useAnalytics(); + return (_jsx(ProfileCard.Link, { profile: profile, style: [a.flex_1], onPress: function () { + ax.metric('suggestedUser:press', { + logContext: 'Explore', + recId: recId, + position: position, + suggestedDid: profile.did, + category: null, + }); + }, children: function (s) { return (_jsxs(_Fragment, { children: [_jsx(SubtleHover, { hover: s.hovered || s.pressed }), _jsx(View, { style: [ + a.flex_1, + a.w_full, + a.py_lg, + a.px_lg, + a.border_t, + t.atoms.border_contrast_low, + ], children: _jsxs(ProfileCard.Outer, { children: [_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.FollowButton, { profile: profile, moderationOpts: moderationOpts, withIcon: false, logContext: "ExploreSuggestedAccounts", onFollow: function () { + ax.metric('suggestedUser:follow', { + logContext: 'Explore', + location: 'Card', + recId: recId, + position: position, + suggestedDid: profile.did, + category: null, + }); + } })] }), _jsx(ProfileCard.Description, { profile: profile, numberOfLines: 2 })] }) })] })); } })); +}; +SuggestedProfileCard = memo(SuggestedProfileCard); +export { SuggestedProfileCard }; +var templateObject_1; diff --git a/src/screens/Search/modules/ExploreTrendingTopics.js b/src/screens/Search/modules/ExploreTrendingTopics.js new file mode 100644 index 0000000000..b66ebcba2e --- /dev/null +++ b/src/screens/Search/modules/ExploreTrendingTopics.js @@ -0,0 +1,163 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { Pressable, View } from 'react-native'; +import { moderateProfile } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useTrendingSettings } from '#/state/preferences/trending'; +import { useGetTrendsQuery } from '#/state/queries/trending/useGetTrendsQuery'; +import { useTrendingConfig } from '#/state/service-config'; +import { LoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { atoms as a, useGutters, useTheme, web } from '#/alf'; +import { AvatarStack } from '#/components/AvatarStack'; +import { Flame_Stroke2_Corner1_Rounded as FlameIcon } from '#/components/icons/Flame'; +import { Trending3_Stroke2_Corner1_Rounded as TrendingIcon } from '#/components/icons/Trending'; +import { Link } from '#/components/Link'; +import { SubtleHover } from '#/components/SubtleHover'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +var TOPIC_COUNT = 5; +export function ExploreTrendingTopics() { + var enabled = useTrendingConfig().enabled; + var trendingDisabled = useTrendingSettings().trendingDisabled; + return enabled && !trendingDisabled ? _jsx(Inner, {}) : null; +} +function Inner() { + var _a; + var ax = useAnalytics(); + var _b = useGetTrendsQuery(), trending = _b.data, error = _b.error, isLoading = _b.isLoading, isRefetching = _b.isRefetching; + var noTopics = !isLoading && !error && !((_a = trending === null || trending === void 0 ? void 0 : trending.trends) === null || _a === void 0 ? void 0 : _a.length); + return isLoading || isRefetching ? (Array.from({ length: TOPIC_COUNT }).map(function (__, i) { return (_jsx(TrendingTopicRowSkeleton, { withPosts: i === 0 }, i)); })) : error || !(trending === null || trending === void 0 ? void 0 : trending.trends) || noTopics ? null : (_jsx(_Fragment, { children: trending.trends.map(function (trend, index) { return (_jsx(TrendRow, { trend: trend, rank: index + 1, onPress: function () { + ax.metric('trendingTopic:click', { context: 'explore' }); + } }, trend.link)); }) })); +} +export function TrendRow(_a) { + var trend = _a.trend, rank = _a.rank, children = _a.children, onPress = _a.onPress; + var t = useTheme(); + var _ = useLingui()._; + var gutters = useGutters([0, 'base']); + var category = useCategoryDisplayName((trend === null || trend === void 0 ? void 0 : trend.category) || 'other'); + var age = Math.floor((Date.now() - new Date(trend.startedAt || Date.now()).getTime()) / + (1000 * 60 * 60)); + var badgeType = trend.status === 'hot' ? 'hot' : age < 2 ? 'new' : age; + var actors = useModerateTrendingActors(trend.actors); + return (_jsx(Link, { testID: trend.link, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Browse topic ", ""], ["Browse topic ", ""])), trend.displayName)), to: trend.link, onPress: onPress, style: [a.border_b, t.atoms.border_contrast_low], PressableComponent: Pressable, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(_Fragment, { children: [_jsx(SubtleHover, { hover: hovered || pressed, native: true }), _jsxs(View, { style: [gutters, a.w_full, a.py_lg, a.flex_row, a.gap_2xs], children: [_jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsxs(View, { style: [a.flex_row], children: [_jsx(Text, { style: [ + a.text_md, + a.font_semi_bold, + a.leading_tight, + { width: 20 }, + ], children: _jsxs(Trans, { comment: 'The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"', children: [rank, "."] }) }), _jsx(Text, { style: [a.text_md, a.font_semi_bold, a.leading_tight], numberOfLines: 1, children: trend.displayName })] }), _jsxs(View, { style: [ + a.flex_row, + a.gap_sm, + a.align_center, + { paddingLeft: 20 }, + ], children: [actors.length > 0 && (_jsx(AvatarStack, { size: 20, profiles: actors })), _jsx(Text, { style: [ + a.text_sm, + t.atoms.text_contrast_medium, + web(a.leading_snug), + ], numberOfLines: 1, children: category })] })] }), _jsx(View, { style: [a.flex_shrink_0], children: _jsx(TrendingIndicator, { type: badgeType }) })] }), children] })); + } })); +} +function TrendingIndicator(_a) { + var type = _a.type; + var t = useTheme(); + var _ = useLingui()._; + var pillStyles = [ + a.flex_row, + a.align_center, + a.gap_xs, + a.rounded_full, + { height: 28, paddingHorizontal: 10 }, + ]; + var Icon = null; + var text = null; + var color = null; + var backgroundColor = null; + switch (type) { + case 'skeleton': { + return (_jsx(View, { style: [ + pillStyles, + { backgroundColor: t.palette.contrast_25, width: 65, height: 28 }, + ] })); + } + case 'hot': { + Icon = FlameIcon; + color = + t.scheme === 'light' ? t.palette.negative_500 : t.palette.negative_950; + backgroundColor = + t.scheme === 'light' ? t.palette.negative_50 : t.palette.negative_200; + text = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Hot"], ["Hot"])))); + break; + } + case 'new': { + Icon = TrendingIcon; + text = _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["New"], ["New"])))); + color = t.palette.positive_600; + backgroundColor = t.palette.positive_50; + break; + } + default: { + text = _(msg({ + message: "".concat(type, "h ago"), + comment: 'trending topic time spent trending. should be as short as possible to fit in a pill', + })); + color = t.atoms.text_contrast_medium.color; + backgroundColor = t.atoms.bg_contrast_25.backgroundColor; + break; + } + } + return (_jsxs(View, { style: [pillStyles, { backgroundColor: backgroundColor }], children: [Icon && _jsx(Icon, { size: "sm", style: { color: color } }), _jsx(Text, { style: [a.text_sm, a.font_medium, { color: color }], children: text })] })); +} +function useCategoryDisplayName(category) { + var _ = useLingui()._; + switch (category) { + case 'sports': + return _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Sports"], ["Sports"])))); + case 'politics': + return _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Politics"], ["Politics"])))); + case 'video-games': + return _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Video Games"], ["Video Games"])))); + case 'pop-culture': + return _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Entertainment"], ["Entertainment"])))); + case 'news': + return _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["News"], ["News"])))); + case 'other': + default: + return null; + } +} +export function TrendingTopicRowSkeleton(_a) { + var t = useTheme(); + var gutters = useGutters([0, 'base']); + return (_jsxs(View, { style: [ + gutters, + a.w_full, + a.py_lg, + a.flex_row, + a.gap_2xs, + a.border_b, + t.atoms.border_contrast_low, + ], children: [_jsxs(View, { style: [a.flex_1, a.gap_sm], children: [_jsxs(View, { style: [a.flex_row, a.align_center], children: [_jsx(View, { style: [{ width: 20 }], children: _jsx(LoadingPlaceholder, { width: 12, height: 12, style: [a.rounded_full] }) }), _jsx(LoadingPlaceholder, { width: 90, height: 17 })] }), _jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center, { paddingLeft: 20 }], children: [_jsx(LoadingPlaceholder, { width: 70, height: 16 }), _jsx(LoadingPlaceholder, { width: 40, height: 16 }), _jsx(LoadingPlaceholder, { width: 60, height: 16 })] })] }), _jsx(View, { style: [a.flex_shrink_0], children: _jsx(TrendingIndicator, { type: "skeleton" }) })] })); +} +function useModerateTrendingActors(actors) { + var moderationOpts = useModerationOpts(); + return useMemo(function () { + if (!moderationOpts) + return []; + return actors + .filter(function (actor) { + var decision = moderateProfile(actor, moderationOpts); + return !decision.ui('avatar').filter && !decision.ui('avatar').blur; + }) + .slice(0, 3); + }, [actors, moderationOpts]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/screens/Search/modules/ExploreTrendingVideos.js b/src/screens/Search/modules/ExploreTrendingVideos.js new file mode 100644 index 0000000000..3127dc0630 --- /dev/null +++ b/src/screens/Search/modules/ExploreTrendingVideos.js @@ -0,0 +1,131 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { ScrollView, View } from 'react-native'; +import { AppBskyEmbedVideo, AtUri } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { VIDEO_FEED_URI } from '#/lib/constants'; +import { makeCustomFeedLink } from '#/lib/routes/links'; +import { RQKEY, usePostFeedQuery } from '#/state/queries/post-feed'; +import { BlockDrawerGesture } from '#/view/shell/BlockDrawerGesture'; +import { atoms as a, tokens, useGutters, useTheme } from '#/alf'; +import { ButtonIcon } from '#/components/Button'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRight } from '#/components/icons/Chevron'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { CompactVideoPostCard, CompactVideoPostCardPlaceholder, } from '#/components/VideoPostCard'; +import { useAnalytics } from '#/analytics'; +var CARD_WIDTH = 100; +var FEED_DESC = "feedgen|".concat(VIDEO_FEED_URI); +var FEED_PARAMS = { + feedCacheKey: 'explore', +}; +export function ExploreTrendingVideos() { + var gutters = useGutters([0, 'base']); + var _a = usePostFeedQuery(FEED_DESC, FEED_PARAMS), data = _a.data, isLoading = _a.isLoading, error = _a.error; + // Refetch on tab change if nothing else is using this query. + var queryClient = useQueryClient(); + useFocusEffect(function () { + return function () { + var query = queryClient + .getQueryCache() + .find({ queryKey: RQKEY(FEED_DESC, FEED_PARAMS) }); + if (query && query.getObserversCount() <= 1) { + query.fetch(); + } + }; + }); + // const {data: saved} = useSavedFeeds() + // const isSavedAlready = useMemo(() => { + // return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI) + // }, [saved]) + // const {mutateAsync: addSavedFeeds, isPending: isPinPending} = + // useAddSavedFeedsMutation() + // const pinFeed = useCallback( + // (e: any) => { + // e.preventDefault() + // addSavedFeeds([ + // { + // type: 'feed', + // value: VIDEO_FEED_URI, + // pinned: true, + // }, + // ]) + // // prevent navigation + // return false + // }, + // [addSavedFeeds], + // ) + if (error) { + return null; + } + return (_jsx(View, { style: [a.pb_xl], children: _jsx(BlockDrawerGesture, { children: _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, decelerationRate: "fast", snapToInterval: CARD_WIDTH + tokens.space.sm, children: _jsx(View, { style: [ + a.pt_lg, + a.flex_row, + a.gap_sm, + { + paddingLeft: gutters.paddingLeft, + paddingRight: gutters.paddingRight, + }, + ], children: isLoading ? (Array(10) + .fill(0) + .map(function (_, i) { return (_jsx(View, { style: [{ width: CARD_WIDTH }], children: _jsx(CompactVideoPostCardPlaceholder, {}) }, i)); })) : error || !data ? (_jsx(Text, { children: _jsx(Trans, { children: "Whoops! Trending videos failed to load." }) })) : (_jsx(VideoCards, { data: data })) }) }) }) })); +} +function VideoCards(_a) { + var data = _a.data; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var items = useMemo(function () { + return data.pages + .flatMap(function (page) { return page.slices; }) + .map(function (slice) { return slice.items[0]; }) + .filter(Boolean) + .filter(function (item) { return AppBskyEmbedVideo.isView(item.post.embed); }) + .slice(0, 8); + }, [data]); + var href = useMemo(function () { + var urip = new AtUri(VIDEO_FEED_URI); + return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'explore'); + }, []); + return (_jsxs(_Fragment, { children: [items.map(function (item) { return (_jsx(View, { style: [{ width: CARD_WIDTH }], children: _jsx(CompactVideoPostCard, { post: item.post, moderation: item.moderation, sourceContext: { + type: 'feedgen', + uri: VIDEO_FEED_URI, + sourceInterstitial: 'explore', + }, onInteract: function () { + ax.metric('videoCard:click', { context: 'interstitial:explore' }); + } }) }, item.post.uri)); }), _jsx(View, { style: [{ width: CARD_WIDTH * 2 }], children: _jsx(Link, { to: href, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["View more"], ["View more"])))), style: [ + a.justify_center, + a.align_center, + a.flex_1, + a.rounded_md, + t.atoms.bg_contrast_25, + ], children: function (_a) { + var pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.gap_md, + { + opacity: pressed ? 0.6 : 1, + }, + ], children: [_jsx(Text, { style: [a.text_md], children: _jsx(Trans, { children: "View more" }) }), _jsx(View, { style: [ + a.align_center, + a.justify_center, + a.rounded_full, + { + width: 34, + height: 34, + backgroundColor: t.palette.primary_500, + }, + ], children: _jsx(ButtonIcon, { icon: ChevronRight }) })] })); + } }) })] })); +} +var templateObject_1; diff --git a/src/screens/Search/util/useSuggestedOnboardingUsers.js b/src/screens/Search/util/useSuggestedOnboardingUsers.js new file mode 100644 index 0000000000..17eb7b5bcf --- /dev/null +++ b/src/screens/Search/util/useSuggestedOnboardingUsers.js @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +import { useInterestsDisplayNames } from '#/lib/interests'; +import { useActorSearch } from '#/state/queries/actor-search'; +import { useGetSuggestedOnboardingUsersQuery } from '#/state/queries/trending/useGetSuggestedOnboardingUsersQuery'; +/** + * Conditional hook, used in case a user is a non-english speaker, in which + * case we fall back to searching for users instead of our more curated set. + */ +export function useSuggestedOnboardingUsers(_a) { + var _b = _a.category, category = _b === void 0 ? null : _b, _c = _a.search, search = _c === void 0 ? false : _c, overrideInterests = _a.overrideInterests; + var interestsDisplayNames = useInterestsDisplayNames(); + var curated = useGetSuggestedOnboardingUsersQuery({ + enabled: !search, + category: category, + overrideInterests: overrideInterests, + }); + var searched = useActorSearch({ + enabled: !!search, + // use user's app language translation for this value + query: category ? interestsDisplayNames[category] : '', + limit: 10, + }); + return useMemo(function () { + var _a; + if (search) { + return { + // we're not paginating right now + data: (searched === null || searched === void 0 ? void 0 : searched.data) + ? { + actors: (_a = searched.data.pages.flatMap(function (p) { return p.actors; })) !== null && _a !== void 0 ? _a : [], + recId: undefined, + } + : undefined, + isLoading: searched.isLoading, + error: searched.error, + isRefetching: searched.isRefetching, + refetch: searched.refetch, + }; + } + else { + return { + data: curated.data, + isLoading: curated.isLoading, + error: curated.error, + isRefetching: curated.isRefetching, + refetch: curated.refetch, + }; + } + }, [curated, searched, search]); +} diff --git a/src/screens/Search/util/useSuggestedUsers.js b/src/screens/Search/util/useSuggestedUsers.js new file mode 100644 index 0000000000..def6a1cfce --- /dev/null +++ b/src/screens/Search/util/useSuggestedUsers.js @@ -0,0 +1,49 @@ +import { useMemo } from 'react'; +import { useInterestsDisplayNames } from '#/lib/interests'; +import { useActorSearch } from '#/state/queries/actor-search'; +import { useGetSuggestedUsersQuery } from '#/state/queries/trending/useGetSuggestedUsersQuery'; +/** + * Conditional hook, used in case a user is a non-english speaker, in which + * case we fall back to searching for users instead of our more curated set. + */ +export function useSuggestedUsers(_a) { + var _b = _a.category, category = _b === void 0 ? null : _b, _c = _a.search, search = _c === void 0 ? false : _c; + var interestsDisplayNames = useInterestsDisplayNames(); + var curated = useGetSuggestedUsersQuery({ + enabled: !search, + category: category, + }); + var searched = useActorSearch({ + enabled: !!search, + // use user's app language translation for this value + query: category ? interestsDisplayNames[category] : '', + limit: 10, + }); + return useMemo(function () { + var _a; + if (search) { + return { + // we're not paginating right now + data: (searched === null || searched === void 0 ? void 0 : searched.data) + ? { + actors: (_a = searched.data.pages.flatMap(function (p) { return p.actors; })) !== null && _a !== void 0 ? _a : [], + recId: undefined, + } + : undefined, + isLoading: searched.isLoading, + error: searched.error, + isRefetching: searched.isRefetching, + refetch: searched.refetch, + }; + } + else { + return { + data: curated.data, + isLoading: curated.isLoading, + error: curated.error, + isRefetching: curated.isRefetching, + refetch: curated.refetch, + }; + } + }, [curated, searched, search]); +} diff --git a/src/screens/Search/utils.js b/src/screens/Search/utils.js new file mode 100644 index 0000000000..0171ff8eda --- /dev/null +++ b/src/screens/Search/utils.js @@ -0,0 +1,43 @@ +export function parseSearchQuery(rawQuery) { + var base = rawQuery; + var rawLiterals = rawQuery.match(/[^:\w\d]".+?"/gi) || []; + // remove literals from base + for (var _i = 0, rawLiterals_1 = rawLiterals; _i < rawLiterals_1.length; _i++) { + var literal = rawLiterals_1[_i]; + base = base.replace(literal.trim(), ''); + } + // find remaining params in base + var rawParams = base.match(/[a-z]+:[a-z-\.@\d:"]+/gi) || []; + for (var _a = 0, rawParams_1 = rawParams; _a < rawParams_1.length; _a++) { + var param = rawParams_1[_a]; + base = base.replace(param, ''); + } + base = base.trim(); + var params = rawParams.reduce(function (params, param) { + var _a = param.split(/:/), name = _a[0], value = _a.slice(1); + params[name] = value.join(':').replace(/"/g, ''); // dates can contain additional colons + return params; + }, {}); + var literals = rawLiterals.map(function (l) { return String(l).trim(); }); + return { + query: [base, literals.join(' ')].filter(Boolean).join(' '), + params: params, + }; +} +export function makeSearchQuery(query, params) { + return [ + query, + Object.entries(params) + .filter(function (_a) { + var _ = _a[0], value = _a[1]; + return value; + }) + .map(function (_a) { + var name = _a[0], value = _a[1]; + return "".concat(name, ":").concat(value); + }) + .join(' '), + ] + .filter(Boolean) + .join(' '); +} diff --git a/src/screens/Settings/AboutSettings.js b/src/screens/Settings/AboutSettings.js new file mode 100644 index 0000000000..1224c30706 --- /dev/null +++ b/src/screens/Settings/AboutSettings.js @@ -0,0 +1,130 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { Platform } from 'react-native'; +import { setStringAsync } from 'expo-clipboard'; +import * as FileSystem from 'expo-file-system/legacy'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation } from '@tanstack/react-query'; +import { STATUS_PAGE_URL } from '#/lib/constants'; +import * as Toast from '#/view/com/util/Toast'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import { Atom_Stroke2_Corner0_Rounded as AtomIcon } from '#/components/icons/Atom'; +import { BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon } from '#/components/icons/BroomSparkle'; +import { CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon } from '#/components/icons/CodeLines'; +import { Globe_Stroke2_Corner0_Rounded as GlobeIcon } from '#/components/icons/Globe'; +import { Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon } from '#/components/icons/Newspaper'; +import { Wrench_Stroke2_Corner2_Rounded as WrenchIcon } from '#/components/icons/Wrench'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import { getDeviceId } from '#/analytics/identifiers'; +import { IS_ANDROID, IS_IOS, IS_NATIVE } from '#/env'; +import * as env from '#/env'; +import { useDemoMode } from '#/storage/hooks/demo-mode'; +import { useDevMode } from '#/storage/hooks/dev-mode'; +import { OTAInfo } from './components/OTAInfo'; +export function AboutSettingsScreen(_a) { + var _this = this; + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var _c = useDevMode(), devModeEnabled = _c[0], setDevModeEnabled = _c[1]; + var _d = useDemoMode(), demoModeEnabled = _d[0], setDemoModeEnabled = _d[1]; + var _e = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var freeSpaceBefore, freeSpaceAfter, spaceDiff; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, FileSystem.getFreeDiskStorageAsync()]; + case 1: + freeSpaceBefore = _a.sent(); + return [4 /*yield*/, Image.clearDiskCache()]; + case 2: + _a.sent(); + return [4 /*yield*/, FileSystem.getFreeDiskStorageAsync()]; + case 3: + freeSpaceAfter = _a.sent(); + spaceDiff = freeSpaceBefore - freeSpaceAfter; + return [2 /*return*/, spaceDiff * -1]; + } + }); + }); }, + onSuccess: function (sizeDiffBytes) { + if (IS_ANDROID) { + Toast.show(_(msg({ + message: "Image cache cleared, freed ".concat(i18n.number(Math.abs(sizeDiffBytes / 1024 / 1024), { + notation: 'compact', + style: 'unit', + unit: 'megabyte', + })), + comment: "Android-only toast message which includes amount of space freed using localized number formatting", + }))); + } + else { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Image cache cleared"], ["Image cache cleared"]))))); + } + }, + }), onClearImageCache = _e.mutate, isClearingImageCache = _e.isPending; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "About" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.LinkItem, { to: "https://bsky.social/about/support/tos", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Terms of Service"], ["Terms of Service"])))), children: [_jsx(SettingsList.ItemIcon, { icon: NewspaperIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Terms of Service" }) })] }), _jsxs(SettingsList.LinkItem, { to: "https://bsky.social/about/support/privacy-policy", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Privacy Policy"], ["Privacy Policy"])))), children: [_jsx(SettingsList.ItemIcon, { icon: NewspaperIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Privacy Policy" }) })] }), _jsxs(SettingsList.LinkItem, { to: STATUS_PAGE_URL, label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Status Page"], ["Status Page"])))), children: [_jsx(SettingsList.ItemIcon, { icon: GlobeIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Status Page" }) })] }), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.LinkItem, { to: "/sys/log", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["System log"], ["System log"])))), children: [_jsx(SettingsList.ItemIcon, { icon: CodeLinesIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "System log" }) })] }), IS_NATIVE && (_jsxs(SettingsList.PressableItem, { onPress: function () { return onClearImageCache(); }, label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Clear image cache"], ["Clear image cache"])))), disabled: isClearingImageCache, children: [_jsx(SettingsList.ItemIcon, { icon: BroomSparkleIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Clear image cache" }) }), isClearingImageCache && _jsx(SettingsList.ItemIcon, { icon: Loader })] })), _jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Version ", ""], ["Version ", ""])), env.APP_VERSION)), accessibilityHint: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Copies build version to clipboard"], ["Copies build version to clipboard"])))), onLongPress: function () { + var newDevModeEnabled = !devModeEnabled; + setDevModeEnabled(newDevModeEnabled); + Toast.show(newDevModeEnabled + ? _(msg({ + message: 'Developer mode enabled', + context: 'toast', + })) + : _(msg({ + message: 'Developer mode disabled', + context: 'toast', + }))); + }, onPress: function () { + var _a; + setStringAsync("Build version: ".concat(env.APP_VERSION, "; Bundle info: ").concat(env.APP_METADATA, "; Bundle date: ").concat(env.BUNDLE_DATE, "; Platform: ").concat(Platform.OS, "; Platform version: ").concat(Platform.Version, "; Device ID: ").concat((_a = getDeviceId()) !== null && _a !== void 0 ? _a : 'N/A')); + Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Copied build version to clipboard"], ["Copied build version to clipboard"]))))); + }, children: [_jsx(SettingsList.ItemIcon, { icon: WrenchIcon }), _jsx(SettingsList.ItemText, { children: _jsxs(Trans, { children: ["Version ", env.APP_VERSION] }) }), _jsx(SettingsList.BadgeText, { children: env.APP_METADATA })] }), devModeEnabled && (_jsxs(_Fragment, { children: [_jsx(OTAInfo, {}), IS_IOS && (_jsxs(SettingsList.PressableItem, { onPress: function () { + var newDemoModeEnabled = !demoModeEnabled; + setDemoModeEnabled(newDemoModeEnabled); + Toast.show('Demo mode ' + + (newDemoModeEnabled ? 'enabled' : 'disabled')); + }, label: demoModeEnabled ? 'Disable demo mode' : 'Enable demo mode', disabled: isClearingImageCache, children: [_jsx(SettingsList.ItemIcon, { icon: AtomIcon }), _jsx(SettingsList.ItemText, { children: demoModeEnabled ? 'Disable demo mode' : 'Enable demo mode' })] }))] }))] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Settings/AccessibilitySettings.js b/src/screens/Settings/AccessibilitySettings.js new file mode 100644 index 0000000000..9675a446c6 --- /dev/null +++ b/src/screens/Settings/AccessibilitySettings.js @@ -0,0 +1,28 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useHapticsDisabled, useRequireAltTextEnabled, useSetHapticsDisabled, useSetRequireAltTextEnabled, } from '#/state/preferences'; +import { useLargeAltBadgeEnabled, useSetLargeAltBadgeEnabled, } from '#/state/preferences/large-alt-badge'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import { atoms as a } from '#/alf'; +import * as Toggle from '#/components/forms/Toggle'; +import { Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon } from '#/components/icons/Accessibility'; +import { Haptic_Stroke2_Corner2_Rounded as HapticIcon } from '#/components/icons/Haptic'; +import * as Layout from '#/components/Layout'; +import { IS_NATIVE } from '#/env'; +export function AccessibilitySettingsScreen(_a) { + var _ = useLingui()._; + var requireAltTextEnabled = useRequireAltTextEnabled(); + var setRequireAltTextEnabled = useSetRequireAltTextEnabled(); + var hapticsDisabled = useHapticsDisabled(); + var setHapticsDisabled = useSetHapticsDisabled(); + var largeAltBadgeEnabled = useLargeAltBadgeEnabled(); + var setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled(); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Accessibility" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Group, { contentContainerStyle: [a.gap_sm], children: [_jsx(SettingsList.ItemIcon, { icon: AccessibilityIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Alt text" }) }), _jsxs(Toggle.Item, { name: "require_alt_text", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Require alt text before posting"], ["Require alt text before posting"])))), value: requireAltTextEnabled !== null && requireAltTextEnabled !== void 0 ? requireAltTextEnabled : false, onChange: function (value) { return setRequireAltTextEnabled(value); }, style: [a.w_full], children: [_jsx(Toggle.LabelText, { style: [a.flex_1], children: _jsx(Trans, { children: "Require alt text before posting" }) }), _jsx(Toggle.Platform, {})] }), _jsxs(Toggle.Item, { name: "large_alt_badge", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Display larger alt text badges"], ["Display larger alt text badges"])))), value: !!largeAltBadgeEnabled, onChange: function (value) { return setLargeAltBadgeEnabled(value); }, style: [a.w_full], children: [_jsx(Toggle.LabelText, { style: [a.flex_1], children: _jsx(Trans, { children: "Display larger alt text badges" }) }), _jsx(Toggle.Platform, {})] })] }), IS_NATIVE && (_jsxs(_Fragment, { children: [_jsx(SettingsList.Divider, {}), _jsxs(SettingsList.Group, { contentContainerStyle: [a.gap_sm], children: [_jsx(SettingsList.ItemIcon, { icon: HapticIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Haptics" }) }), _jsxs(Toggle.Item, { name: "haptics", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Disable haptic feedback"], ["Disable haptic feedback"])))), value: hapticsDisabled !== null && hapticsDisabled !== void 0 ? hapticsDisabled : false, onChange: function (value) { return setHapticsDisabled(value); }, style: [a.w_full], children: [_jsx(Toggle.LabelText, { style: [a.flex_1], children: _jsx(Trans, { children: "Disable haptic feedback" }) }), _jsx(Toggle.Platform, {})] })] })] }))] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Settings/AccountSettings.js b/src/screens/Settings/AccountSettings.js new file mode 100644 index 0000000000..287699080a --- /dev/null +++ b/src/screens/Settings/AccountSettings.js @@ -0,0 +1,57 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useSession } from '#/state/session'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import { atoms as a, useTheme } from '#/alf'; +import { AgeAssuranceAccountCard } from '#/components/ageAssurance/AgeAssuranceAccountCard'; +import { useDialogControl } from '#/components/Dialog'; +import { BirthDateSettingsDialog } from '#/components/dialogs/BirthDateSettings'; +import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog'; +import { At_Stroke2_Corner2_Rounded as AtIcon } from '#/components/icons/At'; +import { BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon } from '#/components/icons/BirthdayCake'; +import { Car_Stroke2_Corner2_Rounded as CarIcon } from '#/components/icons/Car'; +import { Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon } from '#/components/icons/Envelope'; +import { Freeze_Stroke2_Corner2_Rounded as FreezeIcon } from '#/components/icons/Freeze'; +import { Lock_Stroke2_Corner2_Rounded as LockIcon } from '#/components/icons/Lock'; +import { PencilLine_Stroke2_Corner2_Rounded as PencilIcon } from '#/components/icons/Pencil'; +import { ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon } from '#/components/icons/Shield'; +import { Trash_Stroke2_Corner2_Rounded } from '#/components/icons/Trash'; +import * as Layout from '#/components/Layout'; +import { ChangeHandleDialog } from './components/ChangeHandleDialog'; +import { ChangePasswordDialog } from './components/ChangePasswordDialog'; +import { DeactivateAccountDialog } from './components/DeactivateAccountDialog'; +import { DeleteAccountDialog } from './components/DeleteAccountDialog'; +import { ExportCarDialog } from './components/ExportCarDialog'; +export function AccountSettingsScreen(_a) { + var t = useTheme(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var emailDialogControl = useEmailDialogControl(); + var birthdayControl = useDialogControl(); + var changeHandleControl = useDialogControl(); + var changePasswordControl = useDialogControl(); + var exportCarControl = useDialogControl(); + var deactivateAccountControl = useDialogControl(); + var deleteAccountControl = useDialogControl(); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Account" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: EnvelopeIcon }), _jsx(SettingsList.ItemText, { style: [a.flex_0], children: _jsx(Trans, { children: "Email" }) }), currentAccount && (_jsxs(_Fragment, { children: [_jsx(SettingsList.BadgeText, { style: [a.flex_1], children: currentAccount.email || _jsx(Trans, { children: "(no email)" }) }), currentAccount.emailConfirmed && (_jsx(ShieldIcon, { fill: t.palette.primary_500, size: "md" }))] }))] }), currentAccount && !currentAccount.emailConfirmed && (_jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Verify your email"], ["Verify your email"])))), onPress: function () { + return emailDialogControl.open({ + id: EmailDialogScreenID.Verify, + }); + }, style: [ + a.my_xs, + a.mx_lg, + a.rounded_md, + { backgroundColor: t.palette.primary_50 }, + ], hoverStyle: [{ backgroundColor: t.palette.primary_100 }], contentContainerStyle: [a.rounded_md, a.px_lg], children: [_jsx(SettingsList.ItemIcon, { icon: ShieldIcon, color: t.palette.primary_500 }), _jsx(SettingsList.ItemText, { style: [{ color: t.palette.primary_500 }, a.font_semi_bold], children: _jsx(Trans, { children: "Verify your email" }) }), _jsx(SettingsList.Chevron, { color: t.palette.primary_500 })] })), _jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Update email"], ["Update email"])))), onPress: function () { + return emailDialogControl.open({ + id: EmailDialogScreenID.Update, + }); + }, children: [_jsx(SettingsList.ItemIcon, { icon: PencilIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Update email" }) }), _jsx(SettingsList.Chevron, {})] }), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Password"], ["Password"])))), onPress: function () { return changePasswordControl.open(); }, children: [_jsx(SettingsList.ItemIcon, { icon: LockIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Password" }) }), _jsx(SettingsList.Chevron, {})] }), _jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Handle"], ["Handle"])))), accessibilityHint: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Opens change handle dialog"], ["Opens change handle dialog"])))), onPress: function () { return changeHandleControl.open(); }, children: [_jsx(SettingsList.ItemIcon, { icon: AtIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Handle" }) }), _jsx(SettingsList.Chevron, {})] }), _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: BirthdayCakeIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Birthday" }) }), _jsx(SettingsList.BadgeButton, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Edit"], ["Edit"])))), onPress: function () { return birthdayControl.open(); } })] }), _jsx(AgeAssuranceAccountCard, { style: [a.px_xl, a.pt_xs, a.pb_md] }), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Export my data"], ["Export my data"])))), onPress: function () { return exportCarControl.open(); }, children: [_jsx(SettingsList.ItemIcon, { icon: CarIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Export my data" }) }), _jsx(SettingsList.Chevron, {})] }), _jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Deactivate account"], ["Deactivate account"])))), onPress: function () { return deactivateAccountControl.open(); }, destructive: true, children: [_jsx(SettingsList.ItemIcon, { icon: FreezeIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Deactivate account" }) }), _jsx(SettingsList.Chevron, {})] }), _jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Delete account"], ["Delete account"])))), onPress: function () { return deleteAccountControl.open(); }, destructive: true, children: [_jsx(SettingsList.ItemIcon, { icon: Trash_Stroke2_Corner2_Rounded }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Delete account" }) }), _jsx(SettingsList.Chevron, {})] })] }) }), _jsx(BirthDateSettingsDialog, { control: birthdayControl }), _jsx(ChangeHandleDialog, { control: changeHandleControl }), _jsx(ChangePasswordDialog, { control: changePasswordControl }), _jsx(ExportCarDialog, { control: exportCarControl }), _jsx(DeactivateAccountDialog, { control: deactivateAccountControl }), _jsx(DeleteAccountDialog, { control: deleteAccountControl, deactivateDialogControl: deactivateAccountControl })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Settings/ActivityPrivacySettings.js b/src/screens/Settings/ActivityPrivacySettings.js new file mode 100644 index 0000000000..74d66ab873 --- /dev/null +++ b/src/screens/Settings/ActivityPrivacySettings.js @@ -0,0 +1,37 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationDeclarationMutation, useNotificationDeclarationQuery, } from '#/state/queries/activity-subscriptions'; +import { atoms as a, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import * as Toggle from '#/components/forms/Toggle'; +import { BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon } from '#/components/icons/BellRinging'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import * as SettingsList from './components/SettingsList'; +import { ItemTextWithSubtitle } from './NotificationSettings/components/ItemTextWithSubtitle'; +export function ActivityPrivacySettingsScreen(_a) { + var _b = useNotificationDeclarationQuery(), notificationDeclaration = _b.data, isPending = _b.isPending, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Privacy and Security" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: BellRingingIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Allow others to be notified of your posts" }), subtitleText: _jsx(Trans, { children: "This feature allows users to receive notifications for your new posts and replies. Who do you want to enable this for?" }) })] }), _jsx(View, { style: [a.px_xl, a.pt_md], children: isError ? (_jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load preference." }) })) : isPending ? (_jsx(View, { style: [a.w_full, a.pt_5xl, a.align_center], children: _jsx(Loader, { size: "xl" }) })) : (_jsx(Inner, { notificationDeclaration: notificationDeclaration })) })] }) })] })); +} +export function Inner(_a) { + var notificationDeclaration = _a.notificationDeclaration; + var t = useTheme(); + var _ = useLingui()._; + var mutate = useNotificationDeclarationMutation().mutate; + var onChangeFilter = function (_a) { + var declaration = _a[0]; + mutate({ + $type: 'app.bsky.notification.declaration', + allowSubscriptions: declaration, + }); + }; + return (_jsx(Toggle.Group, { type: "radio", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Filter who can opt to receive notifications for your activity"], ["Filter who can opt to receive notifications for your activity"])))), values: [notificationDeclaration.value.allowSubscriptions], onChange: onChangeFilter, children: _jsxs(View, { style: [a.gap_sm], children: [_jsxs(Toggle.Item, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Anyone who follows me"], ["Anyone who follows me"])))), name: "followers", style: [a.flex_row, a.py_xs, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [t.atoms.text, a.font_normal, a.text_md], children: _jsx(Trans, { children: "Anyone who follows me" }) })] }), _jsxs(Toggle.Item, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Only followers who I follow"], ["Only followers who I follow"])))), name: "mutuals", style: [a.flex_row, a.py_xs, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [t.atoms.text, a.font_normal, a.text_md], children: _jsx(Trans, { children: "Only followers who I follow" }) })] }), _jsxs(Toggle.Item, { label: _(msg({ context: 'enable for', message: "No one" })), name: "none", style: [a.flex_row, a.py_xs, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [t.atoms.text, a.font_normal, a.text_md], children: _jsx(Trans, { context: "enable for", children: "No one" }) })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Settings/AppIconSettings/AppIconImage.js b/src/screens/Settings/AppIconSettings/AppIconImage.js new file mode 100644 index 0000000000..37c1d53bbc --- /dev/null +++ b/src/screens/Settings/AppIconSettings/AppIconImage.js @@ -0,0 +1,20 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { Image } from 'expo-image'; +import { atoms as a, platform, useTheme } from '#/alf'; +export function AppIconImage(_a) { + var icon = _a.icon, _b = _a.size, size = _b === void 0 ? 50 : _b; + var t = useTheme(); + return (_jsx(Image, { source: platform({ + ios: icon.iosImage(), + android: icon.androidImage(), + }), style: [ + { width: size, height: size }, + platform({ + ios: { borderRadius: size / 5 }, + android: a.rounded_full, + }), + a.curve_continuous, + t.atoms.border_contrast_medium, + a.border, + ], accessibilityIgnoresInvertColors: true })); +} diff --git a/src/screens/Settings/AppIconSettings/SettingsListItem.js b/src/screens/Settings/AppIconSettings/SettingsListItem.js new file mode 100644 index 0000000000..6e6f207b95 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/SettingsListItem.js @@ -0,0 +1,20 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { AppIconImage } from '#/screens/Settings/AppIconSettings/AppIconImage'; +import { useCurrentAppIcon } from '#/screens/Settings/AppIconSettings/useCurrentAppIcon'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import { atoms as a } from '#/alf'; +import { Shapes_Stroke2_Corner0_Rounded as Shapes } from '#/components/icons/Shapes'; +export function SettingsListItem() { + var _ = useLingui()._; + var icon = useCurrentAppIcon(); + return (_jsxs(SettingsList.LinkItem, { to: "/settings/app-icon", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["App Icon"], ["App Icon"])))), contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: Shapes }), _jsxs(View, { style: [a.flex_1], children: [_jsx(SettingsList.ItemText, { style: [a.pt_xs, a.pb_md], children: _jsx(Trans, { children: "App Icon" }) }), _jsx(AppIconImage, { icon: icon, size: 60 })] })] })); +} +var templateObject_1; diff --git a/src/screens/Settings/AppIconSettings/SettingsListItem.web.js b/src/screens/Settings/AppIconSettings/SettingsListItem.web.js new file mode 100644 index 0000000000..eeeb791cde --- /dev/null +++ b/src/screens/Settings/AppIconSettings/SettingsListItem.web.js @@ -0,0 +1 @@ +export function SettingsListItem() { } diff --git a/src/screens/Settings/AppIconSettings/index.js b/src/screens/Settings/AppIconSettings/index.js new file mode 100644 index 0000000000..b9f9584a73 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/index.js @@ -0,0 +1,137 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { Alert, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'; +import { PressableScale } from '#/lib/custom-animations/PressableScale'; +import { AppIconImage } from '#/screens/Settings/AppIconSettings/AppIconImage'; +import { useAppIconSets } from '#/screens/Settings/AppIconSettings/useAppIconSets'; +import { atoms as a, useTheme } from '#/alf'; +import * as Toggle from '#/components/forms/Toggle'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +import { IS_ANDROID, IS_INTERNAL } from '#/env'; +export function AppIconSettingsScreen(_a) { + var t = useTheme(); + var _ = useLingui()._; + var sets = useAppIconSets(); + var _b = useState(function () { + return getAppIconName(DynamicAppIcon.getAppIcon()); + }), currentAppIcon = _b[0], setCurrentAppIcon = _b[1]; + var onSetAppIcon = function (icon) { + var _a; + if (IS_ANDROID) { + var next = (_a = sets.defaults.find(function (i) { return i.id === icon; })) !== null && _a !== void 0 ? _a : sets.core.find(function (i) { return i.id === icon; }); + Alert.alert(next + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Change app icon to \"", "\""], ["Change app icon to \"", "\""])), next.name)) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Change app icon"], ["Change app icon"])))), + // unfortunately necessary -sfn + _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["The app will be restarted"], ["The app will be restarted"])))), [ + { + text: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Cancel"], ["Cancel"])))), + style: 'cancel', + }, + { + text: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["OK"], ["OK"])))), + onPress: function () { + setCurrentAppIcon(setAppIcon(icon)); + }, + style: 'default', + }, + ]); + } + else { + setCurrentAppIcon(setAppIcon(icon)); + } + }; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "App Icon" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsxs(Layout.Content, { contentContainerStyle: [a.p_lg], children: [_jsx(Group, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Default icons"], ["Default icons"])))), value: currentAppIcon, onChange: onSetAppIcon, children: sets.defaults.map(function (icon, i) { return (_jsxs(Row, { icon: icon, isEnd: i === sets.defaults.length - 1, children: [_jsx(AppIcon, { icon: icon, size: 40 }, icon.id), _jsx(RowText, { children: icon.name })] }, icon.id)); }) }), IS_INTERNAL && (_jsxs(_Fragment, { children: [_jsx(Text, { style: [ + a.text_md, + a.mt_xl, + a.mb_sm, + a.font_semi_bold, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Bluesky+" }) }), _jsx(Group, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Bluesky+ icons"], ["Bluesky+ icons"])))), value: currentAppIcon, onChange: onSetAppIcon, children: sets.core.map(function (icon, i) { return (_jsxs(Row, { icon: icon, isEnd: i === sets.core.length - 1, children: [_jsx(AppIcon, { icon: icon, size: 40 }, icon.id), _jsx(RowText, { children: icon.name })] }, icon.id)); }) })] }))] })] })); +} +function setAppIcon(icon) { + if (icon === 'default_light') { + return getAppIconName(DynamicAppIcon.setAppIcon(null)); + } + else { + return getAppIconName(DynamicAppIcon.setAppIcon(icon)); + } +} +function getAppIconName(icon) { + if (!icon || icon === 'DEFAULT') { + return 'default_light'; + } + else { + return icon; + } +} +function Group(_a) { + var children = _a.children, label = _a.label, value = _a.value, onChange = _a.onChange; + return (_jsx(Toggle.Group, { type: "radio", label: label, values: [value], maxSelections: 1, onChange: function (vals) { + if (vals[0]) + onChange(vals[0]); + }, children: _jsx(View, { style: [a.flex_1, a.rounded_md, a.overflow_hidden], children: children }) })); +} +function Row(_a) { + var icon = _a.icon, children = _a.children, isEnd = _a.isEnd; + var t = useTheme(); + var _ = useLingui()._; + return (_jsx(Toggle.Item, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Set app icon to ", ""], ["Set app icon to ", ""])), icon.name)), name: icon.id, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_1, + a.p_md, + a.flex_row, + a.gap_md, + a.align_center, + t.atoms.bg_contrast_25, + (hovered || pressed) && t.atoms.bg_contrast_50, + t.atoms.border_contrast_high, + !isEnd && a.border_b, + ], children: [children, _jsx(Toggle.Radio, {})] })); + } })); +} +function RowText(_a) { + var children = _a.children; + var t = useTheme(); + return (_jsx(Text, { style: [ + a.text_md, + a.font_semi_bold, + a.flex_1, + t.atoms.text_contrast_medium, + ], emoji: true, children: children })); +} +function AppIcon(_a) { + var icon = _a.icon, _b = _a.size, size = _b === void 0 ? 50 : _b; + var _ = useLingui()._; + return (_jsx(PressableScale, { accessibilityLabel: icon.name, accessibilityHint: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Changes app icon"], ["Changes app icon"])))), targetScale: 0.95, onPress: function () { + if (IS_ANDROID) { + Alert.alert(_(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Change app icon to \"", "\""], ["Change app icon to \"", "\""])), icon.name)), _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["The app will be restarted"], ["The app will be restarted"])))), [ + { + text: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Cancel"], ["Cancel"])))), + style: 'cancel', + }, + { + text: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["OK"], ["OK"])))), + onPress: function () { + DynamicAppIcon.setAppIcon(icon.id); + }, + style: 'default', + }, + ]); + } + else { + DynamicAppIcon.setAppIcon(icon.id); + } + }, children: _jsx(AppIconImage, { icon: icon, size: size }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13; diff --git a/src/screens/Settings/AppIconSettings/index.web.js b/src/screens/Settings/AppIconSettings/index.web.js new file mode 100644 index 0000000000..1f5459ff50 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/index.web.js @@ -0,0 +1,3 @@ +export function AppIconSettingsScreen() { + throw new Error('Not supported on web'); +} diff --git a/src/screens/Settings/AppIconSettings/types.js b/src/screens/Settings/AppIconSettings/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/screens/Settings/AppIconSettings/useAppIconSets.js b/src/screens/Settings/AppIconSettings/useAppIconSets.js new file mode 100644 index 0000000000..3cb8ad4781 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/useAppIconSets.js @@ -0,0 +1,132 @@ +import { useMemo } from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +export function useAppIconSets() { + var _ = useLingui()._; + return useMemo(function () { + var defaults = [ + { + id: 'default_light', + name: _(msg({ context: 'Name of app icon variant', message: 'Light' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_legacy_light.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_legacy_light.png"); + }, + }, + { + id: 'default_dark', + name: _(msg({ context: 'Name of app icon variant', message: 'Dark' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_legacy_dark.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_legacy_dark.png"); + }, + }, + ]; + /** + * Bluesky+ + */ + var core = [ + { + id: 'core_aurora', + name: _(msg({ context: 'Name of app icon variant', message: 'Aurora' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_aurora.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_aurora.png"); + }, + }, + // { + // id: 'core_bonfire', + // name: _(msg({ context: 'Name of app icon variant', message: 'Bonfire' })), + // iosImage: () => { + // return require(`../../../../assets/app-icons/ios_icon_core_bonfire.png`) + // }, + // androidImage: () => { + // return require(`../../../../assets/app-icons/android_icon_core_bonfire.png`) + // }, + // }, + { + id: 'core_sunrise', + name: _(msg({ context: 'Name of app icon variant', message: 'Sunrise' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_sunrise.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_sunrise.png"); + }, + }, + { + id: 'core_sunset', + name: _(msg({ context: 'Name of app icon variant', message: 'Sunset' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_sunset.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_sunset.png"); + }, + }, + { + id: 'core_midnight', + name: _(msg({ context: 'Name of app icon variant', message: 'Midnight' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_midnight.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_midnight.png"); + }, + }, + { + id: 'core_flat_blue', + name: _(msg({ context: 'Name of app icon variant', message: 'Flat Blue' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_flat_blue.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_flat_blue.png"); + }, + }, + { + id: 'core_flat_white', + name: _(msg({ context: 'Name of app icon variant', message: 'Flat White' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_flat_white.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_flat_white.png"); + }, + }, + { + id: 'core_flat_black', + name: _(msg({ context: 'Name of app icon variant', message: 'Flat Black' })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_flat_black.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_flat_black.png"); + }, + }, + { + id: 'core_classic', + name: _(msg({ + context: 'Name of app icon variant', + message: 'Bluesky Classic™', + })), + iosImage: function () { + return require("../../../../assets/app-icons/ios_icon_core_classic.png"); + }, + androidImage: function () { + return require("../../../../assets/app-icons/android_icon_core_classic.png"); + }, + }, + ]; + return { + defaults: defaults, + core: core, + }; + }, [_]); +} diff --git a/src/screens/Settings/AppIconSettings/useCurrentAppIcon.js b/src/screens/Settings/AppIconSettings/useCurrentAppIcon.js new file mode 100644 index 0000000000..d8f9231d01 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/useCurrentAppIcon.js @@ -0,0 +1,18 @@ +import { useCallback, useMemo, useState } from 'react'; +import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'; +import { useFocusEffect } from '@react-navigation/native'; +import { useAppIconSets } from '#/screens/Settings/AppIconSettings/useAppIconSets'; +export function useCurrentAppIcon() { + var appIconSets = useAppIconSets(); + var _a = useState(function () { + return DynamicAppIcon.getAppIcon(); + }), currentAppIcon = _a[0], setCurrentAppIcon = _a[1]; + // refresh current icon when screen is focused + useFocusEffect(useCallback(function () { + setCurrentAppIcon(DynamicAppIcon.getAppIcon()); + }, [])); + return useMemo(function () { + var _a, _b; + return ((_b = (_a = appIconSets.defaults.find(function (i) { return i.id === currentAppIcon; })) !== null && _a !== void 0 ? _a : appIconSets.core.find(function (i) { return i.id === currentAppIcon; })) !== null && _b !== void 0 ? _b : appIconSets.defaults[0]); + }, [appIconSets, currentAppIcon]); +} diff --git a/src/screens/Settings/AppPasswords.js b/src/screens/Settings/AppPasswords.js new file mode 100644 index 0000000000..042bc4b424 --- /dev/null +++ b/src/screens/Settings/AppPasswords.js @@ -0,0 +1,118 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import Animated, { FadeIn, FadeOut, LayoutAnimationConfig, LinearTransition, } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { cleanError } from '#/lib/strings/errors'; +import { useAppPasswordDeleteMutation, useAppPasswordsQuery, } from '#/state/queries/app-passwords'; +import { EmptyState } from '#/view/com/util/EmptyState'; +import { ErrorScreen } from '#/view/com/util/error/ErrorScreen'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { Growth_Stroke2_Corner0_Rounded as Growth } from '#/components/icons/Growth'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import { Warning_Stroke2_Corner0_Rounded as WarningIcon } from '#/components/icons/Warning'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { AddAppPasswordDialog } from './components/AddAppPasswordDialog'; +import * as SettingsList from './components/SettingsList'; +export function AppPasswordsScreen(_a) { + var _ = useLingui()._; + var _b = useAppPasswordsQuery(), appPasswords = _b.data, error = _b.error; + var createAppPasswordControl = useDialogControl(); + return (_jsxs(Layout.Screen, { testID: "AppPasswordsScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "App Passwords" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: error ? (_jsx(ErrorScreen, { title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Oops!"], ["Oops!"])))), message: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["There was an issue fetching your app passwords"], ["There was an issue fetching your app passwords"])))), details: cleanError(error) })) : (_jsxs(SettingsList.Container, { children: [_jsx(SettingsList.Item, { children: _jsx(Admonition, { type: "tip", style: [a.flex_1], children: _jsx(Trans, { children: "Use app passwords to sign in to other Bluesky clients without giving full access to your account or password." }) }) }), _jsx(SettingsList.Item, { children: _jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Add App Password"], ["Add App Password"])))), size: "large", color: "primary", variant: "solid", onPress: function () { return createAppPasswordControl.open(); }, style: [a.flex_1], children: [_jsx(ButtonIcon, { icon: PlusIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Add App Password" }) })] }) }), _jsx(SettingsList.Divider, {}), _jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: appPasswords ? (appPasswords.length > 0 ? (_jsx(View, { style: [a.overflow_hidden], children: appPasswords.map(function (appPassword) { return (_jsx(Animated.View, { style: a.w_full, entering: FadeIn, exiting: FadeOut, layout: LinearTransition.delay(150), children: _jsx(SettingsList.Item, { children: _jsx(AppPasswordCard, { appPassword: appPassword }) }) }, appPassword.name)); }) })) : (_jsx(EmptyState, { icon: Growth, message: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["No app passwords yet"], ["No app passwords yet"])))) }))) : (_jsx(View, { style: [ + a.flex_1, + a.justify_center, + a.align_center, + a.py_4xl, + ], children: _jsx(Loader, { size: "xl" }) })) })] })) }), _jsx(AddAppPasswordDialog, { control: createAppPasswordControl, passwords: (appPasswords === null || appPasswords === void 0 ? void 0 : appPasswords.map(function (p) { return p.name; })) || [] })] })); +} +function AppPasswordCard(_a) { + var _this = this; + var appPassword = _a.appPassword; + var t = useTheme(); + var _b = useLingui(), i18n = _b.i18n, _ = _b._; + var deleteControl = Prompt.usePromptControl(); + var deleteMutation = useAppPasswordDeleteMutation().mutateAsync; + var onDelete = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, deleteMutation({ name: appPassword.name })]; + case 1: + _a.sent(); + Toast.show(_(msg({ message: 'App password deleted', context: 'toast' }))); + return [2 /*return*/]; + } + }); + }); }, [deleteMutation, appPassword.name, _]); + return (_jsxs(View, { style: [ + a.w_full, + a.border, + a.rounded_sm, + a.px_md, + a.py_sm, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + ], children: [_jsxs(View, { style: [ + a.flex_row, + a.justify_between, + a.align_start, + a.w_full, + a.gap_sm, + ], children: [_jsxs(View, { style: [a.gap_xs], children: [_jsx(Text, { style: [t.atoms.text, a.text_md, a.font_semi_bold], children: appPassword.name }), _jsx(Text, { style: [t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Created", ' ', i18n.date(appPassword.createdAt, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })] }) })] }), _jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Delete app password"], ["Delete app password"])))), variant: "ghost", color: "negative", size: "small", shape: "square", style: [a.bg_transparent], onPress: function () { return deleteControl.open(); }, children: _jsx(ButtonIcon, { icon: TrashIcon }) })] }), appPassword.privileged && (_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center, a.mt_md], children: [_jsx(WarningIcon, { style: [{ color: t.palette.yellow }] }), _jsx(Text, { style: t.atoms.text_contrast_high, children: _jsx(Trans, { children: "Allows access to direct messages" }) })] })), _jsx(Prompt.Basic, { control: deleteControl, title: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Delete app password?"], ["Delete app password?"])))), description: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Are you sure you want to delete the app password \"", "\"?"], ["Are you sure you want to delete the app password \"", "\"?"])), appPassword.name)), onConfirm: onDelete, confirmButtonCta: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Delete"], ["Delete"])))), confirmButtonColor: "negative" })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/screens/Settings/AppearanceSettings.js b/src/screens/Settings/AppearanceSettings.js new file mode 100644 index 0000000000..7b9aab0c37 --- /dev/null +++ b/src/screens/Settings/AppearanceSettings.js @@ -0,0 +1,96 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import Animated, { FadeInUp, FadeOutUp, LayoutAnimationConfig, LinearTransition, } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useSetThemePrefs, useThemePrefs } from '#/state/shell'; +import { SettingsListItem as AppIconSettingsListItem } from '#/screens/Settings/AppIconSettings/SettingsListItem'; +import { atoms as a, native, useAlf, useTheme } from '#/alf'; +import * as SegmentedControl from '#/components/forms/SegmentedControl'; +import { Moon_Stroke2_Corner0_Rounded as MoonIcon } from '#/components/icons/Moon'; +import { Phone_Stroke2_Corner0_Rounded as PhoneIcon } from '#/components/icons/Phone'; +import { TextSize_Stroke2_Corner0_Rounded as TextSize } from '#/components/icons/TextSize'; +import { TitleCase_Stroke2_Corner0_Rounded as Aa } from '#/components/icons/TitleCase'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +import { IS_INTERNAL, IS_NATIVE } from '#/env'; +import * as SettingsList from './components/SettingsList'; +export function AppearanceSettingsScreen(_a) { + var _ = useLingui()._; + var fonts = useAlf().fonts; + var _b = useThemePrefs(), colorMode = _b.colorMode, darkTheme = _b.darkTheme; + var _c = useSetThemePrefs(), setColorMode = _c.setColorMode, setDarkTheme = _c.setDarkTheme; + var onChangeAppearance = useCallback(function (value) { + setColorMode(value); + }, [setColorMode]); + var onChangeDarkTheme = useCallback(function (value) { + setDarkTheme(value); + }, [setDarkTheme]); + var onChangeFontFamily = useCallback(function (value) { + fonts.setFontFamily(value); + }, [fonts]); + var onChangeFontScale = useCallback(function (value) { + fonts.setFontScale(value); + }, [fonts]); + return (_jsx(LayoutAnimationConfig, { skipExiting: true, skipEntering: true, children: _jsxs(Layout.Screen, { testID: "preferencesThreadsScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Appearance" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsx(AppearanceToggleButtonGroup, { title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Color mode"], ["Color mode"])))), icon: PhoneIcon, items: [ + { + label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["System"], ["System"])))), + name: 'system', + }, + { + label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Light"], ["Light"])))), + name: 'light', + }, + { + label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Dark"], ["Dark"])))), + name: 'dark', + }, + ], value: colorMode, onChange: onChangeAppearance }), colorMode !== 'light' && (_jsx(Animated.View, { entering: native(FadeInUp), exiting: native(FadeOutUp), children: _jsx(AppearanceToggleButtonGroup, { title: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Dark theme"], ["Dark theme"])))), icon: MoonIcon, items: [ + { + label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Dim"], ["Dim"])))), + name: 'dim', + }, + { + label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Dark"], ["Dark"])))), + name: 'dark', + }, + ], value: darkTheme !== null && darkTheme !== void 0 ? darkTheme : 'dim', onChange: onChangeDarkTheme }) })), _jsxs(Animated.View, { layout: native(LinearTransition), children: [_jsx(SettingsList.Divider, {}), _jsx(AppearanceToggleButtonGroup, { title: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Font"], ["Font"])))), description: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["For the best experience, we recommend using the theme font."], ["For the best experience, we recommend using the theme font."])))), icon: Aa, items: [ + { + label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["System"], ["System"])))), + name: 'system', + }, + { + label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Theme"], ["Theme"])))), + name: 'theme', + }, + ], value: fonts.family, onChange: onChangeFontFamily }), _jsx(AppearanceToggleButtonGroup, { title: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Font size"], ["Font size"])))), icon: TextSize, items: [ + { + label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Smaller"], ["Smaller"])))), + name: '-1', + }, + { + label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Default"], ["Default"])))), + name: '0', + }, + { + label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Larger"], ["Larger"])))), + name: '1', + }, + ], value: fonts.scale, onChange: onChangeFontScale }), IS_NATIVE && IS_INTERNAL && (_jsxs(_Fragment, { children: [_jsx(SettingsList.Divider, {}), _jsx(AppIconSettingsListItem, {})] }))] })] }) })] }) })); +} +export function AppearanceToggleButtonGroup(_a) { + var title = _a.title, description = _a.description, Icon = _a.icon, items = _a.items, value = _a.value, onChange = _a.onChange; + var t = useTheme(); + return (_jsx(_Fragment, { children: _jsxs(SettingsList.Group, { contentContainerStyle: [a.gap_sm], iconInset: false, children: [_jsx(SettingsList.ItemIcon, { icon: Icon }), _jsx(SettingsList.ItemText, { children: title }), description && (_jsx(Text, { style: [ + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + a.w_full, + ], children: description })), _jsx(SegmentedControl.Root, { type: "radio", label: title, value: value, onChange: onChange, children: items.map(function (item) { return (_jsx(SegmentedControl.Item, { label: item.label, value: item.name, children: _jsx(SegmentedControl.ItemText, { children: item.label }) }, item.name)); }) })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15; diff --git a/src/screens/Settings/ContentAndMediaSettings.js b/src/screens/Settings/ContentAndMediaSettings.js new file mode 100644 index 0000000000..3f519e67f8 --- /dev/null +++ b/src/screens/Settings/ContentAndMediaSettings.js @@ -0,0 +1,56 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useAutoplayDisabled, useSetAutoplayDisabled } from '#/state/preferences'; +import { useInAppBrowser, useSetInAppBrowser, } from '#/state/preferences/in-app-browser'; +import { useTrendingSettings, useTrendingSettingsApi, } from '#/state/preferences/trending'; +import { useTrendingConfig } from '#/state/service-config'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import * as Toggle from '#/components/forms/Toggle'; +import { Bubbles_Stroke2_Corner2_Rounded as BubblesIcon } from '#/components/icons/Bubble'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { Hashtag_Stroke2_Corner0_Rounded as HashtagIcon } from '#/components/icons/Hashtag'; +import { Home_Stroke2_Corner2_Rounded as HomeIcon } from '#/components/icons/Home'; +import { Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon } from '#/components/icons/Macintosh'; +import { Play_Stroke2_Corner2_Rounded as PlayIcon } from '#/components/icons/Play'; +import { Trending2_Stroke2_Corner2_Rounded as Graph } from '#/components/icons/Trending'; +import { Window_Stroke2_Corner2_Rounded as WindowIcon } from '#/components/icons/Window'; +import * as Layout from '#/components/Layout'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +export function ContentAndMediaSettingsScreen(_a) { + var _ = useLingui()._; + var ax = useAnalytics(); + var autoplayDisabledPref = useAutoplayDisabled(); + var setAutoplayDisabledPref = useSetAutoplayDisabled(); + var inAppBrowserPref = useInAppBrowser(); + var setUseInAppBrowser = useSetInAppBrowser(); + var trendingEnabled = useTrendingConfig().enabled; + var _b = useTrendingSettings(), trendingDisabled = _b.trendingDisabled, trendingVideoDisabled = _b.trendingVideoDisabled; + var _c = useTrendingSettingsApi(), setTrendingDisabled = _c.setTrendingDisabled, setTrendingVideoDisabled = _c.setTrendingVideoDisabled; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Content & Media" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.LinkItem, { to: "/settings/saved-feeds", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Manage saved feeds"], ["Manage saved feeds"])))), children: [_jsx(SettingsList.ItemIcon, { icon: HashtagIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Manage saved feeds" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/threads", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Thread preferences"], ["Thread preferences"])))), children: [_jsx(SettingsList.ItemIcon, { icon: BubblesIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Thread preferences" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/following-feed", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Following feed preferences"], ["Following feed preferences"])))), children: [_jsx(SettingsList.ItemIcon, { icon: HomeIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Following feed preferences" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/external-embeds", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["External media"], ["External media"])))), children: [_jsx(SettingsList.ItemIcon, { icon: MacintoshIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "External media" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/interests", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Your interests"], ["Your interests"])))), children: [_jsx(SettingsList.ItemIcon, { icon: CircleInfo }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Your interests" }) })] }), _jsx(SettingsList.Divider, {}), IS_NATIVE && (_jsx(Toggle.Item, { name: "use_in_app_browser", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Use in-app browser to open links"], ["Use in-app browser to open links"])))), value: inAppBrowserPref !== null && inAppBrowserPref !== void 0 ? inAppBrowserPref : false, onChange: function (value) { return setUseInAppBrowser(value); }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: WindowIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Use in-app browser to open links" }) }), _jsx(Toggle.Platform, {})] }) })), _jsx(Toggle.Item, { name: "disable_autoplay", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Autoplay videos and GIFs"], ["Autoplay videos and GIFs"])))), value: !autoplayDisabledPref, onChange: function (value) { return setAutoplayDisabledPref(!value); }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: PlayIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Autoplay videos and GIFs" }) }), _jsx(Toggle.Platform, {})] }) }), trendingEnabled ? (_jsxs(_Fragment, { children: [_jsx(SettingsList.Divider, {}), _jsx(Toggle.Item, { name: "show_trending_topics", label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Enable trending topics"], ["Enable trending topics"])))), value: !trendingDisabled, onChange: function (value) { + var hide = Boolean(!value); + if (hide) { + ax.metric('trendingTopics:hide', { context: 'settings' }); + } + else { + ax.metric('trendingTopics:show', { context: 'settings' }); + } + setTrendingDisabled(hide); + }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: Graph }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Enable trending topics" }) }), _jsx(Toggle.Platform, {})] }) }), _jsx(Toggle.Item, { name: "show_trending_videos", label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Enable trending videos in your Discover feed"], ["Enable trending videos in your Discover feed"])))), value: !trendingVideoDisabled, onChange: function (value) { + var hide = Boolean(!value); + if (hide) { + ax.metric('trendingVideos:hide', { context: 'settings' }); + } + else { + ax.metric('trendingVideos:show', { context: 'settings' }); + } + setTrendingVideoDisabled(hide); + }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: Graph }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Enable trending videos in your Discover feed" }) }), _jsx(Toggle.Platform, {})] }) })] })) : null] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Settings/ExternalMediaPreferences.js b/src/screens/Settings/ExternalMediaPreferences.js new file mode 100644 index 0000000000..ba7f0b6e57 --- /dev/null +++ b/src/screens/Settings/ExternalMediaPreferences.js @@ -0,0 +1,35 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Fragment } from 'react'; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { externalEmbedLabels, } from '#/lib/strings/embed-player'; +import { useExternalEmbedsPrefs, useSetExternalEmbedPref, } from '#/state/preferences'; +import { atoms as a, native } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import * as Toggle from '#/components/forms/Toggle'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from './components/SettingsList'; +export function ExternalMediaPreferencesScreen(_a) { + return (_jsxs(Layout.Screen, { testID: "externalMediaPreferencesScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "External Media Preferences" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsx(SettingsList.Item, { children: _jsx(Admonition, { type: "info", style: [a.flex_1], children: _jsx(Trans, { children: "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." }) }) }), _jsxs(SettingsList.Group, { iconInset: false, children: [_jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Enable media players for" }) }), _jsxs(View, { style: [a.mt_sm, a.w_full], children: [native(_jsx(SettingsList.Divider, { style: [a.my_0] })), Object.entries(externalEmbedLabels) + // TODO: Remove special case when we disable the old integration. + .filter(function (_a) { + var key = _a[0]; + return key !== 'tenor'; + }) + .map(function (_a) { + var key = _a[0], label = _a[1]; + return (_jsxs(Fragment, { children: [_jsx(PrefSelector, { source: key, label: label }, key), native(_jsx(SettingsList.Divider, { style: [a.my_0] }))] }, key)); + })] })] })] }) })] })); +} +function PrefSelector(_a) { + var source = _a.source, label = _a.label; + var setExternalEmbedPref = useSetExternalEmbedPref(); + var sources = useExternalEmbedsPrefs(); + return (_jsxs(Toggle.Item, { name: label, label: label, type: "checkbox", value: (sources === null || sources === void 0 ? void 0 : sources[source]) === 'show', onChange: function () { + return setExternalEmbedPref(source, (sources === null || sources === void 0 ? void 0 : sources[source]) === 'show' ? 'hide' : 'show'); + }, style: [ + a.flex_1, + a.py_md, + native([a.justify_between, a.flex_row_reverse]), + ], children: [_jsx(Toggle.Platform, {}), _jsx(Toggle.LabelText, { style: [a.text_md], children: label })] })); +} diff --git a/src/screens/Settings/FindContactsSettings.js b/src/screens/Settings/FindContactsSettings.js new file mode 100644 index 0000000000..a1e310638d --- /dev/null +++ b/src/screens/Settings/FindContactsSettings.js @@ -0,0 +1,333 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useState } from 'react'; +import { View } from 'react-native'; +import * as Contacts from 'expo-contacts'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useIsFocused } from '@react-navigation/native'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { wait } from '#/lib/async/wait'; +import { HITSLOP_10, urls } from '#/lib/constants'; +import { isBlockedOrBlocking, isMuted } from '#/lib/moderation/blocked-and-muted'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { updateProfileShadow, useProfileShadow, } from '#/state/cache/profile-shadow'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { findContactsStatusQueryKey, optimisticRemoveMatch, useContactsMatchesQuery, useContactsSyncStatusQuery, } from '#/state/queries/find-contacts'; +import { useAgent, useSession } from '#/state/session'; +import { ErrorScreen } from '#/view/com/util/error/ErrorScreen'; +import { List } from '#/view/com/util/List'; +import { atoms as a, tokens, useGutters, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ContactsHeroImage } from '#/components/contacts/components/HeroImage'; +import { ArrowRotateClockwise_Stroke2_Corner0_Rounded as ResyncIcon } from '#/components/icons/ArrowRotate'; +import { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText, Link } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +import { bulkWriteFollows } from '../Onboarding/util'; +export function FindContactsSettingsScreen(_a) { + var _ = useLingui()._; + var ax = useAnalytics(); + var _b = useContactsSyncStatusQuery(), data = _b.data, error = _b.error, refetch = _b.refetch; + var isFocused = useIsFocused(); + useEffect(function () { + var _a; + if (data && isFocused) { + ax.metric('contacts:settings:presented', { + hasPreviouslySynced: !!data.syncStatus, + matchCount: (_a = data.syncStatus) === null || _a === void 0 ? void 0 : _a.matchesCount, + }); + } + }, [data, isFocused]); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Find Friends" }) }) }), _jsx(Layout.Header.Slot, {})] }), IS_NATIVE ? (data ? (!data.syncStatus ? (_jsx(Intro, {})) : (_jsx(SyncStatus, { info: data.syncStatus, refetchStatus: refetch }))) : error ? (_jsx(ErrorScreen, { title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Error getting the latest data."], ["Error getting the latest data."])))), message: cleanError(error), onPressTryAgain: refetch })) : (_jsx(View, { style: [a.flex_1, a.justify_center, a.align_center], children: _jsx(Loader, { size: "xl" }) }))) : (_jsx(ErrorScreen, { title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Not available on this platform."], ["Not available on this platform."])))), message: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Please use the native app to import your contacts."], ["Please use the native app to import your contacts."])))) }))] })); +} +function Intro() { + var _this = this; + var gutter = useGutters(['base']); + var t = useTheme(); + var _ = useLingui()._; + var _a = useQuery({ + queryKey: ['contacts-available'], + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Contacts.isAvailableAsync()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); }); }, + }), isAvailable = _a.data, isSuccess = _a.isSuccess; + return (_jsxs(Layout.Content, { contentContainerStyle: [gutter, a.gap_lg], children: [_jsx(ContactsHeroImage, {}), _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Find your friends on Bluesky by verifying your phone number and matching with your contacts. We protect your information and you control what happens next.", ' ', _jsx(InlineLinkText, { to: urls.website.blog.findFriendsAnnouncement, label: _(msg({ + message: "Learn more about importing contacts", + context: "english-only-resource", + })), style: [a.text_md, a.leading_snug], children: _jsx(Trans, { context: "english-only-resource", children: "Learn more" }) })] }) }), isAvailable ? (_jsx(Link, { to: { screen: 'FindContactsFlow' }, label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Import contacts"], ["Import contacts"])))), size: "large", color: "primary", style: [a.flex_1, a.justify_center], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Import contacts" }) }) })) : (isSuccess && (_jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Contact sync is not available on this device, as the app is unable to access your contacts." }) })))] })); +} +function SyncStatus(_a) { + var _this = this; + var _b, _c; + var info = _a.info, refetchStatus = _a.refetchStatus; + var ax = useAnalytics(); + var agent = useAgent(); + var queryClient = useQueryClient(); + var _ = useLingui()._; + var moderationOpts = useModerationOpts(); + var _d = useContactsMatchesQuery(), data = _d.data, isPending = _d.isPending, hasNextPage = _d.hasNextPage, fetchNextPage = _d.fetchNextPage, isFetchingNextPage = _d.isFetchingNextPage, refetchMatches = _d.refetch; + var _e = useState(false), isPTR = _e[0], setIsPTR = _e[1]; + var onRefresh = function () { + setIsPTR(true); + Promise.all([refetchStatus(), refetchMatches()]).finally(function () { + setIsPTR(false); + }); + }; + var dismissMatch = useMutation({ + mutationFn: function (did) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.contact.dismissMatch({ subject: did })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onMutate: function (did) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + ax.metric('contacts:settings:dismiss', {}); + optimisticRemoveMatch(queryClient, did); + return [2 /*return*/]; + }); + }); }, + onError: function (err) { + refetchMatches(); + if (isNetworkError(err)) { + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Could not follow all matches - please check your network connection."], ["Could not follow all matches - please check your network connection."])))), { type: 'error' }); + } + else { + logger.error('Failed to follow all matches', { safeMessage: err }); + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Could not follow all matches. ", ""], ["Could not follow all matches. ", ""])), cleanError(err))), { + type: 'error', + }); + } + }, + }).mutate; + var profiles = (_c = (_b = data === null || data === void 0 ? void 0 : data.pages) === null || _b === void 0 ? void 0 : _b.flatMap(function (page) { return page.matches; })) !== null && _c !== void 0 ? _c : []; + var numProfiles = profiles.length; + var isAnyUnfollowed = profiles.some(function (profile) { var _a; return !((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following); }); + var renderItem = useCallback(function (_a) { + var item = _a.item, index = _a.index; + if (!moderationOpts) + return null; + return (_jsx(MatchItem, { profile: item, isFirst: index === 0, isLast: index === numProfiles - 1, moderationOpts: moderationOpts, dismissMatch: dismissMatch })); + }, [numProfiles, moderationOpts, dismissMatch]); + var onEndReached = function () { + if (!hasNextPage || isFetchingNextPage) + return; + fetchNextPage(); + }; + return (_jsx(List, { data: profiles, renderItem: renderItem, ListHeaderComponent: _jsx(StatusHeader, { numMatches: info.matchesCount, isPending: isPending, isAnyUnfollowed: isAnyUnfollowed }), ListFooterComponent: _jsx(StatusFooter, { syncedAt: info.syncedAt }), onRefresh: onRefresh, refreshing: isPTR, onEndReached: onEndReached })); +} +function MatchItem(_a) { + var _b; + var profile = _a.profile, isFirst = _a.isFirst, isLast = _a.isLast, moderationOpts = _a.moderationOpts, dismissMatch = _a.dismissMatch; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var shadow = useProfileShadow(profile); + return (_jsx(View, { style: [a.px_xl], children: _jsx(View, { style: [ + a.p_md, + a.border_t, + a.border_x, + t.atoms.border_contrast_high, + isFirst && [ + a.curve_continuous, + { borderTopLeftRadius: tokens.borderRadius.lg }, + { borderTopRightRadius: tokens.borderRadius.lg }, + ], + isLast && [ + a.border_b, + a.curve_continuous, + { borderBottomLeftRadius: tokens.borderRadius.lg }, + { borderBottomRightRadius: tokens.borderRadius.lg }, + a.mb_sm, + ], + ], children: _jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.FollowButton, { profile: profile, moderationOpts: moderationOpts, logContext: "FindContacts", onFollow: function () { return ax.metric('contacts:settings:follow', {}); } }), !((_b = shadow.viewer) === null || _b === void 0 ? void 0 : _b.following) && (_jsx(Button, { color: "secondary", variant: "ghost", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Remove suggestion"], ["Remove suggestion"])))), onPress: function () { return dismissMatch(profile.did); }, hoverStyle: [a.bg_transparent, { opacity: 0.5 }], hitSlop: 8, children: _jsx(ButtonIcon, { icon: XIcon }) }))] }) }) })); +} +function StatusHeader(_a) { + var _this = this; + var numMatches = _a.numMatches, isPending = _a.isPending, isAnyUnfollowed = _a.isAnyUnfollowed; + var _ = useLingui()._; + var ax = useAnalytics(); + var agent = useAgent(); + var queryClient = useQueryClient(); + var currentAccount = useSession().currentAccount; + var _b = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var didsToFollow, cursor, page, _i, _a, profile, uris, _b, didsToFollow_1, did, uri; + var _c; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + didsToFollow = []; + _d.label = 1; + case 1: return [4 /*yield*/, agent.app.bsky.contact.getMatches({ + limit: 100, + cursor: cursor, + })]; + case 2: + page = _d.sent(); + cursor = page.data.cursor; + for (_i = 0, _a = page.data.matches; _i < _a.length; _i++) { + profile = _a[_i]; + if (profile.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) && + !isBlockedOrBlocking(profile) && + !isMuted(profile) && + !((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.following)) { + didsToFollow.push(profile.did); + } + } + _d.label = 3; + case 3: + if (cursor) return [3 /*break*/, 1]; + _d.label = 4; + case 4: + ax.metric('contacts:settings:followAll', { + followCount: didsToFollow.length, + }); + return [4 /*yield*/, wait(500, bulkWriteFollows(agent, didsToFollow))]; + case 5: + uris = _d.sent(); + for (_b = 0, didsToFollow_1 = didsToFollow; _b < didsToFollow_1.length; _b++) { + did = didsToFollow_1[_b]; + uri = uris.get(did); + updateProfileShadow(queryClient, did, { + followingUri: uri, + }); + } + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { + Toast.show(_(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Followed all matches"], ["Followed all matches"])))), { type: 'success' }); + }, + onError: function (err) { + if (isNetworkError(err)) { + Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Could not follow all matches - please check your network connection."], ["Could not follow all matches - please check your network connection."])))), { type: 'error' }); + } + else { + logger.error('Failed to follow all matches', { safeMessage: err }); + Toast.show(_(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Could not follow all matches. ", ""], ["Could not follow all matches. ", ""])), cleanError(err))), { + type: 'error', + }); + } + }, + }), onFollowAll = _b.mutate, isFollowingAll = _b.isPending, hasFollowedAll = _b.isSuccess; + if (numMatches > 0) { + if (isPending) { + return (_jsx(View, { style: [a.w_full, a.py_3xl, a.align_center], children: _jsx(Loader, { size: "xl" }) })); + } + return (_jsxs(View, { style: [ + a.pt_xl, + a.px_xl, + a.pb_md, + a.flex_row, + a.justify_between, + a.align_center, + ], children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold], children: _jsx(Plural, { value: numMatches, one: "# contact found", other: "# contacts found" }) }), isAnyUnfollowed && (_jsx(Button, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Follow all"], ["Follow all"])))), color: "primary", size: "small", variant: "ghost", onPress: function () { return onFollowAll(); }, disabled: isFollowingAll || hasFollowedAll, hitSlop: HITSLOP_10, style: [a.px_0, a.py_0, a.rounded_0], hoverStyle: [a.bg_transparent, { opacity: 0.5 }], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Follow all" }) }) }))] })); + } + return null; +} +function StatusFooter(_a) { + var _this = this; + var syncedAt = _a.syncedAt; + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var t = useTheme(); + var ax = useAnalytics(); + var agent = useAgent(); + var queryClient = useQueryClient(); + var _c = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.contact.removeData({})]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onMutate: function () { return ax.metric('contacts:settings:removeData', {}); }, + onSuccess: function () { + Toast.show(_(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Contacts removed"], ["Contacts removed"]))))); + queryClient.setQueryData(findContactsStatusQueryKey, { syncStatus: undefined }); + }, + onError: function (err) { + if (isNetworkError(err)) { + Toast.show(_(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Failed to remove data due to a network error, please check your internet connection."], ["Failed to remove data due to a network error, please check your internet connection."])))), { type: 'error' }); + } + else { + logger.error('Remove data failed', { safeMessage: err }); + Toast.show(_(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Failed to remove data. ", ""], ["Failed to remove data. ", ""])), cleanError(err))), { + type: 'error', + }); + } + }, + }), removeData = _c.mutate, isPending = _c.isPending; + return (_jsxs(View, { style: [a.px_xl, a.py_xl, a.gap_4xl], children: [_jsxs(View, { style: [a.gap_xs, a.align_start], children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold], children: _jsx(Trans, { children: "Contacts imported" }) }), _jsxs(View, { style: [a.gap_2xs], children: [_jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "We will notify you when we find your friends." }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Imported on", ' ', i18n.date(new Date(syncedAt), { + dateStyle: 'long', + })] }) })] }), _jsxs(Link, { label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Resync contacts"], ["Resync contacts"])))), to: { screen: 'FindContactsFlow' }, onPress: function () { + var daysSinceLastSync = Math.floor((Date.now() - new Date(syncedAt).getTime()) / + (1000 * 60 * 60 * 24)); + ax.metric('contacts:settings:resync', { + daysSinceLastSync: daysSinceLastSync, + }); + }, size: "small", color: "primary_subtle", style: [a.mt_xs], children: [_jsx(ButtonIcon, { icon: ResyncIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Resync contacts" }) })] })] }), _jsxs(View, { style: [a.gap_xs, a.align_start], children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold], children: _jsx(Trans, { children: "Delete contacts" }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Bluesky stores your contacts as encoded data. Removing your contacts will immediately delete this data." }) }), _jsxs(Button, { label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Remove all contacts"], ["Remove all contacts"])))), onPress: function () { return removeData(); }, size: "small", color: "negative_subtle", disabled: isPending, style: [a.mt_xs], children: [_jsx(ButtonIcon, { icon: isPending ? Loader : TrashIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Remove all contacts" }) })] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16; diff --git a/src/screens/Settings/FollowingFeedPreferences.js b/src/screens/Settings/FollowingFeedPreferences.js new file mode 100644 index 0000000000..145ea55bd6 --- /dev/null +++ b/src/screens/Settings/FollowingFeedPreferences.js @@ -0,0 +1,46 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { usePreferencesQuery, useSetFeedViewPreferencesMutation, } from '#/state/queries/preferences'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import * as Toggle from '#/components/forms/Toggle'; +import { Beaker_Stroke2_Corner2_Rounded as BeakerIcon } from '#/components/icons/Beaker'; +import { Bubbles_Stroke2_Corner2_Rounded as BubblesIcon } from '#/components/icons/Bubble'; +import { CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon } from '#/components/icons/Quote'; +import { Repost_Stroke2_Corner2_Rounded as RepostIcon } from '#/components/icons/Repost'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from './components/SettingsList'; +export function FollowingFeedPreferencesScreen(_a) { + var _b, _c, _d, _e, _f, _g, _h, _j; + var _ = useLingui()._; + var preferences = usePreferencesQuery().data; + var _k = useSetFeedViewPreferencesMutation(), setFeedViewPref = _k.mutate, variables = _k.variables; + var showReplies = !((_b = variables === null || variables === void 0 ? void 0 : variables.hideReplies) !== null && _b !== void 0 ? _b : (_c = preferences === null || preferences === void 0 ? void 0 : preferences.feedViewPrefs) === null || _c === void 0 ? void 0 : _c.hideReplies); + var showReposts = !((_d = variables === null || variables === void 0 ? void 0 : variables.hideReposts) !== null && _d !== void 0 ? _d : (_e = preferences === null || preferences === void 0 ? void 0 : preferences.feedViewPrefs) === null || _e === void 0 ? void 0 : _e.hideReposts); + var showQuotePosts = !((_f = variables === null || variables === void 0 ? void 0 : variables.hideQuotePosts) !== null && _f !== void 0 ? _f : (_g = preferences === null || preferences === void 0 ? void 0 : preferences.feedViewPrefs) === null || _g === void 0 ? void 0 : _g.hideQuotePosts); + var mergeFeedEnabled = Boolean((_h = variables === null || variables === void 0 ? void 0 : variables.lab_mergeFeedEnabled) !== null && _h !== void 0 ? _h : (_j = preferences === null || preferences === void 0 ? void 0 : preferences.feedViewPrefs) === null || _j === void 0 ? void 0 : _j.lab_mergeFeedEnabled); + return (_jsxs(Layout.Screen, { testID: "followingFeedPreferencesScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Following Feed Preferences" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsx(SettingsList.Item, { children: _jsx(Admonition, { type: "tip", style: [a.flex_1], children: _jsx(Trans, { children: "These settings only apply to the Following feed." }) }) }), _jsx(Toggle.Item, { type: "checkbox", name: "show-replies", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Show replies"], ["Show replies"])))), value: showReplies, onChange: function (value) { + return setFeedViewPref({ + hideReplies: !value, + }); + }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: BubblesIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Show replies" }) }), _jsx(Toggle.Platform, {})] }) }), _jsx(Toggle.Item, { type: "checkbox", name: "show-reposts", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Show reposts"], ["Show reposts"])))), value: showReposts, onChange: function (value) { + return setFeedViewPref({ + hideReposts: !value, + }); + }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: RepostIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Show reposts" }) }), _jsx(Toggle.Platform, {})] }) }), _jsx(Toggle.Item, { type: "checkbox", name: "show-quotes", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Show quote posts"], ["Show quote posts"])))), value: showQuotePosts, onChange: function (value) { + return setFeedViewPref({ + hideQuotePosts: !value, + }); + }, children: _jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: QuoteIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Show quote posts" }) }), _jsx(Toggle.Platform, {})] }) }), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.Group, { children: [_jsx(SettingsList.ItemIcon, { icon: BeakerIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Experimental" }) }), _jsxs(Toggle.Item, { type: "checkbox", name: "merge-feed", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Show samples of your saved feeds in your Following feed"], ["Show samples of your saved feeds in your Following feed"])))), value: mergeFeedEnabled, onChange: function (value) { + return setFeedViewPref({ + lab_mergeFeedEnabled: value, + }); + }, style: [a.w_full, a.gap_md], children: [_jsx(Toggle.LabelText, { style: [a.flex_1], children: _jsx(Trans, { children: "Show samples of your saved feeds in your Following feed" }) }), _jsx(Toggle.Platform, {})] })] })] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Settings/InterestsSettings.js b/src/screens/Settings/InterestsSettings.js new file mode 100644 index 0000000000..5e4aa22c20 --- /dev/null +++ b/src/screens/Settings/InterestsSettings.js @@ -0,0 +1,191 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import debounce from 'lodash.debounce'; +import { interests as allInterests, useInterestsDisplayNames, } from '#/lib/interests'; +import { preferencesQueryKey, usePreferencesQuery, } from '#/state/queries/preferences'; +import { createGetSuggestedFeedsQueryKey } from '#/state/queries/trending/useGetSuggestedFeedsQuery'; +import { createGetSuggestedUsersQueryKey } from '#/state/queries/trending/useGetSuggestedUsersQuery'; +import { createSuggestedStarterPacksQueryKey } from '#/state/queries/useSuggestedStarterPacksQuery'; +import { useAgent } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useGutters, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Divider } from '#/components/Divider'; +import * as Toggle from '#/components/forms/Toggle'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +export function InterestsSettingsScreen(_a) { + var t = useTheme(); + var gutters = useGutters(['base']); + var preferences = usePreferencesQuery().data; + var _b = useState(false), isSaving = _b[0], setIsSaving = _b[1]; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Your interests" }) }) }), _jsx(Layout.Header.Slot, { children: isSaving && _jsx(Loader, {}) })] }), _jsx(Layout.Content, { children: _jsxs(View, { style: [gutters, a.gap_lg], children: [_jsx(Text, { style: [ + a.flex_1, + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "Your selected interests help us serve you content you care about." }) }), _jsx(Divider, {}), preferences ? (_jsx(Inner, { preferences: preferences, setIsSaving: setIsSaving })) : (_jsx(View, { style: [a.flex_row, a.justify_center, a.p_lg], children: _jsx(Loader, { size: "xl" }) }))] }) })] })); +} +function Inner(_a) { + var _this = this; + var preferences = _a.preferences, setIsSaving = _a.setIsSaving; + var _ = useLingui()._; + var agent = useAgent(); + var qc = useQueryClient(); + var interestsDisplayNames = useInterestsDisplayNames(); + var preselectedInterests = useMemo(function () { return preferences.interests.tags || []; }, [preferences.interests.tags]); + var _b = useState(preselectedInterests), interests = _b[0], setInterests = _b[1]; + var saveInterests = useMemo(function () { + return debounce(function (interests) { return __awaiter(_this, void 0, void 0, function () { + var noEdits, error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + noEdits = interests.length === preselectedInterests.length && + preselectedInterests.every(function (pre) { + return interests.find(function (int) { return int === pre; }); + }); + if (noEdits) + return [2 /*return*/]; + setIsSaving(true); + _a.label = 1; + case 1: + _a.trys.push([1, 4, 5, 6]); + return [4 /*yield*/, agent.setInterestsPref({ tags: interests })]; + case 2: + _a.sent(); + qc.setQueriesData({ queryKey: preferencesQueryKey }, function (old) { + if (!old) + return old; + old.interests.tags = interests; + return old; + }); + return [4 /*yield*/, Promise.all([ + qc.resetQueries({ queryKey: createSuggestedStarterPacksQueryKey() }), + qc.resetQueries({ queryKey: createGetSuggestedFeedsQueryKey() }), + qc.resetQueries({ queryKey: createGetSuggestedUsersQueryKey({}) }), + ])]; + case 3: + _a.sent(); + Toast.show(_(msg({ + message: 'Your interests have been updated!', + context: 'toast', + }))); + return [3 /*break*/, 6]; + case 4: + error_1 = _a.sent(); + Toast.show(_(msg({ + message: 'Failed to save your interests.', + context: 'toast', + })), 'xmark'); + return [3 /*break*/, 6]; + case 5: + setIsSaving(false); + return [7 /*endfinally*/]; + case 6: return [2 /*return*/]; + } + }); + }); }, 1500); + }, [_, agent, setIsSaving, qc, preselectedInterests]); + var onChangeInterests = function (interests) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + setInterests(interests); + saveInterests(interests); + return [2 /*return*/]; + }); + }); }; + return (_jsxs(_Fragment, { children: [interests.length === 0 && (_jsx(Admonition, { type: "tip", children: _jsx(Trans, { children: "We recommend selecting at least two interests." }) })), _jsx(Toggle.Group, { values: interests, onChange: onChangeInterests, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select your interests from the options below"], ["Select your interests from the options below"])))), children: _jsx(View, { style: [a.flex_row, a.flex_wrap, a.gap_sm], children: allInterests.map(function (interest) { + var name = interestsDisplayNames[interest]; + if (!name) + return null; + return (_jsx(Toggle.Item, { name: interest, label: interestsDisplayNames[interest], children: _jsx(InterestButton, { interest: interest }) }, interest)); + }) }) })] })); +} +export function InterestButton(_a) { + var interest = _a.interest; + var t = useTheme(); + var interestsDisplayNames = useInterestsDisplayNames(); + var ctx = Toggle.useItemContext(); + var styles = useMemo(function () { + var hovered = [t.atoms.bg_contrast_100]; + var focused = []; + var pressed = []; + var selected = [t.atoms.bg_contrast_900]; + var selectedHover = [t.atoms.bg_contrast_975]; + var textSelected = [t.atoms.text_inverted]; + return { + hovered: hovered, + focused: focused, + pressed: pressed, + selected: selected, + selectedHover: selectedHover, + textSelected: textSelected, + }; + }, [t]); + return (_jsx(View, { style: [ + a.rounded_full, + a.py_md, + a.px_xl, + t.atoms.bg_contrast_50, + ctx.hovered ? styles.hovered : {}, + ctx.focused ? styles.hovered : {}, + ctx.pressed ? styles.hovered : {}, + ctx.selected ? styles.selected : {}, + ctx.selected && (ctx.hovered || ctx.focused || ctx.pressed) + ? styles.selectedHover + : {}, + ], children: _jsx(Text, { selectable: false, style: [ + { + color: t.palette.contrast_900, + }, + a.font_semi_bold, + ctx.selected ? styles.textSelected : {}, + ], children: interestsDisplayNames[interest] }) })); +} +var templateObject_1; diff --git a/src/screens/Settings/LanguageSettings.js b/src/screens/Settings/LanguageSettings.js new file mode 100644 index 0000000000..e65848923a --- /dev/null +++ b/src/screens/Settings/LanguageSettings.js @@ -0,0 +1,90 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { sanitizeAppLanguageSetting } from '#/locale/helpers'; +import { APP_LANGUAGES, LANGUAGES } from '#/locale/languages'; +import { useLanguagePrefs, useLanguagePrefsApi } from '#/state/preferences'; +import { atoms as a, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { LanguageSelectDialog } from '#/components/dialogs/LanguageSelectDialog'; +import * as Toggle from '#/components/forms/Toggle'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import * as Layout from '#/components/Layout'; +import * as Select from '#/components/Select'; +import { Text } from '#/components/Typography'; +import * as SettingsList from './components/SettingsList'; +var DEDUPED_LANGUAGES = LANGUAGES.filter(function (lang, i, arr) { + return lang.code2 && arr.findIndex(function (l) { return l.code2 === lang.code2; }) === i; +}); +export function LanguageSettingsScreen(_a) { + var _ = useLingui()._; + var langPrefs = useLanguagePrefs(); + var setLangPrefs = useLanguagePrefsApi(); + var contentLanguagePrefsControl = useDialogControl(); + var onChangePrimaryLanguage = useCallback(function (value) { + if (!value) + return; + if (langPrefs.primaryLanguage !== value) { + setLangPrefs.setPrimaryLanguage(value); + } + }, [langPrefs, setLangPrefs]); + var onChangeAppLanguage = useCallback(function (value) { + if (!value) + return; + if (langPrefs.appLanguage !== value) { + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)); + } + }, [langPrefs, setLangPrefs]); + var _b = useState(langPrefs.contentLanguages), recentLanguages = _b[0], setRecentLanguages = _b[1]; + var possibleLanguages = useMemo(function () { + return __spreadArray([], new Set(__spreadArray(__spreadArray(__spreadArray([], recentLanguages, true), langPrefs.contentLanguages, true), langPrefs.primaryLanguage, true)), true).map(function (lang) { return LANGUAGES.find(function (l) { return l.code2 === lang; }); }) + .filter(function (x) { return !!x; }); + }, [recentLanguages, langPrefs.contentLanguages, langPrefs.primaryLanguage]); + return (_jsxs(Layout.Screen, { testID: "PreferencesLanguagesScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Languages" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Group, { iconInset: false, children: [_jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "App language" }) }), _jsxs(View, { style: [a.gap_md, a.w_full], children: [_jsx(Text, { style: [a.leading_snug], children: _jsx(Trans, { children: "Select which language to use for the app's user interface." }) }), _jsxs(Select.Root, { value: sanitizeAppLanguageSetting(langPrefs.appLanguage), onValueChange: onChangeAppLanguage, children: [_jsxs(Select.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select app language"], ["Select app language"])))), children: [_jsx(Select.ValueText, {}), _jsx(Select.Icon, {})] }), _jsx(Select.Content, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["App language"], ["App language"])))), renderItem: function (_a) { + var label = _a.label, value = _a.value; + return (_jsxs(Select.Item, { value: value, label: label, children: [_jsx(Select.ItemIndicator, {}), _jsx(Select.ItemText, { children: label })] })); + }, items: APP_LANGUAGES.map(function (l) { return ({ + label: l.name, + value: l.code2, + }); }) })] })] })] }), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.Group, { iconInset: false, children: [_jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Primary language" }) }), _jsxs(View, { style: [a.gap_md, a.w_full], children: [_jsx(Text, { style: [a.leading_snug], children: _jsx(Trans, { children: "Select your preferred language for translations in your feed." }) }), _jsxs(Select.Root, { value: langPrefs.primaryLanguage, onValueChange: onChangePrimaryLanguage, children: [_jsxs(Select.Trigger, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Select primary language"], ["Select primary language"])))), children: [_jsx(Select.ValueText, {}), _jsx(Select.Icon, {})] }), _jsx(Select.Content, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Primary language"], ["Primary language"])))), renderItem: function (_a) { + var label = _a.label, value = _a.value; + return (_jsxs(Select.Item, { value: value, label: label, children: [_jsx(Select.ItemIndicator, {}), _jsx(Select.ItemText, { children: label })] })); + }, items: DEDUPED_LANGUAGES.sort(function (a, b) { + return a.name.localeCompare(b.name, langPrefs.appLanguage); + }).map(function (l) { return ({ + label: l.name, // Pre-generated name using Intl.DisplayNames + value: l.code2, + }); }) })] })] })] }), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.Group, { iconInset: false, children: [_jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Content languages" }) }), _jsxs(View, { style: [a.gap_md], children: [_jsx(Text, { style: [a.leading_snug], children: _jsx(Trans, { children: "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." }) }), langPrefs.contentLanguages.length === 0 && (_jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "All languages will be shown in your feeds." }) })), _jsx(View, { style: [a.w_full, web({ maxWidth: 400 })], children: _jsx(Toggle.Group, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Select content languages"], ["Select content languages"])))), values: langPrefs.contentLanguages, onChange: setLangPrefs.setContentLanguages, children: _jsxs(Toggle.PanelGroup, { children: [possibleLanguages + .sort(function (a, b) { + return a.name.localeCompare(b.name, langPrefs.appLanguage); + }) + .map(function (language, index) { + var name = language.name; // Pre-generated name using Intl.DisplayNames + return (_jsx(Toggle.Item, { name: language.code2, label: name, children: function (_a) { + var selected = _a.selected; + return (_jsxs(Toggle.Panel, { active: selected, adjacent: index === 0 ? 'trailing' : 'both', children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.PanelText, { children: name })] })); + } }, language.code2)); + }), _jsx(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Add more languages\u2026"], ["Add more languages\u2026"])))), onPress: contentLanguagePrefsControl.open, children: _jsxs(Toggle.Panel, { adjacent: "leading", children: [_jsx(Toggle.PanelIcon, { icon: PlusIcon }), _jsx(Toggle.PanelText, { children: "Add more languages\u2026" })] }) })] }) }) }), _jsx(LanguageSelectDialog, { control: contentLanguagePrefsControl, titleText: _jsx(Trans, { children: "Select content languages" }), subtitleText: _jsx(Trans, { children: "If none are selected, all languages will be shown in your feeds." }), currentLanguages: langPrefs.contentLanguages, onSelectLanguages: function (languages) { + setLangPrefs.setContentLanguages(languages); + setRecentLanguages(function (recent) { return __spreadArray([], new Set(__spreadArray(__spreadArray([], recent, true), languages, true)), true); }); + } })] })] })] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/Settings/LegacyNotificationSettings.js b/src/screens/Settings/LegacyNotificationSettings.js new file mode 100644 index 0000000000..6e4830b46f --- /dev/null +++ b/src/screens/Settings/LegacyNotificationSettings.js @@ -0,0 +1,9 @@ +import { useCallback } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; +export function LegacyNotificationSettingsScreen(_a) { + var navigation = _a.navigation; + useFocusEffect(useCallback(function () { + navigation.replace('NotificationSettings'); + }, [navigation])); + return null; +} diff --git a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.js b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.js new file mode 100644 index 0000000000..99adcde215 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.js @@ -0,0 +1,141 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useMemo } from 'react'; +import { Text as RNText, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { createSanitizedDisplayName } from '#/lib/moderation/create-sanitized-display-name'; +import { cleanError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useActivitySubscriptionsQuery } from '#/state/queries/activity-subscriptions'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { List } from '#/view/com/util/List'; +import { atoms as a, useTheme } from '#/alf'; +import { SubscribeProfileDialog } from '#/components/activity-notifications/SubscribeProfileDialog'; +import * as Admonition from '#/components/Admonition'; +import { Button, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon, BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon, } from '#/components/icons/BellRinging'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import { ListFooter } from '#/components/Lists'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import { Text } from '#/components/Typography'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function ActivityNotificationSettingsScreen(_a) { + var _this = this; + var t = useTheme(); + var _ = useLingui()._; + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + var moderationOpts = useModerationOpts(); + var _c = useActivitySubscriptionsQuery(), subscriptions = _c.data, isPending = _c.isPending, error = _c.error, isFetchingNextPage = _c.isFetchingNextPage, fetchNextPage = _c.fetchNextPage, hasNextPage = _c.hasNextPage; + var items = useMemo(function () { + if (!subscriptions) + return []; + return subscriptions === null || subscriptions === void 0 ? void 0 : subscriptions.pages.flatMap(function (page) { return page.subscriptions; }); + }, [subscriptions]); + var renderItem = useCallback(function (_a) { + var item = _a.item; + if (!moderationOpts) + return null; + return (_jsx(ActivitySubscriptionCard, { profile: item, moderationOpts: moderationOpts })); + }, [moderationOpts]); + var onEndReached = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (isFetchingNextPage || !hasNextPage || isError) + return [2 /*return*/]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchNextPage()]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + logger.error('Failed to load more likes', { message: err_1 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(List, { ListHeaderComponent: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: BellRingingIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Activity from others" }), subtitleText: _jsx(Trans, { children: "Get notified about posts and replies from accounts you choose." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition.Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "subscribedPost", preference: preferences === null || preferences === void 0 ? void 0 : preferences.subscribedPost }))] }), data: items, keyExtractor: keyExtractor, renderItem: renderItem, onEndReached: onEndReached, onEndReachedThreshold: 4, ListEmptyComponent: error ? null : (_jsx(View, { style: [a.px_xl, a.py_md], children: !isPending ? (_jsx(Admonition.Outer, { type: "tip", children: _jsxs(Admonition.Row, { children: [_jsx(Admonition.Icon, {}), _jsxs(Admonition.Content, { children: [_jsx(Admonition.Text, { children: _jsxs(Trans, { children: ["Enable notifications for an account by visiting their profile and pressing the", ' ', _jsx(RNText, { style: [ + a.font_semi_bold, + t.atoms.text_contrast_high, + ], children: "bell icon" }), ' ', _jsx(BellRingingFilledIcon, { size: "xs", style: t.atoms.text_contrast_high }), "."] }) }), _jsx(Admonition.Text, { children: _jsxs(Trans, { children: ["If you want to restrict who can receive notifications for your account's activity, you can change this in", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Privacy and Security settings"], ["Privacy and Security settings"])))), to: { screen: 'ActivityPrivacySettings' }, style: [a.font_semi_bold], children: "Settings \u2192 Privacy and Security" }), "."] }) })] })] }) })) : (_jsx(View, { style: [a.flex_1, a.align_center, a.pt_xl], children: _jsx(Loader, { size: "lg" }) })) })), ListFooterComponent: _jsx(ListFooter, { style: [items.length === 0 && a.border_transparent], isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage, hasNextPage: hasNextPage }), windowSize: 11 })] })); +} +function keyExtractor(item) { + return item.did; +} +function ActivitySubscriptionCard(_a) { + var _b; + var profileUnshadowed = _a.profile, moderationOpts = _a.moderationOpts; + var profile = useProfileShadow(profileUnshadowed); + var control = useDialogControl(); + var _ = useLingui()._; + var t = useTheme(); + var preview = useMemo(function () { + var _a; + var actSub = (_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.activitySubscription; + if ((actSub === null || actSub === void 0 ? void 0 : actSub.post) && (actSub === null || actSub === void 0 ? void 0 : actSub.reply)) { + return _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Posts, Replies"], ["Posts, Replies"])))); + } + else if (actSub === null || actSub === void 0 ? void 0 : actSub.post) { + return _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Posts"], ["Posts"])))); + } + else if (actSub === null || actSub === void 0 ? void 0 : actSub.reply) { + return _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Replies"], ["Replies"])))); + } + return _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["None"], ["None"])))); + }, [_, (_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.activitySubscription]); + return (_jsxs(View, { style: [a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low], children: [_jsx(ProfileCard.Outer, { children: _jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts }), _jsxs(View, { style: [a.flex_1, a.gap_2xs], children: [_jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts, inline: true }), _jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_medium], children: preview })] }), _jsx(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Edit notifications from ", ""], ["Edit notifications from ", ""])), createSanitizedDisplayName(profile))), size: "small", color: "primary", variant: "solid", onPress: control.open, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Edit" }) }) })] }) }), _jsx(SubscribeProfileDialog, { control: control, profile: profile, moderationOpts: moderationOpts, includeProfile: true })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/Settings/NotificationSettings/LikeNotificationSettings.js b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.js new file mode 100644 index 0000000000..8abce56285 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Heart2_Stroke2_Corner0_Rounded as HeartIcon } from '#/components/icons/Heart2'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function LikeNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: HeartIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Likes" }), subtitleText: _jsx(Trans, { children: "Get notifications when people like your posts." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "like", preference: preferences === null || preferences === void 0 ? void 0 : preferences.like }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.js b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.js new file mode 100644 index 0000000000..ba418cc86f --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon } from '#/components/icons/Heart2'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function LikesOnRepostsNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: LikeRepostIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Likes of your reposts" }), subtitleText: _jsx(Trans, { children: "Get notifications when people like posts that you've reposted." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "likeViaRepost", preference: preferences === null || preferences === void 0 ? void 0 : preferences.likeViaRepost }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/MentionNotificationSettings.js b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.js new file mode 100644 index 0000000000..631841f863 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { At_Stroke2_Corner2_Rounded as AtIcon } from '#/components/icons/At'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function MentionNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: AtIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Mentions" }), subtitleText: _jsx(Trans, { children: "Get notifications when people mention you." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "mention", preference: preferences === null || preferences === void 0 ? void 0 : preferences.mention }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.js b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.js new file mode 100644 index 0000000000..f3f7a61079 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Shapes_Stroke2_Corner0_Rounded as ShapesIcon } from '#/components/icons/Shapes'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function MiscellaneousNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: ShapesIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Everything else" }), subtitleText: _jsx(Trans, { children: "Notifications for everything else, such as when someone joins via one of your starter packs." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "starterpackJoined", preference: preferences === null || preferences === void 0 ? void 0 : preferences.starterpackJoined, syncOthers: ['verified', 'unverified'], allowDisableInApp: false }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.js b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.js new file mode 100644 index 0000000000..e55b6a24ff --- /dev/null +++ b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon } from '#/components/icons/Person'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function NewFollowerNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: PersonPlusIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "New followers" }), subtitleText: _jsx(Trans, { children: "Get notifications when people follow you." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "follow", preference: preferences === null || preferences === void 0 ? void 0 : preferences.follow }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.js b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.js new file mode 100644 index 0000000000..5a1915191a --- /dev/null +++ b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon } from '#/components/icons/Quote'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function QuoteNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: CloseQuoteIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Quotes" }), subtitleText: _jsx(Trans, { children: "Get notifications when people quote your posts." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "quote", preference: preferences === null || preferences === void 0 ? void 0 : preferences.quote }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.js b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.js new file mode 100644 index 0000000000..404cba3f8e --- /dev/null +++ b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Bubble_Stroke2_Corner2_Rounded as BubbleIcon } from '#/components/icons/Bubble'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function ReplyNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: BubbleIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Replies" }), subtitleText: _jsx(Trans, { children: "Get notifications when people reply to your posts." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "reply", preference: preferences === null || preferences === void 0 ? void 0 : preferences.reply }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/RepostNotificationSettings.js b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.js new file mode 100644 index 0000000000..fdcbc1e525 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Repost_Stroke2_Corner2_Rounded as RepostIcon } from '#/components/icons/Repost'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function RepostNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: RepostIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Reposts" }), subtitleText: _jsx(Trans, { children: "Get notifications when people repost your posts." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "repost", preference: preferences === null || preferences === void 0 ? void 0 : preferences.repost }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.js b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.js new file mode 100644 index 0000000000..7deb2dd3ce --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon } from '#/components/icons/Repost'; +import * as Layout from '#/components/Layout'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +import { PreferenceControls } from './components/PreferenceControls'; +export function RepostsOnRepostsNotificationSettingsScreen(_a) { + var _b = useNotificationSettingsQuery(), preferences = _b.data, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { style: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: RepostRepostIcon }), _jsx(ItemTextWithSubtitle, { bold: true, titleText: _jsx(Trans, { children: "Reposts of your reposts" }), subtitleText: _jsx(Trans, { children: "Get notifications when people repost posts that you've reposted." }) })] }), isError ? (_jsx(View, { style: [a.px_lg, a.pt_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })) : (_jsx(PreferenceControls, { name: "repostViaRepost", preference: preferences === null || preferences === void 0 ? void 0 : preferences.repostViaRepost }))] }) })] })); +} diff --git a/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.js b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.js new file mode 100644 index 0000000000..55362b06bd --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.js @@ -0,0 +1,11 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +import * as Skele from '#/components/Skeleton'; +import { Text } from '#/components/Typography'; +import * as SettingsList from '../../components/SettingsList'; +export function ItemTextWithSubtitle(_a) { + var titleText = _a.titleText, subtitleText = _a.subtitleText, _b = _a.bold, bold = _b === void 0 ? false : _b, _c = _a.showSkeleton, showSkeleton = _c === void 0 ? false : _c; + var t = useTheme(); + return (_jsxs(View, { style: [a.flex_1, bold ? a.gap_xs : a.gap_2xs], children: [_jsx(SettingsList.ItemText, { style: bold && [a.font_semi_bold, a.text_lg], children: titleText }), showSkeleton ? (_jsx(Skele.Text, { style: [a.text_sm, { width: 120 }] })) : (_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug], children: subtitleText }))] })); +} diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.js b/src/screens/Settings/NotificationSettings/components/PreferenceControls.js new file mode 100644 index 0000000000..6e75942df7 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.js @@ -0,0 +1,90 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useMemo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationSettingsUpdateMutation } from '#/state/queries/notifications/settings'; +import { atoms as a, platform, useTheme } from '#/alf'; +import * as Toggle from '#/components/forms/Toggle'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { Divider } from '../../components/SettingsList'; +export function PreferenceControls(_a) { + var name = _a.name, syncOthers = _a.syncOthers, preference = _a.preference, _b = _a.allowDisableInApp, allowDisableInApp = _b === void 0 ? true : _b; + if (!preference) + return (_jsx(View, { style: [a.w_full, a.pt_5xl, a.align_center], children: _jsx(Loader, { size: "xl" }) })); + return (_jsx(Inner, { name: name, syncOthers: syncOthers, preference: preference, allowDisableInApp: allowDisableInApp })); +} +export function Inner(_a) { + var name = _a.name, _b = _a.syncOthers, syncOthers = _b === void 0 ? [] : _b, preference = _a.preference, allowDisableInApp = _a.allowDisableInApp; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var mutate = useNotificationSettingsUpdateMutation().mutate; + var channels = useMemo(function () { + var arr = []; + if (preference.list) + arr.push('list'); + if (preference.push) + arr.push('push'); + return arr; + }, [preference]); + var onChangeChannels = function (change) { + var _a; + var newPreference = __assign(__assign({}, preference), { list: change.includes('list'), push: change.includes('push') }); + ax.metric('activityPreference:changeChannels', { + name: name, + push: newPreference.push, + list: newPreference.list, + }); + mutate(__assign((_a = {}, _a[name] = newPreference, _a), Object.fromEntries(syncOthers.map(function (key) { return [key, newPreference]; })))); + }; + var onChangeFilter = function (_a) { + var _b; + var change = _a[0]; + if (change !== 'all' && change !== 'follows') + throw new Error('Invalid filter'); + var newPreference = __assign(__assign({}, preference), { include: change }); + ax.metric('activityPreference:changeFilter', { name: name, value: change }); + mutate(__assign((_b = {}, _b[name] = newPreference, _b), Object.fromEntries(syncOthers.map(function (key) { return [key, newPreference]; })))); + }; + return (_jsxs(View, { style: [a.px_xl, a.pt_md, a.gap_sm], children: [_jsx(Toggle.Group, { type: "checkbox", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select your preferred notification channels"], ["Select your preferred notification channels"])))), values: channels, onChange: onChangeChannels, children: _jsxs(View, { style: [a.gap_sm], children: [_jsxs(Toggle.Item, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Receive push notifications"], ["Receive push notifications"])))), name: "push", style: [ + a.py_xs, + platform({ + native: [a.justify_between], + web: [a.flex_row_reverse, a.gap_sm], + }), + ], children: [_jsx(Toggle.LabelText, { style: [t.atoms.text, a.font_normal, a.text_md, a.flex_1], children: _jsx(Trans, { children: "Push notifications" }) }), _jsx(Toggle.Platform, {})] }), allowDisableInApp && (_jsxs(Toggle.Item, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Receive in-app notifications"], ["Receive in-app notifications"])))), name: "list", style: [ + a.py_xs, + platform({ + native: [a.justify_between], + web: [a.flex_row_reverse, a.gap_sm], + }), + ], children: [_jsx(Toggle.LabelText, { style: [t.atoms.text, a.font_normal, a.text_md, a.flex_1], children: _jsx(Trans, { children: "In-app notifications" }) }), _jsx(Toggle.Platform, {})] }))] }) }), 'include' in preference && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(Text, { style: [a.font_semi_bold, a.text_md], children: _jsx(Trans, { children: "From" }) }), _jsx(Toggle.Group, { type: "radio", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Filter who you receive notifications from"], ["Filter who you receive notifications from"])))), values: [preference.include], onChange: onChangeFilter, disabled: channels.length === 0, children: _jsxs(View, { style: [a.gap_sm], children: [_jsxs(Toggle.Item, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Everyone"], ["Everyone"])))), name: "all", style: [a.flex_row, a.py_xs, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [ + channels.length > 0 && t.atoms.text, + a.font_normal, + a.text_md, + ], children: _jsx(Trans, { children: "Everyone" }) })] }), _jsxs(Toggle.Item, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["People I follow"], ["People I follow"])))), name: "follows", style: [a.flex_row, a.py_xs, a.gap_sm], children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { style: [ + channels.length > 0 && t.atoms.text, + a.font_normal, + a.text_md, + ], children: _jsx(Trans, { children: "People I follow" }) })] })] }) })] }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/Settings/NotificationSettings/index.js b/src/screens/Settings/NotificationSettings/index.js new file mode 100644 index 0000000000..ccc64d741e --- /dev/null +++ b/src/screens/Settings/NotificationSettings/index.js @@ -0,0 +1,183 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useEffect } from 'react'; +import { Linking, View } from 'react-native'; +import * as Notification from 'expo-notifications'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useAppState } from '#/lib/appState'; +import { useNotificationSettingsQuery } from '#/state/queries/notifications/settings'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { At_Stroke2_Corner2_Rounded as AtIcon } from '#/components/icons/At'; +import { BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon } from '#/components/icons/BellRinging'; +import { Bubble_Stroke2_Corner2_Rounded as BubbleIcon } from '#/components/icons/Bubble'; +import { Haptic_Stroke2_Corner2_Rounded as HapticIcon } from '#/components/icons/Haptic'; +import { Heart2_Stroke2_Corner0_Rounded as HeartIcon, LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon, } from '#/components/icons/Heart2'; +import { PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon } from '#/components/icons/Person'; +import { CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon } from '#/components/icons/Quote'; +import { Repost_Stroke2_Corner2_Rounded as RepostIcon, RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon, } from '#/components/icons/Repost'; +import { Shapes_Stroke2_Corner0_Rounded as ShapesIcon } from '#/components/icons/Shapes'; +import * as Layout from '#/components/Layout'; +import { IS_ANDROID, IS_IOS, IS_WEB } from '#/env'; +import * as SettingsList from '../components/SettingsList'; +import { ItemTextWithSubtitle } from './components/ItemTextWithSubtitle'; +var RQKEY = ['notification-permissions']; +export function NotificationSettingsScreen(_a) { + var _this = this; + var _ = useLingui()._; + var queryClient = useQueryClient(); + var _b = useNotificationSettingsQuery(), settings = _b.data, isError = _b.isError; + var _c = useQuery({ + queryKey: RQKEY, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (IS_WEB) + return [2 /*return*/, null]; + return [4 /*yield*/, Notification.getPermissionsAsync()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }), permissions = _c.data, refetch = _c.refetch; + var appState = useAppState(); + useEffect(function () { + if (appState === 'active') { + refetch(); + } + }, [appState, refetch]); + var onRequestPermissions = function () { return __awaiter(_this, void 0, void 0, function () { + var response, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (IS_WEB) + return [2 /*return*/]; + if (!(permissions === null || permissions === void 0 ? void 0 : permissions.canAskAgain)) return [3 /*break*/, 2]; + return [4 /*yield*/, Notification.requestPermissionsAsync()]; + case 1: + response = _b.sent(); + queryClient.setQueryData(RQKEY, response); + return [3 /*break*/, 8]; + case 2: + if (!IS_ANDROID) return [3 /*break*/, 7]; + _b.label = 3; + case 3: + _b.trys.push([3, 5, , 6]); + return [4 /*yield*/, Linking.sendIntent('android.settings.APP_NOTIFICATION_SETTINGS', [ + { + key: 'android.provider.extra.APP_PACKAGE', + value: 'xyz.blueskyweb.app', + }, + ])]; + case 4: + _b.sent(); + return [3 /*break*/, 6]; + case 5: + _a = _b.sent(); + Linking.openSettings(); + return [3 /*break*/, 6]; + case 6: return [3 /*break*/, 8]; + case 7: + if (IS_IOS) { + Linking.openSettings(); + } + _b.label = 8; + case 8: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Notifications" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [permissions && !permissions.granted && (_jsxs(_Fragment, { children: [_jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Enable push notifications"], ["Enable push notifications"])))), onPress: onRequestPermissions, children: [_jsx(SettingsList.ItemIcon, { icon: HapticIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Enable push notifications" }) })] }), _jsx(SettingsList.Divider, {})] })), isError && (_jsx(View, { style: [a.px_lg, a.pb_md], children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to load notification settings." }) }) })), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Settings for like notifications"], ["Settings for like notifications"])))), to: { screen: 'LikeNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: HeartIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Likes" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.like }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Settings for new follower notifications"], ["Settings for new follower notifications"])))), to: { screen: 'NewFollowerNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: PersonPlusIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "New followers" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.follow }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Settings for reply notifications"], ["Settings for reply notifications"])))), to: { screen: 'ReplyNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: BubbleIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Replies" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.reply }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Settings for mention notifications"], ["Settings for mention notifications"])))), to: { screen: 'MentionNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: AtIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Mentions" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.mention }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Settings for quote notifications"], ["Settings for quote notifications"])))), to: { screen: 'QuoteNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: CloseQuoteIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Quotes" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.quote }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Settings for repost notifications"], ["Settings for repost notifications"])))), to: { screen: 'RepostNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: RepostIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Reposts" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.repost }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Settings for activity from others"], ["Settings for activity from others"])))), to: { screen: 'ActivityNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: BellRingingIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Activity from others" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.subscribedPost }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Settings for notifications for likes of your reposts"], ["Settings for notifications for likes of your reposts"])))), to: { screen: 'LikesOnRepostsNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: LikeRepostIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Likes of your reposts" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.likeViaRepost }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Settings for notifications for reposts of your reposts"], ["Settings for notifications for reposts of your reposts"])))), to: { screen: 'RepostsOnRepostsNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: RepostRepostIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Reposts of your reposts" }), subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.repostViaRepost }), showSkeleton: !settings })] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Settings for notifications for everything else"], ["Settings for notifications for everything else"])))), to: { screen: 'MiscellaneousNotificationSettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: ShapesIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Everything else" }), + // technically a bundle of several settings, but since they're set together + // and are most likely in sync we'll just show the state of one of them + subtitleText: _jsx(SettingPreview, { preference: settings === null || settings === void 0 ? void 0 : settings.starterpackJoined }), showSkeleton: !settings })] })] })] }) })] })); +} +function SettingPreview(_a) { + var preference = _a.preference; + var _ = useLingui()._; + if (!preference) { + return null; + } + else { + if ('include' in preference) { + if (preference.include === 'all') { + if (preference.list && preference.push) { + return _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["In-app, Push, Everyone"], ["In-app, Push, Everyone"])))); + } + else if (preference.list) { + return _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["In-app, Everyone"], ["In-app, Everyone"])))); + } + else if (preference.push) { + return _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Push, Everyone"], ["Push, Everyone"])))); + } + } + else if (preference.include === 'follows') { + if (preference.list && preference.push) { + return _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["In-app, Push, People you follow"], ["In-app, Push, People you follow"])))); + } + else if (preference.list) { + return _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["In-app, People you follow"], ["In-app, People you follow"])))); + } + else if (preference.push) { + return _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Push, People you follow"], ["Push, People you follow"])))); + } + } + } + else { + if (preference.list && preference.push) { + return _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["In-app, Push"], ["In-app, Push"])))); + } + else if (preference.list) { + return _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["In-app"], ["In-app"])))); + } + else if (preference.push) { + return _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Push"], ["Push"])))); + } + } + } + return _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Off"], ["Off"])))); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21; diff --git a/src/screens/Settings/PrivacyAndSecuritySettings.js b/src/screens/Settings/PrivacyAndSecuritySettings.js new file mode 100644 index 0000000000..80d193dfdd --- /dev/null +++ b/src/screens/Settings/PrivacyAndSecuritySettings.js @@ -0,0 +1,50 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNotificationDeclarationQuery } from '#/state/queries/activity-subscriptions'; +import { useAppPasswordsQuery } from '#/state/queries/app-passwords'; +import { useSession } from '#/state/session'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import { atoms as a, useTheme } from '#/alf'; +import * as Admonition from '#/components/Admonition'; +import { BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon } from '#/components/icons/BellRinging'; +import { EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon } from '#/components/icons/EyeSlash'; +import { Key_Stroke2_Corner2_Rounded as KeyIcon } from '#/components/icons/Key'; +import { ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon } from '#/components/icons/Shield'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import { Email2FAToggle } from './components/Email2FAToggle'; +import { PwiOptOut } from './components/PwiOptOut'; +import { ItemTextWithSubtitle } from './NotificationSettings/components/ItemTextWithSubtitle'; +export function PrivacyAndSecuritySettingsScreen(_a) { + var _ = useLingui()._; + var t = useTheme(); + var appPasswords = useAppPasswordsQuery().data; + var currentAccount = useSession().currentAccount; + var _b = useNotificationDeclarationQuery(), notificationDeclaration = _b.data, isPending = _b.isPending, isError = _b.isError; + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Privacy and Security" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: ShieldIcon, color: (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.emailAuthFactor) + ? t.palette.primary_500 + : undefined }), _jsx(SettingsList.ItemText, { children: (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.emailAuthFactor) ? (_jsx(Trans, { children: "Email 2FA enabled" })) : (_jsx(Trans, { children: "Two-factor authentication (2FA)" })) }), _jsx(Email2FAToggle, {})] }), _jsxs(SettingsList.LinkItem, { to: "/settings/app-passwords", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["App passwords"], ["App passwords"])))), children: [_jsx(SettingsList.ItemIcon, { icon: KeyIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "App passwords" }) }), appPasswords && appPasswords.length > 0 && (_jsx(SettingsList.BadgeText, { children: appPasswords.length }))] }), _jsxs(SettingsList.LinkItem, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Settings for allowing others to be notified of your posts"], ["Settings for allowing others to be notified of your posts"])))), to: { screen: 'ActivityPrivacySettings' }, contentContainerStyle: [a.align_start], children: [_jsx(SettingsList.ItemIcon, { icon: BellRingingIcon }), _jsx(ItemTextWithSubtitle, { titleText: _jsx(Trans, { children: "Allow others to be notified of your posts" }), subtitleText: _jsx(NotificationDeclaration, { data: notificationDeclaration, isError: isError }), showSkeleton: isPending })] }), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.Group, { children: [_jsx(SettingsList.ItemIcon, { icon: EyeSlashIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Logged-out visibility" }) }), _jsx(PwiOptOut, {})] }), _jsx(SettingsList.Item, { children: _jsx(Admonition.Outer, { type: "tip", style: [a.flex_1], children: _jsxs(Admonition.Row, { children: [_jsx(Admonition.Icon, {}), _jsxs(Admonition.Content, { children: [_jsx(Admonition.Text, { children: _jsx(Trans, { children: "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." }) }), _jsx(Admonition.Text, { children: _jsx(InlineLinkText, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Learn more about what is public on Bluesky."], ["Learn more about what is public on Bluesky."])))), to: "https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy", children: _jsx(Trans, { children: "Learn more about what is public on Bluesky." }) }) })] })] }) }) })] }) })] })); +} +function NotificationDeclaration(_a) { + var _b; + var data = _a.data, isError = _a.isError; + if (isError) { + return _jsx(Trans, { children: "Error loading preference" }); + } + switch ((_b = data === null || data === void 0 ? void 0 : data.value) === null || _b === void 0 ? void 0 : _b.allowSubscriptions) { + case 'mutuals': + return _jsx(Trans, { children: "Only followers who I follow" }); + case 'none': + return _jsx(Trans, { context: "enable for", children: "No one" }); + case 'followers': + default: + return _jsx(Trans, { children: "Anyone who follows me" }); + } +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Settings/Settings.js b/src/screens/Settings/Settings.js new file mode 100644 index 0000000000..449bda5baa --- /dev/null +++ b/src/screens/Settings/Settings.js @@ -0,0 +1,298 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { Alert, LayoutAnimation, Linking, Pressable, View } from 'react-native'; +import { useReducedMotion } from 'react-native-reanimated'; +import { moderateProfile } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { HELP_DESK_URL } from '#/lib/constants'; +import { useAccountSwitcher } from '#/lib/hooks/useAccountSwitcher'; +import { useApplyPullRequestOTAUpdate } from '#/lib/hooks/useOTAUpdates'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import * as persisted from '#/state/persisted'; +import { clearStorage } from '#/state/persisted'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useDeleteActorDeclaration } from '#/state/queries/messages/actor-declaration'; +import { useProfileQuery, useProfilesQuery } from '#/state/queries/profile'; +import { useAgent } from '#/state/session'; +import { useSession, useSessionApi } from '#/state/session'; +import { useOnboardingDispatch } from '#/state/shell'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { useCloseAllActiveElements } from '#/state/util'; +import * as Toast from '#/view/com/util/Toast'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import * as SettingsList from '#/screens/Settings/components/SettingsList'; +import { atoms as a, platform, tokens, useBreakpoints, useTheme } from '#/alf'; +import { AgeAssuranceDismissibleNotice } from '#/components/ageAssurance/AgeAssuranceDismissibleNotice'; +import { AvatarStackWithFetch } from '#/components/AvatarStack'; +import { Button, ButtonText } from '#/components/Button'; +import { useIsFindContactsFeatureEnabledBasedOnGeolocation } from '#/components/contacts/country-allowlist'; +import { useDialogControl } from '#/components/Dialog'; +import { SwitchAccountDialog } from '#/components/dialogs/SwitchAccount'; +import { Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon } from '#/components/icons/Accessibility'; +import { Bell_Stroke2_Corner0_Rounded as NotificationIcon } from '#/components/icons/Bell'; +import { BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon } from '#/components/icons/BubbleInfo'; +import { ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon } from '#/components/icons/Chevron'; +import { CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon } from '#/components/icons/CircleQuestion'; +import { CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon } from '#/components/icons/CodeBrackets'; +import { Contacts_Stroke2_Corner2_Rounded as ContactsIcon } from '#/components/icons/Contacts'; +import { DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal } from '#/components/icons/DotGrid'; +import { Earth_Stroke2_Corner2_Rounded as EarthIcon } from '#/components/icons/Globe'; +import { Lock_Stroke2_Corner2_Rounded as LockIcon } from '#/components/icons/Lock'; +import { PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon } from '#/components/icons/PaintRoller'; +import { Person_Stroke2_Corner2_Rounded as PersonIcon, PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon, PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon, PersonX_Stroke2_Corner0_Rounded as PersonXIcon, } from '#/components/icons/Person'; +import { RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon } from '#/components/icons/RaisingHand'; +import { Window_Stroke2_Corner2_Rounded as WindowIcon } from '#/components/icons/Window'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import * as Menu from '#/components/Menu'; +import { ID as PolicyUpdate202508 } from '#/components/PolicyUpdateOverlay/updates/202508/config'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { useFullVerificationState } from '#/components/verification'; +import { shouldShowVerificationCheckButton, VerificationCheckButton, } from '#/components/verification/VerificationCheckButton'; +import { useAnalytics } from '#/analytics'; +import { IS_INTERNAL, IS_IOS, IS_NATIVE } from '#/env'; +import { useActorStatus } from '#/features/liveNow'; +import { device, useStorage } from '#/storage'; +import { useActivitySubscriptionsNudged } from '#/storage/hooks/activity-subscriptions-nudged'; +export function SettingsScreen(_a) { + var ax = useAnalytics(); + var _ = useLingui()._; + var reducedMotion = useReducedMotion(); + var logoutEveryAccount = useSessionApi().logoutEveryAccount; + var _b = useSession(), accounts = _b.accounts, currentAccount = _b.currentAccount; + var switchAccountControl = useDialogControl(); + var signOutPromptControl = Prompt.usePromptControl(); + var profile = useProfileQuery({ did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }).data; + var otherProfiles = useProfilesQuery({ + handles: accounts + .filter(function (acc) { return acc.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }) + .map(function (acc) { return acc.handle; }), + }).data; + var _c = useAccountSwitcher(), pendingDid = _c.pendingDid, onPressSwitchAccount = _c.onPressSwitchAccount; + var _d = useState(false), showAccounts = _d[0], setShowAccounts = _d[1]; + var _e = useState(false), showDevOptions = _e[0], setShowDevOptions = _e[1]; + var findContactsEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation(); + return (_jsxs(Layout.Screen, { children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Settings" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsx(AgeAssuranceDismissibleNotice, { style: [a.px_lg, a.pt_xs, a.pb_xl] }), _jsx(View, { style: [ + a.px_xl, + a.pt_md, + a.pb_md, + a.w_full, + a.gap_2xs, + a.align_center, + { minHeight: 160 }, + ], children: profile && _jsx(ProfilePreview, { profile: profile }) }), accounts.length > 1 ? (_jsxs(_Fragment, { children: [_jsxs(SettingsList.PressableItem, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Switch account"], ["Switch account"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Shows other accounts you can switch to"], ["Shows other accounts you can switch to"])))), onPress: function () { + if (!reducedMotion) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + } + setShowAccounts(function (s) { return !s; }); + }, children: [_jsx(SettingsList.ItemIcon, { icon: PersonGroupIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Switch account" }) }), showAccounts ? (_jsx(SettingsList.ItemIcon, { icon: ChevronUpIcon, size: "md" })) : (_jsx(AvatarStackWithFetch, { profiles: accounts + .map(function (acc) { return acc.did; }) + .filter(function (did) { return did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }) + .slice(0, 5) }))] }), showAccounts && (_jsxs(_Fragment, { children: [_jsx(SettingsList.Divider, {}), accounts + .filter(function (acc) { return acc.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }) + .map(function (account) { + var _a; + return (_jsx(AccountRow, { account: account, profile: (_a = otherProfiles === null || otherProfiles === void 0 ? void 0 : otherProfiles.profiles) === null || _a === void 0 ? void 0 : _a.find(function (p) { return p.did === account.did; }), pendingDid: pendingDid, onPressSwitchAccount: onPressSwitchAccount }, account.did)); + }), _jsx(AddAccountRow, {})] }))] })) : (_jsx(AddAccountRow, {})), _jsx(SettingsList.Divider, {}), _jsxs(SettingsList.LinkItem, { to: "/settings/account", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Account"], ["Account"])))), children: [_jsx(SettingsList.ItemIcon, { icon: PersonIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Account" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/privacy-and-security", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Privacy and security"], ["Privacy and security"])))), children: [_jsx(SettingsList.ItemIcon, { icon: LockIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Privacy and security" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/moderation", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Moderation"], ["Moderation"])))), children: [_jsx(SettingsList.ItemIcon, { icon: HandIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Moderation" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/notifications", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Notifications"], ["Notifications"])))), children: [_jsx(SettingsList.ItemIcon, { icon: NotificationIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Notifications" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/content-and-media", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Content and media"], ["Content and media"])))), children: [_jsx(SettingsList.ItemIcon, { icon: WindowIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Content and media" }) })] }), IS_NATIVE && + findContactsEnabled && + !ax.features.enabled(ax.features.ImportContactsSettingsDisable) && (_jsxs(SettingsList.LinkItem, { to: "/settings/find-contacts", label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Find friends from contacts"], ["Find friends from contacts"])))), children: [_jsx(SettingsList.ItemIcon, { icon: ContactsIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Find friends from contacts" }) })] })), _jsxs(SettingsList.LinkItem, { to: "/settings/appearance", label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Appearance"], ["Appearance"])))), children: [_jsx(SettingsList.ItemIcon, { icon: PaintRollerIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Appearance" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/accessibility", label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Accessibility"], ["Accessibility"])))), children: [_jsx(SettingsList.ItemIcon, { icon: AccessibilityIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Accessibility" }) })] }), _jsxs(SettingsList.LinkItem, { to: "/settings/language", label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Languages"], ["Languages"])))), children: [_jsx(SettingsList.ItemIcon, { icon: EarthIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Languages" }) })] }), _jsxs(SettingsList.PressableItem, { onPress: function () { return Linking.openURL(HELP_DESK_URL); }, label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Help"], ["Help"])))), accessibilityHint: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Opens helpdesk in browser"], ["Opens helpdesk in browser"])))), children: [_jsx(SettingsList.ItemIcon, { icon: CircleQuestionIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Help" }) }), _jsx(SettingsList.Chevron, {})] }), _jsxs(SettingsList.LinkItem, { to: "/settings/about", label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["About"], ["About"])))), children: [_jsx(SettingsList.ItemIcon, { icon: BubbleInfoIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "About" }) })] }), _jsx(SettingsList.Divider, {}), _jsx(SettingsList.PressableItem, { destructive: true, onPress: function () { return signOutPromptControl.open(); }, label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Sign out"], ["Sign out"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Sign out" }) }) }), IS_INTERNAL && (_jsxs(_Fragment, { children: [_jsx(SettingsList.Divider, {}), _jsxs(SettingsList.PressableItem, { onPress: function () { + if (!reducedMotion) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + } + setShowDevOptions(function (d) { return !d; }); + }, label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Developer options"], ["Developer options"])))), children: [_jsx(SettingsList.ItemIcon, { icon: CodeBracketsIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Developer options" }) })] }), showDevOptions && _jsx(DevOptions, {})] }))] }) }), _jsx(Prompt.Basic, { control: signOutPromptControl, title: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Sign out?"], ["Sign out?"])))), description: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["You will be signed out of all your accounts."], ["You will be signed out of all your accounts."])))), onConfirm: function () { return logoutEveryAccount('Settings'); }, confirmButtonCta: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Sign out"], ["Sign out"])))), cancelButtonCta: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Cancel"], ["Cancel"])))), confirmButtonColor: "negative" }), _jsx(SwitchAccountDialog, { control: switchAccountControl })] })); +} +function ProfilePreview(_a) { + var _b; + var profile = _a.profile; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var shadow = useProfileShadow(profile); + var moderationOpts = useModerationOpts(); + var verificationState = useFullVerificationState({ + profile: shadow, + }); + var live = useActorStatus(profile).isActive; + if (!moderationOpts) + return null; + var moderation = moderateProfile(profile, moderationOpts); + var displayName = sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')); + return (_jsxs(_Fragment, { children: [_jsx(UserAvatar, { size: 80, avatar: shadow.avatar, moderation: moderation.ui('avatar'), type: ((_b = shadow.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', live: live }), _jsxs(View, { style: [ + a.flex_row, + a.gap_xs, + a.align_center, + a.justify_center, + a.w_full, + ], children: [_jsx(Text, { emoji: true, testID: "profileHeaderDisplayName", numberOfLines: 1, style: [ + a.pt_sm, + t.atoms.text, + gtMobile ? a.text_4xl : a.text_3xl, + a.font_bold, + ], children: displayName }), shouldShowVerificationCheckButton(verificationState) && (_jsx(View, { style: [ + { + marginTop: platform({ web: 8, ios: 8, android: 10 }), + }, + ], children: _jsx(VerificationCheckButton, { profile: shadow, size: "lg" }) }))] }), _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium], children: sanitizeHandle(profile.handle, '@') })] })); +} +function DevOptions() { + var _this = this; + var _ = useLingui()._; + var agent = useAgent(); + var _a = useStorage(device, [ + 'policyUpdateDebugOverride', + ]), override = _a[0], setOverride = _a[1]; + var onboardingDispatch = useOnboardingDispatch(); + var navigation = useNavigation(); + var deleteChatDeclarationRecord = useDeleteActorDeclaration().mutate; + var _b = useApplyPullRequestOTAUpdate(), tryApplyUpdate = _b.tryApplyUpdate, revertToEmbedded = _b.revertToEmbedded, isCurrentlyRunningPullRequestDeployment = _b.isCurrentlyRunningPullRequestDeployment, currentChannel = _b.currentChannel; + var _c = useActivitySubscriptionsNudged(), actyNotifNudged = _c[0], setActyNotifNudged = _c[1]; + var resetOnboarding = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + navigation.navigate('Home'); + onboardingDispatch({ type: 'start' }); + Toast.show(_(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Onboarding reset"], ["Onboarding reset"]))))); + return [2 /*return*/]; + }); + }); }; + var clearAllStorage = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, clearStorage()]; + case 1: + _a.sent(); + Toast.show(_(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Storage cleared, you need to restart the app now."], ["Storage cleared, you need to restart the app now."]))))); + return [2 /*return*/]; + } + }); + }); }; + var onPressUnsnoozeReminder = function () { + var lastEmailConfirm = new Date(); + // wind back 3 days + lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3); + persisted.write('reminders', __assign(__assign({}, persisted.get('reminders')), { lastEmailConfirm: lastEmailConfirm.toISOString() })); + Toast.show(_(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["You probably want to restart the app now."], ["You probably want to restart the app now."]))))); + }; + var onPressActySubsUnNudge = function () { + setActyNotifNudged(false); + }; + var onPressApplyOta = function () { + Alert.prompt('Apply OTA', 'Enter the channel for the OTA you wish to apply.', [ + { + style: 'cancel', + text: 'Cancel', + }, + { + style: 'default', + text: 'Apply', + onPress: function (channel) { + tryApplyUpdate(channel !== null && channel !== void 0 ? channel : ''); + }, + }, + ], 'plain-text', isCurrentlyRunningPullRequestDeployment + ? currentChannel + : 'pull-request-'); + }; + return (_jsxs(_Fragment, { children: [_jsx(SettingsList.PressableItem, { onPress: function () { return navigation.navigate('Log'); }, label: _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Open system log"], ["Open system log"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "System log" }) }) }), _jsx(SettingsList.PressableItem, { onPress: function () { return navigation.navigate('Debug'); }, label: _(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Open storybook page"], ["Open storybook page"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Storybook" }) }) }), _jsx(SettingsList.PressableItem, { onPress: function () { return navigation.navigate('DebugMod'); }, label: _(msg(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Open moderation debug page"], ["Open moderation debug page"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Debug Moderation" }) }) }), _jsx(SettingsList.PressableItem, { onPress: function () { return deleteChatDeclarationRecord(); }, label: _(msg(templateObject_27 || (templateObject_27 = __makeTemplateObject(["Open storybook page"], ["Open storybook page"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Delete chat declaration record" }) }) }), _jsx(SettingsList.PressableItem, { onPress: function () { return resetOnboarding(); }, label: _(msg(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Reset onboarding state"], ["Reset onboarding state"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Reset onboarding state" }) }) }), _jsx(SettingsList.PressableItem, { onPress: onPressUnsnoozeReminder, label: _(msg(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Unsnooze email reminder"], ["Unsnooze email reminder"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Unsnooze email reminder" }) }) }), actyNotifNudged && (_jsx(SettingsList.PressableItem, { onPress: onPressActySubsUnNudge, label: _(msg(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Reset activity subscription nudge"], ["Reset activity subscription nudge"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Reset activity subscription nudge" }) }) })), _jsx(SettingsList.PressableItem, { onPress: function () { return clearAllStorage(); }, label: _(msg(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Clear all storage data"], ["Clear all storage data"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Clear all storage data (restart after this)" }) }) }), IS_IOS ? (_jsx(SettingsList.PressableItem, { onPress: onPressApplyOta, label: _(msg(templateObject_32 || (templateObject_32 = __makeTemplateObject(["Apply Pull Request"], ["Apply Pull Request"])))), children: _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Apply Pull Request" }) }) })) : null, IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? (_jsx(SettingsList.PressableItem, { onPress: revertToEmbedded, label: _(msg(templateObject_33 || (templateObject_33 = __makeTemplateObject(["Unapply Pull Request"], ["Unapply Pull Request"])))), children: _jsx(SettingsList.ItemText, { children: _jsxs(Trans, { children: ["Unapply Pull Request ", currentChannel] }) }) })) : null, _jsx(SettingsList.Divider, {}), _jsxs(View, { style: [a.p_xl, a.gap_md], children: [_jsx(Text, { style: [a.text_lg, a.font_semi_bold], children: "PolicyUpdate202508 Debug" }), _jsxs(View, { style: [a.flex_row, a.align_center, a.justify_between, a.gap_md], children: [_jsx(Button, { onPress: function () { + setOverride(!override); + }, label: "Toggle", color: override ? 'primary' : 'secondary', size: "small", style: [a.flex_1], children: _jsx(ButtonText, { children: override ? 'Disable debug mode' : 'Enable debug mode' }) }), _jsx(Button, { onPress: function () { + device.set([PolicyUpdate202508], false); + agent.bskyAppRemoveNuxs([PolicyUpdate202508]); + Toast.show("Done", 'info'); + }, label: "Reset policy update nux", color: "secondary", size: "small", disabled: !override, children: _jsx(ButtonText, { children: "Reset state" }) })] })] }), _jsx(SettingsList.Divider, {})] })); +} +function AddAccountRow() { + var _ = useLingui()._; + var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut; + var closeEverything = useCloseAllActiveElements(); + var onAddAnotherAccount = function () { + setShowLoggedOut(true); + closeEverything(); + }; + return (_jsxs(SettingsList.PressableItem, { onPress: onAddAnotherAccount, label: _(msg(templateObject_34 || (templateObject_34 = __makeTemplateObject(["Add another account"], ["Add another account"])))), children: [_jsx(SettingsList.ItemIcon, { icon: PersonPlusIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Add another account" }) })] })); +} +function AccountRow(_a) { + var _b; + var profile = _a.profile, account = _a.account, pendingDid = _a.pendingDid, onPressSwitchAccount = _a.onPressSwitchAccount; + var _ = useLingui()._; + var t = useTheme(); + var moderationOpts = useModerationOpts(); + var removePromptControl = Prompt.usePromptControl(); + var removeAccount = useSessionApi().removeAccount; + var live = useActorStatus(profile).isActive; + var onSwitchAccount = function () { + if (pendingDid) + return; + onPressSwitchAccount(account, 'Settings'); + }; + return (_jsxs(View, { style: [a.relative], children: [_jsxs(SettingsList.PressableItem, { onPress: onSwitchAccount, label: _(msg(templateObject_35 || (templateObject_35 = __makeTemplateObject(["Switch account"], ["Switch account"])))), children: [moderationOpts && profile ? (_jsx(UserAvatar, { size: 28, avatar: profile.avatar, moderation: moderateProfile(profile, moderationOpts).ui('avatar'), type: ((_b = profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', live: live, hideLiveBadge: true })) : (_jsx(View, { style: [{ width: 28 }] })), _jsx(SettingsList.ItemText, { numberOfLines: 1, style: [a.pr_2xl, a.leading_snug], children: sanitizeHandle(account.handle, '@') }), pendingDid === account.did && _jsx(SettingsList.ItemIcon, { icon: Loader })] }), !pendingDid && (_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_36 || (templateObject_36 = __makeTemplateObject(["Account options"], ["Account options"])))), children: function (_a) { + var props = _a.props, state = _a.state; + return (_jsx(Pressable, __assign({}, props, { style: [ + a.absolute, + { top: 10, right: tokens.space.lg }, + a.p_xs, + a.rounded_full, + (state.hovered || state.pressed) && t.atoms.bg_contrast_25, + ], children: _jsx(DotsHorizontal, { size: "md", style: t.atoms.text }) }))); + } }), _jsx(Menu.Outer, { showCancel: true, children: _jsxs(Menu.Item, { label: _(msg(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Remove account"], ["Remove account"])))), onPress: function () { return removePromptControl.open(); }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Remove account" }) }), _jsx(Menu.ItemIcon, { icon: PersonXIcon })] }) })] })), _jsx(Prompt.Basic, { control: removePromptControl, title: _(msg(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Remove from quick access?"], ["Remove from quick access?"])))), description: _(msg(templateObject_39 || (templateObject_39 = __makeTemplateObject(["This will remove @", " from the quick access list."], ["This will remove @", " from the quick access list."])), account.handle)), onConfirm: function () { + removeAccount(account); + Toast.show(_(msg(templateObject_40 || (templateObject_40 = __makeTemplateObject(["Account removed from quick access"], ["Account removed from quick access"]))))); + }, confirmButtonCta: _(msg(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Remove"], ["Remove"])))), confirmButtonColor: "negative" })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26, templateObject_27, templateObject_28, templateObject_29, templateObject_30, templateObject_31, templateObject_32, templateObject_33, templateObject_34, templateObject_35, templateObject_36, templateObject_37, templateObject_38, templateObject_39, templateObject_40, templateObject_41; diff --git a/src/screens/Settings/ThreadPreferences.js b/src/screens/Settings/ThreadPreferences.js new file mode 100644 index 0000000000..0c6a7eaef4 --- /dev/null +++ b/src/screens/Settings/ThreadPreferences.js @@ -0,0 +1,26 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { normalizeSort, normalizeView, useThreadPreferences, } from '#/state/queries/preferences/useThreadPreferences'; +import { atoms as a, useTheme } from '#/alf'; +import * as Toggle from '#/components/forms/Toggle'; +import { Bubbles_Stroke2_Corner2_Rounded as BubblesIcon } from '#/components/icons/Bubble'; +import { Tree_Stroke2_Corner0_Rounded as TreeIcon } from '#/components/icons/Tree'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +import * as SettingsList from './components/SettingsList'; +export function ThreadPreferencesScreen(_a) { + var t = useTheme(); + var _ = useLingui()._; + var _b = useThreadPreferences({ save: true }), sort = _b.sort, setSort = _b.setSort, view = _b.view, setView = _b.setView; + return (_jsxs(Layout.Screen, { testID: "threadPreferencesScreen", children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: _jsx(Trans, { children: "Thread Preferences" }) }) }), _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(SettingsList.Container, { children: [_jsxs(SettingsList.Group, { children: [_jsx(SettingsList.ItemIcon, { icon: BubblesIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Sort replies" }) }), _jsxs(View, { style: [a.w_full, a.gap_md], children: [_jsx(Text, { style: [a.flex_1, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Sort replies to the same post by:" }) }), _jsx(Toggle.Group, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sort replies by"], ["Sort replies by"])))), type: "radio", values: sort ? [sort] : [], onChange: function (values) { return setSort(normalizeSort(values[0])); }, children: _jsxs(View, { style: [a.gap_sm, a.flex_1], children: [_jsxs(Toggle.Item, { name: "top", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Top replies first"], ["Top replies first"])))), children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { children: _jsx(Trans, { children: "Top replies first" }) })] }), _jsxs(Toggle.Item, { name: "oldest", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Oldest replies first"], ["Oldest replies first"])))), children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { children: _jsx(Trans, { children: "Oldest replies first" }) })] }), _jsxs(Toggle.Item, { name: "newest", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Newest replies first"], ["Newest replies first"])))), children: [_jsx(Toggle.Radio, {}), _jsx(Toggle.LabelText, { children: _jsx(Trans, { children: "Newest replies first" }) })] })] }) })] })] }), _jsxs(SettingsList.Group, { children: [_jsx(SettingsList.ItemIcon, { icon: TreeIcon }), _jsx(SettingsList.ItemText, { children: _jsx(Trans, { children: "Tree view" }) }), _jsxs(Toggle.Item, { type: "checkbox", name: "threaded-mode", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Tree view"], ["Tree view"])))), value: view === 'tree', onChange: function (value) { + return setView(normalizeView({ treeViewEnabled: value })); + }, style: [a.w_full, a.gap_md], children: [_jsx(Toggle.LabelText, { style: [a.flex_1], children: _jsx(Trans, { children: "Show post replies in a threaded tree view" }) }), _jsx(Toggle.Platform, {})] })] })] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/screens/Settings/components/AddAppPasswordDialog.js b/src/screens/Settings/components/AddAppPasswordDialog.js new file mode 100644 index 0000000000..53f611de10 --- /dev/null +++ b/src/screens/Settings/components/AddAppPasswordDialog.js @@ -0,0 +1,174 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect, useMemo, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import Animated, { FadeIn, FadeOut, LayoutAnimationConfig, LinearTransition, SlideInRight, SlideOutLeft, } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation } from '@tanstack/react-query'; +import { useAppPasswordCreateMutation } from '#/state/queries/app-passwords'; +import { atoms as a, native, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextInput from '#/components/forms/TextField'; +import * as Toggle from '#/components/forms/Toggle'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRight } from '#/components/icons/Chevron'; +import { SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon } from '#/components/icons/SquareBehindSquare4'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +import { CopyButton } from './CopyButton'; +export function AddAppPasswordDialog(_a) { + var control = _a.control, passwords = _a.passwords; + var height = useWindowDimensions().height; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { minHeight: height }, children: [_jsx(Dialog.Handle, {}), _jsx(CreateDialogInner, { passwords: passwords })] })); +} +function CreateDialogInner(_a) { + var _this = this; + var passwords = _a.passwords; + var control = Dialog.useDialogContext(); + var t = useTheme(); + var _ = useLingui()._; + var autogeneratedName = useRandomName(); + var _b = useState(''), name = _b[0], setName = _b[1]; + var _c = useState(false), privileged = _c[0], setPrivileged = _c[1]; + var _d = useAppPasswordCreateMutation(), actuallyCreateAppPassword = _d.mutateAsync, apiError = _d.error, data = _d.data; + var regexFailError = useMemo(function () { + return new DisplayableError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["App password names can only contain letters, numbers, spaces, dashes, and underscores"], ["App password names can only contain letters, numbers, spaces, dashes, and underscores"]))))); + }, [_]); + var _e = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var chosenName; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + chosenName = name.trim() || autogeneratedName; + if (chosenName.length < 4) { + throw new DisplayableError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["App password names must be at least 4 characters long"], ["App password names must be at least 4 characters long"]))))); + } + if (passwords.find(function (p) { return p === chosenName; })) { + throw new DisplayableError(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["App password name must be unique"], ["App password name must be unique"]))))); + } + return [4 /*yield*/, actuallyCreateAppPassword({ name: chosenName, privileged: privileged })]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }), createAppPassword = _e.mutate, validationError = _e.error, isPending = _e.isPending; + var _f = useState(false), hasBeenCopied = _f[0], setHasBeenCopied = _f[1]; + useEffect(function () { + if (hasBeenCopied) { + var timeout_1 = setTimeout(function () { return setHasBeenCopied(false); }, 100); + return function () { return clearTimeout(timeout_1); }; + } + }, [hasBeenCopied]); + var error = validationError || (!name.match(/^[a-zA-Z0-9-_ ]*$/) && regexFailError); + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Add app password"], ["Add app password"])))), children: [_jsx(View, { style: [native(a.pt_md)], children: _jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: !data ? (_jsxs(Animated.View, { style: [a.gap_lg], exiting: native(SlideOutLeft), children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Add App Password" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "Please enter a unique name for this app password or use our randomly generated one." }) }), _jsx(View, { children: _jsx(TextInput.Root, { isInvalid: !!error, children: _jsx(Dialog.Input, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["App Password"], ["App Password"])))), placeholder: autogeneratedName, onChangeText: setName, returnKeyType: "done", onSubmitEditing: function () { return createAppPassword(); }, blurOnSubmit: true, autoCorrect: false, autoComplete: "off", autoCapitalize: "none", autoFocus: true }) }) }), error instanceof DisplayableError && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, children: _jsx(Admonition, { type: "error", children: error.message }) })), _jsxs(Animated.View, { style: [a.gap_lg], layout: native(LinearTransition), children: [_jsxs(Toggle.Item, { name: "privileged", type: "checkbox", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Allow access to your direct messages"], ["Allow access to your direct messages"])))), value: privileged, onChange: setPrivileged, style: [a.flex_1], children: [_jsx(Toggle.Checkbox, {}), _jsx(Toggle.LabelText, { style: [a.font_normal, a.text_md, a.leading_snug], children: _jsx(Trans, { children: "Allow access to your direct messages" }) })] }), _jsxs(Button, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Next"], ["Next"])))), size: "large", variant: "solid", color: "primary", style: [a.flex_1], onPress: function () { return createAppPassword(); }, disabled: isPending, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Next" }) }), _jsx(ButtonIcon, { icon: ChevronRight })] }), !!apiError || + (error && !(error instanceof DisplayableError) && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, children: _jsx(Admonition, { type: "error", children: _jsx(Trans, { children: "Failed to create app password. Please try again." }) }) })))] })] }, 0)) : (_jsxs(Animated.View, { style: [a.gap_lg], entering: IS_WEB ? FadeIn.delay(200) : SlideInRight, children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "Here is your app password!" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "Use this to sign in to the other app along with your handle." }) }), _jsxs(CopyButton, { value: data.password, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Copy App Password"], ["Copy App Password"])))), size: "large", color: "secondary", children: [_jsx(ButtonText, { children: data.password }), _jsx(ButtonIcon, { icon: CopyIcon })] }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "For security reasons, you won't be able to view this again. If you lose this app password, you'll need to generate a new one." }) }), _jsx(Button, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Done"], ["Done"])))), size: "large", variant: "outline", color: "primary", style: [a.flex_1], onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Done" }) }) })] }, 1)) }) }), _jsx(Dialog.Close, {})] })); +} +var DisplayableError = /** @class */ (function (_super) { + __extends(DisplayableError, _super); + function DisplayableError(message) { + var _this = _super.call(this, message) || this; + _this.name = 'DisplayableError'; + return _this; + } + return DisplayableError; +}(Error)); +function useRandomName() { + return useState(function () { return shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)]; })[0]; +} +var shadesOfBlue = [ + 'AliceBlue', + 'Aqua', + 'Aquamarine', + 'Azure', + 'BabyBlue', + 'Blue', + 'BlueViolet', + 'CadetBlue', + 'CornflowerBlue', + 'Cyan', + 'DarkBlue', + 'DarkCyan', + 'DarkSlateBlue', + 'DeepSkyBlue', + 'DodgerBlue', + 'ElectricBlue', + 'LightBlue', + 'LightCyan', + 'LightSkyBlue', + 'LightSteelBlue', + 'MediumAquaMarine', + 'MediumBlue', + 'MediumSlateBlue', + 'MidnightBlue', + 'Navy', + 'PowderBlue', + 'RoyalBlue', + 'SkyBlue', + 'SlateBlue', + 'SteelBlue', + 'Teal', + 'Turquoise', +]; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/screens/Settings/components/ChangeHandleDialog.js b/src/screens/Settings/components/ChangeHandleDialog.js new file mode 100644 index 0000000000..9f8e8b0939 --- /dev/null +++ b/src/screens/Settings/components/ChangeHandleDialog.js @@ -0,0 +1,274 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useMemo, useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import Animated, { FadeIn, FadeOut, LayoutAnimationConfig, LinearTransition, SlideInLeft, SlideInRight, SlideOutLeft, SlideOutRight, } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { HITSLOP_10, urls } from '#/lib/constants'; +import { cleanError } from '#/lib/strings/errors'; +import { createFullHandle, sanitizeHandle, validateServiceHandle, } from '#/lib/strings/handles'; +import { useFetchDid, useUpdateHandleMutation } from '#/state/queries/handle'; +import { RQKEY as RQKEY_PROFILE } from '#/state/queries/profile'; +import { useServiceQuery } from '#/state/queries/service'; +import { useCurrentAccountProfile } from '#/state/queries/useCurrentAccountProfile'; +import { useAgent, useSession } from '#/state/session'; +import { ErrorScreen } from '#/view/com/util/error/ErrorScreen'; +import { atoms as a, native, useBreakpoints, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as SegmentedControl from '#/components/forms/SegmentedControl'; +import * as TextField from '#/components/forms/TextField'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon, ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon, } from '#/components/icons/Arrow'; +import { At_Stroke2_Corner0_Rounded as AtIcon } from '#/components/icons/At'; +import { CheckThick_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon } from '#/components/icons/SquareBehindSquare4'; +import { InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { CopyButton } from './CopyButton'; +export function ChangeHandleDialog(_a) { + var control = _a.control; + var height = useWindowDimensions().height; + return (_jsx(Dialog.Outer, { control: control, nativeOptions: { minHeight: height }, children: _jsx(ChangeHandleDialogInner, {}) })); +} +function ChangeHandleDialogInner() { + var control = Dialog.useDialogContext(); + var _ = useLingui()._; + var agent = useAgent(); + var _a = useServiceQuery(agent.serviceUrl.toString()), serviceInfo = _a.data, serviceInfoError = _a.error, refetch = _a.refetch; + var _b = useState('provided-handle'), page = _b[0], setPage = _b[1]; + var cancelButton = useCallback(function () { return (_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: function () { return control.close(); }, size: "small", color: "primary", variant: "ghost", style: [a.rounded_full], children: _jsx(ButtonText, { style: [a.text_md], children: _jsx(Trans, { children: "Cancel" }) }) })); }, [control, _]); + return (_jsx(Dialog.ScrollableInner, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Change Handle"], ["Change Handle"])))), header: _jsx(Dialog.Header, { renderLeft: cancelButton, children: _jsx(Dialog.HeaderText, { children: _jsx(Trans, { children: "Change Handle" }) }) }), contentContainerStyle: [a.pt_0, a.px_0], children: _jsx(View, { style: [a.flex_1, a.pt_lg, a.px_xl], children: serviceInfoError ? (_jsx(ErrorScreen, { title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Oops!"], ["Oops!"])))), message: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["There was an issue fetching your service info"], ["There was an issue fetching your service info"])))), details: cleanError(serviceInfoError), onPressTryAgain: refetch })) : serviceInfo ? (_jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: page === 'provided-handle' ? (_jsx(Animated.View, { entering: native(SlideInLeft), exiting: native(SlideOutLeft), children: _jsx(ProvidedHandlePage, { serviceInfo: serviceInfo, goToOwnHandle: function () { return setPage('own-handle'); } }) }, page)) : (_jsx(Animated.View, { entering: native(SlideInRight), exiting: native(SlideOutRight), children: _jsx(OwnHandlePage, { goToServiceHandle: function () { return setPage('provided-handle'); } }) }, page)) })) : (_jsx(View, { style: [a.flex_1, a.justify_center, a.align_center, a.py_4xl], children: _jsx(Loader, { size: "xl" }) })) }) })); +} +function ProvidedHandlePage(_a) { + var serviceInfo = _a.serviceInfo, goToOwnHandle = _a.goToOwnHandle; + var _ = useLingui()._; + var _b = useState(''), subdomain = _b[0], setSubdomain = _b[1]; + var agent = useAgent(); + var control = Dialog.useDialogContext(); + var currentAccount = useSession().currentAccount; + var queryClient = useQueryClient(); + var profile = useCurrentAccountProfile(); + var verification = useSimpleVerificationState({ + profile: profile, + }); + var _c = useUpdateHandleMutation({ + onSuccess: function () { + if (currentAccount) { + queryClient.invalidateQueries({ + queryKey: RQKEY_PROFILE(currentAccount.did), + }); + } + agent.resumeSession(agent.session).then(function () { return control.close(); }); + }, + }), changeHandle = _c.mutate, isPending = _c.isPending, error = _c.error, isSuccess = _c.isSuccess; + var host = serviceInfo.availableUserDomains[0]; + var validation = useMemo(function () { return validateServiceHandle(subdomain, host); }, [subdomain, host]); + var isInvalid = !validation.handleChars || + !validation.hyphenStartOrEnd || + !validation.totalLength; + return (_jsx(LayoutAnimationConfig, { skipEntering: true, children: _jsxs(View, { style: [a.flex_1, a.gap_md], children: [isSuccess && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, children: _jsx(SuccessMessage, { text: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Handle changed!"], ["Handle changed!"])))) }) })), error && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, children: _jsx(ChangeHandleError, { error: error }) })), _jsxs(Animated.View, { layout: native(LinearTransition), style: [a.flex_1, a.gap_md], children: [verification.isVerified && verification.role === 'default' && (_jsx(Admonition, { type: "error", children: _jsxs(Trans, { children: ["You are verified. You will lose your verification status if you change your handle.", ' ', _jsx(InlineLinkText, { label: _(msg({ + message: "Learn more", + context: "english-only-resource", + })), to: urls.website.blog.initialVerificationAnnouncement, children: _jsx(Trans, { context: "english-only-resource", children: "Learn more." }) })] }) })), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "New handle" }) }), _jsxs(TextField.Root, { isInvalid: isInvalid, children: [_jsx(TextField.Icon, { icon: AtIcon }), _jsx(Dialog.Input, { editable: !isPending, defaultValue: subdomain, onChangeText: function (text) { return setSubdomain(text); }, label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["New handle"], ["New handle"])))), placeholder: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["e.g. alice"], ["e.g. alice"])))), autoCapitalize: "none", autoCorrect: false }), _jsx(TextField.SuffixText, { label: host, style: [{ maxWidth: '40%' }], children: host })] })] }), _jsx(Text, { children: _jsxs(Trans, { children: ["Your full handle will be", ' ', _jsxs(Text, { style: [a.font_semi_bold], children: ["@", createFullHandle(subdomain, host)] })] }) }), _jsx(Button, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Save new handle"], ["Save new handle"])))), variant: "solid", size: "large", color: validation.overall ? 'primary' : 'secondary', disabled: !validation.overall, onPress: function () { + if (validation.overall) { + changeHandle({ handle: createFullHandle(subdomain, host) }); + } + }, children: isPending ? (_jsx(ButtonIcon, { icon: Loader })) : (_jsx(ButtonText, { children: _jsx(Trans, { children: "Save" }) })) }), _jsx(Text, { style: [a.leading_snug], children: _jsxs(Trans, { children: ["If you have your own domain, you can use that as your handle. This lets you self-verify your identity.", ' ', _jsx(InlineLinkText, { label: _(msg({ + message: "Learn more", + context: "english-only-resource", + })), to: "https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial", style: [a.font_semi_bold], disableMismatchWarning: true, children: "Learn more here." })] }) }), _jsxs(Button, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["I have my own domain"], ["I have my own domain"])))), variant: "outline", color: "primary", size: "large", onPress: goToOwnHandle, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "I have my own domain" }) }), _jsx(ButtonIcon, { icon: ArrowRightIcon, position: "right" })] })] })] }) })); +} +function OwnHandlePage(_a) { + var _this = this; + var _b, _c; + var goToServiceHandle = _a.goToServiceHandle; + var _ = useLingui()._; + var t = useTheme(); + var currentAccount = useSession().currentAccount; + var _d = useState(true), dnsPanel = _d[0], setDNSPanel = _d[1]; + var _e = useState(''), domain = _e[0], setDomain = _e[1]; + var agent = useAgent(); + var control = Dialog.useDialogContext(); + var fetchDid = useFetchDid(); + var queryClient = useQueryClient(); + var _f = useUpdateHandleMutation({ + onSuccess: function () { + if (currentAccount) { + queryClient.invalidateQueries({ + queryKey: RQKEY_PROFILE(currentAccount.did), + }); + } + agent.resumeSession(agent.session).then(function () { return control.close(); }); + }, + }), changeHandle = _f.mutate, isPending = _f.isPending, error = _f.error, isSuccess = _f.isSuccess; + var _g = useMutation({ + mutationKey: ['verify-handle', domain], + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var did; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, fetchDid(domain)]; + case 1: + did = _a.sent(); + if (did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + throw new DidMismatchError(did); + } + return [2 /*return*/, true]; + } + }); + }); }, + }), verify = _g.mutate, isVerifyPending = _g.isPending, isVerified = _g.isSuccess, verifyError = _g.error, resetVerification = _g.reset; + return (_jsxs(View, { style: [a.flex_1, a.gap_lg], children: [isSuccess && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, children: _jsx(SuccessMessage, { text: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Handle changed!"], ["Handle changed!"])))) }) })), error && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, children: _jsx(ChangeHandleError, { error: error }) })), verifyError && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, children: _jsx(Admonition, { type: "error", children: verifyError instanceof DidMismatchError ? (_jsxs(Trans, { children: ["Wrong DID returned from server. Received: ", verifyError.did] })) : (_jsx(Trans, { children: "Failed to verify handle. Please try again." })) }) })), _jsxs(Animated.View, { layout: native(LinearTransition), style: [a.flex_1, a.gap_md, a.overflow_hidden], children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Enter the domain you want to use" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: AtIcon }), _jsx(Dialog.Input, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["New handle"], ["New handle"])))), placeholder: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["e.g. alice.com"], ["e.g. alice.com"])))), editable: !isPending, defaultValue: domain, onChangeText: function (text) { + setDomain(text); + resetVerification(); + }, autoCapitalize: "none", autoCorrect: false })] })] }), _jsxs(SegmentedControl.Root, { label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Choose domain verification method"], ["Choose domain verification method"])))), type: "tabs", value: dnsPanel ? 'dns' : 'file', onChange: function (values) { return setDNSPanel(values === 'dns'); }, children: [_jsx(SegmentedControl.Item, { value: "dns", label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["DNS Panel"], ["DNS Panel"])))), children: _jsx(SegmentedControl.ItemText, { children: _jsx(Trans, { children: "DNS Panel" }) }) }), _jsx(SegmentedControl.Item, { value: "file", label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["No DNS Panel"], ["No DNS Panel"])))), children: _jsx(SegmentedControl.ItemText, { children: _jsx(Trans, { children: "No DNS Panel" }) }) })] }), dnsPanel ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: _jsx(Trans, { children: "Add the following DNS record to your domain:" }) }), _jsxs(View, { style: [ + t.atoms.bg_contrast_25, + a.rounded_sm, + a.p_md, + a.border, + t.atoms.border_contrast_low, + ], children: [_jsx(Text, { style: [t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Host:" }) }), _jsx(View, { style: [a.py_xs], children: _jsxs(CopyButton, { color: "secondary", value: "_atproto", label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Copy host"], ["Copy host"])))), style: [a.bg_transparent], hoverStyle: [a.bg_transparent], hitSlop: HITSLOP_10, children: [_jsx(Text, { style: [a.text_md, a.flex_1], children: "_atproto" }), _jsx(ButtonIcon, { icon: CopyIcon })] }) }), _jsx(Text, { style: [a.mt_xs, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Type:" }) }), _jsx(View, { style: [a.py_xs], children: _jsx(Text, { style: [a.text_md], children: "TXT" }) }), _jsx(Text, { style: [a.mt_xs, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Value:" }) }), _jsx(View, { style: [a.py_xs], children: _jsxs(CopyButton, { color: "secondary", value: 'did=' + (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did), label: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Copy TXT record value"], ["Copy TXT record value"])))), style: [a.bg_transparent], hoverStyle: [a.bg_transparent], hitSlop: HITSLOP_10, children: [_jsxs(Text, { style: [a.text_md, a.flex_1], children: ["did=", currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did] }), _jsx(ButtonIcon, { icon: CopyIcon })] }) })] }), _jsx(Text, { children: _jsx(Trans, { children: "This should create a domain record at:" }) }), _jsx(View, { style: [ + t.atoms.bg_contrast_25, + a.rounded_sm, + a.p_md, + a.border, + t.atoms.border_contrast_low, + ], children: _jsxs(Text, { style: [a.text_md], children: ["_atproto.", domain] }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { children: _jsx(Trans, { children: "Upload a text file to:" }) }), _jsx(View, { style: [ + t.atoms.bg_contrast_25, + a.rounded_sm, + a.p_md, + a.border, + t.atoms.border_contrast_low, + ], children: _jsxs(Text, { style: [a.text_md], children: ["https://", domain, "/.well-known/atproto-did"] }) }), _jsx(Text, { children: _jsx(Trans, { children: "That contains the following:" }) }), _jsxs(CopyButton, { value: (_b = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) !== null && _b !== void 0 ? _b : '', label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Copy DID"], ["Copy DID"])))), size: "large", shape: "rectangular", color: "secondary", style: [ + a.px_md, + a.border, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + ], children: [_jsx(Text, { style: [a.text_md, a.flex_1], children: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }), _jsx(ButtonIcon, { icon: CopyIcon })] })] }))] }), isVerified && (_jsx(Animated.View, { entering: FadeIn, exiting: FadeOut, layout: native(LinearTransition), children: _jsx(SuccessMessage, { text: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Domain verified!"], ["Domain verified!"])))) }) })), _jsxs(Animated.View, { layout: native(LinearTransition), children: [((_c = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.handle) === null || _c === void 0 ? void 0 : _c.endsWith('.bsky.social')) && (_jsx(Admonition, { type: "info", style: [a.mb_md], children: _jsxs(Trans, { children: ["Your current handle", ' ', _jsx(Text, { style: [a.font_semi_bold], children: sanitizeHandle((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.handle) || '', '@') }), ' ', "will automatically remain reserved for you. You can switch back to it at any time from this account."] }) })), _jsx(Button, { label: isVerified + ? _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Update to ", ""], ["Update to ", ""])), domain)) + : dnsPanel + ? _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Verify DNS Record"], ["Verify DNS Record"])))) + : _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Verify Text File"], ["Verify Text File"])))), variant: "solid", size: "large", color: "primary", disabled: domain.trim().length === 0, onPress: function () { + if (isVerified) { + changeHandle({ handle: domain }); + } + else { + verify(); + } + }, children: isPending || isVerifyPending ? (_jsx(ButtonIcon, { icon: Loader })) : (_jsx(ButtonText, { children: isVerified ? (_jsxs(Trans, { children: ["Update to ", domain] })) : dnsPanel ? (_jsx(Trans, { children: "Verify DNS Record" })) : (_jsx(Trans, { children: "Verify Text File" })) })) }), _jsxs(Button, { label: _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Use default provider"], ["Use default provider"])))), accessibilityHint: _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Returns to previous page"], ["Returns to previous page"])))), onPress: goToServiceHandle, variant: "outline", color: "secondary", size: "large", style: [a.mt_sm], children: [_jsx(ButtonIcon, { icon: ArrowLeftIcon, position: "left" }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Nevermind, create a handle for me" }) })] })] })] })); +} +var DidMismatchError = /** @class */ (function (_super) { + __extends(DidMismatchError, _super); + function DidMismatchError(did) { + var _this = _super.call(this, 'DID mismatch') || this; + _this.name = 'DidMismatchError'; + _this.did = did; + return _this; + } + return DidMismatchError; +}(Error)); +function ChangeHandleError(_a) { + var error = _a.error; + var _ = useLingui()._; + var message = _(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Failed to change handle. Please try again."], ["Failed to change handle. Please try again."])))); + if (error instanceof Error) { + if (error.message.startsWith('Handle already taken')) { + message = _(msg(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Handle already taken. Please try a different one."], ["Handle already taken. Please try a different one."])))); + } + else if (error.message === 'Reserved handle') { + message = _(msg(templateObject_27 || (templateObject_27 = __makeTemplateObject(["This handle is reserved. Please try a different one."], ["This handle is reserved. Please try a different one."])))); + } + else if (error.message === 'Handle too long') { + message = _(msg(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Handle too long. Please try a shorter one."], ["Handle too long. Please try a shorter one."])))); + } + else if (error.message === 'Input/handle must be a valid handle') { + message = _(msg(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Invalid handle. Please try a different one."], ["Invalid handle. Please try a different one."])))); + } + else if (error.message === 'Rate Limit Exceeded') { + message = _(msg(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Rate limit exceeded \u2013 you've tried to change your handle too many times in a short period. Please wait a minute before trying again."], ["Rate limit exceeded \u2013 you've tried to change your handle too many times in a short period. Please wait a minute before trying again."])))); + } + } + return _jsx(Admonition, { type: "error", children: message }); +} +function SuccessMessage(_a) { + var text = _a.text; + var gtMobile = useBreakpoints().gtMobile; + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_1, + a.gap_md, + a.flex_row, + a.justify_center, + a.align_center, + gtMobile ? a.px_md : a.px_sm, + a.py_xs, + t.atoms.border_contrast_low, + ], children: [_jsx(View, { style: [ + { height: 20, width: 20 }, + a.rounded_full, + a.align_center, + a.justify_center, + { backgroundColor: t.palette.positive_500 }, + ], children: _jsx(CheckIcon, { fill: t.palette.white, size: "xs" }) }), _jsx(Text, { style: [a.text_md], children: text })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26, templateObject_27, templateObject_28, templateObject_29, templateObject_30; diff --git a/src/screens/Settings/components/ChangePasswordDialog.js b/src/screens/Settings/components/ChangePasswordDialog.js new file mode 100644 index 0000000000..706bee9461 --- /dev/null +++ b/src/screens/Settings/components/ChangePasswordDialog.js @@ -0,0 +1,197 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import * as EmailValidator from 'email-validator'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { checkAndFormatResetCode } from '#/lib/strings/password'; +import { logger } from '#/logger'; +import { useAgent, useSession } from '#/state/session'; +import { ErrorMessage } from '#/view/com/util/error/ErrorMessage'; +import { android, atoms as a, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextField from '#/components/forms/TextField'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +var Stages; +(function (Stages) { + Stages["RequestCode"] = "RequestCode"; + Stages["ChangePassword"] = "ChangePassword"; + Stages["Done"] = "Done"; +})(Stages || (Stages = {})); +export function ChangePasswordDialog(_a) { + var control = _a.control; + var height = useWindowDimensions().height; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: android({ minHeight: height / 2 }), children: [_jsx(Dialog.Handle, {}), _jsx(Inner, {})] })); +} +function Inner() { + var _this = this; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var control = Dialog.useDialogContext(); + var _a = useState(Stages.RequestCode), stage = _a[0], setStage = _a[1]; + var _b = useState(false), isProcessing = _b[0], setIsProcessing = _b[1]; + var _c = useState(''), resetCode = _c[0], setResetCode = _c[1]; + var _d = useState(''), newPassword = _d[0], setNewPassword = _d[1]; + var _e = useState(''), error = _e[0], setError = _e[1]; + var uiStrings = { + RequestCode: { + title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Change your password"], ["Change your password"])))), + message: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["If you want to change your password, we will send you a code to verify that this is your account."], ["If you want to change your password, we will send you a code to verify that this is your account."])))), + }, + ChangePassword: { + title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Enter code"], ["Enter code"])))), + message: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Please enter the code you received and the new password you would like to use."], ["Please enter the code you received and the new password you would like to use."])))), + }, + Done: { + title: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Password changed"], ["Password changed"])))), + message: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on."], ["Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on."])))), + }, + }; + var onRequestCode = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email) || + !EmailValidator.validate(currentAccount.email)) { + return [2 /*return*/, setError(_(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Your email appears to be invalid."], ["Your email appears to be invalid."])))))]; + } + setError(''); + setIsProcessing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, agent.com.atproto.server.requestPasswordReset({ + email: currentAccount.email, + })]; + case 2: + _a.sent(); + setStage(Stages.ChangePassword); + return [3 /*break*/, 5]; + case 3: + e_1 = _a.sent(); + if (isNetworkError(e_1)) { + setError(_(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Unable to contact your service. Please check your internet connection and try again."], ["Unable to contact your service. Please check your internet connection and try again."]))))); + } + else { + logger.error('Failed to request password reset', { safeMessage: e_1 }); + setError(cleanError(e_1)); + } + return [3 /*break*/, 5]; + case 4: + setIsProcessing(false); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }; + var onChangePassword = function () { return __awaiter(_this, void 0, void 0, function () { + var formattedCode, e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + formattedCode = checkAndFormatResetCode(resetCode); + if (!formattedCode) { + setError(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["You have entered an invalid code. It should look like XXXXX-XXXXX."], ["You have entered an invalid code. It should look like XXXXX-XXXXX."]))))); + return [2 /*return*/]; + } + if (!newPassword) { + setError(_(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Please enter a password. It must be at least 8 characters long."], ["Please enter a password. It must be at least 8 characters long."]))))); + return [2 /*return*/]; + } + if (newPassword.length < 8) { + setError(_(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Password must be at least 8 characters long."], ["Password must be at least 8 characters long."]))))); + return [2 /*return*/]; + } + setError(''); + setIsProcessing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, agent.com.atproto.server.resetPassword({ + token: formattedCode, + password: newPassword, + })]; + case 2: + _a.sent(); + setStage(Stages.Done); + return [3 /*break*/, 5]; + case 3: + e_2 = _a.sent(); + if (isNetworkError(e_2)) { + setError(_(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Unable to contact your service. Please check your internet connection and try again."], ["Unable to contact your service. Please check your internet connection and try again."]))))); + } + else if (e_2 === null || e_2 === void 0 ? void 0 : e_2.toString().includes('Token is invalid')) { + setError(_(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["This confirmation code is not valid. Please try again."], ["This confirmation code is not valid. Please try again."]))))); + } + else { + logger.error('Failed to set new password', { safeMessage: e_2 }); + setError(cleanError(e_2)); + } + return [3 /*break*/, 5]; + case 4: + setIsProcessing(false); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }; + var onBlur = function () { + var formattedCode = checkAndFormatResetCode(resetCode); + if (!formattedCode) { + return; + } + setResetCode(formattedCode); + }; + return (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Change password dialog"], ["Change password dialog"])))), style: web({ maxWidth: 400 }), children: [_jsxs(View, { style: [a.gap_xl], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_bold, a.text_2xl], children: uiStrings[stage].title }), error ? (_jsx(View, { style: [a.rounded_sm, a.overflow_hidden], children: _jsx(ErrorMessage, { message: error }) })) : null, _jsx(Text, { style: [a.text_md, a.leading_snug], children: uiStrings[stage].message })] }), stage === Stages.ChangePassword && (_jsxs(View, { style: [a.gap_md], children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Confirmation code" }) }), _jsx(TextField.Root, { children: _jsx(TextField.Input, { label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Confirmation code"], ["Confirmation code"])))), placeholder: "XXXXX-XXXXX", value: resetCode, onChangeText: setResetCode, onBlur: onBlur, autoCapitalize: "none", autoCorrect: false, autoComplete: "one-time-code" }) })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "New password" }) }), _jsx(TextField.Root, { children: _jsx(TextField.Input, { label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["New password"], ["New password"])))), placeholder: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["At least 8 characters"], ["At least 8 characters"])))), value: newPassword, onChangeText: setNewPassword, secureTextEntry: true, autoCapitalize: "none", autoComplete: "new-password", passwordRules: "minlength: 8;" }) })] })] })), _jsx(View, { style: [a.gap_sm], children: stage === Stages.RequestCode ? (_jsxs(_Fragment, { children: [_jsxs(Button, { label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Request code"], ["Request code"])))), color: "primary", size: "large", disabled: isProcessing, onPress: onRequestCode, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Request code" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Button, { label: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Already have a code?"], ["Already have a code?"])))), onPress: function () { return setStage(Stages.ChangePassword); }, size: "large", color: "primary_subtle", disabled: isProcessing, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Already have a code?" }) }) }), IS_NATIVE && (_jsx(Button, { label: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Cancel"], ["Cancel"])))), color: "secondary", size: "large", disabled: isProcessing, onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) }))] })) : stage === Stages.ChangePassword ? (_jsxs(_Fragment, { children: [_jsxs(Button, { label: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Change password"], ["Change password"])))), color: "primary", size: "large", disabled: isProcessing, onPress: onChangePassword, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Change password" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Button, { label: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Back"], ["Back"])))), color: "secondary", size: "large", disabled: isProcessing, onPress: function () { + setResetCode(''); + setStage(Stages.RequestCode); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Back" }) }) })] })) : stage === Stages.Done ? (_jsx(Button, { label: _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Close"], ["Close"])))), color: "primary", size: "large", onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })) : null })] }), _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23; diff --git a/src/screens/Settings/components/CopyButton.js b/src/screens/Settings/components/CopyButton.js new file mode 100644 index 0000000000..d55476fca1 --- /dev/null +++ b/src/screens/Settings/components/CopyButton.js @@ -0,0 +1,61 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useState } from 'react'; +import { View } from 'react-native'; +import Animated, { FadeOutUp, useReducedMotion, ZoomIn, } from 'react-native-reanimated'; +import * as Clipboard from 'expo-clipboard'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { Text } from '#/components/Typography'; +export function CopyButton(_a) { + var style = _a.style, value = _a.value, onPressProp = _a.onPress, props = __rest(_a, ["style", "value", "onPress"]); + var _b = useState(false), hasBeenCopied = _b[0], setHasBeenCopied = _b[1]; + var t = useTheme(); + var isReducedMotionEnabled = useReducedMotion(); + useEffect(function () { + if (hasBeenCopied) { + var timeout_1 = setTimeout(function () { return setHasBeenCopied(false); }, isReducedMotionEnabled ? 2000 : 100); + return function () { return clearTimeout(timeout_1); }; + } + }, [hasBeenCopied, isReducedMotionEnabled]); + var onPress = useCallback(function (evt) { + Clipboard.setStringAsync(value); + setHasBeenCopied(true); + onPressProp === null || onPressProp === void 0 ? void 0 : onPressProp(evt); + }, [value, onPressProp]); + return (_jsxs(View, { style: [a.relative], children: [hasBeenCopied && (_jsx(Animated.View, { entering: ZoomIn.duration(100), exiting: FadeOutUp.duration(2000), style: [ + a.absolute, + { bottom: '100%', right: 0 }, + a.justify_center, + a.gap_sm, + a.z_10, + a.pb_sm, + ], pointerEvents: "none", children: _jsx(Text, { style: [ + a.font_medium, + a.text_right, + a.text_sm, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Copied!" }) }) })), _jsx(Button, __assign({ style: [a.flex_1, a.justify_between, style], onPress: onPress }, props))] })); +} diff --git a/src/screens/Settings/components/DeactivateAccountDialog.js b/src/screens/Settings/components/DeactivateAccountDialog.js new file mode 100644 index 0000000000..78536b3fc1 --- /dev/null +++ b/src/screens/Settings/components/DeactivateAccountDialog.js @@ -0,0 +1,113 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { logger } from '#/logger'; +import { useAgent, useSessionApi } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Divider } from '#/components/Divider'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +export function DeactivateAccountDialog(_a) { + var control = _a.control; + return (_jsx(Prompt.Outer, { control: control, children: _jsx(DeactivateAccountDialogInner, { control: control }) })); +} +function DeactivateAccountDialogInner(_a) { + var _this = this; + var control = _a.control; + var t = useTheme(); + var _ = useLingui()._; + var agent = useAgent(); + var logoutCurrentAccount = useSessionApi().logoutCurrentAccount; + var _b = React.useState(false), pending = _b[0], setPending = _b[1]; + var _c = React.useState(), error = _c[0], setError = _c[1]; + var handleDeactivate = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, 3, 4]); + setPending(true); + return [4 /*yield*/, agent.com.atproto.server.deactivateAccount({})]; + case 1: + _a.sent(); + control.close(function () { + logoutCurrentAccount('Deactivated'); + }); + return [3 /*break*/, 4]; + case 2: + e_1 = _a.sent(); + switch (e_1.message) { + case 'Bad token scope': + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You're signed in with an App Password. Please sign in with your main password to continue deactivating your account."], ["You're signed in with an App Password. Please sign in with your main password to continue deactivating your account."]))))); + break; + default: + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Something went wrong, please try again"], ["Something went wrong, please try again"]))))); + break; + } + logger.error(e_1, { + message: 'Failed to deactivate account', + }); + return [3 /*break*/, 4]; + case 3: + setPending(false); + return [7 /*endfinally*/]; + case 4: return [2 /*return*/]; + } + }); + }); }, [agent, control, logoutCurrentAccount, _, setPending]); + return (_jsxs(_Fragment, { children: [_jsx(Prompt.TitleText, { children: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Deactivate account"], ["Deactivate account"])))) }), _jsx(Prompt.DescriptionText, { children: _jsx(Trans, { children: "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." }) }), _jsxs(View, { style: [a.pb_xl], children: [_jsx(Divider, {}), _jsxs(View, { style: [a.gap_sm, a.pt_lg, a.pb_xl], children: [_jsx(Text, { style: [t.atoms.text_contrast_medium, a.leading_snug], children: _jsx(Trans, { children: "There is no time limit for account deactivation, come back any time." }) }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.leading_snug], children: _jsx(Trans, { children: "If you're trying to change your handle or email, do so before you deactivate." }) })] }), _jsx(Divider, {})] }), _jsxs(Prompt.Actions, { children: [_jsxs(Button, { color: "negative", size: "large", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Yes, deactivate"], ["Yes, deactivate"])))), onPress: handleDeactivate, children: [_jsx(ButtonText, { children: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Yes, deactivate"], ["Yes, deactivate"])))) }), pending && _jsx(ButtonIcon, { icon: Loader, position: "right" })] }), _jsx(Prompt.Cancel, {})] }), error && (_jsxs(View, { style: [ + a.flex_row, + a.gap_sm, + a.mt_md, + a.p_md, + a.rounded_sm, + t.atoms.bg_contrast_25, + ], children: [_jsx(CircleInfo, { size: "md", fill: t.palette.negative_400 }), _jsx(Text, { style: [a.flex_1, a.leading_snug], children: error })] }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/screens/Settings/components/DeleteAccountDialog.js b/src/screens/Settings/components/DeleteAccountDialog.js new file mode 100644 index 0000000000..eb5b169127 --- /dev/null +++ b/src/screens/Settings/components/DeleteAccountDialog.js @@ -0,0 +1,240 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { useAgent, useSession, useSessionApi } from '#/state/session'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { isValidCode, TokenField, } from '#/components/dialogs/EmailDialog/components/TokenField'; +import * as TextField from '#/components/forms/TextField'; +import { Envelope_Stroke2_Corner0_Rounded as Envelope } from '#/components/icons/Envelope'; +import { Lock_Stroke2_Corner0_Rounded as Lock } from '#/components/icons/Lock'; +import { createStaticClick, InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import * as toast from '#/components/Toast'; +import { Span, Text } from '#/components/Typography'; +import { resetToTab } from '#/Navigation'; +var WHITESPACE_RE = /\s/gu; +var PASSWORD_MIN_LENGTH = 8; +var Step; +(function (Step) { + Step[Step["SEND_CODE"] = 0] = "SEND_CODE"; + Step[Step["VERIFY_CODE"] = 1] = "VERIFY_CODE"; + Step[Step["CONFIRM_DELETION"] = 2] = "CONFIRM_DELETION"; +})(Step || (Step = {})); +var EmailState; +(function (EmailState) { + EmailState[EmailState["DEFAULT"] = 0] = "DEFAULT"; + EmailState[EmailState["PENDING"] = 1] = "PENDING"; +})(EmailState || (EmailState = {})); +function isPasswordValid(password) { + return password.length >= PASSWORD_MIN_LENGTH; +} +export function DeleteAccountDialog(_a) { + var control = _a.control, deactivateDialogControl = _a.deactivateDialogControl; + return (_jsx(Prompt.Outer, { control: control, children: _jsx(DeleteAccountDialogInner, { control: control, deactivateDialogControl: deactivateDialogControl }) })); +} +function DeleteAccountDialogInner(_a) { + var _this = this; + var _b, _c; + var control = _a.control, deactivateDialogControl = _a.deactivateDialogControl; + var passwordRef = useRef(null); + var t = useTheme(); + var _ = useLingui()._; + var cleanError = useCleanError(); + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + var removeAccount = useSessionApi().removeAccount; + var _d = useState(EmailState.DEFAULT), emailState = _d[0], setEmailState = _d[1]; + var _e = useState(0), emailSentCount = _e[0], setEmailSentCount = _e[1]; + var _f = useState(Step.SEND_CODE), step = _f[0], setStep = _f[1]; + var _g = useState(''), confirmCode = _g[0], setConfirmCode = _g[1]; + var _h = useState(''), password = _h[0], setPassword = _h[1]; + var _j = useState(''), error = _j[0], setError = _j[1]; + var sendEmail = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var e_1, _a, clean, raw, error_1; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (emailState === EmailState.PENDING) { + return [2 /*return*/]; + } + _b.label = 1; + case 1: + _b.trys.push([1, 3, 4, 5]); + setEmailState(EmailState.PENDING); + return [4 /*yield*/, agent.com.atproto.server.requestAccountDelete()]; + case 2: + _b.sent(); + setError(''); + setEmailSentCount(function (prevCount) { return prevCount + 1; }); + setStep(Step.VERIFY_CODE); + return [3 /*break*/, 5]; + case 3: + e_1 = _b.sent(); + _a = cleanError(e_1), clean = _a.clean, raw = _a.raw; + error_1 = clean || raw || e_1; + setError(error_1); + logger.error(raw || e_1, { + message: 'Failed to send account deletion verification email', + }); + return [3 /*break*/, 5]; + case 4: + setEmailState(EmailState.DEFAULT); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }, [agent, cleanError, emailState, setEmailState]); + var confirmDeletion = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var token, success, e_2, _a, clean, raw, error_2; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 3, , 4]); + setError(''); + if (!(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + throw new Error('Invalid did'); + } + token = confirmCode.replace(WHITESPACE_RE, ''); + return [4 /*yield*/, agent.api.chat.bsky.actor.deleteAccount(undefined, { + headers: DM_SERVICE_HEADERS, + })]; + case 1: + success = (_b.sent()).success; + if (!success) { + throw new Error('Failed to inform chat service of account deletion'); + } + return [4 /*yield*/, agent.com.atproto.server.deleteAccount({ + did: currentAccount.did, + password: password, + token: token, + })]; + case 2: + _b.sent(); + control.close(function () { + toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Your account has been deleted, see ya! \u270C\uFE0F"], ["Your account has been deleted, see ya! \u270C\uFE0F"]))))); + resetToTab('HomeTab'); + removeAccount(currentAccount); + }); + return [3 /*break*/, 4]; + case 3: + e_2 = _b.sent(); + _a = cleanError(e_2), clean = _a.clean, raw = _a.raw; + error_2 = clean || raw || e_2; + setError(error_2); + logger.error(raw || e_2, { + message: 'Failed to delete account', + }); + setConfirmCode(''); + setPassword(''); + setStep(Step.VERIFY_CODE); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [ + _, + agent, + cleanError, + confirmCode, + control, + currentAccount, + password, + removeAccount, + ]); + var handleDeactivate = useCallback(function () { + control.close(function () { return deactivateDialogControl.open(); }); + }, [control, deactivateDialogControl]); + var handleSendEmail = useCallback(function () { + void sendEmail(); + }, [sendEmail]); + var handleSubmitConfirmCode = useCallback(function () { + var _a; + (_a = passwordRef.current) === null || _a === void 0 ? void 0 : _a.focus(); + }, []); + var handleDeleteAccount = useCallback(function () { + setStep(Step.CONFIRM_DELETION); + }, [setStep]); + var handleConfirmDeletion = useCallback(function () { + void confirmDeletion(); + }, [confirmDeletion]); + var currentHandle = sanitizeHandle((_b = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.handle) !== null && _b !== void 0 ? _b : '', '@'); + var currentEmail = (_c = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email) !== null && _c !== void 0 ? _c : '(no email)'; + switch (step) { + case Step.SEND_CODE: + return (_jsxs(_Fragment, { children: [_jsxs(Prompt.Content, { children: [_jsx(Prompt.TitleText, { children: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Delete account \u201C", "\u201D"], ["Delete account \u201C", "\u201D"])), currentHandle)) }), _jsx(Prompt.DescriptionText, { children: _jsxs(Trans, { children: ["For security reasons, we\u2019ll need to send a confirmation code to your email address", ' ', _jsx(Span, { style: [a.font_semi_bold, t.atoms.text], children: currentEmail }), "."] }) })] }), _jsxs(Prompt.Actions, { children: [_jsx(Prompt.Action, { icon: emailState === EmailState.PENDING ? Loader : Envelope, cta: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Send email"], ["Send email"])))), shouldCloseOnPress: false, onPress: handleSendEmail }), _jsx(Prompt.Cancel, {})] }), error && (_jsx(Admonition, { style: [a.mt_lg], type: "error", children: _jsx(Text, { style: [a.flex_1, a.leading_snug], children: error }) })), _jsx(Admonition, { style: [a.mt_lg], type: "tip", children: _jsxs(Trans, { children: ["You can also", ' ', _jsx(Span, { style: [{ color: t.palette.primary_500 }, web(a.underline)], onPress: handleDeactivate, children: "temporarily deactivate" }), ' ', "your account instead. Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in."] }) })] })); + case Step.VERIFY_CODE: + return (_jsxs(_Fragment, { children: [_jsxs(Prompt.Content, { children: [_jsx(Prompt.TitleText, { children: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Delete account \u201C", "\u201D"], ["Delete account \u201C", "\u201D"])), currentHandle)) }), _jsx(Prompt.DescriptionText, { children: _jsxs(Trans, { children: ["Check", ' ', _jsx(Span, { style: [a.font_semi_bold, t.atoms.text], children: currentEmail }), ' ', "for an email with the confirmation code to enter below:"] }) })] }), _jsxs(View, { style: [a.mb_xs], children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Confirmation code" }) }), _jsx(TokenField, { value: confirmCode, onChangeText: setConfirmCode, onSubmitEditing: handleSubmitConfirmCode })] }), _jsxs(Text, { style: [ + a.text_sm, + a.leading_snug, + a.mb_lg, + t.atoms.text_contrast_medium, + ], children: [emailSentCount > 1 ? (_jsxs(Trans, { children: ["Email sent!", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Resend"], ["Resend"])))) }, createStaticClick(function () { + void handleSendEmail(); + }), { children: "Click here to resend." }))] })) : (_jsxs(Trans, { children: ["Don\u2019t see a code?", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Resend"], ["Resend"])))) }, createStaticClick(function () { + void handleSendEmail(); + }), { children: "Click here to resend." }))] })), ' ', _jsx(Span, { style: { top: 1 }, children: emailState === EmailState.PENDING ? _jsx(Loader, { size: "xs" }) : null })] }), _jsxs(View, { style: [a.mb_xl], children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Password" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: Lock }), _jsx(TextField.Input, { inputRef: passwordRef, testID: "newPasswordInput", label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Enter your password"], ["Enter your password"])))), autoCapitalize: "none", autoCorrect: false, returnKeyType: "done", secureTextEntry: true, autoComplete: "off", clearButtonMode: "while-editing", passwordRules: "minlength: ".concat(PASSWORD_MIN_LENGTH, "};"), value: password, onChangeText: setPassword, onSubmitEditing: handleDeleteAccount })] })] }), _jsxs(Prompt.Actions, { children: [_jsx(Prompt.Action, { color: "negative", disabled: !isValidCode(confirmCode) || !isPasswordValid(password), cta: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Delete my account"], ["Delete my account"])))), shouldCloseOnPress: false, onPress: handleDeleteAccount }), _jsx(Prompt.Cancel, {})] }), error && (_jsx(Admonition, { style: [a.mt_lg], type: "error", children: _jsx(Text, { style: [a.flex_1, a.leading_snug], children: error }) }))] })); + case Step.CONFIRM_DELETION: + return (_jsxs(_Fragment, { children: [_jsxs(Prompt.Content, { children: [_jsx(Prompt.TitleText, { children: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Are you really, really sure?"], ["Are you really, really sure?"])))) }), _jsx(Prompt.DescriptionText, { children: _jsxs(Trans, { children: ["This will irreversibly delete your Bluesky account", ' ', _jsx(Span, { style: [a.font_semi_bold, t.atoms.text], children: currentHandle }), ' ', "and all associated data. Note that this will affect any other", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Learn more about the AT Protocol."], ["Learn more about the AT Protocol."])))), style: [a.text_md], to: "https://bsky.social/about/faq", children: "AT Protocol" }), ' ', "services you use with this account."] }) })] }), _jsxs(Prompt.Actions, { children: [_jsx(Prompt.Action, { color: "negative", cta: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Yes, delete my account"], ["Yes, delete my account"])))), shouldCloseOnPress: false, onPress: handleConfirmDeletion }), _jsx(Prompt.Cancel, {})] })] })); + } +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11; diff --git a/src/screens/Settings/components/DisableEmail2FADialog.js b/src/screens/Settings/components/DisableEmail2FADialog.js new file mode 100644 index 0000000000..e6906192a6 --- /dev/null +++ b/src/screens/Settings/components/DisableEmail2FADialog.js @@ -0,0 +1,153 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { cleanError } from '#/lib/strings/errors'; +import { useAgent, useSession } from '#/state/session'; +import { ErrorMessage } from '#/view/com/util/error/ErrorMessage'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import * as TextField from '#/components/forms/TextField'; +import { Lock_Stroke2_Corner0_Rounded as Lock } from '#/components/icons/Lock'; +import { Loader } from '#/components/Loader'; +import { P, Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +var Stages; +(function (Stages) { + Stages[Stages["Email"] = 0] = "Email"; + Stages[Stages["ConfirmCode"] = 1] = "ConfirmCode"; +})(Stages || (Stages = {})); +export function DisableEmail2FADialog(_a) { + var _this = this; + var control = _a.control; + var _ = useLingui()._; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var _b = useState(Stages.Email), stage = _b[0], setStage = _b[1]; + var _c = useState(''), confirmationCode = _c[0], setConfirmationCode = _c[1]; + var _d = useState(false), isProcessing = _d[0], setIsProcessing = _d[1]; + var _e = useState(''), error = _e[0], setError = _e[1]; + var onSendEmail = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setError(''); + setIsProcessing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, agent.com.atproto.server.requestEmailUpdate()]; + case 2: + _a.sent(); + setStage(Stages.ConfirmCode); + return [3 /*break*/, 5]; + case 3: + e_1 = _a.sent(); + setError(cleanError(String(e_1))); + return [3 /*break*/, 5]; + case 4: + setIsProcessing(false); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }; + var onConfirmDisable = function () { return __awaiter(_this, void 0, void 0, function () { + var e_2, errMsg; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setError(''); + setIsProcessing(true); + _a.label = 1; + case 1: + _a.trys.push([1, 5, 6, 7]); + if (!(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email)) return [3 /*break*/, 4]; + return [4 /*yield*/, agent.com.atproto.server.updateEmail({ + email: currentAccount.email, + token: confirmationCode.trim(), + emailAuthFactor: false, + })]; + case 2: + _a.sent(); + return [4 /*yield*/, agent.resumeSession(agent.session)]; + case 3: + _a.sent(); + Toast.show(_(msg({ message: 'Email 2FA disabled', context: 'toast' }))); + _a.label = 4; + case 4: + control.close(); + return [3 /*break*/, 7]; + case 5: + e_2 = _a.sent(); + errMsg = String(e_2); + if (errMsg.includes('Token is invalid')) { + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Invalid 2FA confirmation code."], ["Invalid 2FA confirmation code."]))))); + } + else { + setError(cleanError(errMsg)); + } + return [3 /*break*/, 7]; + case 6: + setIsProcessing(false); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsx(Dialog.ScrollableInner, { accessibilityDescribedBy: "dialog-description", accessibilityLabelledBy: "dialog-title", children: _jsxs(View, { style: [a.relative, a.gap_md, a.w_full], children: [_jsx(Text, { nativeID: "dialog-title", style: [a.text_2xl, a.font_semi_bold, t.atoms.text], children: _jsx(Trans, { children: "Disable Email 2FA" }) }), _jsx(P, { nativeID: "dialog-description", children: stage === Stages.ConfirmCode ? (_jsxs(Trans, { children: ["An email has been sent to", ' ', (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email) || '(no email)', ". It includes a confirmation code which you can enter below."] })) : (_jsx(Trans, { children: "To disable the email 2FA method, please verify your access to the email address." })) }), error ? _jsx(ErrorMessage, { message: error }) : undefined, stage === Stages.Email ? (_jsxs(View, { style: [ + a.gap_sm, + gtMobile && [a.flex_row, a.justify_end, a.gap_md], + ], children: [_jsxs(Button, { testID: "sendEmailButton", variant: "solid", color: "primary", size: gtMobile ? 'small' : 'large', onPress: onSendEmail, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Send verification email"], ["Send verification email"])))), disabled: isProcessing, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Send verification email" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Button, { testID: "haveCodeButton", variant: "ghost", color: "primary", size: gtMobile ? 'small' : 'large', onPress: function () { return setStage(Stages.ConfirmCode); }, label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["I have a code"], ["I have a code"])))), disabled: isProcessing, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "I have a code" }) }) })] })) : stage === Stages.ConfirmCode ? (_jsxs(View, { children: [_jsxs(View, { style: [a.mb_md], children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Confirmation code" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: Lock }), _jsx(Dialog.Input, { testID: "confirmationCode", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Confirmation code"], ["Confirmation code"])))), autoCapitalize: "none", autoFocus: true, autoCorrect: false, autoComplete: "off", value: confirmationCode, onChangeText: setConfirmationCode, onSubmitEditing: onConfirmDisable, editable: !isProcessing })] })] }), _jsxs(View, { style: [ + a.gap_sm, + gtMobile && [a.flex_row, a.justify_end, a.gap_md], + ], children: [_jsx(Button, { testID: "resendCodeBtn", variant: "ghost", color: "primary", size: gtMobile ? 'small' : 'large', onPress: onSendEmail, label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Resend email"], ["Resend email"])))), disabled: isProcessing, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Resend email" }) }) }), _jsxs(Button, { testID: "confirmBtn", variant: "solid", color: "primary", size: gtMobile ? 'small' : 'large', onPress: onConfirmDisable, label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Confirm"], ["Confirm"])))), disabled: isProcessing, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Confirm" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] })] })] })) : undefined, !gtMobile && IS_NATIVE && _jsx(View, { style: { height: 40 } })] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/Settings/components/Email2FAToggle.js b/src/screens/Settings/components/Email2FAToggle.js new file mode 100644 index 0000000000..49d1474953 --- /dev/null +++ b/src/screens/Settings/components/Email2FAToggle.js @@ -0,0 +1,26 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useSession } from '#/state/session'; +import { useDialogControl } from '#/components/Dialog'; +import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog'; +import { DisableEmail2FADialog } from './DisableEmail2FADialog'; +import * as SettingsList from './SettingsList'; +export function Email2FAToggle() { + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var disableDialogControl = useDialogControl(); + var emailDialogControl = useEmailDialogControl(); + var onToggle = React.useCallback(function () { + emailDialogControl.open({ + id: EmailDialogScreenID.Manage2FA, + }); + }, [emailDialogControl]); + return (_jsxs(_Fragment, { children: [_jsx(DisableEmail2FADialog, { control: disableDialogControl }), _jsx(SettingsList.BadgeButton, { label: (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.emailAuthFactor) ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Change"], ["Change"])))) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Enable"], ["Enable"])))), onPress: onToggle })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Settings/components/ExportCarDialog.js b/src/screens/Settings/components/ExportCarDialog.js new file mode 100644 index 0000000000..967493d675 --- /dev/null +++ b/src/screens/Settings/components/ExportCarDialog.js @@ -0,0 +1,151 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { saveBytesToDisk } from '#/lib/media/manip'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Download_Stroke2_Corner0_Rounded as DownloadIcon } from '#/components/icons/Download'; +import { InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +export function ExportCarDialog(_a) { + var _this = this; + var control = _a.control; + var _ = useLingui()._; + var t = useTheme(); + var agent = useAgent(); + var _b = useState(false), loading = _b[0], setLoading = _b[1]; + var download = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var did, downloadRes, saveRes, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!agent.session) { + return [2 /*return*/]; // shouldnt ever happen + } + _a.label = 1; + case 1: + _a.trys.push([1, 4, 5, 6]); + setLoading('repo'); + did = agent.session.did; + return [4 /*yield*/, agent.com.atproto.sync.getRepo({ did: did })]; + case 2: + downloadRes = _a.sent(); + return [4 /*yield*/, saveBytesToDisk('repo.car', downloadRes.data, downloadRes.headers['content-type'] || 'application/vnd.ipld.car')]; + case 3: + saveRes = _a.sent(); + if (saveRes) { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["File saved successfully!"], ["File saved successfully!"]))))); + } + return [3 /*break*/, 6]; + case 4: + e_1 = _a.sent(); + logger.error('Error occurred while downloading CAR file', { message: e_1 }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Error occurred while saving file"], ["Error occurred while saving file"])))), { type: 'error' }); + return [3 /*break*/, 6]; + case 5: + setLoading(false); + control.close(); + return [7 /*endfinally*/]; + case 6: return [2 /*return*/]; + } + }); + }); }, [_, control, agent]); + var downloadChatData = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var res, data, _a, saveRes, e_2; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!agent.session) { + return [2 /*return*/]; + } + _b.label = 1; + case 1: + _b.trys.push([1, 5, 6, 7]); + setLoading('chat'); + return [4 /*yield*/, agent.sessionManager.fetchHandler('/xrpc/chat.bsky.actor.exportAccountData', { headers: DM_SERVICE_HEADERS })]; + case 2: + res = _b.sent(); + if (!res.ok) { + throw new Error("HTTP ".concat(res.status)); + } + _a = Uint8Array.bind; + return [4 /*yield*/, res.arrayBuffer()]; + case 3: + data = new (_a.apply(Uint8Array, [void 0, _b.sent()]))(); + return [4 /*yield*/, saveBytesToDisk('chat.jsonl', data, res.headers.get('content-type') || 'application/jsonl')]; + case 4: + saveRes = _b.sent(); + if (saveRes) { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["File saved successfully!"], ["File saved successfully!"]))))); + } + return [3 /*break*/, 7]; + case 5: + e_2 = _b.sent(); + logger.error('Error occurred while downloading chat data', { message: e_2 }); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Error occurred while saving file"], ["Error occurred while saving file"])))), { type: 'error' }); + return [3 /*break*/, 7]; + case 6: + setLoading(false); + control.close(); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); }, [_, control, agent]); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { accessibilityDescribedBy: "dialog-description", accessibilityLabelledBy: "dialog-title", style: web({ maxWidth: 500 }), children: [_jsxs(View, { style: [a.relative, a.gap_lg, a.w_full], children: [_jsx(Text, { nativeID: "dialog-title", style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "Export My Data" }) }), _jsx(Text, { nativeID: "dialog-description", style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." }) }), _jsxs(Button, { color: "primary", size: "large", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Download CAR file"], ["Download CAR file"])))), disabled: !!loading, onPress: download, children: [_jsx(ButtonIcon, { icon: DownloadIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Download CAR file" }) }), loading === 'repo' && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "You can also download your chat data as a \"JSONL\" file. This file only includes chat messages that you have sent and does not include chat messages that you have received." }) }), _jsxs(Button, { color: "secondary", size: "large", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Download chat data"], ["Download chat data"])))), disabled: !!loading, onPress: downloadChatData, children: [_jsx(ButtonIcon, { icon: DownloadIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Download chat data" }) }), loading === 'chat' && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Text, { style: [ + t.atoms.text_contrast_medium, + a.text_sm, + a.leading_snug, + a.flex_1, + ], children: _jsxs(Trans, { children: ["This feature is in beta. You can read more about repository exports in", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["View blogpost for more details"], ["View blogpost for more details"])))), to: "https://docs.bsky.app/blog/repo-export", style: [a.text_sm], children: "this blogpost" }), "."] }) })] }), _jsx(Dialog.Close, {})] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/screens/Settings/components/OTAInfo.js b/src/screens/Settings/components/OTAInfo.js new file mode 100644 index 0000000000..787ba698de --- /dev/null +++ b/src/screens/Settings/components/OTAInfo.js @@ -0,0 +1,102 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import * as Updates from 'expo-updates'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import * as Toast from '#/view/com/util/Toast'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon } from '#/components/icons/ArrowRotate'; +import { Shapes_Stroke2_Corner0_Rounded as ShapesIcon } from '#/components/icons/Shapes'; +import { Loader } from '#/components/Loader'; +import * as SettingsList from '../components/SettingsList'; +export function OTAInfo() { + var _this = this; + var _ = useLingui()._; + var _a = useQuery({ + queryKey: ['ota-info'], + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var status; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Updates.checkForUpdateAsync()]; + case 1: + status = _a.sent(); + return [2 /*return*/, status.isAvailable]; + } + }); + }); }, + }), isAvailable = _a.data, isPendingInfo = _a.isPending, isFetchingInfo = _a.isFetching, isErrorInfo = _a.isError, refetch = _a.refetch; + var _b = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Updates.fetchUpdateAsync()]; + case 1: + _a.sent(); + return [4 /*yield*/, Updates.reloadAsync()]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onError: function (error) { + return Toast.show("Failed to update: ".concat(error.message), 'xmark'); + }, + }), fetchAndLaunchUpdate = _b.mutate, isPendingUpdate = _b.isPending; + if (!Updates.isEnabled || __DEV__) { + return null; + } + return (_jsxs(SettingsList.Item, { children: [_jsx(SettingsList.ItemIcon, { icon: ShapesIcon }), _jsx(SettingsList.ItemText, { children: isAvailable ? (_jsx(Trans, { children: "OTA status: Available!" })) : isErrorInfo ? (_jsx(Trans, { children: "OTA status: Error fetching update" })) : isPendingInfo ? (_jsx(Trans, { children: "OTA status: ..." })) : (_jsx(Trans, { children: "OTA status: None available" })) }), _jsx(Button, { label: isAvailable ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Update"], ["Update"])))) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Fetch update"], ["Fetch update"])))), disabled: isFetchingInfo || isPendingUpdate, variant: "solid", size: "small", color: isAvailable ? 'primary' : 'secondary_inverted', onPress: function () { + if (isFetchingInfo || isPendingUpdate) + return; + if (isAvailable) { + fetchAndLaunchUpdate(); + } + else { + refetch(); + } + }, children: isAvailable ? (_jsx(ButtonText, { children: _jsx(Trans, { children: "Update" }) })) : (_jsx(ButtonIcon, { icon: isFetchingInfo ? Loader : RetryIcon })) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/Settings/components/OTAInfo.web.js b/src/screens/Settings/components/OTAInfo.web.js new file mode 100644 index 0000000000..b2e2cfab96 --- /dev/null +++ b/src/screens/Settings/components/OTAInfo.web.js @@ -0,0 +1,3 @@ +export function OTAInfo() { + return null; +} diff --git a/src/screens/Settings/components/PwiOptOut.js b/src/screens/Settings/components/PwiOptOut.js new file mode 100644 index 0000000000..45134e77e0 --- /dev/null +++ b/src/screens/Settings/components/PwiOptOut.js @@ -0,0 +1,70 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { ComAtprotoLabelDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useProfileQuery, useProfileUpdateMutation, } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import * as Toggle from '#/components/forms/Toggle'; +import { Text } from '#/components/Typography'; +import * as bsky from '#/types/bsky'; +export function PwiOptOut() { + var _a; + var t = useTheme(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var profile = useProfileQuery({ did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }).data; + var updateProfile = useProfileUpdateMutation(); + var isOptedOut = ((_a = profile === null || profile === void 0 ? void 0 : profile.labels) === null || _a === void 0 ? void 0 : _a.some(function (l) { return l.val === '!no-unauthenticated'; })) || false; + var canToggle = profile && !updateProfile.isPending; + var onToggleOptOut = React.useCallback(function () { + if (!profile) { + return; + } + var wasAdded = false; + updateProfile.mutate({ + profile: profile, + updates: function (existing) { + // create labels attr if needed + var labels = bsky.validate(existing.labels, ComAtprotoLabelDefs.validateSelfLabels) + ? existing.labels + : { + $type: 'com.atproto.label.defs#selfLabels', + values: [], + }; + // toggle the label + var hasLabel = labels.values.some(function (l) { return l.val === '!no-unauthenticated'; }); + if (hasLabel) { + wasAdded = false; + labels.values = labels.values.filter(function (l) { return l.val !== '!no-unauthenticated'; }); + } + else { + wasAdded = true; + labels.values.push({ val: '!no-unauthenticated' }); + } + // delete if no longer needed + if (labels.values.length === 0) { + delete existing.labels; + } + else { + existing.labels = labels; + } + return existing; + }, + checkCommitted: function (res) { + var _a; + var exists = !!((_a = res.data.labels) === null || _a === void 0 ? void 0 : _a.some(function (l) { return l.val === '!no-unauthenticated'; })); + return exists === wasAdded; + }, + }); + }, [updateProfile, profile]); + return (_jsxs(View, { style: [a.flex_1, a.gap_sm], children: [_jsxs(Toggle.Item, { name: "logged_out_visibility", disabled: !canToggle || updateProfile.isPending, value: isOptedOut, onChange: onToggleOptOut, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Discourage apps from showing my account to logged-out users"], ["Discourage apps from showing my account to logged-out users"])))), style: [a.w_full], children: [_jsx(Toggle.LabelText, { style: [a.flex_1], children: _jsx(Trans, { children: "Discourage apps from showing my account to logged-out users" }) }), _jsx(Toggle.Platform, {})] }), _jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_high], children: _jsx(Trans, { children: "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." }) })] })); +} +var templateObject_1; diff --git a/src/screens/Settings/components/SettingsList.js b/src/screens/Settings/components/SettingsList.js new file mode 100644 index 0000000000..b530676595 --- /dev/null +++ b/src/screens/Settings/components/SettingsList.js @@ -0,0 +1,192 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { View, } from 'react-native'; +import { HITSLOP_10 } from '#/lib/constants'; +import { atoms as a, useTheme } from '#/alf'; +import * as Button from '#/components/Button'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon } from '#/components/icons/Chevron'; +import { Link } from '#/components/Link'; +import { createPortalGroup } from '#/components/Portal'; +import { Text } from '#/components/Typography'; +var ItemContext = createContext({ + destructive: false, + withinGroup: false, +}); +ItemContext.displayName = 'SettingsListItemContext'; +var Portal = createPortalGroup(); +export function Container(_a) { + var children = _a.children; + return _jsx(View, { style: [a.flex_1, a.py_md], children: children }); +} +/** + * This uses `Portal` magic ✨ to render the icons and title correctly. ItemIcon and ItemText components + * get teleported to the top row, leaving the rest of the children in the bottom row. + */ +export function Group(_a) { + var children = _a.children, _b = _a.destructive, destructive = _b === void 0 ? false : _b, _c = _a.iconInset, iconInset = _c === void 0 ? true : _c, style = _a.style, contentContainerStyle = _a.contentContainerStyle; + var context = useMemo(function () { return ({ destructive: destructive, withinGroup: true }); }, [destructive]); + return (_jsx(View, { style: [a.w_full, style], children: _jsx(Portal.Provider, { children: _jsxs(ItemContext.Provider, { value: context, children: [_jsx(Item, { style: [a.pb_2xs, { minHeight: 42 }], children: _jsx(Portal.Outlet, {}) }), _jsx(Item, { style: [ + a.flex_col, + a.pt_2xs, + a.align_start, + a.gap_0, + contentContainerStyle, + ], iconInset: iconInset, children: children })] }) }) })); +} +export function Item(_a) { + var children = _a.children, destructive = _a.destructive, _b = _a.iconInset, iconInset = _b === void 0 ? false : _b, style = _a.style; + var context = useContext(ItemContext); + var childContext = useMemo(function () { + if (typeof destructive !== 'boolean') + return context; + return __assign(__assign({}, context), { destructive: destructive }); + }, [context, destructive]); + return (_jsx(View, { style: [ + a.px_xl, + a.py_sm, + a.align_center, + a.gap_sm, + a.w_full, + a.flex_row, + { minHeight: 48 }, + iconInset && { + paddingLeft: + // existing padding + a.pl_xl.paddingLeft + + // icon + 24 + + // gap + a.gap_sm.gap, + }, + style, + ], children: _jsx(ItemContext.Provider, { value: childContext, children: children }) })); +} +export function LinkItem(_a) { + var children = _a.children, _b = _a.destructive, destructive = _b === void 0 ? false : _b, contentContainerStyle = _a.contentContainerStyle, chevronColor = _a.chevronColor, props = __rest(_a, ["children", "destructive", "contentContainerStyle", "chevronColor"]); + var t = useTheme(); + return (_jsx(Link, __assign({}, props, { children: function (args) { return (_jsxs(Item, { destructive: destructive, style: [ + (args.hovered || args.pressed) && [t.atoms.bg_contrast_25], + contentContainerStyle, + ], children: [typeof children === 'function' ? children(args) : children, _jsx(Chevron, { color: chevronColor })] })); } }))); +} +export function PressableItem(_a) { + var children = _a.children, _b = _a.destructive, destructive = _b === void 0 ? false : _b, contentContainerStyle = _a.contentContainerStyle, hoverStyle = _a.hoverStyle, props = __rest(_a, ["children", "destructive", "contentContainerStyle", "hoverStyle"]); + var t = useTheme(); + return (_jsx(Button.Button, __assign({}, props, { children: function (args) { return (_jsx(Item, { destructive: destructive, style: [ + (args.hovered || args.pressed) && [ + t.atoms.bg_contrast_25, + hoverStyle, + ], + contentContainerStyle, + ], children: typeof children === 'function' ? children(args) : children })); } }))); +} +export function ItemIcon(_a) { + var Comp = _a.icon, _b = _a.size, size = _b === void 0 ? 'lg' : _b, colorProp = _a.color; + var t = useTheme(); + var _c = useContext(ItemContext), destructive = _c.destructive, withinGroup = _c.withinGroup; + /* + * Copied here from icons/common.tsx so we can tweak if we need to, but + * also so that we can calculate transforms. + */ + var iconSize = { + '2xs': 8, + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, + '2xl': 32, + '3xl': 40, + }[size]; + var color = colorProp !== null && colorProp !== void 0 ? colorProp : (destructive ? t.palette.negative_500 : t.atoms.text.color); + var content = (_jsx(View, { style: [a.z_20, { width: iconSize, height: iconSize }], children: _jsx(Comp, { width: iconSize, style: [{ color: color }] }) })); + if (withinGroup) { + return _jsx(Portal.Portal, { children: content }); + } + else { + return content; + } +} +export function ItemText(_a) { + var style = _a.style, props = __rest(_a, ["style"]); + var t = useTheme(); + var _b = useContext(ItemContext), destructive = _b.destructive, withinGroup = _b.withinGroup; + var content = (_jsx(Button.ButtonText, __assign({ style: [ + a.text_md, + a.font_normal, + a.text_left, + a.flex_1, + destructive ? { color: t.palette.negative_500 } : t.atoms.text, + style, + ] }, props))); + if (withinGroup) { + return _jsx(Portal.Portal, { children: content }); + } + else { + return content; + } +} +export function Divider(_a) { + var style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.border_t, + t.atoms.border_contrast_low, + a.w_full, + a.my_sm, + style, + ] })); +} +export function Chevron(_a) { + var colorProp = _a.color; + var destructive = useContext(ItemContext).destructive; + var t = useTheme(); + var color = colorProp !== null && colorProp !== void 0 ? colorProp : (destructive ? t.palette.negative_500 : t.palette.contrast_500); + return _jsx(ItemIcon, { icon: ChevronRightIcon, size: "md", color: color }); +} +export function BadgeText(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + return (_jsx(Text, { style: [ + t.atoms.text_contrast_low, + a.text_md, + a.text_right, + a.leading_snug, + style, + ], numberOfLines: 1, children: children })); +} +export function BadgeButton(_a) { + var label = _a.label, onPress = _a.onPress; + var t = useTheme(); + return (_jsx(Button.Button, { label: label, onPress: onPress, hitSlop: HITSLOP_10, children: function (_a) { + var pressed = _a.pressed; + return (_jsx(Button.ButtonText, { style: [ + a.text_md, + a.font_normal, + a.text_right, + { color: pressed ? t.palette.contrast_300 : t.palette.primary_500 }, + ], children: label })); + } })); +} diff --git a/src/screens/Signup/BackNextButtons.js b/src/screens/Signup/BackNextButtons.js new file mode 100644 index 0000000000..bd4d88fb91 --- /dev/null +++ b/src/screens/Signup/BackNextButtons.js @@ -0,0 +1,19 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Loader } from '#/components/Loader'; +export function BackNextButtons(_a) { + var hideNext = _a.hideNext, showRetry = _a.showRetry, isLoading = _a.isLoading, isNextDisabled = _a.isNextDisabled, onBackPress = _a.onBackPress, onNextPress = _a.onNextPress, onRetryPress = _a.onRetryPress, overrideNextText = _a.overrideNextText; + var _ = useLingui()._; + return (_jsxs(View, { style: [a.flex_row, a.justify_between, a.pb_lg, a.pt_3xl], children: [_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go back to previous step"], ["Go back to previous step"])))), variant: "solid", color: "secondary", size: "large", onPress: onBackPress, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Back" }) }) }), !hideNext && + (showRetry ? (_jsxs(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Press to retry"], ["Press to retry"])))), variant: "solid", color: "primary", size: "large", onPress: onRetryPress, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), isLoading && _jsx(ButtonIcon, { icon: Loader })] })) : (_jsxs(Button, { testID: "nextBtn", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Continue to next step"], ["Continue to next step"])))), variant: "solid", color: "primary", size: "large", disabled: isLoading || isNextDisabled, onPress: onNextPress, children: [_jsx(ButtonText, { children: overrideNextText ? overrideNextText : _jsx(Trans, { children: "Next" }) }), isLoading && _jsx(ButtonIcon, { icon: Loader })] })))] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/screens/Signup/StepCaptcha/CaptchaWebView.js b/src/screens/Signup/StepCaptcha/CaptchaWebView.js new file mode 100644 index 0000000000..c1a049bf72 --- /dev/null +++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.js @@ -0,0 +1,73 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useEffect, useMemo, useRef } from 'react'; +import { WebView } from 'react-native-webview'; +var ALLOWED_HOSTS = [ + 'bsky.social', + 'bsky.app', + 'staging.bsky.app', + 'staging.bsky.dev', + 'app.staging.bsky.dev', + 'js.hcaptcha.com', + 'newassets.hcaptcha.com', + 'api2.hcaptcha.com', +]; +var MIN_DELAY = 3500; +export function CaptchaWebView(_a) { + var url = _a.url, stateParam = _a.stateParam, state = _a.state, onComplete = _a.onComplete, onSuccess = _a.onSuccess, onError = _a.onError; + var startedAt = useRef(Date.now()); + var successTo = useRef(undefined); + useEffect(function () { + return function () { + if (successTo.current) { + clearTimeout(successTo.current); + } + }; + }, []); + var redirectHost = useMemo(function () { + if (!(state === null || state === void 0 ? void 0 : state.serviceUrl)) + return 'bsky.app'; + return (state === null || state === void 0 ? void 0 : state.serviceUrl) && + new URL(state === null || state === void 0 ? void 0 : state.serviceUrl).host === 'staging.bsky.dev' + ? 'app.staging.bsky.dev' + : 'bsky.app'; + }, [state === null || state === void 0 ? void 0 : state.serviceUrl]); + var wasSuccessful = useRef(false); + var onShouldStartLoadWithRequest = function (event) { + var urlp = new URL(event.url); + return ALLOWED_HOSTS.includes(urlp.host); + }; + var onNavigationStateChange = function (e) { + if (wasSuccessful.current) + return; + var urlp = new URL(e.url); + if (urlp.host !== redirectHost || urlp.pathname === '/gate/signup') + return; + var code = urlp.searchParams.get('code'); + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError({ error: 'Invalid state or code' }); + return; + } + // We want to delay the completion of this screen ever so slightly so that it doesn't appear to be a glitch if it completes too fast + wasSuccessful.current = true; + onComplete(); + var now = Date.now(); + var timeTaken = now - startedAt.current; + if (timeTaken < MIN_DELAY) { + successTo.current = setTimeout(function () { + onSuccess(code); + }, MIN_DELAY - timeTaken); + } + else { + onSuccess(code); + } + }; + return (_jsx(WebView, { source: { uri: url }, javaScriptEnabled: true, style: { + flex: 1, + backgroundColor: 'transparent', + borderRadius: 10, + }, onShouldStartLoadWithRequest: onShouldStartLoadWithRequest, onNavigationStateChange: onNavigationStateChange, scrollEnabled: false, onError: function (e) { + onError(e.nativeEvent); + }, onHttpError: function (e) { + onError(e.nativeEvent); + } })); +} diff --git a/src/screens/Signup/StepCaptcha/CaptchaWebView.web.js b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.js new file mode 100644 index 0000000000..ab864a3c66 --- /dev/null +++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.js @@ -0,0 +1,53 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { StyleSheet } from 'react-native'; +// @ts-ignore web only, we will always redirect to the app on web (CORS) +var REDIRECT_HOST = new URL(window.location.href).host; +export function CaptchaWebView(_a) { + var url = _a.url, stateParam = _a.stateParam, onSuccess = _a.onSuccess, onError = _a.onError; + React.useEffect(function () { + var timeout = setTimeout(function () { + onError({ + errorMessage: 'User did not complete the captcha within 30 seconds', + }); + }, 30e3); + return function () { + clearTimeout(timeout); + }; + }, [onError]); + var onLoad = React.useCallback(function () { + var _a; + // @ts-ignore web + var frame = document.getElementById('captcha-iframe'); + try { + // @ts-ignore web + var href = (_a = frame === null || frame === void 0 ? void 0 : frame.contentWindow) === null || _a === void 0 ? void 0 : _a.location.href; + if (!href) + return; + var urlp = new URL(href); + // This shouldn't happen with CORS protections, but for good measure + if (urlp.host !== REDIRECT_HOST) + return; + var code = urlp.searchParams.get('code'); + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError({ error: 'Invalid state or code' }); + return; + } + onSuccess(code); + } + catch (e) { + // We don't actually want to record an error here, because this will happen quite a bit. We will only be able to + // get hte href of the iframe if it's on our domain, so all the hcaptcha requests will throw here, although it's + // harmless. Our other indicators of time-to-complete and back press should be more reliable in catching issues. + } + }, [stateParam, onSuccess, onError]); + return (_jsx("iframe", { src: url, style: styles.iframe, id: "captcha-iframe", onLoad: onLoad })); +} +var styles = StyleSheet.create({ + iframe: { + flex: 1, + borderWidth: 0, + borderRadius: 10, + backgroundColor: 'transparent', + }, +}); diff --git a/src/screens/Signup/StepCaptcha/index.js b/src/screens/Signup/StepCaptcha/index.js new file mode 100644 index 0000000000..bead1f01e0 --- /dev/null +++ b/src/screens/Signup/StepCaptcha/index.js @@ -0,0 +1,187 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useEffect, useState } from 'react'; +import { ActivityIndicator, Platform, View } from 'react-native'; +import ReactNativeDeviceAttest from 'react-native-device-attest'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { nanoid } from 'nanoid/non-secure'; +import { createFullHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { useSignupContext } from '#/screens/Signup/state'; +import { CaptchaWebView } from '#/screens/Signup/StepCaptcha/CaptchaWebView'; +import { atoms as a, useTheme } from '#/alf'; +import { FormError } from '#/components/forms/FormError'; +import { useAnalytics } from '#/analytics'; +import { GCP_PROJECT_ID, IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB } from '#/env'; +import { BackNextButtons } from '../BackNextButtons'; +var CAPTCHA_PATH = IS_WEB || GCP_PROJECT_ID === 0 + ? '/gate/signup' + : '/gate/signup/attempt-attest'; +export function StepCaptcha() { + if (IS_WEB) { + return _jsx(StepCaptchaInner, {}); + } + else { + return _jsx(StepCaptchaNative, {}); + } +} +export function StepCaptchaNative() { + var _this = this; + var _a = useState(), token = _a[0], setToken = _a[1]; + var _b = useState(), payload = _b[0], setPayload = _b[1]; + var _c = useState(false), ready = _c[0], setReady = _c[1]; + useEffect(function () { + ; + (function () { return __awaiter(_this, void 0, void 0, function () { + var token_1, _a, token_2, payload_1, e_1; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + logger.debug('trying to generate attestation token...'); + _b.label = 1; + case 1: + _b.trys.push([1, 6, 7, 8]); + if (!IS_IOS) return [3 /*break*/, 3]; + logger.debug('starting to generate devicecheck token...'); + return [4 /*yield*/, ReactNativeDeviceAttest.getDeviceCheckToken()]; + case 2: + token_1 = _b.sent(); + setToken(token_1); + logger.debug("generated devicecheck token: ".concat(token_1)); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, ReactNativeDeviceAttest.getIntegrityToken('signup')]; + case 4: + _a = _b.sent(), token_2 = _a.token, payload_1 = _a.payload; + setToken(token_2); + setPayload(base64UrlEncode(payload_1)); + _b.label = 5; + case 5: return [3 /*break*/, 8]; + case 6: + e_1 = _b.sent(); + logger.error(e_1); + return [3 /*break*/, 8]; + case 7: + setReady(true); + return [7 /*endfinally*/]; + case 8: return [2 /*return*/]; + } + }); + }); })(); + }, []); + if (!ready) { + return _jsx(View, {}); + } + return _jsx(StepCaptchaInner, { token: token, payload: payload }); +} +function StepCaptchaInner(_a) { + var token = _a.token, payload = _a.payload; + var _ = useLingui()._; + var ax = useAnalytics(); + var theme = useTheme(); + var _b = useSignupContext(), state = _b.state, dispatch = _b.dispatch; + var _c = React.useState(false), completed = _c[0], setCompleted = _c[1]; + var stateParam = React.useMemo(function () { return nanoid(15); }, []); + var url = React.useMemo(function () { + var newUrl = new URL(state.serviceUrl); + newUrl.pathname = CAPTCHA_PATH; + newUrl.searchParams.set('handle', createFullHandle(state.handle, state.userDomain)); + newUrl.searchParams.set('state', stateParam); + newUrl.searchParams.set('colorScheme', theme.name); + if (IS_NATIVE && token) { + newUrl.searchParams.set('platform', Platform.OS); + newUrl.searchParams.set('token', token); + if (IS_ANDROID && payload) { + newUrl.searchParams.set('payload', payload); + } + } + return newUrl.href; + }, [ + state.serviceUrl, + state.handle, + state.userDomain, + stateParam, + theme.name, + token, + payload, + ]); + var onSuccess = React.useCallback(function (code) { + setCompleted(true); + ax.metric('signup:captchaSuccess', {}); + dispatch({ + type: 'submit', + task: { verificationCode: code, mutableProcessed: false }, + }); + }, [ax, dispatch]); + var onError = React.useCallback(function (error) { + dispatch({ + type: 'setError', + value: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Error receiving captcha response."], ["Error receiving captcha response."])))), + }); + ax.metric('signup:captchaFailure', {}); + logger.error('Signup Flow Error', { + registrationHandle: state.handle, + error: error, + }); + }, [_, ax, dispatch, state.handle]); + var onBackPress = React.useCallback(function () { + logger.error('Signup Flow Error', { + errorMessage: 'User went back from captcha step. Possibly encountered an error.', + registrationHandle: state.handle, + }); + dispatch({ type: 'prev' }); + }, [dispatch, state.handle]); + return (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.gap_lg, a.pt_lg], children: [_jsx(View, { style: [ + a.w_full, + a.overflow_hidden, + { minHeight: 510 }, + completed && [a.align_center, a.justify_center], + ], children: !completed ? (_jsx(CaptchaWebView, { url: url, stateParam: stateParam, state: state, onComplete: function () { return setCompleted(true); }, onSuccess: onSuccess, onError: onError })) : (_jsx(ActivityIndicator, { size: "large" })) }), _jsx(FormError, { error: state.error })] }), _jsx(BackNextButtons, { hideNext: true, isLoading: state.isLoading, onBackPress: onBackPress })] })); +} +function base64UrlEncode(data) { + var encoder = new TextEncoder(); + var bytes = encoder.encode(data); + var binaryString = String.fromCharCode.apply(String, bytes); + var base64 = btoa(binaryString); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]/g, ''); +} +var templateObject_1; diff --git a/src/screens/Signup/StepHandle/HandleSuggestions.js b/src/screens/Signup/StepHandle/HandleSuggestions.js new file mode 100644 index 0000000000..70e0d09f54 --- /dev/null +++ b/src/screens/Signup/StepHandle/HandleSuggestions.js @@ -0,0 +1,48 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import Animated, { Easing, FadeInDown, FadeOut } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, native, useTheme } from '#/alf'; +import { borderRadius } from '#/alf/tokens'; +import { Button } from '#/components/Button'; +import { Text } from '#/components/Typography'; +export function HandleSuggestions(_a) { + var suggestions = _a.suggestions, onSelect = _a.onSelect; + var t = useTheme(); + var _ = useLingui()._; + return (_jsx(Animated.View, { entering: native(FadeInDown.easing(Easing.out(Easing.exp))), exiting: native(FadeOut), style: [ + a.flex_1, + a.border, + a.rounded_sm, + t.atoms.shadow_sm, + t.atoms.bg, + t.atoms.border_contrast_low, + a.mt_xs, + a.z_50, + a.w_full, + a.zoom_fade_in, + ], children: suggestions.map(function (suggestion, index) { return (_jsxs(Button, { label: _(msg({ + message: "Select ".concat(suggestion.handle), + comment: "Accessibility label for a username suggestion in the account creation flow", + })), onPress: function () { return onSelect(suggestion); }, hoverStyle: [t.atoms.bg_contrast_25], style: [ + a.w_full, + a.flex_row, + a.align_center, + a.justify_between, + a.p_md, + a.border_b, + t.atoms.border_contrast_low, + index === 0 && { + borderTopStartRadius: borderRadius.sm, + borderTopEndRadius: borderRadius.sm, + }, + index === suggestions.length - 1 && [ + { + borderBottomStartRadius: borderRadius.sm, + borderBottomEndRadius: borderRadius.sm, + }, + a.border_b_0, + ], + ], children: [_jsx(Text, { style: [a.text_md], children: suggestion.handle }), _jsx(Text, { style: [a.text_sm, { color: t.palette.positive_700 }], children: _jsx(Trans, { comment: "Shown next to an available username suggestion in the account creation flow", children: "Available" }) })] }, index)); }) })); +} diff --git a/src/screens/Signup/StepHandle/index.js b/src/screens/Signup/StepHandle/index.js new file mode 100644 index 0000000000..662e976ab4 --- /dev/null +++ b/src/screens/Signup/StepHandle/index.js @@ -0,0 +1,185 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import Animated, { FadeIn, FadeOut, LayoutAnimationConfig, LinearTransition, } from 'react-native-reanimated'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { createFullHandle, MAX_SERVICE_HANDLE_LENGTH, validateServiceHandle, } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { checkHandleAvailability, useHandleAvailabilityQuery, } from '#/state/queries/handle-availability'; +import { useSignupContext } from '#/screens/Signup/state'; +import { atoms as a, native, useTheme } from '#/alf'; +import * as TextField from '#/components/forms/TextField'; +import { useThrottledValue } from '#/components/hooks/useThrottledValue'; +import { At_Stroke2_Corner0_Rounded as AtIcon } from '#/components/icons/At'; +import { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { BackNextButtons } from '../BackNextButtons'; +import { HandleSuggestions } from './HandleSuggestions'; +export function StepHandle() { + var _this = this; + var _a, _b; + var _ = useLingui()._; + var ax = useAnalytics(); + var t = useTheme(); + var _c = useSignupContext(), state = _c.state, dispatch = _c.dispatch; + var _d = useState(state.handle), draftValue = _d[0], setDraftValue = _d[1]; + var isNextLoading = useThrottledValue(state.isLoading, 500); + var validCheck = validateServiceHandle(draftValue, state.userDomain); + var _e = useHandleAvailabilityQuery({ + username: draftValue, + serviceDid: (_b = (_a = state.serviceDescription) === null || _a === void 0 ? void 0 : _a.did) !== null && _b !== void 0 ? _b : 'UNKNOWN', + serviceDomain: state.userDomain, + birthDate: state.dateOfBirth.toISOString(), + email: state.email, + enabled: validCheck.overall, + }), debouncedDraftValue = _e.debouncedUsername, queryEnabled = _e.enabled, _f = _e.query, isHandleAvailable = _f.data, isPending = _f.isPending; + var onNextPress = function () { return __awaiter(_this, void 0, void 0, function () { + var handle, handleAvailable, error_1; + var _a, _b, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + handle = draftValue.trim(); + dispatch({ + type: 'setHandle', + value: handle, + }); + if (!validCheck.overall) { + return [2 /*return*/]; + } + dispatch({ type: 'setIsLoading', value: true }); + _e.label = 1; + case 1: + _e.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, checkHandleAvailability(createFullHandle(handle, state.userDomain), (_b = (_a = state.serviceDescription) === null || _a === void 0 ? void 0 : _a.did) !== null && _b !== void 0 ? _b : 'UNKNOWN', {})]; + case 2: + handleAvailable = (_e.sent()).available; + if (!handleAvailable) { + ax.metric('signup:handleTaken', { typeahead: false }); + dispatch({ + type: 'setError', + value: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["That username is already taken"], ["That username is already taken"])))), + field: 'handle', + }); + return [2 /*return*/]; + } + else { + ax.metric('signup:handleAvailable', { typeahead: false }); + } + return [3 /*break*/, 5]; + case 3: + error_1 = _e.sent(); + logger.error('Failed to check handle availability on next press', { + safeMessage: error_1, + }); + return [3 /*break*/, 5]; + case 4: + dispatch({ type: 'setIsLoading', value: false }); + return [7 /*endfinally*/]; + case 5: + ax.metric('signup:nextPressed', { + activeStep: state.activeStep, + phoneVerificationRequired: (_c = state.serviceDescription) === null || _c === void 0 ? void 0 : _c.phoneVerificationRequired, + }); + // phoneVerificationRequired is actually whether a captcha is required + if (!((_d = state.serviceDescription) === null || _d === void 0 ? void 0 : _d.phoneVerificationRequired)) { + dispatch({ + type: 'submit', + task: { verificationCode: undefined, mutableProcessed: false }, + }); + return [2 /*return*/]; + } + dispatch({ type: 'next' }); + return [2 /*return*/]; + } + }); + }); }; + var onBackPress = function () { + var handle = draftValue.trim(); + dispatch({ + type: 'setHandle', + value: handle, + }); + dispatch({ type: 'prev' }); + ax.metric('signup:backPressed', { activeStep: state.activeStep }); + }; + var hasDebounceSettled = draftValue === debouncedDraftValue; + var isHandleTaken = !isPending && + queryEnabled && + isHandleAvailable && + !isHandleAvailable.available; + var isNotReady = isPending || !hasDebounceSettled; + var isNextDisabled = !validCheck.overall || !!state.error || isNotReady ? true : isHandleTaken; + var textFieldInvalid = isHandleTaken || + !validCheck.frontLengthNotTooLong || + !validCheck.handleChars || + !validCheck.hyphenStartOrEnd || + !validCheck.totalLength; + return (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.gap_sm, a.pt_lg, a.z_10], children: [_jsx(View, { children: _jsxs(TextField.Root, { isInvalid: textFieldInvalid, children: [_jsx(TextField.Icon, { icon: AtIcon }), _jsx(TextField.Input, { testID: "handleInput", onChangeText: function (val) { + if (state.error) { + dispatch({ type: 'setError', value: '' }); + } + setDraftValue(val.toLocaleLowerCase()); + }, label: state.userDomain, value: draftValue, keyboardType: "ascii-capable" // fix for iOS replacing -- with — + , autoCapitalize: "none", autoCorrect: false, autoFocus: true, autoComplete: "off" }), draftValue.length > 0 && (_jsx(TextField.GhostText, { value: state.userDomain, children: draftValue })), (isHandleAvailable === null || isHandleAvailable === void 0 ? void 0 : isHandleAvailable.available) && (_jsx(CheckIcon, { testID: "handleAvailableCheck", style: [{ color: t.palette.positive_500 }, a.z_20] }))] }) }), _jsx(LayoutAnimationConfig, { skipEntering: true, skipExiting: true, children: _jsxs(View, { style: [a.gap_xs], children: [state.error && (_jsx(Requirement, { children: _jsx(RequirementText, { children: state.error }) })), isHandleTaken && validCheck.overall && (_jsxs(_Fragment, { children: [_jsx(Requirement, { children: _jsx(RequirementText, { children: _jsxs(Trans, { children: [createFullHandle(draftValue, state.userDomain), " is not available"] }) }) }), isHandleAvailable.suggestions && + isHandleAvailable.suggestions.length > 0 && (_jsx(HandleSuggestions, { suggestions: isHandleAvailable.suggestions, onSelect: function (suggestion) { + setDraftValue(suggestion.handle.slice(0, state.userDomain.length * -1)); + ax.metric('signup:handleSuggestionSelected', { + method: suggestion.method, + }); + } }))] })), (!validCheck.handleChars || !validCheck.hyphenStartOrEnd) && (_jsx(Requirement, { children: !validCheck.hyphenStartOrEnd ? (_jsx(RequirementText, { children: _jsx(Trans, { children: "Username cannot begin or end with a hyphen" }) })) : (_jsx(RequirementText, { children: _jsx(Trans, { children: "Username must only contain letters (a-z), numbers, and hyphens" }) })) })), _jsx(Requirement, { children: (!validCheck.frontLengthNotTooLong || + !validCheck.totalLength) && (_jsx(RequirementText, { children: _jsxs(Trans, { children: ["Username cannot be longer than", ' ', _jsx(Plural, { value: MAX_SERVICE_HANDLE_LENGTH, other: "# characters" })] }) })) })] }) })] }), _jsx(Animated.View, { layout: native(LinearTransition), children: _jsx(BackNextButtons, { isLoading: isNextLoading, isNextDisabled: isNextDisabled, onBackPress: onBackPress, onNextPress: onNextPress }) })] })); +} +function Requirement(_a) { + var children = _a.children; + return (_jsx(Animated.View, { style: [a.w_full], layout: native(LinearTransition), entering: native(FadeIn), exiting: native(FadeOut), children: children })); +} +function RequirementText(_a) { + var children = _a.children; + var t = useTheme(); + return (_jsx(Text, { style: [a.text_sm, a.flex_1, { color: t.palette.negative_500 }], children: children })); +} +var templateObject_1; diff --git a/src/screens/Signup/StepInfo/Policies.js b/src/screens/Signup/StepInfo/Policies.js new file mode 100644 index 0000000000..8bc1c327c2 --- /dev/null +++ b/src/screens/Signup/StepInfo/Policies.js @@ -0,0 +1,47 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { atoms as a, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export var Policies = function (_a) { + var _b, _c; + var serviceDescription = _a.serviceDescription; + var t = useTheme(); + var _ = useLingui()._; + if (!serviceDescription) { + return _jsx(View, {}); + } + var tos = validWebLink((_b = serviceDescription.links) === null || _b === void 0 ? void 0 : _b.termsOfService); + var pp = validWebLink((_c = serviceDescription.links) === null || _c === void 0 ? void 0 : _c.privacyPolicy); + if (!tos && !pp) { + return (_jsx(View, { style: [a.gap_sm], children: _jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "This service has not provided terms of service or a privacy policy." }) }) })); + } + var els; + if (tos && pp) { + els = (_jsxs(Trans, { children: ["By creating an account you agree to the", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Read the Bluesky Terms of Service"], ["Read the Bluesky Terms of Service"])))), to: tos, children: "Terms of Service" }, "tos"), ' ', "and", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Read the Bluesky Privacy Policy"], ["Read the Bluesky Privacy Policy"])))), to: pp, children: "Privacy Policy" }, "pp"), "."] })); + } + else if (tos) { + els = (_jsxs(Trans, { children: ["By creating an account you agree to the", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Read the Bluesky Terms of Service"], ["Read the Bluesky Terms of Service"])))), to: tos, children: "Terms of Service" }, "tos"), "."] })); + } + else if (pp) { + els = (_jsxs(Trans, { children: ["By creating an account you agree to the", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Read the Bluesky Privacy Policy"], ["Read the Bluesky Privacy Policy"])))), to: pp, children: "Privacy Policy" }, "pp"), "."] })); + } + else { + return null; + } + return els ? (_jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_medium], children: els })) : null; +}; +function validWebLink(url) { + return url && (url.startsWith('http://') || url.startsWith('https://')) + ? url + : undefined; +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Signup/StepInfo/index.js b/src/screens/Signup/StepInfo/index.js new file mode 100644 index 0000000000..5f4f5d78a4 --- /dev/null +++ b/src/screens/Signup/StepInfo/index.js @@ -0,0 +1,200 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useRef } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import * as EmailValidator from 'email-validator'; +import { isEmailMaybeInvalid } from '#/lib/strings/email'; +import { logger } from '#/logger'; +import { useSignupContext } from '#/screens/Signup/state'; +import { Policies } from '#/screens/Signup/StepInfo/Policies'; +import { atoms as a, native } from '#/alf'; +import * as Admonition from '#/components/Admonition'; +import * as Dialog from '#/components/Dialog'; +import { DeviceLocationRequestDialog } from '#/components/dialogs/DeviceLocationRequestDialog'; +import * as DateField from '#/components/forms/DateField'; +import { FormError } from '#/components/forms/FormError'; +import { HostingProvider } from '#/components/forms/HostingProvider'; +import * as TextField from '#/components/forms/TextField'; +import { Envelope_Stroke2_Corner0_Rounded as Envelope } from '#/components/icons/Envelope'; +import { Lock_Stroke2_Corner0_Rounded as Lock } from '#/components/icons/Lock'; +import { Ticket_Stroke2_Corner0_Rounded as Ticket } from '#/components/icons/Ticket'; +import { createStaticClick, SimpleInlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { usePreemptivelyCompleteActivePolicyUpdate } from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate'; +import * as Toast from '#/components/Toast'; +import { isUnderAge, MIN_ACCESS_AGE, useAgeAssuranceRegionConfigWithFallback, } from '#/ageAssurance/util'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +import { useDeviceGeolocationApi, useIsDeviceGeolocationGranted, } from '#/geolocation'; +import { BackNextButtons } from '../BackNextButtons'; +function sanitizeDate(date) { + if (!date || date.toString() === 'Invalid Date') { + logger.error("Create account: handled invalid date for birthDate", { + hasDate: !!date, + }); + return new Date(); + } + return date; +} +export function StepInfo(_a) { + var onPressBack = _a.onPressBack, isServerError = _a.isServerError, refetchServer = _a.refetchServer, isLoadingStarterPack = _a.isLoadingStarterPack; + var _ = useLingui()._; + var ax = useAnalytics(); + var _b = useSignupContext(), state = _b.state, dispatch = _b.dispatch; + var preemptivelyCompleteActivePolicyUpdate = usePreemptivelyCompleteActivePolicyUpdate(); + var inviteCodeValueRef = useRef(state.inviteCode); + var emailValueRef = useRef(state.email); + var prevEmailValueRef = useRef(state.email); + var passwordValueRef = useRef(state.password); + var emailInputRef = useRef(null); + var passwordInputRef = useRef(null); + var birthdateInputRef = useRef(null); + var aaRegionConfig = useAgeAssuranceRegionConfigWithFallback(); + var setDeviceGeolocation = useDeviceGeolocationApi().setDeviceGeolocation; + var locationControl = Dialog.useDialogControl(); + var isOverRegionMinAccessAge = state.dateOfBirth + ? !isUnderAge(state.dateOfBirth.toISOString(), aaRegionConfig.minAccessAge) + : true; + var isOverAppMinAccessAge = state.dateOfBirth + ? !isUnderAge(state.dateOfBirth.toISOString(), MIN_ACCESS_AGE) + : true; + var isOverMinAdultAge = state.dateOfBirth + ? !isUnderAge(state.dateOfBirth.toISOString(), 18) + : true; + var isDeviceGeolocationGranted = useIsDeviceGeolocationGranted(); + var _c = React.useState(false), hasWarnedEmail = _c[0], setHasWarnedEmail = _c[1]; + var tldtsRef = React.useRef(undefined); + React.useEffect(function () { + // @ts-expect-error - valid path + import('tldts/dist/index.cjs.min.js').then(function (tldts) { + tldtsRef.current = tldts; + }); + // This will get used in the avatar creator a few steps later, so lets preload it now + // @ts-expect-error - valid path + import('react-native-view-shot/src/index'); + }, []); + var onNextPress = function () { + var _a; + var inviteCode = inviteCodeValueRef.current; + var email = emailValueRef.current; + var emailChanged = prevEmailValueRef.current !== email; + var password = passwordValueRef.current; + if (!isOverRegionMinAccessAge) { + return; + } + if (((_a = state.serviceDescription) === null || _a === void 0 ? void 0 : _a.inviteCodeRequired) && !inviteCode) { + return dispatch({ + type: 'setError', + value: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Please enter your invite code."], ["Please enter your invite code."])))), + field: 'invite-code', + }); + } + if (!email) { + return dispatch({ + type: 'setError', + value: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please enter your email."], ["Please enter your email."])))), + field: 'email', + }); + } + if (!EmailValidator.validate(email)) { + return dispatch({ + type: 'setError', + value: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Your email appears to be invalid."], ["Your email appears to be invalid."])))), + field: 'email', + }); + } + if (emailChanged && tldtsRef.current) { + if (isEmailMaybeInvalid(email, tldtsRef.current)) { + prevEmailValueRef.current = email; + setHasWarnedEmail(true); + return dispatch({ + type: 'setError', + value: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Please double-check that you have entered your email address correctly."], ["Please double-check that you have entered your email address correctly."])))), + }); + } + } + else if (hasWarnedEmail) { + setHasWarnedEmail(false); + } + prevEmailValueRef.current = email; + if (!password) { + return dispatch({ + type: 'setError', + value: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Please choose your password."], ["Please choose your password."])))), + field: 'password', + }); + } + if (password.length < 8) { + return dispatch({ + type: 'setError', + value: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Your password must be at least 8 characters long."], ["Your password must be at least 8 characters long."])))), + field: 'password', + }); + } + preemptivelyCompleteActivePolicyUpdate(); + dispatch({ type: 'setInviteCode', value: inviteCode }); + dispatch({ type: 'setEmail', value: email }); + dispatch({ type: 'setPassword', value: password }); + dispatch({ type: 'next' }); + ax.metric('signup:nextPressed', { + activeStep: state.activeStep, + }); + }; + return (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.gap_md, a.pt_lg], children: [_jsx(FormError, { error: state.error }), _jsx(HostingProvider, { minimal: true, serviceUrl: state.serviceUrl, onSelectServiceUrl: function (v) { return dispatch({ type: 'setServiceUrl', value: v }); } }), state.isLoading || isLoadingStarterPack ? (_jsx(View, { style: [a.align_center], children: _jsx(Loader, { size: "xl" }) })) : state.serviceDescription ? (_jsxs(_Fragment, { children: [state.serviceDescription.inviteCodeRequired && (_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Invite code" }) }), _jsxs(TextField.Root, { isInvalid: state.errorField === 'invite-code', children: [_jsx(TextField.Icon, { icon: Ticket }), _jsx(TextField.Input, { onChangeText: function (value) { + inviteCodeValueRef.current = value.trim(); + if (state.errorField === 'invite-code' && + value.trim().length > 0) { + dispatch({ type: 'clearError' }); + } + }, label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Required for this provider"], ["Required for this provider"])))), defaultValue: state.inviteCode, autoCapitalize: "none", autoComplete: "email", keyboardType: "email-address", returnKeyType: "next", submitBehavior: native('submit'), onSubmitEditing: native(function () { var _a; return (_a = emailInputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }) })] })] })), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Email" }) }), _jsxs(TextField.Root, { isInvalid: state.errorField === 'email', children: [_jsx(TextField.Icon, { icon: Envelope }), _jsx(TextField.Input, { testID: "emailInput", inputRef: emailInputRef, onChangeText: function (value) { + emailValueRef.current = value.trim(); + if (hasWarnedEmail) { + setHasWarnedEmail(false); + } + if (state.errorField === 'email' && + value.trim().length > 0 && + EmailValidator.validate(value.trim())) { + dispatch({ type: 'clearError' }); + } + }, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Enter your email address"], ["Enter your email address"])))), defaultValue: state.email, autoCapitalize: "none", autoComplete: "email", keyboardType: "email-address", returnKeyType: "next", submitBehavior: native('submit'), onSubmitEditing: native(function () { var _a; return (_a = passwordInputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }) })] })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Password" }) }), _jsxs(TextField.Root, { isInvalid: state.errorField === 'password', children: [_jsx(TextField.Icon, { icon: Lock }), _jsx(TextField.Input, { testID: "passwordInput", inputRef: passwordInputRef, onChangeText: function (value) { + passwordValueRef.current = value; + if (state.errorField === 'password' && value.length >= 8) { + dispatch({ type: 'clearError' }); + } + }, label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Choose your password"], ["Choose your password"])))), defaultValue: state.password, secureTextEntry: true, autoComplete: "new-password", autoCapitalize: "none", returnKeyType: "next", submitBehavior: native('blurAndSubmit'), onSubmitEditing: native(function () { var _a; return (_a = birthdateInputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }), passwordRules: "minlength: 8;" })] })] }), _jsxs(View, { children: [_jsx(DateField.LabelText, { children: _jsx(Trans, { children: "Your birth date" }) }), _jsx(DateField.DateField, { testID: "date", inputRef: birthdateInputRef, value: state.dateOfBirth, onChangeDate: function (date) { + dispatch({ + type: 'setDateOfBirth', + value: sanitizeDate(new Date(date)), + }); + }, label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Date of birth"], ["Date of birth"])))), accessibilityHint: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Select your date of birth"], ["Select your date of birth"])))), maximumDate: new Date() })] }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Policies, { serviceDescription: state.serviceDescription }), !isOverRegionMinAccessAge || !isOverAppMinAccessAge ? (_jsx(Admonition.Outer, { type: "error", children: _jsxs(Admonition.Row, { children: [_jsx(Admonition.Icon, {}), _jsxs(Admonition.Content, { children: [_jsx(Admonition.Text, { children: !isOverAppMinAccessAge ? (_jsx(Plural, { value: MIN_ACCESS_AGE, other: "You must be # years of age or older to create an account." })) : (_jsx(Plural, { value: aaRegionConfig.minAccessAge, other: "You must be # years of age or older to create an account in your region." })) }), IS_NATIVE && + !isDeviceGeolocationGranted && + isOverAppMinAccessAge && (_jsx(Admonition.Text, { children: _jsxs(Trans, { children: ["Have we got your location wrong?", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Tap here to confirm your location with GPS."], ["Tap here to confirm your location with GPS."])))) }, createStaticClick(function () { + locationControl.open(); + }), { children: "Tap here to confirm your location with GPS." }))] }) }))] })] }) })) : !isOverMinAdultAge ? (_jsx(Admonition.Admonition, { type: "warning", children: _jsx(Trans, { children: "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." }) })) : undefined] }), IS_NATIVE && (_jsx(DeviceLocationRequestDialog, { control: locationControl, onLocationAcquired: function (props) { + props.closeDialog(function () { + // set this after close! + setDeviceGeolocation(props.geolocation); + Toast.show(_(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Your location has been updated."], ["Your location has been updated."])))), { + type: 'success', + }); + }); + } }))] })) : undefined] }), _jsx(BackNextButtons, { hideNext: !isOverRegionMinAccessAge, showRetry: isServerError, isLoading: state.isLoading, onBackPress: onPressBack, onNextPress: onNextPress, onRetryPress: refetchServer, overrideNextText: hasWarnedEmail ? _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["It's correct"], ["It's correct"])))) : undefined })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14; diff --git a/src/screens/Signup/index.js b/src/screens/Signup/index.js new file mode 100644 index 0000000000..b62a1f7ec9 --- /dev/null +++ b/src/screens/Signup/index.js @@ -0,0 +1,135 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect, useReducer, useState } from 'react'; +import { AppState, View } from 'react-native'; +import ReactNativeDeviceAttest from 'react-native-device-attest'; +import Animated, { FadeIn, LayoutAnimationConfig } from 'react-native-reanimated'; +import { AppBskyGraphStarterpack } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { FEEDBACK_FORM_URL } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useServiceQuery } from '#/state/queries/service'; +import { useStarterPackQuery } from '#/state/queries/starter-packs'; +import { useActiveStarterPack } from '#/state/shell/starter-pack'; +import { LoggedOutLayout } from '#/view/com/util/layouts/LoggedOutLayout'; +import { initialState, reducer, SignupContext, SignupStep, useSubmitSignup, } from '#/screens/Signup/state'; +import { StepCaptcha } from '#/screens/Signup/StepCaptcha'; +import { StepHandle } from '#/screens/Signup/StepHandle'; +import { StepInfo } from '#/screens/Signup/StepInfo'; +import { atoms as a, native, useBreakpoints, useTheme } from '#/alf'; +import { AppLanguageDropdown } from '#/components/AppLanguageDropdown'; +import { Divider } from '#/components/Divider'; +import { LinearGradientBackground } from '#/components/LinearGradientBackground'; +import { InlineLinkText } from '#/components/Link'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { GCP_PROJECT_ID, IS_ANDROID } from '#/env'; +import * as bsky from '#/types/bsky'; +export function Signup(_a) { + var _b; + var onPressBack = _a.onPressBack; + var ax = useAnalytics(); + var _ = useLingui()._; + var t = useTheme(); + var _c = useReducer(reducer, __assign(__assign({}, initialState), { analytics: ax })), state = _c[0], dispatch = _c[1]; + var gtMobile = useBreakpoints().gtMobile; + var submit = useSubmitSignup(); + useEffect(function () { + dispatch({ + type: 'setAnalytics', + value: ax, + }); + }, [ax]); + var activeStarterPack = useActiveStarterPack(); + var _d = useStarterPackQuery({ + uri: activeStarterPack === null || activeStarterPack === void 0 ? void 0 : activeStarterPack.uri, + }), starterPack = _d.data, isFetchingStarterPack = _d.isFetching, isErrorStarterPack = _d.isError; + var isFetchedAtMount = useState(starterPack != null)[0]; + var showStarterPackCard = (activeStarterPack === null || activeStarterPack === void 0 ? void 0 : activeStarterPack.uri) && !isFetchingStarterPack && starterPack; + var _e = useServiceQuery(state.serviceUrl), serviceInfo = _e.data, isFetching = _e.isFetching, isError = _e.isError, refetch = _e.refetch; + useEffect(function () { + if (isFetching) { + dispatch({ type: 'setIsLoading', value: true }); + } + else if (!isFetching) { + dispatch({ type: 'setIsLoading', value: false }); + } + }, [isFetching]); + useEffect(function () { + if (isError) { + dispatch({ type: 'setServiceDescription', value: undefined }); + dispatch({ + type: 'setError', + value: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Unable to contact your service. Please check your Internet connection."], ["Unable to contact your service. Please check your Internet connection."])))), + }); + } + else if (serviceInfo) { + dispatch({ type: 'setServiceDescription', value: serviceInfo }); + dispatch({ type: 'setError', value: '' }); + } + }, [_, serviceInfo, isError]); + useEffect(function () { + if (state.pendingSubmit) { + if (!state.pendingSubmit.mutableProcessed) { + state.pendingSubmit.mutableProcessed = true; + submit(state, dispatch); + } + } + }, [state, dispatch, submit]); + // Track app backgrounding during signup + useEffect(function () { + var subscription = AppState.addEventListener('change', function (nextAppState) { + if (nextAppState === 'background') { + dispatch({ type: 'incrementBackgroundCount' }); + } + }); + return function () { return subscription.remove(); }; + }, []); + // On Android, warmup the Play Integrity API on the signup screen so it is ready by the time we get to the gate screen. + useEffect(function () { + if (!IS_ANDROID) { + return; + } + ReactNativeDeviceAttest.warmupIntegrity(GCP_PROJECT_ID).catch(function (err) { + return logger.error(err); + }); + }, []); + return (_jsx(Animated.View, { exiting: native(FadeIn.duration(90)), style: a.flex_1, children: _jsx(SignupContext.Provider, { value: { state: state, dispatch: dispatch }, children: _jsx(LoggedOutLayout, { leadin: "", title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Create Account"], ["Create Account"])))), description: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["We're so excited to have you join us!"], ["We're so excited to have you join us!"])))), scrollable: true, children: _jsxs(View, { testID: "createAccount", style: a.flex_1, children: [showStarterPackCard && + bsky.dangerousIsType(starterPack.record, AppBskyGraphStarterpack.isRecord) ? (_jsx(Animated.View, { entering: !isFetchedAtMount ? FadeIn : undefined, children: _jsxs(LinearGradientBackground, { style: [a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_xl, { color: 'white' }], children: starterPack.record.name }), _jsx(Text, { style: [{ color: 'white' }], children: ((_b = starterPack.feeds) === null || _b === void 0 ? void 0 : _b.length) ? (_jsx(Trans, { children: "You'll follow the suggested users and feeds once you finish creating your account!" })) : (_jsx(Trans, { children: "You'll follow the suggested users once you finish creating your account!" })) })] }) })) : null, _jsx(LayoutAnimationConfig, { skipEntering: true, children: _jsx(ScreenTransition, { direction: state.screenTransitionDirection, children: _jsxs(View, { style: [ + a.flex_1, + a.px_xl, + a.pt_2xl, + !gtMobile && { paddingBottom: 100 }, + ], children: [_jsxs(View, { style: [a.gap_sm, a.pb_3xl], children: [_jsx(Text, { style: [a.font_semi_bold, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Step ", state.activeStep + 1, " of", ' ', state.serviceDescription && + !state.serviceDescription.phoneVerificationRequired + ? '2' + : '3'] }) }), _jsx(Text, { style: [a.text_3xl, a.font_semi_bold], children: state.activeStep === SignupStep.INFO ? (_jsx(Trans, { children: "Your account" })) : state.activeStep === SignupStep.HANDLE ? (_jsx(Trans, { children: "Choose your username" })) : (_jsx(Trans, { children: "Complete the challenge" })) })] }), state.activeStep === SignupStep.INFO ? (_jsx(StepInfo, { onPressBack: onPressBack, isLoadingStarterPack: isFetchingStarterPack && !isErrorStarterPack, isServerError: isError, refetchServer: refetch })) : state.activeStep === SignupStep.HANDLE ? (_jsx(StepHandle, {})) : (_jsx(StepCaptcha, {})), _jsx(Divider, {}), _jsxs(View, { style: [ + a.w_full, + a.py_lg, + a.flex_row, + a.gap_md, + a.align_center, + ], children: [_jsx(AppLanguageDropdown, {}), _jsxs(Text, { style: [ + a.flex_1, + t.atoms.text_contrast_medium, + !gtMobile && a.text_md, + ], children: [_jsx(Trans, { children: "Having trouble?" }), ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Contact support"], ["Contact support"])))), to: FEEDBACK_FORM_URL({ email: state.email }), style: [!gtMobile && a.text_md], children: _jsx(Trans, { children: "Contact support" }) })] })] })] }) }, state.activeStep) })] }) }) }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/Signup/state.js b/src/screens/Signup/state.js new file mode 100644 index 0000000000..2dd68a4e43 --- /dev/null +++ b/src/screens/Signup/state.js @@ -0,0 +1,339 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React, { useCallback } from 'react'; +import { LayoutAnimation } from 'react-native'; +import { ComAtprotoServerCreateAccount, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import * as EmailValidator from 'email-validator'; +import { DEFAULT_SERVICE } from '#/lib/constants'; +import { cleanError } from '#/lib/strings/errors'; +import { createFullHandle } from '#/lib/strings/handles'; +import { getAge } from '#/lib/strings/time'; +import { useSessionApi } from '#/state/session'; +import { useOnboardingDispatch } from '#/state/shell'; +import { useAnalytics } from '#/analytics'; +var DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20); // default to 20 years ago +export var SignupStep; +(function (SignupStep) { + SignupStep[SignupStep["INFO"] = 0] = "INFO"; + SignupStep[SignupStep["HANDLE"] = 1] = "HANDLE"; + SignupStep[SignupStep["CAPTCHA"] = 2] = "CAPTCHA"; +})(SignupStep || (SignupStep = {})); +export var initialState = { + analytics: undefined, + hasPrev: false, + activeStep: SignupStep.INFO, + screenTransitionDirection: 'Forward', + serviceUrl: DEFAULT_SERVICE, + serviceDescription: undefined, + userDomain: '', + dateOfBirth: DEFAULT_DATE, + email: '', + password: '', + handle: '', + inviteCode: '', + error: '', + errorField: undefined, + isLoading: false, + pendingSubmit: null, + // Tracking + signupStartTime: Date.now(), + fieldErrors: { + 'invite-code': 0, + email: 0, + handle: 0, + password: 0, + 'date-of-birth': 0, + }, + backgroundCount: 0, +}; +export function is13(date) { + return getAge(date) >= 13; +} +export function is18(date) { + return getAge(date) >= 18; +} +export function reducer(s, a) { + var _a, _b, _c, _d, _e, _f; + var next = __assign({}, s); + switch (a.type) { + case 'setAnalytics': { + next.analytics = a.value; + break; + } + case 'prev': { + if (s.activeStep !== SignupStep.INFO) { + next.screenTransitionDirection = 'Backward'; + next.activeStep--; + next.error = ''; + next.errorField = undefined; + } + break; + } + case 'next': { + if (s.activeStep !== SignupStep.CAPTCHA) { + next.screenTransitionDirection = 'Forward'; + next.activeStep++; + next.error = ''; + next.errorField = undefined; + } + break; + } + case 'setStep': { + next.activeStep = a.value; + break; + } + case 'setServiceUrl': { + next.serviceUrl = a.value; + break; + } + case 'setServiceDescription': { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + next.serviceDescription = a.value; + next.userDomain = (_b = (_a = a.value) === null || _a === void 0 ? void 0 : _a.availableUserDomains[0]) !== null && _b !== void 0 ? _b : ''; + next.isLoading = false; + break; + } + case 'setEmail': { + next.email = a.value; + break; + } + case 'setPassword': { + next.password = a.value; + break; + } + case 'setDateOfBirth': { + next.dateOfBirth = a.value; + break; + } + case 'setInviteCode': { + next.inviteCode = a.value; + break; + } + case 'setHandle': { + next.handle = a.value; + break; + } + case 'setIsLoading': { + next.isLoading = a.value; + break; + } + case 'setError': { + next.error = a.value; + next.errorField = a.field; + // Track field errors + if (a.field) { + next.fieldErrors[a.field] = (next.fieldErrors[a.field] || 0) + 1; + // Log the field error + (_c = s.analytics) === null || _c === void 0 ? void 0 : _c.metric('signup:fieldError', { + field: a.field, + errorCount: next.fieldErrors[a.field], + errorMessage: a.value, + activeStep: next.activeStep, + }); + } + break; + } + case 'clearError': { + next.error = ''; + next.errorField = undefined; + break; + } + case 'submit': { + next.pendingSubmit = a.task; + break; + } + case 'incrementBackgroundCount': { + next.backgroundCount = s.backgroundCount + 1; + // Log background/foreground event during signup + (_d = s.analytics) === null || _d === void 0 ? void 0 : _d.metric('signup:backgrounded', { + activeStep: next.activeStep, + backgroundCount: next.backgroundCount, + }); + break; + } + } + next.hasPrev = next.activeStep !== SignupStep.INFO; + (_e = s.analytics) === null || _e === void 0 ? void 0 : _e.logger.debug('signup', next); + if (s.activeStep !== next.activeStep) { + (_f = s.analytics) === null || _f === void 0 ? void 0 : _f.logger.debug('signup: step changed', { + activeStep: next.activeStep, + }); + } + return next; +} +export var SignupContext = React.createContext({}); +SignupContext.displayName = 'SignupContext'; +export var useSignupContext = function () { return React.useContext(SignupContext); }; +export function useSubmitSignup() { + var _this = this; + var ax = useAnalytics(); + var _ = useLingui()._; + var createAccount = useSessionApi().createAccount; + var onboardingDispatch = useOnboardingDispatch(); + return useCallback(function (state, dispatch) { return __awaiter(_this, void 0, void 0, function () { + var e_1, errMsg, error, isHandleError; + var _a, _b, _c; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + if (!state.email) { + dispatch({ type: 'setStep', value: SignupStep.INFO }); + return [2 /*return*/, dispatch({ + type: 'setError', + value: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Please enter your email."], ["Please enter your email."])))), + field: 'email', + })]; + } + if (!EmailValidator.validate(state.email)) { + dispatch({ type: 'setStep', value: SignupStep.INFO }); + return [2 /*return*/, dispatch({ + type: 'setError', + value: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Your email appears to be invalid."], ["Your email appears to be invalid."])))), + field: 'email', + })]; + } + if (!state.password) { + dispatch({ type: 'setStep', value: SignupStep.INFO }); + return [2 /*return*/, dispatch({ + type: 'setError', + value: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Please choose your password."], ["Please choose your password."])))), + field: 'password', + })]; + } + if (!state.handle) { + dispatch({ type: 'setStep', value: SignupStep.HANDLE }); + return [2 /*return*/, dispatch({ + type: 'setError', + value: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Please choose your handle."], ["Please choose your handle."])))), + field: 'handle', + })]; + } + if (((_a = state.serviceDescription) === null || _a === void 0 ? void 0 : _a.phoneVerificationRequired) && + !((_b = state.pendingSubmit) === null || _b === void 0 ? void 0 : _b.verificationCode)) { + dispatch({ type: 'setStep', value: SignupStep.CAPTCHA }); + ax.logger.error('Signup Flow Error', { + errorMessage: 'Verification captcha code was not set.', + registrationHandle: state.handle, + }); + return [2 /*return*/, dispatch({ + type: 'setError', + value: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Please complete the verification captcha."], ["Please complete the verification captcha."])))), + })]; + } + dispatch({ type: 'setError', value: '' }); + dispatch({ type: 'setIsLoading', value: true }); + _d.label = 1; + case 1: + _d.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, createAccount({ + service: state.serviceUrl, + email: state.email, + handle: createFullHandle(state.handle, state.userDomain), + password: state.password, + birthDate: state.dateOfBirth, + inviteCode: state.inviteCode.trim(), + verificationCode: (_c = state.pendingSubmit) === null || _c === void 0 ? void 0 : _c.verificationCode, + }, { + signupDuration: Date.now() - state.signupStartTime, + fieldErrorsTotal: Object.values(state.fieldErrors).reduce(function (a, b) { return a + b; }, 0), + backgroundCount: state.backgroundCount, + }) + /* + * Must happen last so that if the user has multiple tabs open and + * createAccount fails, one tab is not stuck in onboarding — Eric + */ + ]; + case 2: + _d.sent(); + /* + * Must happen last so that if the user has multiple tabs open and + * createAccount fails, one tab is not stuck in onboarding — Eric + */ + onboardingDispatch({ type: 'start' }); + return [3 /*break*/, 5]; + case 3: + e_1 = _d.sent(); + errMsg = e_1.toString(); + if (e_1 instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { + dispatch({ + type: 'setError', + value: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Invite code not accepted. Check that you input it correctly and try again."], ["Invite code not accepted. Check that you input it correctly and try again."])))), + field: 'invite-code', + }); + dispatch({ type: 'setStep', value: SignupStep.INFO }); + return [2 /*return*/]; + } + error = cleanError(errMsg); + isHandleError = error.toLowerCase().includes('handle'); + dispatch({ type: 'setIsLoading', value: false }); + dispatch({ + type: 'setError', + value: error, + field: isHandleError ? 'handle' : undefined, + }); + dispatch({ type: 'setStep', value: isHandleError ? 2 : 1 }); + ax.logger.error('Signup Flow Error', { + errorMessage: error, + registrationHandle: state.handle, + }); + return [3 /*break*/, 5]; + case 4: + dispatch({ type: 'setIsLoading', value: false }); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }, [_, onboardingDispatch, createAccount]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/screens/SignupQueued.js b/src/screens/SignupQueued.js new file mode 100644 index 0000000000..59893919ab --- /dev/null +++ b/src/screens/SignupQueued.js @@ -0,0 +1,176 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Modal, ScrollView, View } from 'react-native'; +import { SystemBars } from 'react-native-edge-to-edge'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg, plural } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { logger } from '#/logger'; +import { isSignupQueued, useAgent, useSessionApi } from '#/state/session'; +import { useOnboardingDispatch } from '#/state/shell'; +import { Logo } from '#/view/icons/Logo'; +import { atoms as a, native, useBreakpoints, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Loader } from '#/components/Loader'; +import { P, Text } from '#/components/Typography'; +import { IS_IOS, IS_LIQUID_GLASS, IS_WEB } from '#/env'; +var COL_WIDTH = 400; +export function SignupQueued() { + var _this = this; + var _ = useLingui()._; + var t = useTheme(); + var insets = useSafeAreaInsets(); + var gtMobile = useBreakpoints().gtMobile; + var onboardingDispatch = useOnboardingDispatch(); + var logoutCurrentAccount = useSessionApi().logoutCurrentAccount; + var agent = useAgent(); + var _a = React.useState(false), isProcessing = _a[0], setProcessing = _a[1]; + var _b = React.useState(undefined), estimatedTime = _b[0], setEstimatedTime = _b[1]; + var _c = React.useState(undefined), placeInQueue = _c[0], setPlaceInQueue = _c[1]; + var checkStatus = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var res, e_1; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + setProcessing(true); + _b.label = 1; + case 1: + _b.trys.push([1, 6, 7, 8]); + return [4 /*yield*/, agent.com.atproto.temp.checkSignupQueue()]; + case 2: + res = _b.sent(); + if (!res.data.activated) return [3 /*break*/, 4]; + // ready to go, exchange the access token for a usable one and kick off onboarding + return [4 /*yield*/, agent.sessionManager.refreshSession()]; + case 3: + // ready to go, exchange the access token for a usable one and kick off onboarding + _b.sent(); + if (!isSignupQueued((_a = agent.session) === null || _a === void 0 ? void 0 : _a.accessJwt)) { + onboardingDispatch({ type: 'start' }); + } + return [3 /*break*/, 5]; + case 4: + // not ready, update UI + setEstimatedTime(msToString(res.data.estimatedTimeMs)); + if (typeof res.data.placeInQueue !== 'undefined') { + setPlaceInQueue(Math.max(res.data.placeInQueue, 1)); + } + _b.label = 5; + case 5: return [3 /*break*/, 8]; + case 6: + e_1 = _b.sent(); + logger.error('Failed to check signup queue', { err: e_1.toString() }); + return [3 /*break*/, 8]; + case 7: + setProcessing(false); + return [7 /*endfinally*/]; + case 8: return [2 /*return*/]; + } + }); + }); }, [ + setProcessing, + setEstimatedTime, + setPlaceInQueue, + onboardingDispatch, + agent, + ]); + React.useEffect(function () { + checkStatus(); + var interval = setInterval(checkStatus, 60e3); + return function () { return clearInterval(interval); }; + }, [checkStatus]); + var checkBtn = (_jsxs(Button, { variant: "solid", color: "primary", size: "large", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Check my status"], ["Check my status"])))), onPress: checkStatus, disabled: isProcessing, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Check my status" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] })); + var logoutBtn = (_jsx(Button, { variant: "ghost", size: "large", color: "primary", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Sign out"], ["Sign out"])))), onPress: function () { return logoutCurrentAccount('SignupQueued'); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Sign out" }) }) })); + var webLayout = IS_WEB && gtMobile; + return (_jsxs(Modal, { visible: true, animationType: native('slide'), presentationStyle: "formSheet", style: [web(a.util_screen_outer)], children: [IS_IOS && !IS_LIQUID_GLASS && (_jsx(SystemBars, { style: { statusBar: 'light' } })), _jsx(ScrollView, { style: [a.flex_1, t.atoms.bg], contentContainerStyle: { borderWidth: 0 }, bounces: false, children: _jsx(View, { style: [ + a.flex_row, + a.justify_center, + gtMobile ? a.pt_4xl : [a.px_xl, a.pt_xl], + ], children: _jsxs(View, { style: [a.flex_1, { maxWidth: COL_WIDTH }], children: [_jsx(View, { style: [a.w_full, a.justify_center, a.align_center, a.my_4xl], children: _jsx(Logo, { width: 120 }) }), _jsx(Text, { style: [a.text_4xl, a.font_bold, a.pb_sm], children: _jsx(Trans, { children: "You're in line" }) }), _jsx(P, { style: [t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can." }) }), _jsxs(View, { style: [ + a.rounded_sm, + a.px_2xl, + a.py_4xl, + a.mt_2xl, + a.mb_md, + a.border, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_medium, + ], children: [typeof placeInQueue === 'number' && (_jsx(Text, { style: [a.text_5xl, a.text_center, a.font_bold, a.mb_2xl], children: placeInQueue })), _jsxs(P, { style: [a.text_center], children: [typeof placeInQueue === 'number' ? (_jsx(Trans, { children: "left to go." })) : (_jsx(Trans, { children: "You are in line." })), ' ', estimatedTime ? (_jsxs(Trans, { children: ["We estimate ", estimatedTime, " until your account is ready."] })) : (_jsx(Trans, { children: "We will let you know when your account is ready." }))] })] }), webLayout && (_jsxs(View, { style: [ + a.w_full, + a.flex_row, + a.justify_between, + a.pt_5xl, + { paddingBottom: 200 }, + ], children: [logoutBtn, checkBtn] }))] }) }) }), !webLayout && (_jsx(View, { style: [ + a.align_center, + t.atoms.bg, + gtMobile ? a.px_5xl : a.px_xl, + { paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom) }, + ], children: _jsxs(View, { style: [a.w_full, a.gap_sm, { maxWidth: COL_WIDTH }], children: [checkBtn, logoutBtn] }) }))] })); +} +function msToString(ms) { + if (ms && ms > 0) { + var estimatedTimeMins = Math.ceil(ms / 60e3); + if (estimatedTimeMins > 59) { + var estimatedTimeHrs = Math.round(estimatedTimeMins / 60); + if (estimatedTimeHrs > 6) { + // dont even bother + return undefined; + } + // hours + return "".concat(estimatedTimeHrs, " ").concat(plural(estimatedTimeHrs, { + one: 'hour', + other: 'hours', + })); + } + // minutes + return "".concat(estimatedTimeMins, " ").concat(plural(estimatedTimeMins, { + one: 'minute', + other: 'minutes', + })); + } + return undefined; +} +var templateObject_1, templateObject_2; diff --git a/src/screens/StarterPack/StarterPackLandingScreen.js b/src/screens/StarterPack/StarterPackLandingScreen.js new file mode 100644 index 0000000000..abbea34a50 --- /dev/null +++ b/src/screens/StarterPack/StarterPackLandingScreen.js @@ -0,0 +1,189 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Pressable, View } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri, } from '@atproto/api'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { JOINED_THIS_WEEK } from '#/lib/constants'; +import { useWebMediaQueries } from '#/lib/hooks/useWebMediaQueries'; +import { createStarterPackGooglePlayUri } from '#/lib/strings/starter-pack'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useStarterPackQuery } from '#/state/queries/starter-packs'; +import { useActiveStarterPack, useSetActiveStarterPack, } from '#/state/shell/starter-pack'; +import { LoggedOutScreenState } from '#/view/com/auth/LoggedOut'; +import { formatCount } from '#/view/com/util/numeric/format'; +import { Logo } from '#/view/icons/Logo'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import * as FeedCard from '#/components/FeedCard'; +import { useRichText } from '#/components/hooks/useRichText'; +import * as Layout from '#/components/Layout'; +import { LinearGradientBackground } from '#/components/LinearGradientBackground'; +import { ListMaybePlaceholder } from '#/components/Lists'; +import { Default as ProfileCard } from '#/components/ProfileCard'; +import * as Prompt from '#/components/Prompt'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB, IS_WEB_MOBILE_ANDROID } from '#/env'; +import * as bsky from '#/types/bsky'; +var AnimatedPressable = Animated.createAnimatedComponent(Pressable); +export function postAppClipMessage(message) { + // @ts-expect-error safari webview only + window.webkit.messageHandlers.onMessage.postMessage(JSON.stringify(message)); +} +export function LandingScreen(_a) { + var setScreenState = _a.setScreenState; + var moderationOpts = useModerationOpts(); + var activeStarterPack = useActiveStarterPack(); + var _b = useStarterPackQuery({ + uri: activeStarterPack === null || activeStarterPack === void 0 ? void 0 : activeStarterPack.uri, + }), starterPack = _b.data, isErrorStarterPack = _b.isError, isFetching = _b.isFetching; + var isValid = starterPack && + starterPack.list && + AppBskyGraphDefs.validateStarterPackView(starterPack) && + AppBskyGraphStarterpack.validateRecord(starterPack.record); + React.useEffect(function () { + if (isErrorStarterPack || (starterPack && !isValid)) { + setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount); + } + }, [isErrorStarterPack, setScreenState, isValid, starterPack]); + if (isFetching || !starterPack || !isValid || !moderationOpts) { + return _jsx(ListMaybePlaceholder, { isLoading: true }); + } + // Just for types, this cannot be hit + if (!bsky.dangerousIsType(starterPack.record, AppBskyGraphStarterpack.isRecord)) { + return null; + } + return (_jsx(LandingScreenLoaded, { starterPack: starterPack, starterPackRecord: starterPack.record, setScreenState: setScreenState, moderationOpts: moderationOpts })); +} +function LandingScreenLoaded(_a) { + var _b, _c, _d; + var starterPack = _a.starterPack, record = _a.starterPackRecord, setScreenState = _a.setScreenState, + // TODO apply this to profile card + moderationOpts = _a.moderationOpts; + var creator = starterPack.creator, listItemsSample = starterPack.listItemsSample, feeds = starterPack.feeds; + var _e = useLingui(), _ = _e._, i18n = _e.i18n; + var ax = useAnalytics(); + var t = useTheme(); + var activeStarterPack = useActiveStarterPack(); + var setActiveStarterPack = useSetActiveStarterPack(); + var isTabletOrDesktop = useWebMediaQueries().isTabletOrDesktop; + var androidDialogControl = useDialogControl(); + var descriptionRt = useRichText(record.description || '')[0]; + var _f = React.useState(false), appClipOverlayVisible = _f[0], setAppClipOverlayVisible = _f[1]; + var listItemsCount = (_c = (_b = starterPack.list) === null || _b === void 0 ? void 0 : _b.listItemCount) !== null && _c !== void 0 ? _c : 0; + var onContinue = function () { + setScreenState(LoggedOutScreenState.S_CreateAccount); + }; + var onJoinPress = function () { + if (activeStarterPack === null || activeStarterPack === void 0 ? void 0 : activeStarterPack.isClip) { + setAppClipOverlayVisible(true); + postAppClipMessage({ + action: 'present', + }); + } + else if (IS_WEB_MOBILE_ANDROID) { + androidDialogControl.open(); + } + else { + onContinue(); + } + ax.metric('starterPack:ctaPress', { + starterPack: starterPack.uri, + }); + }; + var onJoinWithoutPress = function () { + if (activeStarterPack === null || activeStarterPack === void 0 ? void 0 : activeStarterPack.isClip) { + setAppClipOverlayVisible(true); + postAppClipMessage({ + action: 'present', + }); + } + else { + setActiveStarterPack(undefined); + setScreenState(LoggedOutScreenState.S_CreateAccount); + } + }; + return (_jsxs(View, { style: [a.flex_1], children: [_jsxs(Layout.Content, { ignoreTabletLayoutOffset: true, children: [_jsxs(LinearGradientBackground, { style: [ + a.align_center, + a.gap_sm, + a.px_lg, + a.py_2xl, + isTabletOrDesktop && [a.mt_2xl, a.rounded_md], + (activeStarterPack === null || activeStarterPack === void 0 ? void 0 : activeStarterPack.isClip) && { + paddingTop: 100, + }, + ], children: [_jsx(View, { style: [a.flex_row, a.gap_md, a.pb_sm], children: _jsx(Logo, { width: 76, fill: "white" }) }), _jsx(Text, { style: [ + a.font_semi_bold, + a.text_4xl, + a.text_center, + a.leading_tight, + { color: 'white' }, + ], children: record.name }), _jsxs(Text, { style: [ + a.text_center, + a.font_semi_bold, + a.text_md, + { color: 'white' }, + ], children: ["Starter pack by ", "@".concat(creator.handle)] })] }), _jsxs(View, { style: [a.gap_2xl, a.mx_lg, a.my_2xl], children: [record.description ? (_jsx(RichText, { value: descriptionRt, style: [a.text_md] })) : null, _jsxs(View, { style: [a.gap_sm], children: [_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Join Bluesky"], ["Join Bluesky"])))), onPress: onJoinPress, variant: "solid", color: "primary", size: "large", children: _jsx(ButtonText, { style: [a.text_lg], children: _jsx(Trans, { children: "Join Bluesky" }) }) }), _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: [_jsx(FontAwesomeIcon, { icon: "arrow-trend-up", size: 12, color: t.atoms.text_contrast_medium.color }), _jsx(Text, { style: [ + a.font_semi_bold, + a.text_sm, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: _jsxs(Trans, { children: [formatCount(i18n, JOINED_THIS_WEEK), " joined this week"] }) })] })] }), _jsxs(View, { style: [a.gap_3xl], children: [Boolean(listItemsSample === null || listItemsSample === void 0 ? void 0 : listItemsSample.length) && (_jsxs(View, { style: [a.gap_md], children: [_jsx(Text, { style: [a.font_bold, a.text_lg], children: listItemsCount <= 8 ? (_jsx(Trans, { children: "You'll follow these people right away" })) : (_jsxs(Trans, { children: ["You'll follow these people and ", listItemsCount - 8, " others"] })) }), _jsx(View, { style: isTabletOrDesktop && [ + a.border, + a.rounded_md, + t.atoms.border_contrast_low, + ], children: (_d = starterPack.listItemsSample) === null || _d === void 0 ? void 0 : _d.filter(function (p) { var _a; return !((_a = p.subject.associated) === null || _a === void 0 ? void 0 : _a.labeler); }).slice(0, 8).map(function (item, i) { return (_jsx(View, { style: [ + a.py_lg, + a.px_md, + (!isTabletOrDesktop || i !== 0) && a.border_t, + t.atoms.border_contrast_low, + { pointerEvents: 'none' }, + ], children: _jsx(ProfileCard, { profile: item.subject, moderationOpts: moderationOpts }) }, item.subject.did)); }) })] })), (feeds === null || feeds === void 0 ? void 0 : feeds.length) ? (_jsxs(View, { style: [a.gap_md], children: [_jsx(Text, { style: [a.font_bold, a.text_lg], children: _jsx(Trans, { children: "You'll stay updated with these feeds" }) }), _jsx(View, { style: [ + { pointerEvents: 'none' }, + isTabletOrDesktop && [ + a.border, + a.rounded_md, + t.atoms.border_contrast_low, + ], + ], children: feeds === null || feeds === void 0 ? void 0 : feeds.map(function (feed, i) { return (_jsx(View, { style: [ + a.py_lg, + a.px_md, + (!isTabletOrDesktop || i !== 0) && a.border_t, + t.atoms.border_contrast_low, + ], children: _jsx(FeedCard.Default, { view: feed }) }, feed.uri)); }) })] })) : null] }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Create an account without using this starter pack"], ["Create an account without using this starter pack"])))), variant: "solid", color: "secondary", size: "large", style: [a.py_lg], onPress: onJoinWithoutPress, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Create an account without using this starter pack" }) }) })] })] }), _jsx(AppClipOverlay, { visible: appClipOverlayVisible, setIsVisible: setAppClipOverlayVisible }), _jsxs(Prompt.Outer, { control: androidDialogControl, children: [_jsxs(Prompt.Content, { children: [_jsx(Prompt.TitleText, { children: _jsx(Trans, { children: "Download Bluesky" }) }), _jsx(Prompt.DescriptionText, { children: _jsx(Trans, { children: "The experience is better in the app. Download Bluesky now and we'll pick back up where you left off." }) })] }), _jsxs(Prompt.Actions, { children: [_jsx(Prompt.Action, { cta: "Download on Google Play", color: "primary", onPress: function () { + var rkey = new AtUri(starterPack.uri).rkey; + if (!rkey) + return; + var googlePlayUri = createStarterPackGooglePlayUri(creator.handle, rkey); + if (!googlePlayUri) + return; + window.location.href = googlePlayUri; + } }), _jsx(Prompt.Action, { cta: "Continue on web", color: "secondary", onPress: onContinue })] })] }), IS_WEB && (_jsx("meta", { name: "apple-itunes-app", content: "app-id=xyz.blueskyweb.app, app-clip-bundle-id=xyz.blueskyweb.app.AppClip, app-clip-display=card" }))] })); +} +export function AppClipOverlay(_a) { + var visible = _a.visible, setIsVisible = _a.setIsVisible; + if (!visible) + return; + return (_jsx(AnimatedPressable, { accessibilityRole: "button", style: [ + a.absolute, + a.inset_0, + { + backgroundColor: 'rgba(0, 0, 0, 0.95)', + zIndex: 1, + }, + ], entering: FadeIn, exiting: FadeOut, onPress: function () { return setIsVisible(false); }, children: _jsx(View, { style: [a.flex_1, a.px_lg, { marginTop: 250 }], children: _jsxs(View, { style: [a.gap_md, { zIndex: 2 }], children: [_jsx(Text, { style: [ + a.font_semi_bold, + a.text_4xl, + { lineHeight: 40, color: 'white' }, + ], children: "Download Bluesky to get started!" }), _jsx(Text, { style: [a.text_lg, { color: 'white' }], children: "We'll remember the starter pack you chose and use it when you create an account in the app." })] }) }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/StarterPack/StarterPackScreen.js b/src/screens/StarterPack/StarterPackScreen.js new file mode 100644 index 0000000000..8b458f5849 --- /dev/null +++ b/src/screens/StarterPack/StarterPackScreen.js @@ -0,0 +1,463 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri, RichText as RichTextAPI, } from '@atproto/api'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useNavigation } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { batchedUpdates } from '#/lib/batchedUpdates'; +import { HITSLOP_20 } from '#/lib/constants'; +import { isBlockedOrBlocking, isMuted } from '#/lib/moderation/blocked-and-muted'; +import { makeProfileLink, makeStarterPackLink } from '#/lib/routes/links'; +import { cleanError } from '#/lib/strings/errors'; +import { getStarterPackOgCard } from '#/lib/strings/starter-pack'; +import { logger } from '#/logger'; +import { updateProfileShadow } from '#/state/cache/profile-shadow'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { getAllListMembers } from '#/state/queries/list-members'; +import { useResolvedStarterPackShortLink } from '#/state/queries/resolve-short-link'; +import { useResolveDidQuery } from '#/state/queries/resolve-uri'; +import { useShortenLink } from '#/state/queries/shorten-link'; +import { useDeleteStarterPackMutation, useStarterPackQuery, } from '#/state/queries/starter-packs'; +import { useAgent, useSession } from '#/state/session'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { ProgressGuideAction, useProgressGuideControls, } from '#/state/shell/progress-guide'; +import { useSetActiveStarterPack } from '#/state/shell/starter-pack'; +import { PagerWithHeader } from '#/view/com/pager/PagerWithHeader'; +import { ProfileSubpageHeader } from '#/view/com/profile/ProfileSubpageHeader'; +import * as Toast from '#/view/com/util/Toast'; +import { bulkWriteFollows } from '#/screens/Onboarding/util'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { CreateListFromStarterPackDialog } from '#/components/dialogs/lists/CreateListFromStarterPackDialog'; +import { ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon } from '#/components/icons/ArrowOutOfBox'; +import { ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon } from '#/components/icons/ChainLink'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis } from '#/components/icons/DotGrid'; +import { ListSparkle_Stroke2_Corner0_Rounded as ListSparkle } from '#/components/icons/ListSparkle'; +import { Pencil_Stroke2_Corner0_Rounded as Pencil } from '#/components/icons/Pencil'; +import { Trash_Stroke2_Corner0_Rounded as Trash } from '#/components/icons/Trash'; +import * as Layout from '#/components/Layout'; +import { ListMaybePlaceholder } from '#/components/Lists'; +import { Loader } from '#/components/Loader'; +import * as Menu from '#/components/Menu'; +import { ReportDialog, useReportDialogControl, } from '#/components/moderation/ReportDialog'; +import * as Prompt from '#/components/Prompt'; +import { RichText } from '#/components/RichText'; +import { FeedsList } from '#/components/StarterPack/Main/FeedsList'; +import { PostsList } from '#/components/StarterPack/Main/PostsList'; +import { ProfilesList } from '#/components/StarterPack/Main/ProfilesList'; +import { QrCodeDialog } from '#/components/StarterPack/QrCodeDialog'; +import { ShareDialog } from '#/components/StarterPack/ShareDialog'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import * as bsky from '#/types/bsky'; +export function StarterPackScreen(_a) { + var route = _a.route; + return (_jsx(Layout.Screen, { children: _jsx(StarterPackScreenInner, { routeParams: route.params }) })); +} +export function StarterPackScreenShort(_a) { + var route = _a.route; + var _ = useLingui()._; + var _b = useResolvedStarterPackShortLink({ + code: route.params.code, + }), resolvedStarterPack = _b.data, isLoading = _b.isLoading, isError = _b.isError; + if (isLoading || isError || !resolvedStarterPack) { + return (_jsx(Layout.Screen, { children: _jsx(ListMaybePlaceholder, { isLoading: isLoading, isError: isError, errorMessage: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["That starter pack could not be found."], ["That starter pack could not be found."])))), emptyMessage: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["That starter pack could not be found."], ["That starter pack could not be found."])))) }) })); + } + return (_jsx(Layout.Screen, { children: _jsx(StarterPackScreenInner, { routeParams: resolvedStarterPack }) })); +} +export function StarterPackScreenInner(_a) { + var _b; + var routeParams = _a.routeParams; + var name = routeParams.name, rkey = routeParams.rkey; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var moderationOpts = useModerationOpts(); + var _c = useResolveDidQuery(name), did = _c.data, isLoadingDid = _c.isLoading, isErrorDid = _c.isError; + var _d = useStarterPackQuery({ did: did, rkey: rkey }), starterPack = _d.data, isLoadingStarterPack = _d.isLoading, isErrorStarterPack = _d.isError; + var isValid = starterPack && + (starterPack.list || ((_b = starterPack === null || starterPack === void 0 ? void 0 : starterPack.creator) === null || _b === void 0 ? void 0 : _b.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) && + AppBskyGraphDefs.validateStarterPackView(starterPack) && + AppBskyGraphStarterpack.validateRecord(starterPack.record); + if (!did || !starterPack || !isValid || !moderationOpts) { + return (_jsx(ListMaybePlaceholder, { isLoading: isLoadingDid || isLoadingStarterPack || !moderationOpts, isError: isErrorDid || isErrorStarterPack || !isValid, errorMessage: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["That starter pack could not be found."], ["That starter pack could not be found."])))), emptyMessage: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["That starter pack could not be found."], ["That starter pack could not be found."])))) })); + } + if (!starterPack.list && starterPack.creator.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + return _jsx(InvalidStarterPack, { rkey: rkey }); + } + return (_jsx(StarterPackScreenLoaded, { starterPack: starterPack, routeParams: routeParams, moderationOpts: moderationOpts })); +} +function StarterPackScreenLoaded(_a) { + var _b; + var starterPack = _a.starterPack, routeParams = _a.routeParams, moderationOpts = _a.moderationOpts; + var showPeopleTab = Boolean(starterPack.list); + var showFeedsTab = Boolean((_b = starterPack.feeds) === null || _b === void 0 ? void 0 : _b.length); + var showPostsTab = Boolean(starterPack.list); + var _ = useLingui()._; + var ax = useAnalytics(); + var tabs = __spreadArray(__spreadArray(__spreadArray([], (showPeopleTab ? [_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["People"], ["People"]))))] : []), true), (showFeedsTab ? [_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Feeds"], ["Feeds"]))))] : []), true), (showPostsTab ? [_(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Posts"], ["Posts"]))))] : []), true); + var qrCodeDialogControl = useDialogControl(); + var shareDialogControl = useDialogControl(); + var shortenLink = useShortenLink(); + var _c = React.useState(), link = _c[0], setLink = _c[1]; + var _d = React.useState(false), imageLoaded = _d[0], setImageLoaded = _d[1]; + React.useEffect(function () { + ax.metric('starterPack:opened', { + starterPack: starterPack.uri, + }); + }, [ax, starterPack.uri]); + var onOpenShareDialog = React.useCallback(function () { + var rkey = new AtUri(starterPack.uri).rkey; + shortenLink(makeStarterPackLink(starterPack.creator.did, rkey)).then(function (res) { + setLink(res.url); + }); + Image.prefetch(getStarterPackOgCard(starterPack)) + .then(function () { + setImageLoaded(true); + }) + .catch(function () { + setImageLoaded(true); + }); + shareDialogControl.open(); + }, [shareDialogControl, shortenLink, starterPack]); + React.useEffect(function () { + if (routeParams.new) { + onOpenShareDialog(); + } + }, [onOpenShareDialog, routeParams.new, shareDialogControl]); + return (_jsxs(_Fragment, { children: [_jsxs(PagerWithHeader, { items: tabs, isHeaderReady: true, renderHeader: function () { return (_jsx(Header, { starterPack: starterPack, routeParams: routeParams, onOpenShareDialog: onOpenShareDialog })); }, children: [showPeopleTab + ? function (_a) { + var headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + return (_jsx(ProfilesList + // Validated above + , { + // Validated above + listUri: starterPack.list.uri, headerHeight: headerHeight, + // @ts-expect-error + scrollElRef: scrollElRef, moderationOpts: moderationOpts })); + } + : null, showFeedsTab + ? function (_a) { + var headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + return (_jsx(FeedsList + // @ts-expect-error ? + , { + // @ts-expect-error ? + feeds: starterPack === null || starterPack === void 0 ? void 0 : starterPack.feeds, headerHeight: headerHeight, + // @ts-expect-error + scrollElRef: scrollElRef })); + } + : null, showPostsTab + ? function (_a) { + var headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + return (_jsx(PostsList + // Validated above + , { + // Validated above + listUri: starterPack.list.uri, headerHeight: headerHeight, + // @ts-expect-error + scrollElRef: scrollElRef, moderationOpts: moderationOpts })); + } + : null] }), _jsx(QrCodeDialog, { control: qrCodeDialogControl, starterPack: starterPack, link: link }), _jsx(ShareDialog, { control: shareDialogControl, qrDialogControl: qrCodeDialogControl, starterPack: starterPack, link: link, imageLoaded: imageLoaded })] })); +} +function Header(_a) { + var _this = this; + var _b; + var starterPack = _a.starterPack, routeParams = _a.routeParams, onOpenShareDialog = _a.onOpenShareDialog; + var _ = useLingui()._; + var t = useTheme(); + var _c = useSession(), currentAccount = _c.currentAccount, hasSession = _c.hasSession; + var agent = useAgent(); + var queryClient = useQueryClient(); + var setActiveStarterPack = useSetActiveStarterPack(); + var requestSwitchToAccount = useLoggedOutViewControls().requestSwitchToAccount; + var captureAction = useProgressGuideControls().captureAction; + var _d = React.useState(false), isProcessing = _d[0], setIsProcessing = _d[1]; + var record = starterPack.record, creator = starterPack.creator; + var isOwn = (creator === null || creator === void 0 ? void 0 : creator.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var joinedAllTimeCount = (_b = starterPack.joinedAllTimeCount) !== null && _b !== void 0 ? _b : 0; + var ax = useAnalytics(); + var navigation = useNavigation(); + React.useEffect(function () { + var onFocus = function () { + if (hasSession) + return; + setActiveStarterPack({ + uri: starterPack.uri, + }); + }; + var onBeforeRemove = function () { + if (hasSession) + return; + setActiveStarterPack(undefined); + }; + navigation.addListener('focus', onFocus); + navigation.addListener('beforeRemove', onBeforeRemove); + return function () { + navigation.removeListener('focus', onFocus); + navigation.removeListener('beforeRemove', onBeforeRemove); + }; + }, [hasSession, navigation, setActiveStarterPack, starterPack.uri]); + var onFollowAll = function () { return __awaiter(_this, void 0, void 0, function () { + var listItems, e_1, dids, followUris, e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!starterPack.list) + return [2 /*return*/]; + setIsProcessing(true); + listItems = []; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, getAllListMembers(agent, starterPack.list.uri)]; + case 2: + listItems = _a.sent(); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + setIsProcessing(false); + Toast.show(_(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["An error occurred while trying to follow all"], ["An error occurred while trying to follow all"])))), 'xmark'); + logger.error('Failed to get list members for starter pack', { + safeMessage: e_1, + }); + return [2 /*return*/]; + case 4: + dids = listItems + .filter(function (li) { + var _a; + return li.subject.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) && + !isBlockedOrBlocking(li.subject) && + !isMuted(li.subject) && + !((_a = li.subject.viewer) === null || _a === void 0 ? void 0 : _a.following); + }) + .map(function (li) { return li.subject.did; }); + _a.label = 5; + case 5: + _a.trys.push([5, 7, , 8]); + return [4 /*yield*/, bulkWriteFollows(agent, dids, { + uri: starterPack.uri, + cid: starterPack.cid, + })]; + case 6: + followUris = _a.sent(); + return [3 /*break*/, 8]; + case 7: + e_2 = _a.sent(); + setIsProcessing(false); + Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["An error occurred while trying to follow all"], ["An error occurred while trying to follow all"])))), 'xmark'); + logger.error('Failed to follow all accounts', { safeMessage: e_2 }); + return [3 /*break*/, 8]; + case 8: + setIsProcessing(false); + batchedUpdates(function () { + for (var _i = 0, dids_1 = dids; _i < dids_1.length; _i++) { + var did = dids_1[_i]; + updateProfileShadow(queryClient, did, { + followingUri: followUris.get(did), + }); + } + }); + Toast.show(_(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["All accounts have been followed!"], ["All accounts have been followed!"]))))); + captureAction(ProgressGuideAction.Follow, dids.length); + ax.metric('starterPack:followAll', { + logContext: 'StarterPackProfilesList', + starterPack: starterPack.uri, + count: dids.length, + }); + return [2 /*return*/]; + } + }); + }); }; + if (!bsky.dangerousIsType(record, AppBskyGraphStarterpack.isRecord)) { + return null; + } + var richText = record.description + ? new RichTextAPI({ + text: record.description, + facets: record.descriptionFacets, + }) + : undefined; + return (_jsxs(_Fragment, { children: [_jsx(ProfileSubpageHeader, { isLoading: false, href: makeProfileLink(creator), title: record.name, isOwner: isOwn, avatar: undefined, creator: creator, purpose: "app.bsky.graph.defs#referencelist", avatarType: "starter-pack", children: hasSession ? (_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center], children: [isOwn ? (_jsx(Button, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Share this starter pack"], ["Share this starter pack"])))), hitSlop: HITSLOP_20, variant: "solid", color: "primary", size: "small", onPress: onOpenShareDialog, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Share" }) }) })) : (_jsxs(Button, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Follow all"], ["Follow all"])))), variant: "solid", color: "primary", size: "small", disabled: isProcessing, onPress: onFollowAll, style: [a.flex_row, a.gap_xs, a.align_center], children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Follow all" }) }), isProcessing && _jsx(ButtonIcon, { icon: Loader })] })), _jsx(OverflowMenu, { routeParams: routeParams, starterPack: starterPack, onOpenShareDialog: onOpenShareDialog })] })) : null }), !hasSession || richText || joinedAllTimeCount >= 25 ? (_jsxs(View, { style: [a.px_lg, a.pt_md, a.pb_sm, a.gap_md], children: [richText ? _jsx(RichText, { value: richText, style: [a.text_md] }) : null, !hasSession ? (_jsx(Button, { label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Join Bluesky"], ["Join Bluesky"])))), onPress: function () { + setActiveStarterPack({ + uri: starterPack.uri, + }); + requestSwitchToAccount({ requestedAccount: 'new' }); + }, variant: "solid", color: "primary", size: "large", children: _jsx(ButtonText, { style: [a.text_lg], children: _jsx(Trans, { children: "Join Bluesky" }) }) })) : null, joinedAllTimeCount >= 25 ? (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: [_jsx(FontAwesomeIcon, { icon: "arrow-trend-up", size: 12, color: t.atoms.text_contrast_medium.color }), _jsx(Text, { style: [ + a.font_semi_bold, + a.text_sm, + t.atoms.text_contrast_medium, + ], children: _jsxs(Trans, { comment: "Number of users (always at least 25) who have joined Bluesky using a specific starter pack", children: [_jsx(Plural, { value: starterPack.joinedAllTimeCount || 0, other: "# people have" }), ' ', "joined Bluesky via this starter pack!"] }) })] })) : null] })) : null] })); +} +function OverflowMenu(_a) { + var _this = this; + var starterPack = _a.starterPack, routeParams = _a.routeParams, onOpenShareDialog = _a.onOpenShareDialog; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var gtMobile = useBreakpoints().gtMobile; + var currentAccount = useSession().currentAccount; + var reportDialogControl = useReportDialogControl(); + var deleteDialogControl = useDialogControl(); + var convertToListDialogControl = useDialogControl(); + var navigation = useNavigation(); + var _b = useDeleteStarterPackMutation({ + onSuccess: function () { + ax.metric('starterPack:delete', {}); + deleteDialogControl.close(function () { + if (navigation.canGoBack()) { + navigation.popToTop(); + } + else { + navigation.navigate('Home'); + } + }); + }, + onError: function (e) { + logger.error('Failed to delete starter pack', { safeMessage: e }); + }, + }), deleteStarterPack = _b.mutate, isDeletePending = _b.isPending, deleteError = _b.error; + var isOwn = starterPack.creator.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var onDeleteStarterPack = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (!starterPack.list) { + logger.error("Unable to delete starterpack because list is missing"); + return [2 /*return*/]; + } + deleteStarterPack({ + rkey: routeParams.rkey, + listUri: starterPack.list.uri, + }); + ax.metric('starterPack:delete', {}); + return [2 /*return*/]; + }); + }); }; + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Repost or quote post"], ["Repost or quote post"])))), children: function (_a) { + var props = _a.props; + return (_jsx(Button, __assign({}, props, { testID: "headerDropdownBtn", label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Open starter pack menu"], ["Open starter pack menu"])))), hitSlop: HITSLOP_20, variant: "solid", color: "secondary", size: "small", shape: "round", children: _jsx(ButtonIcon, { icon: Ellipsis }) }))); + } }), _jsx(Menu.Outer, { style: { minWidth: 170 }, children: isOwn ? (_jsxs(_Fragment, { children: [_jsxs(Menu.Item, { label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Edit starter pack"], ["Edit starter pack"])))), testID: "editStarterPackLinkBtn", onPress: function () { + navigation.navigate('StarterPackEdit', { + rkey: routeParams.rkey, + }); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Edit" }) }), _jsx(Menu.ItemIcon, { icon: Pencil, position: "right" })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Delete starter pack"], ["Delete starter pack"])))), testID: "deleteStarterPackBtn", onPress: function () { + deleteDialogControl.open(); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Delete" }) }), _jsx(Menu.ItemIcon, { icon: Trash, position: "right" })] }), _jsxs(Menu.Item, { label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Create a list from this starter pack"], ["Create a list from this starter pack"])))), testID: "convertToListBtn", onPress: function () { + convertToListDialogControl.open(); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Create list from members" }) }), _jsx(Menu.ItemIcon, { icon: ListSparkle, position: "right" })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Menu.Group, { children: _jsxs(Menu.Item, { label: IS_WEB + ? _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Copy link to starter pack"], ["Copy link to starter pack"])))) + : _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Share via..."], ["Share via..."])))), testID: "shareStarterPackLinkBtn", onPress: onOpenShareDialog, children: [_jsx(Menu.ItemText, { children: IS_WEB ? (_jsx(Trans, { children: "Copy link" })) : (_jsx(Trans, { children: "Share via..." })) }), _jsx(Menu.ItemIcon, { icon: IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon, position: "right" })] }) }), _jsxs(Menu.Item, { label: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Report starter pack"], ["Report starter pack"])))), onPress: function () { return reportDialogControl.open(); }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Report starter pack" }) }), _jsx(Menu.ItemIcon, { icon: CircleInfo, position: "right" })] })] })) })] }), starterPack.list && (_jsx(ReportDialog, { control: reportDialogControl, subject: __assign(__assign({}, starterPack), { $type: 'app.bsky.graph.defs#starterPackView' }) })), _jsxs(Prompt.Outer, { control: deleteDialogControl, children: [_jsx(Prompt.TitleText, { children: _jsx(Trans, { children: "Delete starter pack?" }) }), _jsx(Prompt.DescriptionText, { children: _jsx(Trans, { children: "Are you sure you want to delete this starter pack?" }) }), deleteError && (_jsxs(View, { style: [ + a.flex_row, + a.gap_sm, + a.rounded_sm, + a.p_md, + a.mb_lg, + a.border, + t.atoms.border_contrast_medium, + t.atoms.bg_contrast_25, + ], children: [_jsxs(View, { style: [a.flex_1, a.gap_2xs], children: [_jsx(Text, { style: [a.font_semi_bold], children: _jsx(Trans, { children: "Unable to delete" }) }), _jsx(Text, { style: [a.leading_snug], children: cleanError(deleteError) })] }), _jsx(CircleInfo, { size: "sm", fill: t.palette.negative_400 })] })), _jsxs(Prompt.Actions, { children: [_jsxs(Button, { variant: "solid", color: "negative", size: gtMobile ? 'small' : 'large', label: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Yes, delete this starter pack"], ["Yes, delete this starter pack"])))), onPress: onDeleteStarterPack, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Delete" }) }), isDeletePending && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Prompt.Cancel, {})] })] }), _jsx(CreateListFromStarterPackDialog, { control: convertToListDialogControl, starterPack: starterPack })] })); +} +function InvalidStarterPack(_a) { + var rkey = _a.rkey; + var _ = useLingui()._; + var t = useTheme(); + var navigation = useNavigation(); + var gtMobile = useBreakpoints().gtMobile; + var _b = React.useState(false), isProcessing = _b[0], setIsProcessing = _b[1]; + var goBack = function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.replace('Home'); + } + }; + var deleteStarterPack = useDeleteStarterPackMutation({ + onSuccess: function () { + setIsProcessing(false); + goBack(); + }, + onError: function (e) { + setIsProcessing(false); + logger.error('Failed to delete invalid starter pack', { safeMessage: e }); + Toast.show(_(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Failed to delete starter pack"], ["Failed to delete starter pack"])))), 'xmark'); + }, + }).mutate; + return (_jsx(Layout.Content, { centerContent: true, children: _jsxs(View, { style: [a.py_4xl, a.px_xl, a.align_center, a.gap_5xl], children: [_jsxs(View, { style: [a.w_full, a.align_center, a.gap_lg], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_3xl], children: _jsx(Trans, { children: "Starter pack is invalid" }) }), _jsx(Text, { style: [ + a.text_md, + a.text_center, + t.atoms.text_contrast_high, + { lineHeight: 1.4 }, + gtMobile ? { width: 450 } : [a.w_full, a.px_lg], + ], children: _jsx(Trans, { children: "The starter pack that you are trying to view is invalid. You may delete this starter pack instead." }) })] }), _jsxs(View, { style: [a.gap_md, gtMobile ? { width: 350 } : [a.w_full, a.px_lg]], children: [_jsxs(Button, { variant: "solid", color: "primary", label: _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Delete starter pack"], ["Delete starter pack"])))), size: "large", style: [a.rounded_sm, a.overflow_hidden, { paddingVertical: 10 }], disabled: isProcessing, onPress: function () { + setIsProcessing(true); + deleteStarterPack({ rkey: rkey }); + }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Delete" }) }), isProcessing && _jsx(Loader, { size: "xs", color: "white" })] }), _jsx(Button, { variant: "solid", color: "secondary", label: _(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Return to previous page"], ["Return to previous page"])))), size: "large", style: [a.rounded_sm, a.overflow_hidden, { paddingVertical: 10 }], disabled: isProcessing, onPress: goBack, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go Back" }) }) })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25; diff --git a/src/screens/StarterPack/Wizard/State.js b/src/screens/StarterPack/Wizard/State.js new file mode 100644 index 0000000000..12ceedd9e3 --- /dev/null +++ b/src/screens/StarterPack/Wizard/State.js @@ -0,0 +1,119 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { AppBskyGraphStarterpack, } from '@atproto/api'; +import { msg, plural } from '@lingui/core/macro'; +import { STARTER_PACK_MAX_SIZE } from '#/lib/constants'; +import * as Toast from '#/view/com/util/Toast'; +import * as bsky from '#/types/bsky'; +var steps = ['Details', 'Profiles', 'Feeds']; +var StateContext = React.createContext([ + {}, + function (_) { }, +]); +StateContext.displayName = 'StarterPackWizardStateContext'; +export var useWizardState = function () { return React.useContext(StateContext); }; +function reducer(state, action) { + var _a, _b; + var updatedState = state; + // -- Navigation + var currentIndex = steps.indexOf(state.currentStep); + if (action.type === 'Next' && state.currentStep !== 'Feeds') { + updatedState = __assign(__assign({}, state), { currentStep: steps[currentIndex + 1], transitionDirection: 'Forward' }); + } + else if (action.type === 'Back' && state.currentStep !== 'Details') { + updatedState = __assign(__assign({}, state), { currentStep: steps[currentIndex - 1], transitionDirection: 'Backward' }); + } + switch (action.type) { + case 'SetName': + updatedState = __assign(__assign({}, state), { name: action.name.slice(0, 50) }); + break; + case 'SetDescription': + updatedState = __assign(__assign({}, state), { description: action.description }); + break; + case 'AddProfile': + if (state.profiles.length > STARTER_PACK_MAX_SIZE) { + Toast.show((_a = msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You may only add up to ", ""], ["You may only add up to ", ""])), plural(STARTER_PACK_MAX_SIZE, { + other: "".concat(STARTER_PACK_MAX_SIZE, " profiles"), + })).message) !== null && _a !== void 0 ? _a : '', 'info'); + } + else { + updatedState = __assign(__assign({}, state), { profiles: __spreadArray(__spreadArray([], state.profiles, true), [action.profile], false) }); + } + break; + case 'RemoveProfile': + updatedState = __assign(__assign({}, state), { profiles: state.profiles.filter(function (profile) { return profile.did !== action.profileDid; }) }); + break; + case 'AddFeed': + if (state.feeds.length >= 3) { + Toast.show((_b = msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You may only add up to 3 feeds"], ["You may only add up to 3 feeds"]))).message) !== null && _b !== void 0 ? _b : '', 'info'); + } + else { + updatedState = __assign(__assign({}, state), { feeds: __spreadArray(__spreadArray([], state.feeds, true), [action.feed], false) }); + } + break; + case 'RemoveFeed': + updatedState = __assign(__assign({}, state), { feeds: state.feeds.filter(function (f) { return f.uri !== action.feedUri; }) }); + break; + case 'SetProcessing': + updatedState = __assign(__assign({}, state), { processing: action.processing }); + break; + } + return updatedState; +} +export function Provider(_a) { + var starterPack = _a.starterPack, listItems = _a.listItems, targetProfile = _a.targetProfile, children = _a.children; + var createInitialState = function () { + var _a, _b; + var targetDid = targetProfile === null || targetProfile === void 0 ? void 0 : targetProfile.did; + if (starterPack && + bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)) { + return { + canNext: true, + currentStep: 'Details', + name: starterPack.record.name, + description: starterPack.record.description, + profiles: (_a = listItems === null || listItems === void 0 ? void 0 : listItems.map(function (i) { return i.subject; })) !== null && _a !== void 0 ? _a : [], + feeds: (_b = starterPack.feeds) !== null && _b !== void 0 ? _b : [], + processing: false, + transitionDirection: 'Forward', + targetDid: targetDid, + }; + } + return { + canNext: true, + currentStep: 'Details', + profiles: [targetProfile], + feeds: [], + processing: false, + transitionDirection: 'Forward', + targetDid: targetDid, + }; + }; + var _b = React.useReducer(reducer, null, createInitialState), state = _b[0], dispatch = _b[1]; + return (_jsx(StateContext.Provider, { value: [state, dispatch], children: children })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/StarterPack/Wizard/StepDetails.js b/src/screens/StarterPack/Wizard/StepDetails.js new file mode 100644 index 0000000000..6706a5bffa --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepDetails.js @@ -0,0 +1,38 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { useWizardState } from '#/screens/StarterPack/Wizard/State'; +import { atoms as a, useTheme } from '#/alf'; +import * as TextField from '#/components/forms/TextField'; +import { StarterPack } from '#/components/icons/StarterPack'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { Text } from '#/components/Typography'; +export function StepDetails() { + var _a, _b, _c; + var _ = useLingui()._; + var t = useTheme(); + var _d = useWizardState(), state = _d[0], dispatch = _d[1]; + var currentAccount = useSession().currentAccount; + var currentProfile = useProfileQuery({ + did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + staleTime: Infinity, + }).data; + var name = (currentProfile === null || currentProfile === void 0 ? void 0 : currentProfile.displayName) || (currentProfile === null || currentProfile === void 0 ? void 0 : currentProfile.handle); + return (_jsx(ScreenTransition, { direction: state.transitionDirection, enabledWeb: true, children: _jsxs(View, { style: [a.px_xl, a.gap_xl, a.mt_4xl], children: [_jsxs(View, { style: [a.gap_md, a.align_center, a.px_md, a.mb_md], children: [_jsx(StarterPack, { width: 90, gradient: "sky" }), _jsx(Text, { style: [a.font_semi_bold, a.text_3xl], children: _jsx(Trans, { children: "Invites, but personal" }) }), _jsx(Text, { style: [a.text_center, a.text_md, a.px_md], children: _jsx(Trans, { children: "Invite your friends to follow your favorite feeds and people" }) })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "What do you want to call your starter pack?" }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Input, { label: name ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["", "'s starter pack"], ["", "'s starter pack"])), name)) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["My starter pack"], ["My starter pack"])))), value: state.name, onChangeText: function (text) { return dispatch({ type: 'SetName', name: text }); } }), _jsx(TextField.SuffixText, { label: _(msg({ + comment: 'Accessibility label describing how many characters the user has entered out of a 50-character limit in a text input field', + message: "".concat((_a = state.name) === null || _a === void 0 ? void 0 : _a.length, " out of 50"), + })), children: _jsxs(Text, { style: [t.atoms.text_contrast_medium], children: [(_c = (_b = state.name) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0, "/50"] }) })] })] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Tell us a little more" }) }), _jsx(TextField.Root, { children: _jsx(TextField.Input, { label: name + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["", "'s favorite feeds and people - join me!"], ["", "'s favorite feeds and people - join me!"])), name)) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["My favorite feeds and people - join me!"], ["My favorite feeds and people - join me!"])))), value: state.description, onChangeText: function (text) { + return dispatch({ type: 'SetDescription', description: text }); + }, multiline: true, style: { minHeight: 150 } }) })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/StarterPack/Wizard/StepFeeds.js b/src/screens/StarterPack/Wizard/StepFeeds.js new file mode 100644 index 0000000000..baf6a988b8 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepFeeds.js @@ -0,0 +1,66 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; +import { Trans } from '@lingui/react/macro'; +import { DISCOVER_FEED_URI } from '#/lib/constants'; +import { useA11y } from '#/state/a11y'; +import { useGetPopularFeedsQuery, usePopularFeedsSearch, useSavedFeeds, } from '#/state/queries/feed'; +import { List } from '#/view/com/util/List'; +import { useWizardState } from '#/screens/StarterPack/Wizard/State'; +import { atoms as a, useTheme } from '#/alf'; +import { SearchInput } from '#/components/forms/SearchInput'; +import { useThrottledValue } from '#/components/hooks/useThrottledValue'; +import { Loader } from '#/components/Loader'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { WizardFeedCard } from '#/components/StarterPack/Wizard/WizardListCard'; +import { Text } from '#/components/Typography'; +function keyExtractor(item) { + return item.uri; +} +export function StepFeeds(_a) { + var _b; + var moderationOpts = _a.moderationOpts; + var t = useTheme(); + var _c = useWizardState(), state = _c[0], dispatch = _c[1]; + var _d = useState(''), query = _d[0], setQuery = _d[1]; + var throttledQuery = useThrottledValue(query, 500); + var screenReaderEnabled = useA11y().screenReaderEnabled; + var _e = useSavedFeeds(), savedFeedsAndLists = _e.data, isFetchedSavedFeeds = _e.isFetchedAfterMount; + var savedFeeds = savedFeedsAndLists === null || savedFeedsAndLists === void 0 ? void 0 : savedFeedsAndLists.feeds.filter(function (f) { return f.type === 'feed' && f.view.uri !== DISCOVER_FEED_URI; }).map(function (f) { return f.view; }); + var _f = useGetPopularFeedsQuery({ + limit: 30, + }), popularFeedsPages = _f.data, fetchNextPage = _f.fetchNextPage, isLoadingPopularFeeds = _f.isLoading; + var popularFeeds = (_b = popularFeedsPages === null || popularFeedsPages === void 0 ? void 0 : popularFeedsPages.pages.flatMap(function (p) { return p.feeds; })) !== null && _b !== void 0 ? _b : []; + // If we have saved feeds already loaded, display them immediately + // Then, when popular feeds have loaded we can concat them to the saved feeds + var suggestedFeeds = savedFeeds || isFetchedSavedFeeds + ? popularFeeds + ? savedFeeds.concat(popularFeeds.filter(function (f) { return !savedFeeds.some(function (sf) { return sf.uri === f.uri; }); })) + : savedFeeds + : undefined; + var _g = usePopularFeedsSearch({ query: throttledQuery }), searchedFeeds = _g.data, isFetchingSearchedFeeds = _g.isFetching; + var isLoading = !isFetchedSavedFeeds || isLoadingPopularFeeds || isFetchingSearchedFeeds; + var renderItem = function (_a) { + var item = _a.item; + return (_jsx(WizardFeedCard, { generator: item, btnType: "checkbox", state: state, dispatch: dispatch, moderationOpts: moderationOpts })); + }; + return (_jsxs(ScreenTransition, { style: [a.flex_1], direction: state.transitionDirection, enabledWeb: true, children: [_jsx(View, { style: [a.border_b, t.atoms.border_contrast_medium], children: _jsx(View, { style: [a.py_sm, a.px_md, { height: 60 }], children: _jsx(SearchInput, { value: query, onChangeText: function (t) { return setQuery(t); }, onClearText: function () { return setQuery(''); } }) }) }), _jsx(List, { data: query ? searchedFeeds : suggestedFeeds, renderItem: renderItem, keyExtractor: keyExtractor, onEndReached: !query && !screenReaderEnabled ? function () { return fetchNextPage(); } : undefined, onEndReachedThreshold: 2, keyboardDismissMode: "on-drag", renderScrollComponent: function (props) { return _jsx(KeyboardAwareScrollView, __assign({}, props)); }, keyboardShouldPersistTaps: "handled", disableFullWindowScroll: true, sideBorders: false, style: { flex: 1 }, ListEmptyComponent: _jsx(View, { style: [a.flex_1, a.align_center, a.mt_lg, a.px_lg], children: isLoading ? (_jsx(Loader, { size: "lg" })) : (_jsx(Text, { style: [ + a.font_semi_bold, + a.text_lg, + a.text_center, + a.mt_lg, + a.leading_snug, + ], children: _jsx(Trans, { children: "No feeds found. Try searching for something else." }) })) }) })] })); +} diff --git a/src/screens/StarterPack/Wizard/StepProfiles.js b/src/screens/StarterPack/Wizard/StepProfiles.js new file mode 100644 index 0000000000..2e08bff484 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepProfiles.js @@ -0,0 +1,57 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; +import { Trans } from '@lingui/react/macro'; +import { useA11y } from '#/state/a11y'; +import { useActorAutocompleteQuery } from '#/state/queries/actor-autocomplete'; +import { useActorSearch } from '#/state/queries/actor-search'; +import { List } from '#/view/com/util/List'; +import { useWizardState } from '#/screens/StarterPack/Wizard/State'; +import { atoms as a, useTheme } from '#/alf'; +import { SearchInput } from '#/components/forms/SearchInput'; +import { Loader } from '#/components/Loader'; +import { ScreenTransition } from '#/components/ScreenTransition'; +import { WizardProfileCard } from '#/components/StarterPack/Wizard/WizardListCard'; +import { Text } from '#/components/Typography'; +import { IS_NATIVE } from '#/env'; +function keyExtractor(item) { + var _a; + return (_a = item === null || item === void 0 ? void 0 : item.did) !== null && _a !== void 0 ? _a : ''; +} +export function StepProfiles(_a) { + var moderationOpts = _a.moderationOpts; + var t = useTheme(); + var _b = useWizardState(), state = _b[0], dispatch = _b[1]; + var _c = useState(''), query = _c[0], setQuery = _c[1]; + var screenReaderEnabled = useA11y().screenReaderEnabled; + var _d = useActorSearch({ + query: encodeURIComponent('*'), + }), topPages = _d.data, fetchNextPage = _d.fetchNextPage, isLoadingTopPages = _d.isLoading; + var topFollowers = topPages === null || topPages === void 0 ? void 0 : topPages.pages.flatMap(function (p) { return p.actors; }).filter(function (p) { var _a; return !((_a = p.associated) === null || _a === void 0 ? void 0 : _a.labeler); }); + var _e = useActorAutocompleteQuery(query, true, 12), resultsUnfiltered = _e.data, isFetchingResults = _e.isFetching; + var results = resultsUnfiltered === null || resultsUnfiltered === void 0 ? void 0 : resultsUnfiltered.filter(function (p) { var _a; return !((_a = p.associated) === null || _a === void 0 ? void 0 : _a.labeler); }); + var isLoading = isLoadingTopPages || isFetchingResults; + var renderItem = function (_a) { + var item = _a.item; + return (_jsx(WizardProfileCard, { profile: item, btnType: "checkbox", state: state, dispatch: dispatch, moderationOpts: moderationOpts })); + }; + return (_jsxs(ScreenTransition, { style: [a.flex_1], direction: state.transitionDirection, enabledWeb: true, children: [_jsx(View, { style: [a.border_b, t.atoms.border_contrast_medium], children: _jsx(View, { style: [a.py_sm, a.px_md, { height: 60 }], children: _jsx(SearchInput, { value: query, onChangeText: setQuery, onClearText: function () { return setQuery(''); } }) }) }), _jsx(List, { data: query ? results : topFollowers, renderItem: renderItem, keyExtractor: keyExtractor, renderScrollComponent: function (props) { return _jsx(KeyboardAwareScrollView, __assign({}, props)); }, keyboardShouldPersistTaps: "handled", disableFullWindowScroll: true, sideBorders: false, style: [a.flex_1], onEndReached: !query && !screenReaderEnabled ? function () { return fetchNextPage(); } : undefined, onEndReachedThreshold: IS_NATIVE ? 2 : 0.25, keyboardDismissMode: "on-drag", ListEmptyComponent: _jsx(View, { style: [a.flex_1, a.align_center, a.mt_lg, a.px_lg], children: isLoading ? (_jsx(Loader, { size: "lg" })) : (_jsx(Text, { style: [ + a.font_semi_bold, + a.text_lg, + a.text_center, + a.mt_lg, + a.leading_snug, + ], children: _jsx(Trans, { children: "Nobody was found. Try searching for someone else." }) })) }) })] })); +} diff --git a/src/screens/StarterPack/Wizard/index.js b/src/screens/StarterPack/Wizard/index.js new file mode 100644 index 0000000000..7eba23eada --- /dev/null +++ b/src/screens/StarterPack/Wizard/index.js @@ -0,0 +1,320 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { Keyboard, View } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Image } from 'expo-image'; +import { AtUri, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; +import { STARTER_PACK_MAX_SIZE } from '#/lib/constants'; +import { createSanitizedDisplayName } from '#/lib/moderation/create-sanitized-display-name'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { enforceLen } from '#/lib/strings/helpers'; +import { getStarterPackOgCard, parseStarterPackUri, } from '#/lib/strings/starter-pack'; +import { logger } from '#/logger'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useAllListMembersQuery } from '#/state/queries/list-members'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useCreateStarterPackMutation, useEditStarterPackMutation, useStarterPackQuery, } from '#/state/queries/starter-packs'; +import { useSession } from '#/state/session'; +import { useSetMinimalShellMode } from '#/state/shell'; +import * as Toast from '#/view/com/util/Toast'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { useWizardState, } from '#/screens/StarterPack/Wizard/State'; +import { StepDetails } from '#/screens/StarterPack/Wizard/StepDetails'; +import { StepFeeds } from '#/screens/StarterPack/Wizard/StepFeeds'; +import { StepProfiles } from '#/screens/StarterPack/Wizard/StepProfiles'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import * as Layout from '#/components/Layout'; +import { ListMaybePlaceholder } from '#/components/Lists'; +import { Loader } from '#/components/Loader'; +import { WizardEditListDialog } from '#/components/StarterPack/Wizard/WizardEditListDialog'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +import { Provider } from './State'; +export function Wizard(_a) { + var _b, _c; + var route = _a.route; + var params = (_b = route.params) !== null && _b !== void 0 ? _b : {}; + var rkey = 'rkey' in params ? params.rkey : undefined; + var fromDialog = 'fromDialog' in params ? params.fromDialog : false; + var targetDid = 'targetDid' in params ? params.targetDid : undefined; + var onSuccess = 'onSuccess' in params ? params.onSuccess : undefined; + var currentAccount = useSession().currentAccount; + var moderationOpts = useModerationOpts(); + var _ = useLingui()._; + // Use targetDid if provided (from dialog), otherwise use current account + var profileDid = targetDid || currentAccount.did; + var _d = useStarterPackQuery({ did: currentAccount.did, rkey: rkey }), starterPack = _d.data, isLoadingStarterPack = _d.isLoading, isErrorStarterPack = _d.isError; + var listUri = (_c = starterPack === null || starterPack === void 0 ? void 0 : starterPack.list) === null || _c === void 0 ? void 0 : _c.uri; + var _e = useAllListMembersQuery(listUri), listItems = _e.data, isLoadingProfiles = _e.isLoading, isErrorProfiles = _e.isError; + var _f = useProfileQuery({ did: profileDid }), profile = _f.data, isLoadingProfile = _f.isLoading, isErrorProfile = _f.isError; + var isEdit = Boolean(rkey); + var isReady = (!isEdit || (isEdit && starterPack && listItems)) && + profile && + moderationOpts; + if (!isReady) { + return (_jsx(Layout.Screen, { children: _jsx(ListMaybePlaceholder, { isLoading: isLoadingStarterPack || isLoadingProfiles || isLoadingProfile, isError: isErrorStarterPack || isErrorProfiles || isErrorProfile, errorMessage: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["That starter pack could not be found."], ["That starter pack could not be found."])))) }) })); + } + else if (isEdit && (starterPack === null || starterPack === void 0 ? void 0 : starterPack.creator.did) !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + return (_jsx(Layout.Screen, { children: _jsx(ListMaybePlaceholder, { isLoading: false, isError: true, errorMessage: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["That starter pack could not be found."], ["That starter pack could not be found."])))) }) })); + } + return (_jsx(Layout.Screen, { testID: "starterPackWizardScreen", style: web([{ minHeight: 0 }, a.flex_1]), children: _jsx(Provider, { starterPack: starterPack, listItems: listItems, targetProfile: profile, children: _jsx(WizardInner, { currentStarterPack: starterPack, currentListItems: listItems, profile: profile, moderationOpts: moderationOpts, fromDialog: fromDialog, onSuccess: onSuccess }) }) })); +} +function WizardInner(_a) { + var _this = this; + var currentStarterPack = _a.currentStarterPack, currentListItems = _a.currentListItems, profile = _a.profile, moderationOpts = _a.moderationOpts, fromDialog = _a.fromDialog, onSuccess = _a.onSuccess; + var navigation = useNavigation(); + var ax = useAnalytics(); + var _ = useLingui()._; + var setMinimalShellMode = useSetMinimalShellMode(); + var _b = useWizardState(), state = _b[0], dispatch = _b[1]; + var currentAccount = useSession().currentAccount; + var currentProfile = useProfileQuery({ + did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + staleTime: 0, + }).data; + var parsed = parseStarterPackUri(currentStarterPack === null || currentStarterPack === void 0 ? void 0 : currentStarterPack.uri); + React.useEffect(function () { + navigation.setOptions({ + gestureEnabled: false, + }); + }, [navigation]); + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(true); + return function () { + setMinimalShellMode(false); + }; + }, [setMinimalShellMode])); + var getDefaultName = function () { + var displayName = createSanitizedDisplayName(currentProfile, true); + return _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["", "'s Starter Pack"], ["", "'s Starter Pack"])), displayName)).slice(0, 50); + }; + var wizardUiStrings = { + Details: { + header: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Starter Pack"], ["Starter Pack"])))), + nextBtn: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Next"], ["Next"])))), + }, + Profiles: { + header: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Choose People"], ["Choose People"])))), + nextBtn: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Next"], ["Next"])))), + }, + Feeds: { + header: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Choose Feeds"], ["Choose Feeds"])))), + nextBtn: state.feeds.length === 0 ? _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Skip"], ["Skip"])))) : _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Finish"], ["Finish"])))), + }, + }; + var currUiStrings = wizardUiStrings[state.currentStep]; + var onSuccessCreate = function (data) { + var rkey = new AtUri(data.uri).rkey; + ax.metric('starterPack:create', { + setName: state.name != null, + setDescription: state.description != null, + profilesCount: state.profiles.length, + feedsCount: state.feeds.length, + }); + Image.prefetch([getStarterPackOgCard(currentProfile.did, rkey)]); + dispatch({ type: 'SetProcessing', processing: false }); + if (fromDialog) { + navigation.goBack(); + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(); + } + else { + navigation.replace('StarterPack', { + name: profile.handle, + rkey: rkey, + new: true, + }); + } + }; + var onSuccessEdit = function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.replace('StarterPack', { + name: currentAccount.handle, + rkey: parsed.rkey, + }); + } + }; + var createStarterPack = useCreateStarterPackMutation({ + onSuccess: onSuccessCreate, + onError: function (e) { + logger.error('Failed to create starter pack', { safeMessage: e }); + dispatch({ type: 'SetProcessing', processing: false }); + Toast.show(_(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Failed to create starter pack"], ["Failed to create starter pack"])))), 'xmark'); + }, + }).mutate; + var editStarterPack = useEditStarterPackMutation({ + onSuccess: onSuccessEdit, + onError: function (e) { + logger.error('Failed to edit starter pack', { safeMessage: e }); + dispatch({ type: 'SetProcessing', processing: false }); + Toast.show(_(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Failed to create starter pack"], ["Failed to create starter pack"])))), 'xmark'); + }, + }).mutate; + var submit = function () { return __awaiter(_this, void 0, void 0, function () { + var _a, _b, _c, _d; + return __generator(this, function (_e) { + dispatch({ type: 'SetProcessing', processing: true }); + if (currentStarterPack && currentListItems) { + editStarterPack({ + name: ((_a = state.name) === null || _a === void 0 ? void 0 : _a.trim()) || getDefaultName(), + description: (_b = state.description) === null || _b === void 0 ? void 0 : _b.trim(), + profiles: state.profiles, + feeds: state.feeds, + currentStarterPack: currentStarterPack, + currentListItems: currentListItems, + }); + } + else { + createStarterPack({ + name: ((_c = state.name) === null || _c === void 0 ? void 0 : _c.trim()) || getDefaultName(), + description: (_d = state.description) === null || _d === void 0 ? void 0 : _d.trim(), + profiles: state.profiles, + feeds: state.feeds, + }); + } + return [2 /*return*/]; + }); + }); }; + var onNext = function () { + if (state.currentStep === 'Feeds') { + submit(); + return; + } + var keyboardVisible = Keyboard.isVisible(); + Keyboard.dismiss(); + setTimeout(function () { + dispatch({ type: 'Next' }); + }, keyboardVisible ? 16 : 0); + }; + var items = state.currentStep === 'Profiles' ? state.profiles : state.feeds; + var isEditEnabled = (state.currentStep === 'Profiles' && items.length > 1) || + (state.currentStep === 'Feeds' && items.length > 0); + var editDialogControl = useDialogControl(); + return (_jsxs(Layout.Center, { style: [a.flex_1], children: [_jsxs(Layout.Header.Outer, { children: [_jsx(Layout.Header.BackButton, { label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Back"], ["Back"])))), accessibilityHint: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Returns to the previous step"], ["Returns to the previous step"])))), onPress: function (evt) { + if (state.currentStep !== 'Details') { + evt.preventDefault(); + dispatch({ type: 'Back' }); + } + } }), _jsx(Layout.Header.Content, { align: "left", children: _jsx(Layout.Header.TitleText, { children: currUiStrings.header }) }), isEditEnabled ? (_jsx(Button, { label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Edit"], ["Edit"])))), color: "secondary", size: "small", onPress: editDialogControl.open, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Edit" }) }) })) : (_jsx(Layout.Header.Slot, {}))] }), _jsx(Container, { children: state.currentStep === 'Details' ? (_jsx(StepDetails, {})) : state.currentStep === 'Profiles' ? (_jsx(StepProfiles, { moderationOpts: moderationOpts })) : state.currentStep === 'Feeds' ? (_jsx(StepFeeds, { moderationOpts: moderationOpts })) : null }), state.currentStep !== 'Details' && (_jsx(Footer, { onNext: onNext, nextBtnText: currUiStrings.nextBtn })), _jsx(WizardEditListDialog, { control: editDialogControl, state: state, dispatch: dispatch, moderationOpts: moderationOpts, profile: profile })] })); +} +function Container(_a) { + var children = _a.children; + var _ = useLingui()._; + var _b = useWizardState(), state = _b[0], dispatch = _b[1]; + if (state.currentStep === 'Profiles' || state.currentStep === 'Feeds') { + return _jsx(View, { style: [a.flex_1], children: children }); + } + return (_jsxs(KeyboardAwareScrollView, { style: [a.flex_1], keyboardShouldPersistTaps: "handled", children: [children, state.currentStep === 'Details' && (_jsx(_Fragment, { children: _jsx(Button, { label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Next"], ["Next"])))), variant: "solid", color: "primary", size: "large", style: [a.mx_xl, a.mb_lg, { marginTop: 35 }], onPress: function () { return dispatch({ type: 'Next' }); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Next" }) }) }) }))] })); +} +function Footer(_a) { + var onNext = _a.onNext, nextBtnText = _a.nextBtnText; + var t = useTheme(); + var state = useWizardState()[0]; + var bottomInset = useSafeAreaInsets().bottom; + var currentAccount = useSession().currentAccount; + var items = state.currentStep === 'Profiles' ? state.profiles : state.feeds; + var minimumItems = state.currentStep === 'Profiles' ? 8 : 0; + var textStyles = [a.text_md]; + return (_jsxs(View, { style: [ + a.border_t, + a.align_center, + a.px_lg, + a.pt_xl, + a.gap_md, + t.atoms.bg, + t.atoms.border_contrast_medium, + { + paddingBottom: a.pb_lg.paddingBottom + bottomInset, + }, + IS_NATIVE && [ + a.border_l, + a.border_r, + t.atoms.shadow_md, + { + borderTopLeftRadius: 14, + borderTopRightRadius: 14, + }, + ], + ], children: [items.length > minimumItems && (_jsx(View, { style: [a.absolute, { right: 14, top: 31 }], children: _jsxs(Text, { style: [a.font_semi_bold], children: [items.length, "/", state.currentStep === 'Profiles' ? STARTER_PACK_MAX_SIZE : 3] }) })), _jsx(View, { style: [a.flex_row], children: items.slice(0, 6).map(function (p, index) { return (_jsx(View, { style: [ + a.rounded_full, + { + borderWidth: 0.5, + borderColor: t.atoms.bg.backgroundColor, + }, + state.currentStep === 'Profiles' + ? { zIndex: 1 - index, marginLeft: index > 0 ? -8 : 0 } + : { marginRight: 4 }, + ], children: _jsx(UserAvatar, { avatar: p.avatar, size: 32, type: state.currentStep === 'Profiles' ? 'user' : 'algo' }) }, index)); }) }), state.currentStep === 'Profiles' ? (_jsx(Text, { style: [a.text_center, textStyles], children: items.length < 2 ? ((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === items[0].did ? (_jsx(Trans, { children: "It's just you right now! Add more people to your starter pack by searching above." })) : (_jsxs(Trans, { children: ["It's just", ' ', _jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[0]), ' '] }), "right now! Add more people to your starter pack by searching above."] }))) : items.length === 2 ? ((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === items[0].did ? (_jsxs(Trans, { children: [_jsx(Text, { style: [a.font_semi_bold, textStyles], children: "You" }), " and", _jsx(Text, { children: " " }), _jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[1] /* [0] is self, skip it */), ' '] }), "are included in your starter pack"] })) : (_jsxs(Trans, { children: [_jsx(Text, { style: [a.font_semi_bold, textStyles], children: getName(items[0]) }), ' ', "and", _jsx(Text, { children: " " }), _jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[1] /* [0] is self, skip it */), ' '] }), "are included in your starter pack"] }))) : items.length > 2 ? (_jsxs(Trans, { context: "profiles", children: [_jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[1] /* [0] is self, skip it */), ",", ' '] }), _jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[2]), ",", ' '] }), "and", ' ', _jsx(Plural, { value: items.length - 2, one: "# other", other: "# others" }), ' ', "are included in your starter pack"] })) : null /* Should not happen. */ })) : state.currentStep === 'Feeds' ? (items.length === 0 ? (_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_center, textStyles], children: _jsx(Trans, { children: "Add some feeds to your starter pack!" }) }), _jsx(Text, { style: [a.text_center, textStyles], children: _jsx(Trans, { children: "Search for feeds that you want to suggest to others." }) })] })) : (_jsx(Text, { style: [a.text_center, textStyles], children: items.length === 1 ? (_jsxs(Trans, { children: [_jsx(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: getName(items[0]) }), ' ', "is included in your starter pack"] })) : items.length === 2 ? (_jsxs(Trans, { children: [_jsx(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: getName(items[0]) }), ' ', "and", _jsx(Text, { children: " " }), _jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[1]), ' '] }), "are included in your starter pack"] })) : items.length > 2 ? (_jsxs(Trans, { context: "feeds", children: [_jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[0]), ",", ' '] }), _jsxs(Text, { style: [a.font_semi_bold, textStyles], emoji: true, children: [getName(items[1]), ",", ' '] }), "and", ' ', _jsx(Plural, { value: items.length - 2, one: "# other", other: "# others" }), ' ', "are included in your starter pack"] })) : null /* Should not happen. */ }))) : null /* Should not happen. */, _jsxs(View, { style: [ + a.w_full, + a.align_center, + a.gap_2xl, + IS_NATIVE ? a.mt_sm : a.mt_md, + ], children: [state.currentStep === 'Profiles' && items.length < 8 && (_jsx(Text, { style: [ + a.font_semi_bold, + textStyles, + t.atoms.text_contrast_medium, + ], children: _jsxs(Trans, { children: ["Add ", 8 - items.length, " more to continue"] }) })), _jsxs(Button, { label: nextBtnText, style: [a.w_full, a.py_md, a.px_2xl], color: "primary", size: "large", onPress: onNext, disabled: !state.canNext || + state.processing || + (state.currentStep === 'Profiles' && items.length < 8), children: [_jsx(ButtonText, { children: nextBtnText }), state.processing && _jsx(ButtonIcon, { icon: Loader })] })] })] })); +} +function getName(item) { + if (typeof item.displayName === 'string') { + return enforceLen(sanitizeDisplayName(item.displayName), 28, true); + } + else if ('handle' in item && typeof item.handle === 'string') { + return enforceLen(sanitizeHandle(item.handle), 28, true); + } + return ''; +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16; diff --git a/src/screens/Takendown.js b/src/screens/Takendown.js new file mode 100644 index 0000000000..b49372f99a --- /dev/null +++ b/src/screens/Takendown.js @@ -0,0 +1,140 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState } from 'react'; +import { View } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { ToolsOzoneReportDefs } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useMutation } from '@tanstack/react-query'; +import { countGraphemes } from 'unicode-segmenter/grapheme'; +import { BLUESKY_MOD_SERVICE_HEADERS, MAX_REPORT_REASON_GRAPHEME_LENGTH, } from '#/lib/constants'; +import { cleanError } from '#/lib/strings/errors'; +import { useAgent, useSession, useSessionApi } from '#/state/session'; +import { CharProgress } from '#/view/com/composer/char-progress/CharProgress'; +import { Logo } from '#/view/icons/Logo'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as TextField from '#/components/forms/TextField'; +import { SimpleInlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { P, Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +var COL_WIDTH = 400; +export function Takendown() { + var _this = this; + var _ = useLingui()._; + var t = useTheme(); + var insets = useSafeAreaInsets(); + var gtMobile = useBreakpoints().gtMobile; + var currentAccount = useSession().currentAccount; + var logoutCurrentAccount = useSessionApi().logoutCurrentAccount; + var agent = useAgent(); + var _a = useState(false), isAppealling = _a[0], setIsAppealling = _a[1]; + var _b = useState(''), reason = _b[0], setReason = _b[1]; + var reasonGraphemeLength = countGraphemes(reason); + var isOverMaxLength = reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH; + var _c = useMutation({ + mutationFn: function (appealText) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!currentAccount) + throw new Error('No session'); + return [4 /*yield*/, agent.com.atproto.moderation.createReport({ + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: currentAccount.did, + }, + reason: appealText, + }, { + encoding: 'application/json', + headers: BLUESKY_MOD_SERVICE_HEADERS, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { return setReason(''); }, + }), submitAppeal = _c.mutate, isPending = _c.isPending, isSuccess = _c.isSuccess, error = _c.error; + var primaryBtn = isAppealling && !isSuccess ? (_jsxs(Button, { color: "primary", size: "large", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Submit appeal"], ["Submit appeal"])))), onPress: function () { return submitAppeal(reason); }, disabled: isPending || isOverMaxLength, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Submit Appeal" }) }), isPending && _jsx(ButtonIcon, { icon: Loader })] })) : (_jsx(Button, { size: "large", color: "secondary_inverted", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Sign out"], ["Sign out"])))), onPress: function () { return logoutCurrentAccount('Takendown'); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Sign Out" }) }) })); + var secondaryBtn = isAppealling ? (!isSuccess && (_jsx(Button, { variant: "ghost", size: "large", color: "secondary", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: function () { return setIsAppealling(false); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) }))) : (_jsx(Button, { variant: "ghost", size: "large", color: "secondary", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Appeal suspension"], ["Appeal suspension"])))), onPress: function () { return setIsAppealling(true); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Appeal Suspension" }) }) })); + var webLayout = IS_WEB && gtMobile; + return (_jsxs(View, { style: [a.util_screen_outer, a.flex_1], children: [_jsx(KeyboardAwareScrollView, { style: [a.flex_1, t.atoms.bg], centerContent: true, children: _jsx(View, { style: [ + a.flex_row, + a.justify_center, + gtMobile ? a.pt_4xl : [a.px_xl, a.pt_4xl], + ], children: _jsxs(View, { style: [a.flex_1, { maxWidth: COL_WIDTH, minHeight: COL_WIDTH }], children: [_jsx(View, { style: [a.pb_xl], children: _jsx(Logo, { width: 64 }) }), _jsx(Text, { style: [a.text_4xl, a.font_bold, a.pb_md], children: isAppealling ? (_jsx(Trans, { children: "Appeal suspension" })) : (_jsx(Trans, { children: "Your account has been suspended" })) }), isAppealling ? (_jsxs(View, { style: [a.relative, a.w_full, a.mt_xl], children: [isSuccess ? (_jsx(P, { style: [t.atoms.text_contrast_medium, a.text_center], children: _jsx(Trans, { children: "Your appeal has been submitted. If your appeal succeeds, you will receive an email." }) })) : (_jsxs(_Fragment, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Reason for appeal" }) }), _jsx(TextField.Root, { isInvalid: reasonGraphemeLength > + MAX_REPORT_REASON_GRAPHEME_LENGTH || !!error, children: _jsx(TextField.Input, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Reason for appeal"], ["Reason for appeal"])))), defaultValue: reason, onChangeText: setReason, placeholder: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Why are you appealing?"], ["Why are you appealing?"])))), multiline: true, numberOfLines: 5, autoFocus: true, style: { paddingBottom: 40, minHeight: 150 }, maxLength: MAX_REPORT_REASON_GRAPHEME_LENGTH * 10 }) }), _jsx(View, { style: [ + a.absolute, + a.flex_row, + a.align_center, + a.pr_md, + a.pb_sm, + { + bottom: 0, + right: 0, + }, + ], children: _jsx(CharProgress, { count: reasonGraphemeLength, max: MAX_REPORT_REASON_GRAPHEME_LENGTH }) })] })), error && (_jsx(Text, { style: [ + a.text_md, + a.leading_snug, + { color: t.palette.negative_500 }, + a.mt_lg, + ], children: cleanError(error) }))] })) : (_jsx(P, { style: [t.atoms.text_contrast_medium, a.leading_snug], children: _jsxs(Trans, { children: ["Your account was found to be in violation of the", ' ', _jsx(SimpleInlineLinkText, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Bluesky Social Terms of Service"], ["Bluesky Social Terms of Service"])))), to: "https://bsky.social/about/support/tos", style: [a.text_md, a.leading_snug], children: "Bluesky Social Terms of Service" }), ". You have been sent an email outlining the specific violation and suspension period, if applicable. You can appeal this decision if you believe it was made in error."] }) })), webLayout && (_jsxs(View, { style: [ + a.w_full, + a.flex_row, + a.justify_between, + a.pt_5xl, + { paddingBottom: 200 }, + ], children: [secondaryBtn, primaryBtn] }))] }) }) }), !webLayout && (_jsx(View, { style: [ + a.align_center, + t.atoms.bg, + gtMobile ? a.px_5xl : a.px_xl, + { paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom) }, + ], children: _jsxs(View, { style: [a.w_full, a.gap_sm, { maxWidth: COL_WIDTH }], children: [primaryBtn, secondaryBtn] }) }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/screens/Topic.js b/src/screens/Topic.js new file mode 100644 index 0000000000..f598fe6363 --- /dev/null +++ b/src/screens/Topic.js @@ -0,0 +1,154 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect } from '@react-navigation/native'; +import { HITSLOP_10 } from '#/lib/constants'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { usePostViewTracking } from '#/lib/hooks/usePostViewTracking'; +import { shareUrl } from '#/lib/sharing'; +import { cleanError } from '#/lib/strings/errors'; +import { enforceLen } from '#/lib/strings/helpers'; +import { useSearchPostsQuery } from '#/state/queries/search-posts'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { Pager } from '#/view/com/pager/Pager'; +import { TabBar } from '#/view/com/pager/TabBar'; +import { Post } from '#/view/com/post/Post'; +import { List } from '#/view/com/util/List'; +import { atoms as a, web } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share } from '#/components/icons/ArrowOutOfBox'; +import * as Layout from '#/components/Layout'; +import { ListFooter, ListMaybePlaceholder } from '#/components/Lists'; +var renderItem = function (_a) { + var item = _a.item; + return _jsx(Post, { post: item }); +}; +var keyExtractor = function (item, index) { + return "".concat(item.uri, "-").concat(index); +}; +export default function TopicScreen(_a) { + var route = _a.route; + var topic = route.params.topic; + var _ = useLingui()._; + var headerTitle = React.useMemo(function () { + return enforceLen(decodeURIComponent(topic), 24, true, 'middle'); + }, [topic]); + var onShare = React.useCallback(function () { + var url = new URL('https://bsky.app'); + url.pathname = "/topic/".concat(topic); + shareUrl(url.toString()); + }, [topic]); + var _b = React.useState(0), activeTab = _b[0], setActiveTab = _b[1]; + var setMinimalShellMode = useSetMinimalShellMode(); + useFocusEffect(React.useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var onPageSelected = React.useCallback(function (index) { + setMinimalShellMode(false); + setActiveTab(index); + }, [setMinimalShellMode]); + var sections = React.useMemo(function () { + return [ + { + title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Top"], ["Top"])))), + component: (_jsx(TopicScreenTab, { topic: topic, sort: "top", active: activeTab === 0 })), + }, + { + title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Latest"], ["Latest"])))), + component: (_jsx(TopicScreenTab, { topic: topic, sort: "latest", active: activeTab === 1 })), + }, + ]; + }, [_, topic, activeTab]); + return (_jsx(Layout.Screen, { children: _jsx(Pager, { onPageSelected: onPageSelected, renderTabBar: function (props) { return (_jsxs(Layout.Center, { style: [a.z_10, web([a.sticky, { top: 0 }])], children: [_jsxs(Layout.Header.Outer, { noBottomBorder: true, children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, { children: _jsx(Layout.Header.TitleText, { children: headerTitle }) }), _jsx(Layout.Header.Slot, { children: _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Share"], ["Share"])))), size: "small", variant: "ghost", color: "primary", shape: "round", onPress: onShare, hitSlop: HITSLOP_10, style: [{ right: -3 }], children: _jsx(ButtonIcon, { icon: Share, size: "md" }) }) })] }), _jsx(TabBar, __assign({ items: sections.map(function (section) { return section.title; }) }, props))] })); }, initialPage: 0, children: sections.map(function (section, i) { return (_jsx(View, { children: section.component }, i)); }) }) })); +} +function TopicScreenTab(_a) { + var _this = this; + var topic = _a.topic, sort = _a.sort, active = _a.active; + var _ = useLingui()._; + var initialNumToRender = useInitialNumToRender(); + var _b = React.useState(false), isPTR = _b[0], setIsPTR = _b[1]; + var trackPostView = usePostViewTracking('Topic'); + var _c = useSearchPostsQuery({ + query: decodeURIComponent(topic), + sort: sort, + enabled: active, + }), data = _c.data, isFetched = _c.isFetched, isFetchingNextPage = _c.isFetchingNextPage, isLoading = _c.isLoading, isError = _c.isError, error = _c.error, refetch = _c.refetch, fetchNextPage = _c.fetchNextPage, hasNextPage = _c.hasNextPage; + var posts = React.useMemo(function () { + return (data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { return page.posts; })) || []; + }, [data]); + var onRefresh = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTR(true); + return [4 /*yield*/, refetch()]; + case 1: + _a.sent(); + setIsPTR(false); + return [2 /*return*/]; + } + }); + }); }, [refetch]); + var onEndReached = React.useCallback(function () { + if (isFetchingNextPage || !hasNextPage || error) + return; + fetchNextPage(); + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]); + return (_jsx(_Fragment, { children: posts.length < 1 ? (_jsx(ListMaybePlaceholder, { isLoading: isLoading || !isFetched, isError: isError, onRetry: refetch, emptyType: "results", emptyMessage: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["We couldn't find any results for that topic."], ["We couldn't find any results for that topic."])))) })) : (_jsx(List, { data: posts, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTR, onRefresh: onRefresh, onEndReached: onEndReached, onEndReachedThreshold: 4, onItemSeen: trackPostView, + // @ts-ignore web only -prf + desktopFixedHeight: true, ListFooterComponent: _jsx(ListFooter, { isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage }), initialNumToRender: initialNumToRender, windowSize: 11 })) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/screens/VideoFeed/components/Header.js b/src/screens/VideoFeed/components/Header.js new file mode 100644 index 0000000000..c953cd37c0 --- /dev/null +++ b/src/screens/VideoFeed/components/Header.js @@ -0,0 +1,131 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { HITSLOP_30 } from '#/lib/constants'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useFeedSourceInfoQuery } from '#/state/queries/feed'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Button } from '#/components/Button'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft } from '#/components/icons/Arrow'; +import * as Layout from '#/components/Layout'; +import { BUTTON_VISUAL_ALIGNMENT_OFFSET } from '#/components/Layout/const'; +import { Text } from '#/components/Typography'; +export function HeaderPlaceholder() { + return (_jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [_jsx(View, { style: [ + a.rounded_sm, + { + width: 36, + height: 36, + backgroundColor: 'white', + opacity: 0.8, + }, + ] }), _jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsx(View, { style: [ + a.w_full, + a.rounded_xs, + { + backgroundColor: 'white', + height: 14, + width: 80, + opacity: 0.8, + }, + ] }), _jsx(View, { style: [ + a.w_full, + a.rounded_xs, + { + backgroundColor: 'white', + height: 10, + width: 140, + opacity: 0.6, + }, + ] })] })] })); +} +export function Header(_a) { + var sourceContext = _a.sourceContext; + var content = null; + switch (sourceContext.type) { + case 'feedgen': { + content = _jsx(FeedHeader, { sourceContext: sourceContext }); + break; + } + case 'author': + // TODO + default: { + break; + } + } + return (_jsxs(Layout.Header.Outer, { noBottomBorder: true, children: [_jsx(BackButton, {}), _jsx(Layout.Header.Content, { align: "left", children: content })] })); +} +export function FeedHeader(_a) { + var sourceContext = _a.sourceContext; + var gtMobile = useBreakpoints().gtMobile; + var _b = useFeedSourceInfoQuery({ uri: sourceContext.uri }), info = _b.data, isLoading = _b.isLoading, error = _b.error; + if (sourceContext.sourceInterstitial !== undefined) { + // For now, don't show the header if coming from an interstitial. + return null; + } + if (isLoading) { + return _jsx(HeaderPlaceholder, {}); + } + else if (error || !info) { + return null; + } + return (_jsxs(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: [info.avatar && _jsx(UserAvatar, { size: 36, type: "algo", avatar: info.avatar }), _jsxs(View, { style: [a.flex_1], children: [_jsx(Text, { style: [ + a.text_md, + a.font_bold, + a.leading_tight, + gtMobile && a.text_lg, + ], numberOfLines: 2, children: info.displayName }), _jsx(View, { style: [a.flex_row, { gap: 6 }], children: _jsx(Text, { style: [a.flex_shrink, a.text_sm, a.leading_snug], numberOfLines: 1, children: sanitizeHandle(info.creatorHandle, '@') }) })] })] })); +} +// TODO: This customization should be a part of the layout component +export function BackButton(_a) { + var onPress = _a.onPress, style = _a.style, props = __rest(_a, ["onPress", "style"]); + var _ = useLingui()._; + var navigation = useNavigation(); + var onPressBack = useCallback(function (evt) { + onPress === null || onPress === void 0 ? void 0 : onPress(evt); + if (evt.defaultPrevented) + return; + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }, [onPress, navigation]); + return (_jsx(Layout.Header.Slot, { children: _jsx(Button, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go back"], ["Go back"])))), size: "small", variant: "ghost", color: "secondary", shape: "round", onPress: onPressBack, hitSlop: HITSLOP_30, style: [ + { marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET }, + a.bg_transparent, + style, + ] }, props, { children: _jsx(ArrowLeft, { size: "lg", fill: "white" }) })) })); +} +var templateObject_1; diff --git a/src/screens/VideoFeed/components/Scrubber.js b/src/screens/VideoFeed/components/Scrubber.js new file mode 100644 index 0000000000..a05d9c591b --- /dev/null +++ b/src/screens/VideoFeed/components/Scrubber.js @@ -0,0 +1,159 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { Gesture, GestureDetector, } from 'react-native-gesture-handler'; +import Animated, { clamp, interpolate, runOnJS, runOnUI, useAnimatedReaction, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; +import { useEventListener } from 'expo'; +import { tokens } from '#/alf'; +import { atoms as a } from '#/alf'; +import { formatTime } from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils'; +import { Text } from '#/components/Typography'; +// magic number that is roughly the min height of the write reply button +// we inset the video by this amount +export var VIDEO_PLAYER_BOTTOM_INSET = 57; +export function Scrubber(_a) { + var active = _a.active, player = _a.player, seekingAnimationSV = _a.seekingAnimationSV, scrollGesture = _a.scrollGesture, children = _a.children; + var screenWidth = useSafeAreaFrame().width; + var insets = useSafeAreaInsets(); + var currentTimeSV = useSharedValue(0); + var durationSV = useSharedValue(0); + var _b = useState(0), currentSeekTime = _b[0], setCurrentSeekTime = _b[1]; + var _c = useState(0), duration = _c[0], setDuration = _c[1]; + var updateTime = function (currentTime, duration) { + 'worklet'; + currentTimeSV.set(currentTime); + if (duration !== 0) { + durationSV.set(duration); + } + }; + var isSeekingSV = useSharedValue(false); + var seekProgressSV = useSharedValue(0); + useAnimatedReaction(function () { return Math.round(seekProgressSV.get()); }, function (progress, prevProgress) { + if (progress !== prevProgress) { + runOnJS(setCurrentSeekTime)(progress); + } + }); + var seekBy = useCallback(function (time) { + player === null || player === void 0 ? void 0 : player.seekBy(time); + setTimeout(function () { + runOnUI(function () { + 'worklet'; + isSeekingSV.set(false); + seekingAnimationSV.set(withTiming(0, { duration: 500 })); + })(); + }, 50); + }, [player, isSeekingSV, seekingAnimationSV]); + var scrubPanGesture = useMemo(function () { + return Gesture.Pan() + .blocksExternalGesture(scrollGesture) + .activeOffsetX([-10, 10]) + .failOffsetY([-10, 10]) + .onStart(function () { + 'worklet'; + seekProgressSV.set(currentTimeSV.get()); + isSeekingSV.set(true); + seekingAnimationSV.set(withTiming(1, { duration: 500 })); + }) + .onUpdate(function (evt) { + 'worklet'; + var progress = evt.x / screenWidth; + seekProgressSV.set(clamp(progress * durationSV.get(), 0, durationSV.get())); + }) + .onEnd(function (evt) { + 'worklet'; + isSeekingSV.get(); + var progress = evt.x / screenWidth; + var newTime = clamp(progress * durationSV.get(), 0, durationSV.get()); + // optimisically set the progress bar + seekProgressSV.set(newTime); + // it's seek by, so offset by the current time + // seekBy sets isSeekingSV back to false, so no need to do that here + runOnJS(seekBy)(newTime - currentTimeSV.get()); + }); + }, [ + scrollGesture, + seekingAnimationSV, + seekBy, + screenWidth, + currentTimeSV, + durationSV, + isSeekingSV, + seekProgressSV, + ]); + var timeStyle = useAnimatedStyle(function () { + return { + display: seekingAnimationSV.get() === 0 ? 'none' : 'flex', + opacity: seekingAnimationSV.get(), + }; + }); + var barStyle = useAnimatedStyle(function () { + var currentTime = isSeekingSV.get() + ? seekProgressSV.get() + : currentTimeSV.get(); + var progress = currentTime === 0 ? 0 : currentTime / durationSV.get(); + var isSeeking = seekingAnimationSV.get(); + return { + height: isSeeking * 3 + 1, + opacity: interpolate(isSeeking, [0, 1], [0.4, 0.6]), + width: "".concat(progress * 100, "%"), + }; + }); + var trackStyle = useAnimatedStyle(function () { + return { + height: seekingAnimationSV.get() * 3 + 1, + }; + }); + var childrenStyle = useAnimatedStyle(function () { + return { + opacity: 1 - seekingAnimationSV.get(), + }; + }); + return (_jsxs(_Fragment, { children: [player && active && (_jsx(PlayerListener, { player: player, setDuration: setDuration, updateTime: updateTime })), _jsx(Animated.View, { style: [ + a.absolute, + { + left: 0, + right: 0, + bottom: insets.bottom + 80, + }, + timeStyle, + ], pointerEvents: "none", children: _jsxs(Text, { style: [a.text_center, a.font_semi_bold], children: [_jsx(Text, { style: [a.text_5xl, { fontVariant: ['tabular-nums'] }], children: formatTime(currentSeekTime) }), _jsx(Text, { style: [a.text_2xl, { opacity: 0.8 }], children: ' / ' }), _jsx(Text, { style: [ + a.text_5xl, + { opacity: 0.8 }, + { fontVariant: ['tabular-nums'] }, + ], children: formatTime(duration) })] }) }), _jsx(GestureDetector, { gesture: scrubPanGesture, children: _jsxs(View, { style: [ + a.relative, + a.w_full, + a.justify_end, + { + paddingBottom: insets.bottom, + minHeight: + // bottom padding + insets.bottom + + // scrubber height + tokens.space.lg + + // write reply height + VIDEO_PLAYER_BOTTOM_INSET, + }, + a.z_10, + ], children: [_jsxs(View, { style: [a.w_full, a.relative], children: [_jsx(Animated.View, { style: [ + a.w_full, + { backgroundColor: 'white', opacity: 0.2 }, + trackStyle, + ] }), _jsx(Animated.View, { style: [ + a.absolute, + { top: 0, left: 0, backgroundColor: 'white' }, + barStyle, + ] })] }), _jsx(Animated.View, { style: [{ minHeight: VIDEO_PLAYER_BOTTOM_INSET }, childrenStyle], children: children })] }) })] })); +} +function PlayerListener(_a) { + var player = _a.player, setDuration = _a.setDuration, updateTime = _a.updateTime; + useEventListener(player, 'timeUpdate', function (evt) { + var duration = player.duration; + if (duration !== 0) { + setDuration(Math.round(duration)); + } + runOnUI(updateTime)(evt.currentTime, duration); + }); + return null; +} diff --git a/src/screens/VideoFeed/index.js b/src/screens/VideoFeed/index.js new file mode 100644 index 0000000000..b1b80633e8 --- /dev/null +++ b/src/screens/VideoFeed/index.js @@ -0,0 +1,641 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { LayoutAnimation, Pressable, ScrollView, View, } from 'react-native'; +import { Gesture, GestureDetector, } from 'react-native-gesture-handler'; +import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; +import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; +import { useEvent, useEventListener } from 'expo'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { createVideoPlayer, VideoView } from 'expo-video'; +import { AppBskyEmbedVideo, AppBskyFeedPost, AtUri, RichText as RichTextAPI, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { useFocusEffect, useIsFocused, useNavigation, useRoute, } from '@react-navigation/native'; +import { HITSLOP_20 } from '#/lib/constants'; +import { useHaptics } from '#/lib/haptics'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { cleanError } from '#/lib/strings/errors'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { logger } from '#/logger'; +import { useA11y } from '#/state/a11y'; +import { POST_TOMBSTONE, usePostShadow, } from '#/state/cache/post-shadow'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { FeedFeedbackProvider, useFeedFeedback, useFeedFeedbackContext, } from '#/state/feed-feedback'; +import { useFeedInfo } from '#/state/queries/feed'; +import { usePostLikeMutationQueue } from '#/state/queries/post'; +import { usePostFeedQuery, } from '#/state/queries/post-feed'; +import { useProfileFollowMutationQueue } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { useSetMinimalShellMode } from '#/state/shell'; +import { useSetLightStatusBar } from '#/state/shell/light-status-bar'; +import { List } from '#/view/com/util/List'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { ThreadComposePrompt } from '#/screens/PostThread/components/ThreadComposePrompt'; +import { Header } from '#/screens/VideoFeed/components/Header'; +import { atoms as a, ios, platform, ThemeProvider, useTheme } from '#/alf'; +import { setSystemUITheme } from '#/alf/util/systemUI'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { Divider } from '#/components/Divider'; +import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon } from '#/components/icons/Arrow'; +import { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { EyeSlash_Stroke2_Corner0_Rounded as Eye } from '#/components/icons/EyeSlash'; +import { Leaf_Stroke2_Corner0_Rounded as LeafIcon } from '#/components/icons/Leaf'; +import { KeepAwake } from '#/components/KeepAwake'; +import * as Layout from '#/components/Layout'; +import { Link } from '#/components/Link'; +import { ListFooter } from '#/components/Lists'; +import * as Hider from '#/components/moderation/Hider'; +import { PostControls } from '#/components/PostControls'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_ANDROID } from '#/env'; +import * as bsky from '#/types/bsky'; +import { Scrubber, VIDEO_PLAYER_BOTTOM_INSET } from './components/Scrubber'; +function createThreeVideoPlayers(sources) { + var _a, _b, _c; + // android is typically slower and can't keep up with a 0.1 interval + var eventInterval = platform({ + ios: 0.2, + android: 0.5, + default: 0, + }); + var p1 = createVideoPlayer((_a = sources === null || sources === void 0 ? void 0 : sources[0]) !== null && _a !== void 0 ? _a : ''); + p1.loop = true; + p1.timeUpdateEventInterval = eventInterval; + var p2 = createVideoPlayer((_b = sources === null || sources === void 0 ? void 0 : sources[1]) !== null && _b !== void 0 ? _b : ''); + p2.loop = true; + p2.timeUpdateEventInterval = eventInterval; + var p3 = createVideoPlayer((_c = sources === null || sources === void 0 ? void 0 : sources[2]) !== null && _c !== void 0 ? _c : ''); + p3.loop = true; + p3.timeUpdateEventInterval = eventInterval; + return [p1, p2, p3]; +} +export function VideoFeed(_a) { + var top = useSafeAreaInsets().top; + var params = useRoute().params; + var t = useTheme(); + var setMinShellMode = useSetMinimalShellMode(); + useFocusEffect(useCallback(function () { + setMinShellMode(true); + setSystemUITheme('lightbox', t); + return function () { + setMinShellMode(false); + setSystemUITheme('theme', t); + }; + }, [setMinShellMode, t])); + var isFocused = useIsFocused(); + useSetLightStatusBar(isFocused); + return (_jsx(ThemeProvider, { theme: "dark", children: _jsxs(Layout.Screen, { noInsetTop: true, style: { backgroundColor: 'black' }, children: [_jsx(KeepAwake, {}), _jsx(View, { style: [ + a.absolute, + a.z_50, + { top: 0, left: 0, right: 0, paddingTop: top }, + ], children: _jsx(Header, { sourceContext: params }) }), _jsx(Feed, {})] }) })); +} +var viewabilityConfig = { + itemVisiblePercentThreshold: 100, + minimumViewTime: 0, +}; +function Feed() { + var params = useRoute().params; + var isFocused = useIsFocused(); + var hasSession = useSession().hasSession; + var height = useSafeAreaFrame().height; + var feedDesc = useMemo(function () { + switch (params.type) { + case 'feedgen': + return "feedgen|".concat(params.uri); + case 'author': + return "author|".concat(params.did, "|").concat(params.filter); + default: + throw new Error("Invalid video feed params ".concat(JSON.stringify(params))); + } + }, [params]); + var feedUri = params.type === 'feedgen' ? params.uri : undefined; + var feedInfo = useFeedInfo(feedUri).data; + var feedFeedback = useFeedFeedback(feedInfo !== null && feedInfo !== void 0 ? feedInfo : undefined, hasSession); + var _a = usePostFeedQuery(feedDesc, params.type === 'feedgen' && params.sourceInterstitial !== 'none' + ? { feedCacheKey: params.sourceInterstitial } + : undefined), data = _a.data, error = _a.error, hasNextPage = _a.hasNextPage, isFetchingNextPage = _a.isFetchingNextPage, fetchNextPage = _a.fetchNextPage; + var videos = useMemo(function () { + var _a; + var vids = (_a = data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { + var items = []; + var _loop_1 = function (slice) { + var feedPost = slice.items.find(function (item) { return item.uri === slice.feedPostUri; }); + if (feedPost && AppBskyEmbedVideo.isView(feedPost.post.embed)) { + items.push({ + _reactKey: feedPost._reactKey, + moderation: feedPost.moderation, + post: feedPost.post, + video: feedPost.post.embed, + feedContext: slice.feedContext, + reqId: slice.reqId, + }); + } + }; + for (var _i = 0, _a = page.slices; _i < _a.length; _i++) { + var slice = _a[_i]; + _loop_1(slice); + } + return items; + })) !== null && _a !== void 0 ? _a : []; + var startingVideoIndex = vids === null || vids === void 0 ? void 0 : vids.findIndex(function (video) { + return video.post.uri === params.initialPostUri; + }); + if (vids && startingVideoIndex && startingVideoIndex > -1) { + vids = vids.slice(startingVideoIndex); + } + return vids; + }, [data, params.initialPostUri]); + var _b = useState([null, null, null]), currentSources = _b[0], setCurrentSources = _b[1]; + var _c = useState(null), players = _c[0], setPlayers = _c[1]; + var _d = useState(0), currentIndex = _d[0], setCurrentIndex = _d[1]; + var scrollGesture = useMemo(function () { return Gesture.Native(); }, []); + var renderItem = useCallback(function (_a) { + var item = _a.item, index = _a.index; + var post = item.post, video = item.video; + var player = players === null || players === void 0 ? void 0 : players[index % 3]; + var currentSource = currentSources[index % 3]; + return (_jsx(VideoItem, { player: player, post: post, embed: video, active: isFocused && + index === currentIndex && + (currentSource === null || currentSource === void 0 ? void 0 : currentSource.source) === video.playlist, adjacent: index === currentIndex - 1 || index === currentIndex + 1, moderation: item.moderation, scrollGesture: scrollGesture, feedContext: item.feedContext, reqId: item.reqId })); + }, [players, currentIndex, isFocused, currentSources, scrollGesture]); + var updateVideoState = useCallback(function (index) { + var _a, _b, _c, _d, _e, _f; + if (!videos.length) + return; + var prevSlice = videos.at(index - 1); + var prevPost = prevSlice === null || prevSlice === void 0 ? void 0 : prevSlice.post; + var prevEmbed = prevPost === null || prevPost === void 0 ? void 0 : prevPost.embed; + var prevVideo = prevEmbed && AppBskyEmbedVideo.isView(prevEmbed) + ? prevEmbed.playlist + : null; + var currSlice = videos.at(index); + var currPost = currSlice === null || currSlice === void 0 ? void 0 : currSlice.post; + var currEmbed = currPost === null || currPost === void 0 ? void 0 : currPost.embed; + var currVideo = currEmbed && AppBskyEmbedVideo.isView(currEmbed) + ? currEmbed.playlist + : null; + var currVideoModeration = currSlice === null || currSlice === void 0 ? void 0 : currSlice.moderation; + var nextSlice = videos.at(index + 1); + var nextPost = nextSlice === null || nextSlice === void 0 ? void 0 : nextSlice.post; + var nextEmbed = nextPost === null || nextPost === void 0 ? void 0 : nextPost.embed; + var nextVideo = nextEmbed && AppBskyEmbedVideo.isView(nextEmbed) + ? nextEmbed.playlist + : null; + var prevPlayerCurrentSource = currentSources[(index + 2) % 3]; + var currPlayerCurrentSource = currentSources[index % 3]; + var nextPlayerCurrentSource = currentSources[(index + 1) % 3]; + if (!players) { + var args = ['', '', '']; + if (prevVideo) + args[(index + 2) % 3] = prevVideo; + if (currVideo) + args[index % 3] = currVideo; + if (nextVideo) + args[(index + 1) % 3] = nextVideo; + var _g = createThreeVideoPlayers(args), player1 = _g[0], player2 = _g[1], player3 = _g[2]; + setPlayers([player1, player2, player3]); + if (currVideo) { + var currPlayer = [player1, player2, player3][index % 3]; + currPlayer.play(); + } + } + else { + var player1 = players[0], player2 = players[1], player3 = players[2]; + var prevPlayer = [player1, player2, player3][(index + 2) % 3]; + var currPlayer = [player1, player2, player3][index % 3]; + var nextPlayer = [player1, player2, player3][(index + 1) % 3]; + if (prevVideo && prevVideo !== (prevPlayerCurrentSource === null || prevPlayerCurrentSource === void 0 ? void 0 : prevPlayerCurrentSource.source)) { + prevPlayer.replace(prevVideo); + } + prevPlayer.pause(); + if (currVideo) { + if (currVideo !== (currPlayerCurrentSource === null || currPlayerCurrentSource === void 0 ? void 0 : currPlayerCurrentSource.source)) { + currPlayer.replace(currVideo); + } + if (currVideoModeration && + (currVideoModeration.ui('contentView').blur || + currVideoModeration.ui('contentMedia').blur)) { + currPlayer.pause(); + } + else { + currPlayer.play(); + } + } + if (nextVideo && nextVideo !== (nextPlayerCurrentSource === null || nextPlayerCurrentSource === void 0 ? void 0 : nextPlayerCurrentSource.source)) { + nextPlayer.replace(nextVideo); + } + nextPlayer.pause(); + } + var updatedSources = __spreadArray([], currentSources, true); + if (prevVideo && prevVideo !== (prevPlayerCurrentSource === null || prevPlayerCurrentSource === void 0 ? void 0 : prevPlayerCurrentSource.source)) { + updatedSources[(index + 2) % 3] = { + source: prevVideo, + }; + } + if (currVideo && currVideo !== (currPlayerCurrentSource === null || currPlayerCurrentSource === void 0 ? void 0 : currPlayerCurrentSource.source)) { + updatedSources[index % 3] = { + source: currVideo, + }; + } + if (nextVideo && nextVideo !== (nextPlayerCurrentSource === null || nextPlayerCurrentSource === void 0 ? void 0 : nextPlayerCurrentSource.source)) { + updatedSources[(index + 1) % 3] = { + source: nextVideo, + }; + } + if (((_a = updatedSources[0]) === null || _a === void 0 ? void 0 : _a.source) !== ((_b = currentSources[0]) === null || _b === void 0 ? void 0 : _b.source) || + ((_c = updatedSources[1]) === null || _c === void 0 ? void 0 : _c.source) !== ((_d = currentSources[1]) === null || _d === void 0 ? void 0 : _d.source) || + ((_e = updatedSources[2]) === null || _e === void 0 ? void 0 : _e.source) !== ((_f = currentSources[2]) === null || _f === void 0 ? void 0 : _f.source)) { + setCurrentSources(updatedSources); + } + }, [videos, currentSources, players]); + var updateVideoStateInitially = useNonReactiveCallback(function () { + updateVideoState(currentIndex); + }); + useFocusEffect(useCallback(function () { + if (!players) { + // create players, set sources, start playing + updateVideoStateInitially(); + } + return function () { + if (players) { + // manually release players when offscreen + players.forEach(function (p) { return p.release(); }); + setPlayers(null); + } + }; + }, [players, updateVideoStateInitially])); + var onViewableItemsChanged = useCallback(function (_a) { + var viewableItems = _a.viewableItems; + if (viewableItems[0] && viewableItems[0].index !== null) { + var newIndex = viewableItems[0].index; + setCurrentIndex(newIndex); + updateVideoState(newIndex); + } + }, [updateVideoState]); + var renderEndMessage = useCallback(function () { return _jsx(EndMessage, {}); }, []); + return (_jsx(FeedFeedbackProvider, { value: feedFeedback, children: _jsx(GestureDetector, { gesture: scrollGesture, children: _jsx(List, { data: videos, renderItem: renderItem, keyExtractor: keyExtractor, initialNumToRender: 3, maxToRenderPerBatch: 3, windowSize: 6, pagingEnabled: true, ListFooterComponent: _jsx(ListFooter, { hasNextPage: hasNextPage, isFetchingNextPage: isFetchingNextPage, error: cleanError(error), onRetry: fetchNextPage, height: height, showEndMessage: true, renderEndMessage: renderEndMessage, style: [a.justify_center, a.border_0] }), onEndReached: function () { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, showsVerticalScrollIndicator: false, onViewableItemsChanged: onViewableItemsChanged, viewabilityConfig: viewabilityConfig }) }) })); +} +function keyExtractor(item) { + return item._reactKey; +} +var VideoItem = function (_a) { + var player = _a.player, post = _a.post, embed = _a.embed, active = _a.active, adjacent = _a.adjacent, scrollGesture = _a.scrollGesture, moderation = _a.moderation, feedContext = _a.feedContext, reqId = _a.reqId; + var ax = useAnalytics(); + var postShadow = usePostShadow(post); + var _b = useSafeAreaFrame(), width = _b.width, height = _b.height; + var _c = useFeedFeedbackContext(), sendInteraction = _c.sendInteraction, feedDescriptor = _c.feedDescriptor; + var hasTrackedView = useRef(false); + useEffect(function () { + if (active) { + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionSeen', + feedContext: feedContext, + reqId: reqId, + }); + // Track post:view event + if (!hasTrackedView.current) { + hasTrackedView.current = true; + ax.metric('post:view', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'ImmersiveVideo', + feedDescriptor: feedDescriptor, + }); + } + } + }, [ + active, + post.uri, + post.author.did, + feedContext, + reqId, + sendInteraction, + feedDescriptor, + ]); + // TODO: high-performance android phones should also + // be capable of rendering 3 video players, but currently + // we can't distinguish between them + var shouldRenderVideo = active || ios(adjacent); + return (_jsx(View, { style: [a.relative, { height: height, width: width }], children: postShadow === POST_TOMBSTONE ? (_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.z_20, + a.align_center, + a.justify_center, + { backgroundColor: 'rgba(0, 0, 0, 0.8)' }, + ], children: _jsx(Text, { style: [ + a.text_2xl, + a.font_bold, + a.text_center, + a.leading_tight, + a.mx_xl, + ], children: _jsx(Trans, { children: "Post has been deleted" }) }) })) : (_jsxs(_Fragment, { children: [_jsx(VideoItemPlaceholder, { embed: embed }), shouldRenderVideo && player && (_jsx(VideoItemInner, { player: player, embed: embed })), moderation && (_jsx(Overlay, { player: player, post: postShadow, embed: embed, active: active, scrollGesture: scrollGesture, moderation: moderation, feedContext: feedContext, reqId: reqId }))] })) })); +}; +VideoItem = memo(VideoItem); +function VideoItemInner(_a) { + var player = _a.player, embed = _a.embed; + var bottom = useSafeAreaInsets().bottom; + var _b = useState(!IS_ANDROID), isReady = _b[0], setIsReady = _b[1]; + useEventListener(player, 'timeUpdate', function (evt) { + if (IS_ANDROID && !isReady && evt.currentTime >= 0.05) { + setIsReady(true); + } + }); + return (_jsx(VideoView, { accessible: false, style: [ + a.absolute, + { + top: 0, + left: 0, + right: 0, + bottom: bottom + VIDEO_PLAYER_BOTTOM_INSET, + }, + !isReady && { opacity: 0 }, + ], player: player, nativeControls: false, contentFit: isTallAspectRatio(embed.aspectRatio) ? 'cover' : 'contain', accessibilityIgnoresInvertColors: true })); +} +function ModerationOverlay(_a) { + var embed = _a.embed, onPressShow = _a.onPressShow; + var _ = useLingui()._; + var hider = Hider.useHider(); + var bottom = useSafeAreaInsets().bottom; + var onShow = useCallback(function () { + hider.setIsContentVisible(true); + onPressShow(); + }, [hider, onPressShow]); + return (_jsxs(View, { style: [a.absolute, a.inset_0, a.z_20], children: [_jsx(VideoItemPlaceholder, { blur: true, embed: embed }), _jsxs(View, { style: [ + a.absolute, + a.inset_0, + a.z_20, + a.justify_center, + a.align_center, + { backgroundColor: 'rgba(0, 0, 0, 0.8)' }, + ], children: [_jsxs(View, { style: [a.align_center, a.gap_sm], children: [_jsx(Eye, { width: 36, fill: "white" }), _jsx(Text, { style: [a.text_center, a.leading_snug, a.pb_xs], children: _jsx(Trans, { children: "Hidden by your moderation settings." }) }), _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Show anyway"], ["Show anyway"])))), size: "small", variant: "solid", color: "secondary_inverted", onPress: onShow, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Show anyway" }) }) })] }), _jsxs(View, { style: [ + a.absolute, + a.inset_0, + a.px_xl, + a.pt_4xl, + { + top: 'auto', + paddingBottom: bottom, + }, + ], children: [_jsx(LinearGradient, { colors: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.4)'], style: [a.absolute, a.inset_0] }), _jsx(Divider, { style: { borderColor: 'white' } }), _jsx(View, { children: _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["View details"], ["View details"])))), onPress: function () { + hider.showInfoDialog(); + }, style: [ + a.w_full, + { + height: 60, + }, + ], children: function (_a) { + var pressed = _a.pressed; + return (_jsx(Text, { style: [ + a.text_sm, + a.font_semi_bold, + a.text_center, + { opacity: pressed ? 0.5 : 1 }, + ], children: _jsx(Trans, { children: "View details" }) })); + } }) })] })] })] })); +} +function Overlay(_a) { + var _b, _c, _d, _e, _f, _g; + var player = _a.player, post = _a.post, embed = _a.embed, active = _a.active, scrollGesture = _a.scrollGesture, moderation = _a.moderation, feedContext = _a.feedContext, reqId = _a.reqId; + var _ = useLingui()._; + var t = useTheme(); + var openComposer = useOpenComposer().openComposer; + var currentAccount = useSession().currentAccount; + var navigation = useNavigation(); + var seekingAnimationSV = useSharedValue(0); + var profile = useProfileShadow(post.author); + var _h = useProfileFollowMutationQueue(profile, 'ImmersiveVideo'), queueFollow = _h[0], queueUnfollow = _h[1]; + var rkey = new AtUri(post.uri).rkey; + var record = bsky.dangerousIsType(post.record, AppBskyFeedPost.isRecord) + ? post.record + : undefined; + var richText = new RichTextAPI({ + text: (record === null || record === void 0 ? void 0 : record.text) || '', + facets: record === null || record === void 0 ? void 0 : record.facets, + }); + var handle = sanitizeHandle(post.author.handle, '@'); + var animatedStyle = useAnimatedStyle(function () { return ({ + opacity: 1 - seekingAnimationSV.get(), + }); }); + var onPressShow = useCallback(function () { + player === null || player === void 0 ? void 0 : player.play(); + }, [player]); + var mergedModui = useMemo(function () { + var modui = moderation.ui('contentView'); + var mediaModui = moderation.ui('contentMedia'); + modui.alerts = __spreadArray(__spreadArray([], modui.alerts, true), mediaModui.alerts, true); + modui.blurs = __spreadArray(__spreadArray([], modui.blurs, true), mediaModui.blurs, true); + modui.filters = __spreadArray(__spreadArray([], modui.filters, true), mediaModui.filters, true); + modui.informs = __spreadArray(__spreadArray([], modui.informs, true), mediaModui.informs, true); + return modui; + }, [moderation]); + var onPressReply = useCallback(function () { + openComposer({ + replyTo: { + uri: post.uri, + cid: post.cid, + text: (record === null || record === void 0 ? void 0 : record.text) || '', + author: post.author, + embed: post.embed, + langs: record === null || record === void 0 ? void 0 : record.langs, + }, + logContext: 'PostReply', + }); + }, [openComposer, post, record]); + return (_jsxs(Hider.Outer, { modui: mergedModui, children: [_jsx(Hider.Mask, { children: _jsx(ModerationOverlay, { embed: embed, onPressShow: onPressShow }) }), _jsx(Hider.Content, { children: _jsxs(View, { style: [a.absolute, a.inset_0, a.z_20], children: [_jsx(View, { style: [a.flex_1], children: player && (_jsx(PlayPauseTapArea, { player: player, post: post, feedContext: feedContext, reqId: reqId })) }), _jsxs(LinearGradient, { colors: [ + 'rgba(0,0,0,0)', + 'rgba(0,0,0,0.7)', + 'rgba(0,0,0,0.95)', + 'rgba(0,0,0,0.95)', + ], style: [a.w_full, a.pt_md], children: [_jsxs(Animated.View, { style: [a.px_md, animatedStyle], children: [_jsxs(View, { style: [a.w_full, a.flex_row, a.align_center, a.gap_md], children: [_jsxs(Link, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["View ", "'s profile"], ["View ", "'s profile"])), sanitizeDisplayName(post.author.displayName || post.author.handle))), to: { + screen: 'Profile', + params: { name: post.author.did }, + }, style: [a.flex_1, a.flex_row, a.gap_md, a.align_center], children: [_jsx(UserAvatar, { type: "user", avatar: post.author.avatar, size: 32 }), _jsxs(View, { style: [a.flex_1], children: [_jsx(Text, { style: [a.text_md, a.font_bold], emoji: true, numberOfLines: 1, children: sanitizeDisplayName(post.author.displayName || post.author.handle) }), _jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_high], numberOfLines: 1, children: handle })] })] }), post.author.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) && + !((_b = post.author.viewer) === null || _b === void 0 ? void 0 : _b.following) && (_jsxs(Button, { label: ((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.following) + ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Following ", ""], ["Following ", ""])), handle)) + : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Follow ", ""], ["Follow ", ""])), handle)), accessibilityHint: ((_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.following) + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Unfollows the user"], ["Unfollows the user"])))) + : '', size: "small", variant: "solid", color: "secondary_inverted", style: [a.mb_xs], onPress: function () { + var _a; + return ((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following) + ? queueUnfollow() + : queueFollow(); + }, children: [!!((_e = profile.viewer) === null || _e === void 0 ? void 0 : _e.following) && (_jsx(ButtonIcon, { icon: CheckIcon })), _jsx(ButtonText, { children: ((_f = profile.viewer) === null || _f === void 0 ? void 0 : _f.following) ? (_jsx(Trans, { children: "Following" })) : (_jsx(Trans, { children: "Follow" })) })] }))] }), ((_g = record === null || record === void 0 ? void 0 : record.text) === null || _g === void 0 ? void 0 : _g.trim()) && (_jsx(ExpandableRichTextView, { value: richText, authorHandle: post.author.handle })), record && (_jsx(View, { style: [{ left: -5 }], children: _jsx(PostControls, { richText: richText, post: post, record: record, feedContext: feedContext, logContext: "FeedItem", onPressReply: function () { + return navigation.navigate('PostThread', { + name: post.author.did, + rkey: rkey, + }); + }, big: true }) }))] }), _jsx(Scrubber, { active: active, player: player, seekingAnimationSV: seekingAnimationSV, scrollGesture: scrollGesture, children: _jsx(ThreadComposePrompt, { onPressCompose: onPressReply, style: [a.pt_md, a.pb_sm] }) })] })] }) })] })); +} +function ExpandableRichTextView(_a) { + var value = _a.value, authorHandle = _a.authorHandle; + var screenHeight = useSafeAreaFrame().height; + var _b = useState(false), expanded = _b[0], setExpanded = _b[1]; + var _c = useState(false), hasBeenExpanded = _c[0], setHasBeenExpanded = _c[1]; + var _d = useState(false), constrained = _d[0], setConstrained = _d[1]; + var _e = useState(0), contentHeight = _e[0], setContentHeight = _e[1]; + var _ = useLingui()._; + var screenReaderEnabled = useA11y().screenReaderEnabled; + if (expanded && !hasBeenExpanded) { + setHasBeenExpanded(true); + } + return (_jsxs(ScrollView, { scrollEnabled: expanded, onContentSizeChange: function (_w, h) { + if (hasBeenExpanded) { + LayoutAnimation.configureNext({ + duration: 500, + update: { type: 'spring', springDamping: 0.6 }, + }); + } + setContentHeight(h); + }, style: { height: Math.min(contentHeight, screenHeight * 0.5) }, contentContainerStyle: [ + a.py_sm, + a.gap_xs, + expanded ? [a.align_start] : a.flex_row, + ], children: [_jsx(RichText, { value: value, style: [a.text_sm, a.flex_1, a.leading_relaxed], authorHandle: authorHandle, enableTags: true, numberOfLines: expanded || screenReaderEnabled ? undefined : constrained ? 2 : 2, onTextLayout: function (evt) { + if (!constrained && evt.nativeEvent.lines.length > 1) { + setConstrained(true); + } + } }), constrained && !screenReaderEnabled && (_jsx(Pressable, { accessibilityHint: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Expands or collapses post text"], ["Expands or collapses post text"])))), accessibilityLabel: expanded ? _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Read less"], ["Read less"])))) : _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Read more"], ["Read more"])))), hitSlop: HITSLOP_20, onPress: function () { return setExpanded(function (prev) { return !prev; }); }, style: [a.absolute, a.inset_0] }))] })); +} +function VideoItemPlaceholder(_a) { + var embed = _a.embed, style = _a.style, blur = _a.blur; + var bottom = useSafeAreaInsets().bottom; + var src = embed.thumbnail; + var contentFit = isTallAspectRatio(embed.aspectRatio) + ? 'cover' + : 'contain'; + if (blur) { + contentFit = 'cover'; + } + return src ? (_jsx(Image, { accessibilityIgnoresInvertColors: true, source: { uri: src }, style: [ + a.absolute, + blur + ? a.inset_0 + : { + top: 0, + left: 0, + right: 0, + bottom: bottom + VIDEO_PLAYER_BOTTOM_INSET, + }, + style, + ], contentFit: contentFit, blurRadius: blur ? 100 : 0 })) : null; +} +function PlayPauseTapArea(_a) { + var player = _a.player, post = _a.post, feedContext = _a.feedContext, reqId = _a.reqId; + var _ = useLingui()._; + var doubleTapRef = useRef(null); + var playHaptic = useHaptics(); + // TODO: implement viaRepost -sfn + var queueLike = usePostLikeMutationQueue(post, undefined, undefined, 'ImmersiveVideo')[0]; + var sendInteraction = useFeedFeedbackContext().sendInteraction; + var isPlaying = useEvent(player, 'playingChange', { + isPlaying: player.playing, + }).isPlaying; + var isMounted = useRef(false); + useEffect(function () { + isMounted.current = true; + return function () { + isMounted.current = false; + }; + }, []); + var togglePlayPause = useNonReactiveCallback(function () { + // gets called after a timeout, so guard against being called after unmount -sfn + if (!player || !isMounted.current) + return; + doubleTapRef.current = null; + try { + if (player.playing) { + player.pause(); + } + else { + player.play(); + } + } + catch (err) { + logger.error('Could not toggle play/pause', { safeMessage: err }); + } + }); + var onPress = function () { + if (doubleTapRef.current) { + clearTimeout(doubleTapRef.current); + doubleTapRef.current = null; + playHaptic('Light'); + queueLike(); + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionLike', + feedContext: feedContext, + reqId: reqId, + }); + } + else { + doubleTapRef.current = setTimeout(togglePlayPause, 200); + } + }; + return (_jsx(Button, { disabled: !player, "aria-valuetext": isPlaying ? _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Video is playing"], ["Video is playing"])))) : _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Video is paused"], ["Video is paused"])))), label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Video from ", ". Tap to play or pause the video"], ["Video from ", ". Tap to play or pause the video"])), sanitizeHandle(post.author.handle, '@'))), accessibilityHint: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Double tap to like"], ["Double tap to like"])))), onPress: onPress, style: [a.absolute, a.inset_0, a.z_10], children: _jsx(View, {}) })); +} +function EndMessage() { + var navigation = useNavigation(); + var _ = useLingui()._; + var t = useTheme(); + return (_jsxs(View, { style: [ + a.w_full, + a.gap_3xl, + a.px_lg, + a.mx_auto, + a.align_center, + { maxWidth: 350 }, + ], children: [_jsx(View, { style: [ + { height: 100, width: 100 }, + a.rounded_full, + t.atoms.bg_contrast_700, + a.align_center, + a.justify_center, + ], children: _jsx(LeafIcon, { width: 64, fill: "black" }) }), _jsxs(View, { style: [a.w_full, a.gap_md], children: [_jsx(Text, { style: [a.text_3xl, a.text_center, a.font_bold], children: _jsx(Trans, { children: "That's everything!" }) }), _jsx(Text, { style: [ + a.text_lg, + a.text_center, + t.atoms.text_contrast_high, + a.leading_snug, + ], children: _jsx(Trans, { children: "You've run out of videos to watch. Maybe it's a good time to take a break?" }) })] }), _jsxs(Button, { testID: "videoFeedGoBackButton", onPress: function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }, variant: "solid", color: "secondary_inverted", size: "small", label: _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Go back"], ["Go back"])))), accessibilityHint: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Returns to previous page"], ["Returns to previous page"])))), children: [_jsx(ButtonIcon, { icon: ArrowLeftIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Go back" }) })] })] })); +} +/* + * If the video is taller than 9:16 + */ +function isTallAspectRatio(aspectRatio) { + var _a, _b; + var videoAspectRatio = ((_a = aspectRatio === null || aspectRatio === void 0 ? void 0 : aspectRatio.width) !== null && _a !== void 0 ? _a : 1) / ((_b = aspectRatio === null || aspectRatio === void 0 ? void 0 : aspectRatio.height) !== null && _b !== void 0 ? _b : 1); + return videoAspectRatio <= 9 / 16; +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15; diff --git a/src/screens/VideoFeed/index.web.js b/src/screens/VideoFeed/index.web.js new file mode 100644 index 0000000000..a229f21aca --- /dev/null +++ b/src/screens/VideoFeed/index.web.js @@ -0,0 +1,3 @@ +export function VideoFeed() { + return null; +} diff --git a/src/screens/VideoFeed/types.js b/src/screens/VideoFeed/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/screens/VideoFeed/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/state/__mocks__/birthdate.js b/src/state/__mocks__/birthdate.js new file mode 100644 index 0000000000..d1938550de --- /dev/null +++ b/src/state/__mocks__/birthdate.js @@ -0,0 +1 @@ +export var snoozeBirthdateUpdateAllowedForDid = function () { }; diff --git a/src/state/a11y.js b/src/state/a11y.js new file mode 100644 index 0000000000..5184e0662c --- /dev/null +++ b/src/state/a11y.js @@ -0,0 +1,98 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { AccessibilityInfo } from 'react-native'; +import { IS_WEB } from '#/env'; +import { PlatformInfo } from '../../modules/expo-bluesky-swiss-army'; +var Context = React.createContext({ + reduceMotionEnabled: false, + screenReaderEnabled: false, +}); +Context.displayName = 'A11yContext'; +export function useA11y() { + return React.useContext(Context); +} +export function Provider(_a) { + var _this = this; + var children = _a.children; + var _b = React.useState(function () { + return PlatformInfo.getIsReducedMotionEnabled(); + }), reduceMotionEnabled = _b[0], setReduceMotionEnabled = _b[1]; + var _c = React.useState(false), screenReaderEnabled = _c[0], setScreenReaderEnabled = _c[1]; + React.useEffect(function () { + var reduceMotionChangedSubscription = AccessibilityInfo.addEventListener('reduceMotionChanged', function (enabled) { + setReduceMotionEnabled(enabled); + }); + var screenReaderChangedSubscription = AccessibilityInfo.addEventListener('screenReaderChanged', function (enabled) { + setScreenReaderEnabled(enabled); + }); + (function () { return __awaiter(_this, void 0, void 0, function () { + var _a, _reduceMotionEnabled, _screenReaderEnabled; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, Promise.all([ + AccessibilityInfo.isReduceMotionEnabled(), + AccessibilityInfo.isScreenReaderEnabled(), + ])]; + case 1: + _a = _b.sent(), _reduceMotionEnabled = _a[0], _screenReaderEnabled = _a[1]; + setReduceMotionEnabled(_reduceMotionEnabled); + setScreenReaderEnabled(_screenReaderEnabled); + return [2 /*return*/]; + } + }); + }); })(); + return function () { + reduceMotionChangedSubscription.remove(); + screenReaderChangedSubscription.remove(); + }; + }, []); + var ctx = React.useMemo(function () { + return { + reduceMotionEnabled: reduceMotionEnabled, + /** + * Always returns true on web. For now, we're using this for mobile a11y, + * so we reset to false on web. + * + * @see https://github.com/necolas/react-native-web/discussions/2072 + */ + screenReaderEnabled: IS_WEB ? false : screenReaderEnabled, + }; + }, [reduceMotionEnabled, screenReaderEnabled]); + return _jsx(Context.Provider, { value: ctx, children: children }); +} diff --git a/src/state/appConfig.js b/src/state/appConfig.js new file mode 100644 index 0000000000..0fdb035f1f --- /dev/null +++ b/src/state/appConfig.js @@ -0,0 +1,137 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext } from 'react'; +import { QueryClient, useQuery } from '@tanstack/react-query'; +import { APP_CONFIG_URL } from '#/env'; +var qc = new QueryClient(); +var appConfigQueryKey = ['app-config']; +export var DEFAULT_APP_CONFIG_RESPONSE = { + liveNow: { + allow: [], + exceptions: [], + }, +}; +var fetchAppConfigPromise; +function fetchAppConfig() { + return __awaiter(this, void 0, void 0, function () { + var e_1; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + if (!fetchAppConfigPromise) { + fetchAppConfigPromise = (function () { return __awaiter(_this, void 0, void 0, function () { + var r, _a, data; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, fetch("".concat(APP_CONFIG_URL, "/config"))]; + case 1: + r = _b.sent(); + if (!!r.ok) return [3 /*break*/, 3]; + _a = Error.bind; + return [4 /*yield*/, r.text()]; + case 2: throw new (_a.apply(Error, [void 0, _b.sent()]))(); + case 3: return [4 /*yield*/, r.json()]; + case 4: + data = _b.sent(); + return [2 /*return*/, data]; + } + }); + }); })(); + } + return [4 /*yield*/, fetchAppConfigPromise]; + case 1: return [2 /*return*/, _a.sent()]; + case 2: + e_1 = _a.sent(); + fetchAppConfigPromise = undefined; + throw e_1; + case 3: return [2 /*return*/]; + } + }); + }); +} +var Context = createContext(DEFAULT_APP_CONFIG_RESPONSE); +export function Provider(_a) { + var children = _a.children; + var data = useQuery({ + staleTime: Infinity, + queryKey: appConfigQueryKey, + refetchInterval: function (query) { + // refetch regularly if fetch failed, otherwise never refetch + return query.state.status === 'error' ? 60e3 : Infinity; + }, + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, fetchAppConfig()]; + }); + }); + }, + }, qc).data; + return (_jsx(Context.Provider, { value: data !== null && data !== void 0 ? data : DEFAULT_APP_CONFIG_RESPONSE, children: children })); +} +export function prefetchAppConfig() { + return __awaiter(this, void 0, void 0, function () { + var data, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, fetchAppConfig()]; + case 1: + data = _b.sent(); + if (data) { + qc.setQueryData(appConfigQueryKey, data); + } + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); +} +export function useAppConfig() { + var ctx = useContext(Context); + if (!ctx) { + throw new Error('useAppConfig must be used within a Provider'); + } + return ctx; +} diff --git a/src/state/birthdate.js b/src/state/birthdate.js new file mode 100644 index 0000000000..7110a11806 --- /dev/null +++ b/src/state/birthdate.js @@ -0,0 +1,125 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMemo } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { preferencesQueryKey } from '#/state/queries/preferences'; +import { useAgent, useSession } from '#/state/session'; +import { usePatchAgeAssuranceOtherRequiredData } from '#/ageAssurance'; +import { IS_DEV } from '#/env'; +import { account } from '#/storage'; +// 6s in dev, 48h in prod +var BIRTHDATE_DELAY_HOURS = IS_DEV ? 0.001 : 48; +/** + * Stores the timestamp of the birthday update locally. This is used to + * debounce birthday updates globally. + * + * Use {@link useIsBirthDateUpdateAllowed} to check if an update is allowed. + */ +export function snoozeBirthdateUpdateAllowedForDid(did) { + account.set([did, 'birthdateLastUpdatedAt'], new Date().toISOString()); +} +/** + * Checks if we've already snoozed bday updates. In some cases, if one is + * present, we don't need to set another, such as in AA when reading initial + * data on load. + */ +export function hasSnoozedBirthdateUpdateForDid(did) { + return !!account.get([did, 'birthdateLastUpdatedAt']); +} +/** + * Returns whether a birthdate update is currently allowed, based on the + * last update timestamp stored locally. + */ +export function useIsBirthdateUpdateAllowed() { + var currentAccount = useSession().currentAccount; + return useMemo(function () { + if (!currentAccount) + return false; + var lastUpdated = account.get([ + currentAccount.did, + 'birthdateLastUpdatedAt', + ]); + if (!lastUpdated) + return true; + var lastUpdatedDate = new Date(lastUpdated); + var diffMs = Date.now() - lastUpdatedDate.getTime(); + var diffHours = diffMs / (1000 * 60 * 60); + return diffHours >= BIRTHDATE_DELAY_HOURS; + }, [currentAccount]); +} +export function useBirthdateMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + var patchOtherRequiredData = usePatchAgeAssuranceOtherRequiredData(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var bday; + var birthDate = _b.birthDate; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + bday = birthDate.toISOString(); + return [4 /*yield*/, agent.setPersonalDetails({ birthDate: bday }) + // triggers a refetch + ]; + case 1: + _c.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + /** + * Also patch the age assurance other required data with the new + * birthdate, which may change the user's age assurance access level. + */ + ]; + case 2: + // triggers a refetch + _c.sent(); + /** + * Also patch the age assurance other required data with the new + * birthdate, which may change the user's age assurance access level. + */ + patchOtherRequiredData({ birthdate: bday }); + snoozeBirthdateUpdateAllowedForDid(agent.sessionManager.did); + return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/state/cache/post-shadow.js b/src/state/cache/post-shadow.js new file mode 100644 index 0000000000..c4af93a308 --- /dev/null +++ b/src/state/cache/post-shadow.js @@ -0,0 +1,250 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useEffect, useMemo, useState } from 'react'; +import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, } from '@atproto/api'; +import EventEmitter from 'eventemitter3'; +import { batchedUpdates } from '#/lib/batchedUpdates'; +import { findAllPostsInQueryData as findAllPostsInBookmarksQueryData } from '#/state/queries/bookmarks/useBookmarksQuery'; +import { findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData } from '#/state/queries/explore-feed-previews'; +import { findAllPostsInQueryData as findAllPostsInNotifsQueryData } from '#/state/queries/notifications/feed'; +import { findAllPostsInQueryData as findAllPostsInFeedQueryData } from '#/state/queries/post-feed'; +import { findAllPostsInQueryData as findAllPostsInQuoteQueryData } from '#/state/queries/post-quotes'; +import { findAllPostsInQueryData as findAllPostsInSearchQueryData } from '#/state/queries/search-posts'; +import { findAllPostsInQueryData as findAllPostsInThreadV2QueryData } from '#/state/queries/usePostThread/queryCache'; +import { castAsShadow } from './types'; +export var POST_TOMBSTONE = Symbol('PostTombstone'); +var emitter = new EventEmitter(); +var shadows = new WeakMap(); +/** + * Use with caution! This function returns the raw shadow data for a post. + * Prefer using `usePostShadow`. + */ +export function dangerousGetPostShadow(post) { + return shadows.get(post); +} +export function usePostShadow(post) { + var _a = useState(function () { return shadows.get(post); }), shadow = _a[0], setShadow = _a[1]; + var _b = useState(post), prevPost = _b[0], setPrevPost = _b[1]; + if (post !== prevPost) { + setPrevPost(post); + setShadow(shadows.get(post)); + } + useEffect(function () { + function onUpdate() { + setShadow(shadows.get(post)); + } + emitter.addListener(post.uri, onUpdate); + return function () { + emitter.removeListener(post.uri, onUpdate); + }; + }, [post, setShadow]); + return useMemo(function () { + if (shadow) { + return mergeShadow(post, shadow); + } + else { + return castAsShadow(post); + } + }, [post, shadow]); +} +function mergeShadow(post, shadow) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; + if (shadow.isDeleted) { + return POST_TOMBSTONE; + } + var likeCount = (_a = post.likeCount) !== null && _a !== void 0 ? _a : 0; + if ('likeUri' in shadow) { + var wasLiked = !!((_b = post.viewer) === null || _b === void 0 ? void 0 : _b.like); + var isLiked = !!shadow.likeUri; + if (wasLiked && !isLiked) { + likeCount--; + } + else if (!wasLiked && isLiked) { + likeCount++; + } + likeCount = Math.max(0, likeCount); + } + var bookmarkCount = (_c = post.bookmarkCount) !== null && _c !== void 0 ? _c : 0; + if ('bookmarked' in shadow) { + var wasBookmarked = !!((_d = post.viewer) === null || _d === void 0 ? void 0 : _d.bookmarked); + var isBookmarked = !!shadow.bookmarked; + if (wasBookmarked && !isBookmarked) { + bookmarkCount--; + } + else if (!wasBookmarked && isBookmarked) { + bookmarkCount++; + } + bookmarkCount = Math.max(0, bookmarkCount); + } + var repostCount = (_e = post.repostCount) !== null && _e !== void 0 ? _e : 0; + if ('repostUri' in shadow) { + var wasReposted = !!((_f = post.viewer) === null || _f === void 0 ? void 0 : _f.repost); + var isReposted = !!shadow.repostUri; + if (wasReposted && !isReposted) { + repostCount--; + } + else if (!wasReposted && isReposted) { + repostCount++; + } + repostCount = Math.max(0, repostCount); + } + var replyCount = (_g = post.replyCount) !== null && _g !== void 0 ? _g : 0; + if ('optimisticReplyCount' in shadow) { + replyCount = (_h = shadow.optimisticReplyCount) !== null && _h !== void 0 ? _h : replyCount; + } + var embed; + if ('embed' in shadow) { + if ((AppBskyEmbedRecord.isView(post.embed) && + AppBskyEmbedRecord.isView(shadow.embed)) || + (AppBskyEmbedRecordWithMedia.isView(post.embed) && + AppBskyEmbedRecordWithMedia.isView(shadow.embed))) { + embed = shadow.embed; + } + } + return castAsShadow(__assign(__assign({}, post), { embed: embed || post.embed, likeCount: likeCount, repostCount: repostCount, replyCount: replyCount, bookmarkCount: bookmarkCount, viewer: __assign(__assign({}, (post.viewer || {})), { like: 'likeUri' in shadow ? shadow.likeUri : (_j = post.viewer) === null || _j === void 0 ? void 0 : _j.like, repost: 'repostUri' in shadow ? shadow.repostUri : (_k = post.viewer) === null || _k === void 0 ? void 0 : _k.repost, pinned: 'pinned' in shadow ? shadow.pinned : (_l = post.viewer) === null || _l === void 0 ? void 0 : _l.pinned, bookmarked: 'bookmarked' in shadow ? shadow.bookmarked : (_m = post.viewer) === null || _m === void 0 ? void 0 : _m.bookmarked }) })); +} +export function updatePostShadow(queryClient, uri, value) { + var cachedPosts = findPostsInCache(queryClient, uri); + for (var _i = 0, cachedPosts_1 = cachedPosts; _i < cachedPosts_1.length; _i++) { + var post = cachedPosts_1[_i]; + shadows.set(post, __assign(__assign({}, shadows.get(post)), value)); + } + batchedUpdates(function () { + emitter.emit(uri); + }); +} +function findPostsInCache(queryClient, uri) { + var _i, _a, post, _b, _c, post, _d, _e, post, _f, _g, post, _h, _j, post, _k, _l, post, _m, _o, post; + return __generator(this, function (_p) { + switch (_p.label) { + case 0: + _i = 0, _a = findAllPostsInFeedQueryData(queryClient, uri); + _p.label = 1; + case 1: + if (!(_i < _a.length)) return [3 /*break*/, 4]; + post = _a[_i]; + return [4 /*yield*/, post]; + case 2: + _p.sent(); + _p.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: + _b = 0, _c = findAllPostsInNotifsQueryData(queryClient, uri); + _p.label = 5; + case 5: + if (!(_b < _c.length)) return [3 /*break*/, 8]; + post = _c[_b]; + return [4 /*yield*/, post]; + case 6: + _p.sent(); + _p.label = 7; + case 7: + _b++; + return [3 /*break*/, 5]; + case 8: + _d = 0, _e = findAllPostsInThreadV2QueryData(queryClient, uri); + _p.label = 9; + case 9: + if (!(_d < _e.length)) return [3 /*break*/, 12]; + post = _e[_d]; + return [4 /*yield*/, post]; + case 10: + _p.sent(); + _p.label = 11; + case 11: + _d++; + return [3 /*break*/, 9]; + case 12: + _f = 0, _g = findAllPostsInSearchQueryData(queryClient, uri); + _p.label = 13; + case 13: + if (!(_f < _g.length)) return [3 /*break*/, 16]; + post = _g[_f]; + return [4 /*yield*/, post]; + case 14: + _p.sent(); + _p.label = 15; + case 15: + _f++; + return [3 /*break*/, 13]; + case 16: + _h = 0, _j = findAllPostsInQuoteQueryData(queryClient, uri); + _p.label = 17; + case 17: + if (!(_h < _j.length)) return [3 /*break*/, 20]; + post = _j[_h]; + return [4 /*yield*/, post]; + case 18: + _p.sent(); + _p.label = 19; + case 19: + _h++; + return [3 /*break*/, 17]; + case 20: + _k = 0, _l = findAllPostsInExploreFeedPreviewsQueryData(queryClient, uri); + _p.label = 21; + case 21: + if (!(_k < _l.length)) return [3 /*break*/, 24]; + post = _l[_k]; + return [4 /*yield*/, post]; + case 22: + _p.sent(); + _p.label = 23; + case 23: + _k++; + return [3 /*break*/, 21]; + case 24: + _m = 0, _o = findAllPostsInBookmarksQueryData(queryClient, uri); + _p.label = 25; + case 25: + if (!(_m < _o.length)) return [3 /*break*/, 28]; + post = _o[_m]; + return [4 /*yield*/, post]; + case 26: + _p.sent(); + _p.label = 27; + case 27: + _m++; + return [3 /*break*/, 25]; + case 28: return [2 /*return*/]; + } + }); +} diff --git a/src/state/cache/profile-shadow.js b/src/state/cache/profile-shadow.js new file mode 100644 index 0000000000..23f52717df --- /dev/null +++ b/src/state/cache/profile-shadow.js @@ -0,0 +1,314 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +import { useEffect, useMemo, useState } from 'react'; +import EventEmitter from 'eventemitter3'; +import { batchedUpdates } from '#/lib/batchedUpdates'; +import { findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData } from '#/state/queries/activity-subscriptions'; +import { findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData } from '#/state/queries/actor-search'; +import { findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData } from '#/state/queries/explore-feed-previews'; +import { findAllProfilesInQueryData as findAllProfilesInContactMatchesQueryData } from '#/state/queries/find-contacts'; +import { findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData } from '#/state/queries/known-followers'; +import { findAllProfilesInQueryData as findAllProfilesInListMembersQueryData } from '#/state/queries/list-members'; +import { findAllProfilesInQueryData as findAllProfilesInListConvosQueryData } from '#/state/queries/messages/list-conversations'; +import { findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData } from '#/state/queries/my-blocked-accounts'; +import { findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData } from '#/state/queries/my-muted-accounts'; +import { findAllProfilesInQueryData as findAllProfilesInNotifsQueryData } from '#/state/queries/notifications/feed'; +import { findAllProfilesInQueryData as findAllProfilesInFeedsQueryData, } from '#/state/queries/post-feed'; +import { findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData } from '#/state/queries/post-liked-by'; +import { findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData } from '#/state/queries/post-quotes'; +import { findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData } from '#/state/queries/post-reposted-by'; +import { findAllProfilesInQueryData as findAllProfilesInProfileQueryData } from '#/state/queries/profile'; +import { findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData } from '#/state/queries/profile-followers'; +import { findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData } from '#/state/queries/profile-follows'; +import { findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData } from '#/state/queries/suggested-follows'; +import { findAllProfilesInQueryData as findAllProfilesInSuggestedOnboardingUsersQueryData } from '#/state/queries/trending/useGetSuggestedOnboardingUsersQuery'; +import { findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData } from '#/state/queries/trending/useGetSuggestedUsersQuery'; +import { findAllProfilesInQueryData as findAllProfilesInPostThreadV2QueryData } from '#/state/queries/usePostThread/queryCache'; +import { castAsShadow } from './types'; +var shadows = new WeakMap(); +var emitter = new EventEmitter(); +export function useProfileShadow(profile) { + var _a = useState(function () { return shadows.get(profile); }), shadow = _a[0], setShadow = _a[1]; + var _b = useState(profile), prevPost = _b[0], setPrevPost = _b[1]; + if (profile !== prevPost) { + setPrevPost(profile); + setShadow(shadows.get(profile)); + } + useEffect(function () { + function onUpdate() { + setShadow(shadows.get(profile)); + } + emitter.addListener(profile.did, onUpdate); + return function () { + emitter.removeListener(profile.did, onUpdate); + }; + }, [profile]); + return useMemo(function () { + if (shadow) { + return mergeShadow(profile, shadow); + } + else { + return castAsShadow(profile); + } + }, [profile, shadow]); +} +/** + * Same as useProfileShadow, but allows for the profile to be undefined. + * This is useful for when the profile is not guaranteed to be loaded yet. + */ +export function useMaybeProfileShadow(profile) { + var _a = useState(function () { + return profile ? shadows.get(profile) : undefined; + }), shadow = _a[0], setShadow = _a[1]; + var _b = useState(profile), prevPost = _b[0], setPrevPost = _b[1]; + if (profile !== prevPost) { + setPrevPost(profile); + setShadow(profile ? shadows.get(profile) : undefined); + } + useEffect(function () { + if (!profile) + return; + function onUpdate() { + if (!profile) + return; + setShadow(shadows.get(profile)); + } + emitter.addListener(profile.did, onUpdate); + return function () { + emitter.removeListener(profile.did, onUpdate); + }; + }, [profile]); + return useMemo(function () { + if (!profile) + return undefined; + if (shadow) { + return mergeShadow(profile, shadow); + } + else { + return castAsShadow(profile); + } + }, [profile, shadow]); +} +/** + * Takes a list of posts, and returns a list of DIDs that should be filtered out + * + * Note: it doesn't retroactively scan the cache, but only listens to new updates. + * The use case here is intended for removing a post from a feed after you mute the author + */ +export function usePostAuthorShadowFilter(data) { + var _a; + var _b = useState(function () { + var _a; + return (_a = data === null || data === void 0 ? void 0 : data.flatMap(function (page) { + return page.slices.flatMap(function (slice) { + return slice.items.map(function (item) { return item.post.author.did; }); + }); + })) !== null && _a !== void 0 ? _a : []; + }), trackedDids = _b[0], setTrackedDids = _b[1]; + var _c = useState(new Map()), authors = _c[0], setAuthors = _c[1]; + var _d = useState(data), prevData = _d[0], setPrevData = _d[1]; + if (data !== prevData) { + var newAuthors = new Set(trackedDids); + var hasNew = false; + for (var _i = 0, _e = (_a = data === null || data === void 0 ? void 0 : data.flatMap(function (page) { return page.slices; })) !== null && _a !== void 0 ? _a : []; _i < _e.length; _i++) { + var slice = _e[_i]; + for (var _f = 0, _g = slice.items; _f < _g.length; _f++) { + var item = _g[_f]; + var author = item.post.author; + if (!newAuthors.has(author.did)) { + hasNew = true; + newAuthors.add(author.did); + } + } + } + if (hasNew) + setTrackedDids(__spreadArray([], newAuthors, true)); + setPrevData(data); + } + useEffect(function () { + var unsubs = []; + var _loop_1 = function (did) { + function onUpdate(value) { + setAuthors(function (prev) { + var _a, _b, _c, _d; + var prevValue = prev.get(did); + var next = new Map(prev); + next.set(did, { + blocked: Boolean((_b = (_a = value.blockingUri) !== null && _a !== void 0 ? _a : prevValue === null || prevValue === void 0 ? void 0 : prevValue.blocked) !== null && _b !== void 0 ? _b : false), + muted: Boolean((_d = (_c = value.muted) !== null && _c !== void 0 ? _c : prevValue === null || prevValue === void 0 ? void 0 : prevValue.muted) !== null && _d !== void 0 ? _d : false), + }); + return next; + }); + } + emitter.addListener(did, onUpdate); + unsubs.push(function () { + emitter.removeListener(did, onUpdate); + }); + }; + for (var _i = 0, trackedDids_1 = trackedDids; _i < trackedDids_1.length; _i++) { + var did = trackedDids_1[_i]; + _loop_1(did); + } + return function () { + unsubs.map(function (fn) { return fn(); }); + }; + }, [trackedDids]); + return useMemo(function () { + var dids = []; + for (var _i = 0, _a = authors.entries(); _i < _a.length; _i++) { + var _b = _a[_i], did = _b[0], value = _b[1]; + if (value.blocked || value.muted) { + dids.push(did); + } + } + return dids; + }, [authors]); +} +export function updateProfileShadow(queryClient, did, value) { + var cachedProfiles = findProfilesInCache(queryClient, did); + for (var _i = 0, cachedProfiles_1 = cachedProfiles; _i < cachedProfiles_1.length; _i++) { + var profile = cachedProfiles_1[_i]; + shadows.set(profile, __assign(__assign({}, shadows.get(profile)), value)); + } + batchedUpdates(function () { + emitter.emit(did, value); + }); +} +function mergeShadow(profile, shadow) { + var _a, _b, _c, _d; + return castAsShadow(__assign(__assign({}, profile), { viewer: __assign(__assign({}, (profile.viewer || {})), { following: 'followingUri' in shadow + ? shadow.followingUri + : (_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following, muted: 'muted' in shadow ? shadow.muted : (_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.muted, blocking: 'blockingUri' in shadow ? shadow.blockingUri : (_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.blocking, activitySubscription: 'activitySubscription' in shadow + ? shadow.activitySubscription + : (_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.activitySubscription }), verification: 'verification' in shadow ? shadow.verification : profile.verification, status: 'status' in shadow + ? shadow.status + : 'status' in profile + ? profile.status + : undefined })); +} +function findProfilesInCache(queryClient, did) { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [5 /*yield**/, __values(findAllProfilesInListMembersQueryData(queryClient, did))]; + case 1: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInMyBlockedAccountsQueryData(queryClient, did))]; + case 2: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInMyMutedAccountsQueryData(queryClient, did))]; + case 3: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInPostLikedByQueryData(queryClient, did))]; + case 4: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInPostRepostedByQueryData(queryClient, did))]; + case 5: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInPostQuotesQueryData(queryClient, did))]; + case 6: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInProfileQueryData(queryClient, did))]; + case 7: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInProfileFollowersQueryData(queryClient, did))]; + case 8: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInProfileFollowsQueryData(queryClient, did))]; + case 9: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInSuggestedOnboardingUsersQueryData(queryClient, did))]; + case 10: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInSuggestedUsersQueryData(queryClient, did))]; + case 11: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInSuggestedFollowsQueryData(queryClient, did))]; + case 12: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInActorSearchQueryData(queryClient, did))]; + case 13: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInListConvosQueryData(queryClient, did))]; + case 14: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInFeedsQueryData(queryClient, did))]; + case 15: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInPostThreadV2QueryData(queryClient, did))]; + case 16: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInKnownFollowersQueryData(queryClient, did))]; + case 17: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did))]; + case 18: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInActivitySubscriptionsQueryData(queryClient, did))]; + case 19: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInNotifsQueryData(queryClient, did))]; + case 20: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInContactMatchesQueryData(queryClient, did))]; + case 21: + _a.sent(); + return [2 /*return*/]; + } + }); +} diff --git a/src/state/cache/thread-mutes.js b/src/state/cache/thread-mutes.js new file mode 100644 index 0000000000..79b639499d --- /dev/null +++ b/src/state/cache/thread-mutes.js @@ -0,0 +1,127 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React, { useEffect } from 'react'; +import * as persisted from '#/state/persisted'; +import { useAgent, useSession } from '../session'; +var stateContext = React.createContext(new Map()); +stateContext.displayName = 'ThreadMutesStateContext'; +var setStateContext = React.createContext(function (_) { return false; }); +setStateContext.displayName = 'ThreadMutesSetStateContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(function () { return new Map(); }), state = _b[0], setState = _b[1]; + var setThreadMute = React.useCallback(function (uri, value) { + setState(function (prev) { + var next = new Map(prev); + next.set(uri, value); + return next; + }); + }, [setState]); + useMigrateMutes(setThreadMute); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setStateContext.Provider, { value: setThreadMute, children: children }) })); +} +export function useMutedThreads() { + return React.useContext(stateContext); +} +export function useIsThreadMuted(uri, defaultValue) { + var _a; + if (defaultValue === void 0) { defaultValue = false; } + var state = React.useContext(stateContext); + return (_a = state.get(uri)) !== null && _a !== void 0 ? _a : defaultValue; +} +export function useSetThreadMute() { + return React.useContext(setStateContext); +} +function useMigrateMutes(setThreadMute) { + var _this = this; + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + useEffect(function () { + if (currentAccount) { + if (!persisted + .get('mutedThreads') + .some(function (uri) { return uri.includes(currentAccount.did); })) { + return; + } + var cancelled_1 = false; + var migrate = function () { return __awaiter(_this, void 0, void 0, function () { + var _loop_1, state_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _loop_1 = function () { + var threads, root; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + threads = persisted.get('mutedThreads'); + root = threads.findLast(function (uri) { return uri.includes(currentAccount.did); }); + if (!root) + return [2 /*return*/, "break"]; + persisted.write('mutedThreads', threads.filter(function (uri) { return uri !== root; })); + setThreadMute(root, true); + return [4 /*yield*/, agent.api.app.bsky.graph + .muteThread({ root: root }) + // not a big deal if this fails, since the post might have been deleted + .catch(console.error)]; + case 1: + _b.sent(); + return [2 /*return*/]; + } + }); + }; + _a.label = 1; + case 1: + if (!!cancelled_1) return [3 /*break*/, 3]; + return [5 /*yield**/, _loop_1()]; + case 2: + state_1 = _a.sent(); + if (state_1 === "break") + return [3 /*break*/, 3]; + return [3 /*break*/, 1]; + case 3: return [2 /*return*/]; + } + }); + }); }; + migrate(); + return function () { + cancelled_1 = true; + }; + } + }, [agent, currentAccount, setThreadMute]); +} diff --git a/src/state/cache/types.js b/src/state/cache/types.js new file mode 100644 index 0000000000..af5cfc939d --- /dev/null +++ b/src/state/cache/types.js @@ -0,0 +1,3 @@ +export function castAsShadow(value) { + return value; +} diff --git a/src/state/dialogs/index.js b/src/state/dialogs/index.js new file mode 100644 index 0000000000..2337fdcead --- /dev/null +++ b/src/state/dialogs/index.js @@ -0,0 +1,64 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { Provider as GlobalDialogsProvider } from '#/components/dialogs/Context'; +import { IS_WEB } from '#/env'; +import { BottomSheetNativeComponent } from '../../../modules/bottom-sheet'; +var DialogContext = React.createContext({}); +DialogContext.displayName = 'DialogContext'; +var DialogControlContext = React.createContext({}); +DialogControlContext.displayName = 'DialogControlContext'; +/** + * The number of dialogs that are fully expanded. This is used to determine the background color of the status bar + * on iOS. + */ +var DialogFullyExpandedCountContext = React.createContext(0); +DialogFullyExpandedCountContext.displayName = 'DialogFullyExpandedCountContext'; +export function useDialogStateContext() { + return React.useContext(DialogContext); +} +export function useDialogStateControlContext() { + return React.useContext(DialogControlContext); +} +/** The number of dialogs that are fully expanded */ +export function useDialogFullyExpandedCountContext() { + return React.useContext(DialogFullyExpandedCountContext); +} +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(0), fullyExpandedCount = _b[0], setFullyExpandedCount = _b[1]; + var activeDialogs = React.useRef(new Map()); + var openDialogs = React.useRef(new Set()); + var closeAllDialogs = React.useCallback(function () { + if (IS_WEB) { + openDialogs.current.forEach(function (id) { + var dialog = activeDialogs.current.get(id); + if (dialog) + dialog.current.close(); + }); + return openDialogs.current.size > 0; + } + else { + BottomSheetNativeComponent.dismissAll(); + return false; + } + }, []); + var setDialogIsOpen = React.useCallback(function (id, isOpen) { + if (isOpen) { + openDialogs.current.add(id); + } + else { + openDialogs.current.delete(id); + } + }, []); + var context = React.useMemo(function () { return ({ + activeDialogs: activeDialogs, + openDialogs: openDialogs, + }); }, [activeDialogs, openDialogs]); + var controls = React.useMemo(function () { return ({ + closeAllDialogs: closeAllDialogs, + setDialogIsOpen: setDialogIsOpen, + setFullyExpandedCount: setFullyExpandedCount, + }); }, [closeAllDialogs, setDialogIsOpen, setFullyExpandedCount]); + return (_jsx(DialogContext.Provider, { value: context, children: _jsx(DialogControlContext.Provider, { value: controls, children: _jsx(DialogFullyExpandedCountContext.Provider, { value: fullyExpandedCount, children: _jsx(GlobalDialogsProvider, { children: children }) }) }) })); +} +Provider.displayName = 'DialogsProvider'; diff --git a/src/state/email-verification.js b/src/state/email-verification.js new file mode 100644 index 0000000000..4067d42e4a --- /dev/null +++ b/src/state/email-verification.js @@ -0,0 +1,39 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { BSKY_SERVICE } from '#/lib/constants'; +import { getHostnameFromUrl } from '#/lib/strings/url-helpers'; +import { STALE } from '#/state/queries'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useCheckEmailConfirmed } from '#/state/service-config'; +import { useSession } from '#/state/session'; +var EmailVerificationContext = createContext(null); +EmailVerificationContext.displayName = 'EmailVerificationContext'; +export function Provider(_a) { + var children = _a.children; + var currentAccount = useSession().currentAccount; + var profile = useProfileQuery({ + did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + staleTime: STALE.INFINITY, + }).data; + var checkEmailConfirmed = useCheckEmailConfirmed(); + // Date set for 11 AM PST on the 18th of November + var isNewEnough = !!(profile === null || profile === void 0 ? void 0 : profile.createdAt) && + Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z'); + var isSelfHost = currentAccount && + getHostnameFromUrl(currentAccount.service) !== + getHostnameFromUrl(BSKY_SERVICE); + var needsEmailVerification = !isSelfHost && + checkEmailConfirmed && + !(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.emailConfirmed) && + isNewEnough; + var value = useMemo(function () { return ({ needsEmailVerification: needsEmailVerification }); }, [needsEmailVerification]); + return (_jsx(EmailVerificationContext.Provider, { value: value, children: children })); +} +Provider.displayName = 'EmailVerificationProvider'; +export function useEmail() { + var ctx = useContext(EmailVerificationContext); + if (!ctx) { + throw new Error('useEmail must be used within a EmailVerificationProvider'); + } + return ctx; +} diff --git a/src/state/events.js b/src/state/events.js new file mode 100644 index 0000000000..add9853b96 --- /dev/null +++ b/src/state/events.js @@ -0,0 +1,39 @@ +import EventEmitter from 'eventemitter3'; +var emitter = new EventEmitter(); +// a "soft reset" typically means scrolling to top and loading latest +// but it can depend on the screen +export function emitSoftReset() { + emitter.emit('soft-reset'); +} +export function listenSoftReset(fn) { + emitter.on('soft-reset', fn); + return function () { return emitter.off('soft-reset', fn); }; +} +export function emitSessionDropped() { + emitter.emit('session-dropped'); +} +export function listenSessionDropped(fn) { + emitter.on('session-dropped', fn); + return function () { return emitter.off('session-dropped', fn); }; +} +export function emitNetworkConfirmed() { + emitter.emit('network-confirmed'); +} +export function listenNetworkConfirmed(fn) { + emitter.on('network-confirmed', fn); + return function () { return emitter.off('network-confirmed', fn); }; +} +export function emitNetworkLost() { + emitter.emit('network-lost'); +} +export function listenNetworkLost(fn) { + emitter.on('network-lost', fn); + return function () { return emitter.off('network-lost', fn); }; +} +export function emitPostCreated() { + emitter.emit('post-created'); +} +export function listenPostCreated(fn) { + emitter.on('post-created', fn); + return function () { return emitter.off('post-created', fn); }; +} diff --git a/src/state/feed-feedback.js b/src/state/feed-feedback.js new file mode 100644 index 0000000000..4ccaf865ea --- /dev/null +++ b/src/state/feed-feedback.js @@ -0,0 +1,246 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from 'react'; +import { AppState } from 'react-native'; +import throttle from 'lodash.throttle'; +import { PROD_FEEDS, STAGING_FEEDS } from '#/lib/constants'; +import { isFeedSourceFeedInfo, } from '#/state/queries/feed'; +import { getItemsForFeedback } from '#/view/com/posts/PostFeed'; +import { useAnalytics } from '#/analytics'; +import { useAgent } from './session'; +export var FEEDBACK_FEEDS = __spreadArray(__spreadArray([], PROD_FEEDS, true), STAGING_FEEDS, true); +export var THIRD_PARTY_ALLOWED_INTERACTIONS = new Set([ + // These are explicit actions and are therefore fine to send. + 'app.bsky.feed.defs#requestLess', + 'app.bsky.feed.defs#requestMore', + // These can be inferred from the firehose and are therefore fine to send. + 'app.bsky.feed.defs#interactionLike', + 'app.bsky.feed.defs#interactionQuote', + 'app.bsky.feed.defs#interactionReply', + 'app.bsky.feed.defs#interactionRepost', + // This can be inferred from pagination requests for everything except the very last page + // so it is fine to send. It is crucial for third party algorithmic feeds to receive these. + 'app.bsky.feed.defs#interactionSeen', +]); +var stateContext = createContext({ + enabled: false, + onItemSeen: function (_item) { }, + sendInteraction: function (_interaction) { }, + feedDescriptor: undefined, + feedSourceInfo: undefined, +}); +stateContext.displayName = 'FeedFeedbackContext'; +export function useFeedFeedback(feedSourceInfo, hasSession) { + var _a; + var ax = useAnalytics(); + var logger = ax.logger.useChild(ax.logger.Context.FeedFeedback); + var agent = useAgent(); + var feed = !!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo) + ? feedSourceInfo + : undefined; + var isDiscover = isDiscoverFeed(feed === null || feed === void 0 ? void 0 : feed.feedDescriptor); + var acceptsInteractions = Boolean(isDiscover || (feed === null || feed === void 0 ? void 0 : feed.acceptsInteractions)); + var proxyDid = (_a = feed === null || feed === void 0 ? void 0 : feed.view) === null || _a === void 0 ? void 0 : _a.did; + var enabled = Boolean(feed) && Boolean(proxyDid) && acceptsInteractions && hasSession; + var queue = useRef(new Set()); + var history = useRef(new WeakSet()); + var flushEvents = useCallback(function (stats, feedDescriptor) { + if (stats === null) { + return; + } + if (stats.clickthroughCount > 0) { + ax.metric('feed:clickthrough', { + count: stats.clickthroughCount, + feed: feedDescriptor, + }); + stats.clickthroughCount = 0; + } + if (stats.engagedCount > 0) { + ax.metric('feed:engaged', { + count: stats.engagedCount, + feed: feedDescriptor, + }); + stats.engagedCount = 0; + } + if (stats.seenCount > 0) { + ax.metric('feed:seen', { + count: stats.seenCount, + feed: feedDescriptor, + }); + stats.seenCount = 0; + } + }, [ax]); + var aggregatedStats = useRef(null); + var throttledFlushAggregatedStats = useMemo(function () { + return throttle(function () { + var _a; + return flushEvents(aggregatedStats.current, (_a = feed === null || feed === void 0 ? void 0 : feed.feedDescriptor) !== null && _a !== void 0 ? _a : 'unknown'); + }, 45e3, { + leading: true, // The outer call is already throttled somewhat. + trailing: true, + }); + }, [feed === null || feed === void 0 ? void 0 : feed.feedDescriptor, flushEvents]); + var sendToFeedNoDelay = useCallback(function () { + var interactions = Array.from(queue.current).map(toInteraction); + queue.current.clear(); + var interactionsToSend = interactions.filter(function (interaction) { + return interaction.event && + isInteractionAllowed(enabled, feed, interaction.event); + }); + if (interactionsToSend.length === 0) { + return; + } + // Send to the feed + agent.app.bsky.feed + .sendInteractions({ interactions: interactionsToSend }, { + encoding: 'application/json', + headers: { + 'atproto-proxy': "".concat(proxyDid, "#bsky_fg"), + }, + }) + .catch(function () { }); // ignore upstream errors + if (aggregatedStats.current === null) { + aggregatedStats.current = createAggregatedStats(); + } + sendOrAggregateInteractionsForStats(aggregatedStats.current, interactionsToSend); + throttledFlushAggregatedStats(); + logger.debug('flushed'); + }, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed]); + var sendToFeed = useMemo(function () { + return throttle(sendToFeedNoDelay, 10e3, { + leading: false, + trailing: true, + }); + }, [sendToFeedNoDelay]); + useEffect(function () { + if (!enabled) { + return; + } + var sub = AppState.addEventListener('change', function (state) { + if (state === 'background') { + sendToFeed.flush(); + } + }); + return function () { return sub.remove(); }; + }, [enabled, sendToFeed]); + var onItemSeen = useCallback(function (feedItem) { + if (!enabled) { + return; + } + var items = getItemsForFeedback(feedItem); + for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { + var _a = items_1[_i], postItem = _a.item, feedContext = _a.feedContext, reqId = _a.reqId; + if (!history.current.has(postItem)) { + history.current.add(postItem); + queue.current.add(toString({ + item: postItem.uri, + event: 'app.bsky.feed.defs#interactionSeen', + feedContext: feedContext, + reqId: reqId, + })); + sendToFeed(); + } + } + }, [enabled, sendToFeed]); + var sendInteraction = useCallback(function (interaction) { + if (!enabled) { + return; + } + logger.debug('sendInteraction', __assign({}, interaction)); + if (!history.current.has(interaction)) { + history.current.add(interaction); + queue.current.add(toString(interaction)); + sendToFeed(); + } + }, [enabled, sendToFeed]); + return useMemo(function () { + return { + enabled: enabled, + // pass this method to the onItemSeen + onItemSeen: onItemSeen, + // call on various events + // queues the event to be sent with the throttled sendToFeed call + sendInteraction: sendInteraction, + feedDescriptor: feed === null || feed === void 0 ? void 0 : feed.feedDescriptor, + feedSourceInfo: typeof feed === 'object' ? feed : undefined, + }; + }, [enabled, onItemSeen, sendInteraction, feed]); +} +export var FeedFeedbackProvider = stateContext.Provider; +export function useFeedFeedbackContext() { + return useContext(stateContext); +} +// TODO +// We will introduce a permissions framework for 3p feeds to +// take advantage of the feed feedback API. Until that's in +// place, we're hardcoding it to the discover feed. +// -prf +export function isDiscoverFeed(feed) { + return !!feed && FEEDBACK_FEEDS.includes(feed); +} +function isInteractionAllowed(enabled, feed, interaction) { + if (!enabled || !feed) { + return false; + } + var isDiscover = isDiscoverFeed(feed.feedDescriptor); + return isDiscover ? true : THIRD_PARTY_ALLOWED_INTERACTIONS.has(interaction); +} +function toString(interaction) { + return "".concat(interaction.item, "|").concat(interaction.event, "|").concat(interaction.feedContext || '', "|").concat(interaction.reqId || ''); +} +function toInteraction(str) { + var _a = str.split('|'), item = _a[0], event = _a[1], feedContext = _a[2], reqId = _a[3]; + return { item: item, event: event, feedContext: feedContext, reqId: reqId }; +} +function createAggregatedStats() { + return { + clickthroughCount: 0, + engagedCount: 0, + seenCount: 0, + }; +} +function sendOrAggregateInteractionsForStats(stats, interactions) { + for (var _i = 0, interactions_1 = interactions; _i < interactions_1.length; _i++) { + var interaction = interactions_1[_i]; + switch (interaction.event) { + // The events are aggregated and sent later in batches. + case 'app.bsky.feed.defs#clickthroughAuthor': + case 'app.bsky.feed.defs#clickthroughEmbed': + case 'app.bsky.feed.defs#clickthroughItem': + case 'app.bsky.feed.defs#clickthroughReposter': { + stats.clickthroughCount++; + break; + } + case 'app.bsky.feed.defs#interactionLike': + case 'app.bsky.feed.defs#interactionQuote': + case 'app.bsky.feed.defs#interactionReply': + case 'app.bsky.feed.defs#interactionRepost': + case 'app.bsky.feed.defs#interactionShare': { + stats.engagedCount++; + break; + } + case 'app.bsky.feed.defs#interactionSeen': { + stats.seenCount++; + break; + } + } + } +} diff --git a/src/state/gallery.js b/src/state/gallery.js new file mode 100644 index 0000000000..3581962a7a --- /dev/null +++ b/src/state/gallery.js @@ -0,0 +1,392 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { cacheDirectory, copyAsync, deleteAsync, makeDirectoryAsync, moveAsync, } from 'expo-file-system/legacy'; +import { manipulateAsync, SaveFormat, } from 'expo-image-manipulator'; +import { nanoid } from 'nanoid/non-secure'; +import { POST_IMG_MAX } from '#/lib/constants'; +import { getImageDim } from '#/lib/media/manip'; +import { openCropper } from '#/lib/media/picker'; +import { getDataUriSize } from '#/lib/media/util'; +import { isCancelledError } from '#/lib/strings/errors'; +import { IS_NATIVE, IS_WEB } from '#/env'; +var _imageCacheDirectory; +function getImageCacheDirectory() { + if (IS_NATIVE) { + return (_imageCacheDirectory !== null && _imageCacheDirectory !== void 0 ? _imageCacheDirectory : (_imageCacheDirectory = joinPath(cacheDirectory, 'bsky-composer'))); + } + return null; +} +export function createComposerImage(raw) { + return __awaiter(this, void 0, void 0, function () { + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + _a = { + alt: '' + }; + _b = { + id: nanoid() + }; + return [4 /*yield*/, copyToCache(raw.path)]; + case 1: return [2 /*return*/, (_a.source = ( + // Copy to cache to ensure file survives OS temporary file cleanup + _b.path = _c.sent(), + _b.width = raw.width, + _b.height = raw.height, + _b.mime = raw.mime, + _b), + _a)]; + } + }); + }); +} +export function createInitialImages(uris) { + if (uris === void 0) { uris = []; } + return uris.map(function (_a) { + var uri = _a.uri, width = _a.width, height = _a.height, _b = _a.altText, altText = _b === void 0 ? '' : _b; + return { + alt: altText, + source: { + id: nanoid(), + path: uri, + width: width, + height: height, + mime: 'image/jpeg', + }, + }; + }); +} +export function pasteImage(uri) { + return __awaiter(this, void 0, void 0, function () { + var _a, width, height, match; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, getImageDim(uri)]; + case 1: + _a = _b.sent(), width = _a.width, height = _a.height; + match = /^data:(.+?);/.exec(uri); + return [2 /*return*/, { + alt: '', + source: { + id: nanoid(), + path: uri, + width: width, + height: height, + mime: match ? match[1] : 'image/jpeg', + }, + }]; + } + }); + }); +} +export function cropImage(img) { + return __awaiter(this, void 0, void 0, function () { + var source, cropped, e_1; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!IS_NATIVE) { + return [2 /*return*/, img]; + } + source = img.source; + _c.label = 1; + case 1: + _c.trys.push([1, 4, , 5]); + return [4 /*yield*/, openCropper({ + imageUri: source.path, + })]; + case 2: + cropped = _c.sent(); + _a = { + alt: img.alt, + source: source + }; + _b = {}; + return [4 /*yield*/, moveIfNecessary(cropped.path)]; + case 3: return [2 /*return*/, (_a.transformed = (_b.path = _c.sent(), + _b.width = cropped.width, + _b.height = cropped.height, + _b.mime = cropped.mime, + _b), + _a)]; + case 4: + e_1 = _c.sent(); + if (!isCancelledError(e_1)) { + return [2 /*return*/, img]; + } + throw e_1; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function manipulateImage(img, trans) { + return __awaiter(this, void 0, void 0, function () { + var rawActions, actions, source, result; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + rawActions = [trans.crop && { crop: trans.crop }]; + actions = rawActions.filter(function (a) { return a !== undefined; }); + if (actions.length === 0) { + if (img.transformed === undefined) { + return [2 /*return*/, img]; + } + return [2 /*return*/, { alt: img.alt, source: img.source }]; + } + source = img.source; + return [4 /*yield*/, manipulateAsync(source.path, actions, { + format: SaveFormat.PNG, + })]; + case 1: + result = _c.sent(); + _a = { + alt: img.alt, + source: img.source + }; + _b = {}; + return [4 /*yield*/, moveIfNecessary(result.uri)]; + case 2: return [2 /*return*/, (_a.transformed = (_b.path = _c.sent(), + _b.width = result.width, + _b.height = result.height, + _b.mime = 'image/png', + _b), + _a.manips = trans, + _a)]; + } + }); + }); +} +export function resetImageManipulation(img) { + if (img.transformed !== undefined) { + return { alt: img.alt, source: img.source }; + } + return img; +} +export function compressImage(img) { + return __awaiter(this, void 0, void 0, function () { + var source, _a, w, h, minQualityPercentage, maxQualityPercentage, newDataUri, qualityPercentage, res, base64, size; + var _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + source = img.transformed || img.source; + _a = containImageRes(source.width, source.height, POST_IMG_MAX), w = _a[0], h = _a[1]; + minQualityPercentage = 0; + maxQualityPercentage = 101 // exclusive + ; + _c.label = 1; + case 1: + if (!(maxQualityPercentage - minQualityPercentage > 1)) return [3 /*break*/, 6]; + qualityPercentage = Math.round((maxQualityPercentage + minQualityPercentage) / 2); + return [4 /*yield*/, manipulateAsync(source.path, [{ resize: { width: w, height: h } }], { + compress: qualityPercentage / 100, + format: SaveFormat.JPEG, + base64: true, + })]; + case 2: + res = _c.sent(); + base64 = res.base64; + size = base64 ? getDataUriSize(base64) : 0; + if (!(base64 && size <= POST_IMG_MAX.size)) return [3 /*break*/, 4]; + minQualityPercentage = qualityPercentage; + _b = {}; + return [4 /*yield*/, moveIfNecessary(res.uri)]; + case 3: + newDataUri = (_b.path = _c.sent(), + _b.width = res.width, + _b.height = res.height, + _b.mime = 'image/jpeg', + _b.size = size, + _b); + return [3 /*break*/, 5]; + case 4: + maxQualityPercentage = qualityPercentage; + _c.label = 5; + case 5: return [3 /*break*/, 1]; + case 6: + if (newDataUri) { + return [2 /*return*/, newDataUri]; + } + throw new Error("Unable to compress image"); + } + }); + }); +} +function moveIfNecessary(from) { + return __awaiter(this, void 0, void 0, function () { + var cacheDir, to; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + cacheDir = IS_NATIVE && getImageCacheDirectory(); + if (!(cacheDir && from.startsWith(cacheDir))) return [3 /*break*/, 3]; + to = joinPath(cacheDir, nanoid(36)); + return [4 /*yield*/, makeDirectoryAsync(cacheDir, { intermediates: true })]; + case 1: + _a.sent(); + return [4 /*yield*/, moveAsync({ from: from, to: to })]; + case 2: + _a.sent(); + return [2 /*return*/, to]; + case 3: return [2 /*return*/, from]; + } + }); + }); +} +/** + * Copy a file from a potentially temporary location to our cache directory. + * This ensures picker files are available for draft saving even if the original + * temporary files are cleaned up by the OS. + * + * On web, converts blob URLs to data URIs immediately to prevent revocation issues. + */ +function copyToCache(from) { + return __awaiter(this, void 0, void 0, function () { + var response, blob, e_2, cacheDir, to, normalizedFrom; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // Data URIs don't need any conversion + if (from.startsWith('data:')) { + return [2 /*return*/, from]; + } + if (!IS_WEB) return [3 /*break*/, 7]; + if (!from.startsWith('blob:')) return [3 /*break*/, 6]; + _a.label = 1; + case 1: + _a.trys.push([1, 5, , 6]); + return [4 /*yield*/, fetch(from)]; + case 2: + response = _a.sent(); + return [4 /*yield*/, response.blob()]; + case 3: + blob = _a.sent(); + return [4 /*yield*/, blobToDataUri(blob)]; + case 4: return [2 /*return*/, _a.sent()]; + case 5: + e_2 = _a.sent(); + // Blob URL was likely revoked, return as-is for downstream error handling + return [2 /*return*/, from]; + case 6: + // Other URLs on web don't need conversion + return [2 /*return*/, from]; + case 7: + cacheDir = getImageCacheDirectory(); + if (!cacheDir || from.startsWith(cacheDir)) { + return [2 /*return*/, from]; + } + to = joinPath(cacheDir, nanoid(36)); + return [4 /*yield*/, makeDirectoryAsync(cacheDir, { intermediates: true })]; + case 8: + _a.sent(); + normalizedFrom = from; + if (!from.startsWith('file://') && from.startsWith('/')) { + normalizedFrom = "file://".concat(from); + } + return [4 /*yield*/, copyAsync({ from: normalizedFrom, to: to })]; + case 9: + _a.sent(); + return [2 /*return*/, to]; + } + }); + }); +} +/** + * Convert a Blob to a data URI + */ +function blobToDataUri(blob) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onloadend = function () { + if (typeof reader.result === 'string') { + resolve(reader.result); + } + else { + reject(new Error('Failed to convert blob to data URI')); + } + }; + reader.onerror = function () { return reject(reader.error); }; + reader.readAsDataURL(blob); + }); +} +/** Purge files that were created to accomodate image manipulation */ +export function purgeTemporaryImageFiles() { + return __awaiter(this, void 0, void 0, function () { + var cacheDir; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + cacheDir = IS_NATIVE && getImageCacheDirectory(); + if (!cacheDir) return [3 /*break*/, 3]; + return [4 /*yield*/, deleteAsync(cacheDir, { idempotent: true })]; + case 1: + _a.sent(); + return [4 /*yield*/, makeDirectoryAsync(cacheDir)]; + case 2: + _a.sent(); + _a.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); +} +function joinPath(a, b) { + if (a.endsWith('/')) { + if (b.startsWith('/')) { + return a.slice(0, -1) + b; + } + return a + b; + } + else if (b.startsWith('/')) { + return a + b; + } + return a + '/' + b; +} +function containImageRes(w, h, _a) { + var maxW = _a.width, maxH = _a.height; + var scale = 1; + if (w > maxW || h > maxH) { + scale = w > h ? maxW / w : maxH / h; + w = Math.floor(w * scale); + h = Math.floor(h * scale); + } + return [w, h]; +} diff --git a/src/state/global-gesture-events/index.js b/src/state/global-gesture-events/index.js new file mode 100644 index 0000000000..b19ff96d44 --- /dev/null +++ b/src/state/global-gesture-events/index.js @@ -0,0 +1,52 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { Gesture, GestureDetector, } from 'react-native-gesture-handler'; +import EventEmitter from 'eventemitter3'; +var Context = createContext({ + events: new EventEmitter(), + register: function () { }, + unregister: function () { }, +}); +Context.displayName = 'GlobalGestureEventsContext'; +export function GlobalGestureEventsProvider(_a) { + var children = _a.children, style = _a.style; + var refCount = useRef(0); + var events = useMemo(function () { return new EventEmitter(); }, []); + var _b = useState(false), enabled = _b[0], setEnabled = _b[1]; + var ctx = useMemo(function () { return ({ + events: events, + register: function () { + refCount.current += 1; + if (refCount.current === 1) { + setEnabled(true); + } + }, + unregister: function () { + refCount.current -= 1; + if (refCount.current === 0) { + setEnabled(false); + } + }, + }); }, [events, setEnabled]); + var gesture = Gesture.Pan() + .runOnJS(true) + .enabled(enabled) + .simultaneousWithExternalGesture() + .onBegin(function (e) { + events.emit('begin', e); + }) + .onUpdate(function (e) { + events.emit('update', e); + }) + .onEnd(function (e) { + events.emit('end', e); + }) + .onFinalize(function (e) { + events.emit('finalize', e); + }); + return (_jsx(Context.Provider, { value: ctx, children: _jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { collapsable: false, style: style, children: children }) }) })); +} +export function useGlobalGestureEvents() { + return useContext(Context); +} diff --git a/src/state/global-gesture-events/index.web.js b/src/state/global-gesture-events/index.web.js new file mode 100644 index 0000000000..4adf6cb625 --- /dev/null +++ b/src/state/global-gesture-events/index.web.js @@ -0,0 +1,6 @@ +export function GlobalGestureEventsProvider(_props) { + throw new Error('GlobalGestureEventsProvider is not supported on web.'); +} +export function useGlobalGestureEvents() { + throw new Error('useGlobalGestureEvents is not supported on web.'); +} diff --git a/src/state/home-badge.js b/src/state/home-badge.js new file mode 100644 index 0000000000..de882a00af --- /dev/null +++ b/src/state/home-badge.js @@ -0,0 +1,17 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +var stateContext = React.createContext(false); +stateContext.displayName = 'HomeBadgeStateContext'; +var apiContext = React.createContext(function (_) { }); +apiContext.displayName = 'HomeBadgeApiContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(false), state = _b[0], setState = _b[1]; + return (_jsx(stateContext.Provider, { value: state, children: _jsx(apiContext.Provider, { value: setState, children: children }) })); +} +export function useHomeBadge() { + return React.useContext(stateContext); +} +export function useSetHomeBadge() { + return React.useContext(apiContext); +} diff --git a/src/state/lightbox.js b/src/state/lightbox.js new file mode 100644 index 0000000000..c060c96a0c --- /dev/null +++ b/src/state/lightbox.js @@ -0,0 +1,59 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { nanoid } from 'nanoid/non-secure'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +var LightboxContext = React.createContext({ + activeLightbox: null, +}); +LightboxContext.displayName = 'LightboxContext'; +var LightboxControlContext = React.createContext({ + openLightbox: function () { }, + closeLightbox: function () { return false; }, +}); +LightboxControlContext.displayName = 'LightboxControlContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(null), activeLightbox = _b[0], setActiveLightbox = _b[1]; + var openLightbox = useNonReactiveCallback(function (lightbox) { + setActiveLightbox(function (prevLightbox) { + if (prevLightbox) { + // Ignore duplicate open requests. If it's already open, + // the user has to explicitly close the previous one first. + return prevLightbox; + } + else { + return __assign(__assign({}, lightbox), { id: nanoid() }); + } + }); + }); + var closeLightbox = useNonReactiveCallback(function () { + var wasActive = !!activeLightbox; + setActiveLightbox(null); + return wasActive; + }); + var state = React.useMemo(function () { return ({ + activeLightbox: activeLightbox, + }); }, [activeLightbox]); + var methods = React.useMemo(function () { return ({ + openLightbox: openLightbox, + closeLightbox: closeLightbox, + }); }, [openLightbox, closeLightbox]); + return (_jsx(LightboxContext.Provider, { value: state, children: _jsx(LightboxControlContext.Provider, { value: methods, children: children }) })); +} +export function useLightbox() { + return React.useContext(LightboxContext); +} +export function useLightboxControls() { + return React.useContext(LightboxControlContext); +} diff --git a/src/state/messages/__tests__/convo.test.js b/src/state/messages/__tests__/convo.test.js new file mode 100644 index 0000000000..b7bc3198c0 --- /dev/null +++ b/src/state/messages/__tests__/convo.test.js @@ -0,0 +1,51 @@ +import { describe, it } from '@jest/globals'; +describe("#/state/messages/convo", function () { + describe("init", function () { + it.todo("fails if sender and recipients aren't found"); + it.todo("cannot re-initialize from a non-unintialized state"); + it.todo("can re-initialize from a failed state"); + }); + describe("resume", function () { + it.todo("restores previous state if resume fails"); + }); + describe("suspend", function () { + it.todo("cannot be interacted with when suspended"); + it.todo("polling is stopped when suspended"); + }); + describe("read states", function () { + it.todo("should mark messages as read as they come in"); + }); + describe("history fetching", function () { + it.todo("fetches initial chat history"); + it.todo("fetches additional chat history"); + it.todo("handles history fetch failure"); + it.todo("does not insert deleted messages"); + }); + describe("sending messages", function () { + it.todo("optimistically adds sending messages"); + it.todo("sends messages in order"); + it.todo("failed message send fails all sending messages"); + it.todo("can retry all failed messages via retry ConvoItem"); + it.todo("successfully sent messages are re-ordered, if needed, by events received from server"); + it.todo("pending messages are cleaned up from state after firehose event"); + }); + describe("deleting messages", function () { + it.todo("messages are optimistically deleted from the chat"); + it.todo("messages are confirmed deleted via events from the server"); + it.todo("deleted messages are cleaned up from state after firehose event"); + }); + describe("log handling", function () { + it.todo("updates rev to latest message received"); + it.todo("only handles log events for this convoId"); + it.todo("does not insert deleted messages"); + }); + describe("item ordering", function () { + it.todo("pending items are first, and in order"); + it.todo("new message items are next, and in order"); + it.todo("past message items are next, and in order"); + }); + describe("inactivity", function () { + it.todo("below a certain threshold of inactivity, restore entirely from log"); + it.todo("above a certain threshold of inactivity, rehydrate entirely fresh state"); + }); +}); diff --git a/src/state/messages/convo/agent.js b/src/state/messages/convo/agent.js new file mode 100644 index 0000000000..b56fed5c0b --- /dev/null +++ b/src/state/messages/convo/agent.js @@ -0,0 +1,1285 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { ChatBskyConvoDefs, } from '@atproto/api'; +import { XRPCError } from '@atproto/xrpc'; +import EventEmitter from 'eventemitter3'; +import { nanoid } from 'nanoid/non-secure'; +import { networkRetry } from '#/lib/async/retry'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { isErrorMaybeAppPasswordPermissions, isNetworkError, } from '#/lib/strings/errors'; +import { Logger } from '#/logger'; +import { ACTIVE_POLL_INTERVAL, BACKGROUND_POLL_INTERVAL, INACTIVE_TIMEOUT, NETWORK_FAILURE_STATUSES, } from '#/state/messages/convo/const'; +import { ConvoDispatchEvent, ConvoErrorCode, ConvoItemError, ConvoStatus, } from '#/state/messages/convo/types'; +import { IS_NATIVE } from '#/env'; +var logger = Logger.create(Logger.Context.ConversationAgent); +export function isConvoItemMessage(item) { + if (!item) + return false; + return (item.type === 'message' || + item.type === 'deleted-message' || + item.type === 'pending-message'); +} +var Convo = /** @class */ (function () { + function Convo(params) { + this.status = ConvoStatus.Uninitialized; + this.oldestRev = undefined; + this.isFetchingHistory = false; + this.latestRev = undefined; + this.pastMessages = new Map(); + this.newMessages = new Map(); + this.pendingMessages = new Map(); + this.deletedMessages = new Set(); + this.isProcessingPendingMessages = false; + this.emitter = new EventEmitter(); + this.subscribers = []; + this.pendingMessageFailure = null; + this.id = nanoid(3); + this.convoId = params.convoId; + this.agent = params.agent; + this.events = params.events; + this.senderUserDid = params.agent.assertDid; + if (params.placeholderData) { + this.setupPlaceholderData(params.placeholderData); + } + this.subscribe = this.subscribe.bind(this); + this.getSnapshot = this.getSnapshot.bind(this); + this.sendMessage = this.sendMessage.bind(this); + this.deleteMessage = this.deleteMessage.bind(this); + this.fetchMessageHistory = this.fetchMessageHistory.bind(this); + this.ingestFirehose = this.ingestFirehose.bind(this); + this.onFirehoseConnect = this.onFirehoseConnect.bind(this); + this.onFirehoseError = this.onFirehoseError.bind(this); + this.markConvoAccepted = this.markConvoAccepted.bind(this); + this.addReaction = this.addReaction.bind(this); + this.removeReaction = this.removeReaction.bind(this); + } + Convo.prototype.commit = function () { + this.snapshot = undefined; + this.subscribers.forEach(function (subscriber) { return subscriber(); }); + }; + Convo.prototype.subscribe = function (subscriber) { + var _this = this; + if (this.subscribers.length === 0) + this.init(); + this.subscribers.push(subscriber); + return function () { + _this.subscribers = _this.subscribers.filter(function (s) { return s !== subscriber; }); + if (_this.subscribers.length === 0) + _this.suspend(); + }; + }; + Convo.prototype.getSnapshot = function () { + if (!this.snapshot) + this.snapshot = this.generateSnapshot(); + // logger.debug('snapshotted', {}) + return this.snapshot; + }; + Convo.prototype.generateSnapshot = function () { + switch (this.status) { + case ConvoStatus.Initializing: { + return { + status: ConvoStatus.Initializing, + items: [], + convo: this.convo, + error: undefined, + sender: this.sender, + recipients: this.recipients, + isFetchingHistory: this.isFetchingHistory, + deleteMessage: undefined, + sendMessage: undefined, + fetchMessageHistory: undefined, + markConvoAccepted: undefined, + addReaction: undefined, + removeReaction: undefined, + }; + } + case ConvoStatus.Disabled: + case ConvoStatus.Suspended: + case ConvoStatus.Backgrounded: + case ConvoStatus.Ready: { + return { + status: this.status, + items: this.getItems(), + convo: this.convo, + error: undefined, + sender: this.sender, + recipients: this.recipients, + isFetchingHistory: this.isFetchingHistory, + deleteMessage: this.deleteMessage, + sendMessage: this.sendMessage, + fetchMessageHistory: this.fetchMessageHistory, + markConvoAccepted: this.markConvoAccepted, + addReaction: this.addReaction, + removeReaction: this.removeReaction, + }; + } + case ConvoStatus.Error: { + return { + status: ConvoStatus.Error, + items: [], + convo: undefined, + error: this.error, + sender: undefined, + recipients: undefined, + isFetchingHistory: false, + deleteMessage: undefined, + sendMessage: undefined, + fetchMessageHistory: undefined, + markConvoAccepted: undefined, + addReaction: undefined, + removeReaction: undefined, + }; + } + default: { + return { + status: ConvoStatus.Uninitialized, + items: [], + convo: this.convo, + error: undefined, + sender: this.sender, + recipients: this.recipients, + isFetchingHistory: false, + deleteMessage: undefined, + sendMessage: undefined, + fetchMessageHistory: undefined, + markConvoAccepted: undefined, + addReaction: undefined, + removeReaction: undefined, + }; + } + } + }; + Convo.prototype.dispatch = function (action) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + var prevStatus = this.status; + switch (this.status) { + case ConvoStatus.Uninitialized: { + switch (action.event) { + case ConvoDispatchEvent.Init: { + this.status = ConvoStatus.Initializing; + this.setup(); + this.setupFirehose(); + this.requestPollInterval(ACTIVE_POLL_INTERVAL); + break; + } + } + break; + } + case ConvoStatus.Initializing: { + switch (action.event) { + case ConvoDispatchEvent.Ready: { + this.status = ConvoStatus.Ready; + this.fetchMessageHistory(); + break; + } + case ConvoDispatchEvent.Background: { + this.status = ConvoStatus.Backgrounded; + this.fetchMessageHistory(); + this.requestPollInterval(BACKGROUND_POLL_INTERVAL); + break; + } + case ConvoDispatchEvent.Suspend: { + this.status = ConvoStatus.Suspended; + (_a = this.cleanupFirehoseConnection) === null || _a === void 0 ? void 0 : _a.call(this); + this.withdrawRequestedPollInterval(); + break; + } + case ConvoDispatchEvent.Error: { + this.status = ConvoStatus.Error; + this.error = action.payload; + (_b = this.cleanupFirehoseConnection) === null || _b === void 0 ? void 0 : _b.call(this); + this.withdrawRequestedPollInterval(); + break; + } + case ConvoDispatchEvent.Disable: { + this.status = ConvoStatus.Disabled; + this.fetchMessageHistory(); // finish init + (_c = this.cleanupFirehoseConnection) === null || _c === void 0 ? void 0 : _c.call(this); + this.withdrawRequestedPollInterval(); + break; + } + } + break; + } + case ConvoStatus.Ready: { + switch (action.event) { + case ConvoDispatchEvent.Resume: { + this.refreshConvo(); + this.requestPollInterval(ACTIVE_POLL_INTERVAL); + break; + } + case ConvoDispatchEvent.Background: { + this.status = ConvoStatus.Backgrounded; + this.requestPollInterval(BACKGROUND_POLL_INTERVAL); + break; + } + case ConvoDispatchEvent.Suspend: { + this.status = ConvoStatus.Suspended; + (_d = this.cleanupFirehoseConnection) === null || _d === void 0 ? void 0 : _d.call(this); + this.withdrawRequestedPollInterval(); + break; + } + case ConvoDispatchEvent.Error: { + this.status = ConvoStatus.Error; + this.error = action.payload; + (_e = this.cleanupFirehoseConnection) === null || _e === void 0 ? void 0 : _e.call(this); + this.withdrawRequestedPollInterval(); + break; + } + case ConvoDispatchEvent.Disable: { + this.status = ConvoStatus.Disabled; + (_f = this.cleanupFirehoseConnection) === null || _f === void 0 ? void 0 : _f.call(this); + this.withdrawRequestedPollInterval(); + break; + } + } + break; + } + case ConvoStatus.Backgrounded: { + switch (action.event) { + case ConvoDispatchEvent.Resume: { + if (this.wasChatInactive()) { + this.reset(); + } + else { + if (this.convo) { + this.status = ConvoStatus.Ready; + this.refreshConvo(); + this.maybeRecoverFromNetworkError(); + } + else { + this.status = ConvoStatus.Initializing; + this.setup(); + } + this.requestPollInterval(ACTIVE_POLL_INTERVAL); + } + break; + } + case ConvoDispatchEvent.Suspend: { + this.status = ConvoStatus.Suspended; + (_g = this.cleanupFirehoseConnection) === null || _g === void 0 ? void 0 : _g.call(this); + this.withdrawRequestedPollInterval(); + break; + } + case ConvoDispatchEvent.Error: { + this.status = ConvoStatus.Error; + this.error = action.payload; + (_h = this.cleanupFirehoseConnection) === null || _h === void 0 ? void 0 : _h.call(this); + this.withdrawRequestedPollInterval(); + break; + } + case ConvoDispatchEvent.Disable: { + this.status = ConvoStatus.Disabled; + (_j = this.cleanupFirehoseConnection) === null || _j === void 0 ? void 0 : _j.call(this); + this.withdrawRequestedPollInterval(); + break; + } + } + break; + } + case ConvoStatus.Suspended: { + switch (action.event) { + case ConvoDispatchEvent.Init: { + this.reset(); + break; + } + case ConvoDispatchEvent.Resume: { + this.reset(); + break; + } + case ConvoDispatchEvent.Error: { + this.status = ConvoStatus.Error; + this.error = action.payload; + break; + } + case ConvoDispatchEvent.Disable: { + this.status = ConvoStatus.Disabled; + break; + } + } + break; + } + case ConvoStatus.Error: { + switch (action.event) { + case ConvoDispatchEvent.Init: { + this.reset(); + break; + } + case ConvoDispatchEvent.Resume: { + this.reset(); + break; + } + case ConvoDispatchEvent.Suspend: { + this.status = ConvoStatus.Suspended; + break; + } + case ConvoDispatchEvent.Error: { + this.status = ConvoStatus.Error; + this.error = action.payload; + break; + } + case ConvoDispatchEvent.Disable: { + this.status = ConvoStatus.Disabled; + break; + } + } + break; + } + case ConvoStatus.Disabled: { + // can't do anything + break; + } + default: + break; + } + logger.debug("dispatch '".concat(action.event, "'"), { + id: this.id, + prev: prevStatus, + next: this.status, + }); + this.updateLastActiveTimestamp(); + this.commit(); + }; + Convo.prototype.reset = function () { + this.convo = undefined; + this.sender = undefined; + this.recipients = undefined; + this.snapshot = undefined; + this.status = ConvoStatus.Uninitialized; + this.error = undefined; + this.oldestRev = undefined; + this.latestRev = undefined; + this.pastMessages = new Map(); + this.newMessages = new Map(); + this.pendingMessages = new Map(); + this.deletedMessages = new Set(); + this.pendingMessageFailure = null; + this.fetchMessageHistoryError = undefined; + this.firehoseError = undefined; + this.dispatch({ event: ConvoDispatchEvent.Init }); + }; + Convo.prototype.maybeRecoverFromNetworkError = function () { + if (this.firehoseError) { + this.firehoseError.retry(); + this.firehoseError = undefined; + this.commit(); + } + else { + this.batchRetryPendingMessages(); + } + if (this.fetchMessageHistoryError) { + this.fetchMessageHistoryError.retry(); + this.fetchMessageHistoryError = undefined; + this.commit(); + } + }; + /** + * Initialises the convo with placeholder data, if provided. We still refetch it before rendering the convo, + * but this allows us to render the convo header immediately. + */ + Convo.prototype.setupPlaceholderData = function (data) { + var _this = this; + this.convo = data.convo; + this.sender = data.convo.members.find(function (m) { return m.did === _this.senderUserDid; }); + this.recipients = data.convo.members.filter(function (m) { return m.did !== _this.senderUserDid; }); + }; + Convo.prototype.setup = function () { + return __awaiter(this, void 0, void 0, function () { + var _a, convo, sender, recipients, userIsDisabled, e_1; + var _this = this; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, this.fetchConvo()]; + case 1: + _a = _b.sent(), convo = _a.convo, sender = _a.sender, recipients = _a.recipients; + this.convo = convo; + this.sender = sender; + this.recipients = recipients; + /* + * Some validation prior to `Ready` status + */ + if (!this.convo) { + throw new Error('could not find convo'); + } + if (!this.sender) { + throw new Error('could not find sender in convo'); + } + if (!this.recipients) { + throw new Error('could not find recipients in convo'); + } + userIsDisabled = Boolean(this.sender.chatDisabled); + if (userIsDisabled) { + this.dispatch({ event: ConvoDispatchEvent.Disable }); + } + else { + this.dispatch({ event: ConvoDispatchEvent.Ready }); + } + return [3 /*break*/, 3]; + case 2: + e_1 = _b.sent(); + if (!isNetworkError(e_1) && !isErrorMaybeAppPasswordPermissions(e_1)) { + logger.error('setup failed', { + safeMessage: e_1.message, + }); + } + this.dispatch({ + event: ConvoDispatchEvent.Error, + payload: { + exception: e_1, + code: ConvoErrorCode.InitFailed, + retry: function () { + _this.reset(); + }, + }, + }); + this.commit(); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); + }; + Convo.prototype.init = function () { + this.dispatch({ event: ConvoDispatchEvent.Init }); + }; + Convo.prototype.resume = function () { + this.dispatch({ event: ConvoDispatchEvent.Resume }); + }; + Convo.prototype.background = function () { + this.dispatch({ event: ConvoDispatchEvent.Background }); + }; + Convo.prototype.suspend = function () { + this.dispatch({ event: ConvoDispatchEvent.Suspend }); + }; + /** + * Called on any state transition, like when the chat is backgrounded. This + * value is then checked on background -> foreground transitions. + */ + Convo.prototype.updateLastActiveTimestamp = function () { + this.lastActiveTimestamp = Date.now(); + }; + Convo.prototype.wasChatInactive = function () { + if (!this.lastActiveTimestamp) + return true; + return Date.now() - this.lastActiveTimestamp > INACTIVE_TIMEOUT; + }; + Convo.prototype.requestPollInterval = function (interval) { + this.withdrawRequestedPollInterval(); + this.requestedPollInterval = this.events.requestPollInterval(interval); + }; + Convo.prototype.withdrawRequestedPollInterval = function () { + if (this.requestedPollInterval) { + this.requestedPollInterval(); + } + }; + Convo.prototype.fetchConvo = function () { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + if (this.pendingFetchConvo) + return [2 /*return*/, this.pendingFetchConvo]; + this.pendingFetchConvo = new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () { + var response, convo, e_2; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, 3, 4]); + return [4 /*yield*/, networkRetry(2, function () { + return _this.agent.api.chat.bsky.convo.getConvo({ + convoId: _this.convoId, + }, { headers: DM_SERVICE_HEADERS }); + })]; + case 1: + response = _a.sent(); + convo = response.data.convo; + resolve({ + convo: convo, + sender: convo.members.find(function (m) { return m.did === _this.senderUserDid; }), + recipients: convo.members.filter(function (m) { return m.did !== _this.senderUserDid; }), + }); + return [3 /*break*/, 4]; + case 2: + e_2 = _a.sent(); + reject(e_2); + return [3 /*break*/, 4]; + case 3: + this.pendingFetchConvo = undefined; + return [7 /*endfinally*/]; + case 4: return [2 /*return*/]; + } + }); + }); }); + return [2 /*return*/, this.pendingFetchConvo]; + }); + }); + }; + Convo.prototype.refreshConvo = function () { + return __awaiter(this, void 0, void 0, function () { + var _a, convo, sender, recipients, e_3; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, this.fetchConvo() + // throw new Error('UNCOMMENT TO TEST REFRESH FAILURE') + ]; + case 1: + _a = _b.sent(), convo = _a.convo, sender = _a.sender, recipients = _a.recipients; + // throw new Error('UNCOMMENT TO TEST REFRESH FAILURE') + this.convo = convo || this.convo; + this.sender = sender || this.sender; + this.recipients = recipients || this.recipients; + return [3 /*break*/, 3]; + case 2: + e_3 = _b.sent(); + if (!isNetworkError(e_3) && !isErrorMaybeAppPasswordPermissions(e_3)) { + logger.error("failed to refresh convo", { + safeMessage: e_3.message, + }); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); + }; + Convo.prototype.fetchMessageHistory = function () { + return __awaiter(this, void 0, void 0, function () { + var nextCursor_1, response, _a, cursor, messages, _i, messages_1, message, e_4; + var _this = this; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + logger.debug('fetch message history', {}); + /* + * If oldestRev is null, we've fetched all history. + */ + if (this.oldestRev === null) + return [2 /*return*/]; + /* + * Don't fetch again if a fetch is already in progress + */ + if (this.isFetchingHistory) + return [2 /*return*/]; + /* + * If we've rendered a retry state for history fetching, exit. Upon retry, + * this will be removed and we'll try again. + */ + if (this.fetchMessageHistoryError) + return [2 /*return*/]; + _b.label = 1; + case 1: + _b.trys.push([1, 3, 4, 5]); + this.isFetchingHistory = true; + this.commit(); + nextCursor_1 = this.oldestRev // for TS + ; + return [4 /*yield*/, networkRetry(2, function () { + return _this.agent.api.chat.bsky.convo.getMessages({ + cursor: nextCursor_1, + convoId: _this.convoId, + limit: IS_NATIVE ? 30 : 60, + }, { headers: DM_SERVICE_HEADERS }); + })]; + case 2: + response = _b.sent(); + _a = response.data, cursor = _a.cursor, messages = _a.messages; + this.oldestRev = cursor !== null && cursor !== void 0 ? cursor : null; + for (_i = 0, messages_1 = messages; _i < messages_1.length; _i++) { + message = messages_1[_i]; + if (ChatBskyConvoDefs.isMessageView(message) || + ChatBskyConvoDefs.isDeletedMessageView(message)) { + /* + * If this message is already in new messages, it was added by the + * firehose ingestion, and we can safely overwrite it. This trusts + * the server on ordering, and keeps it in sync. + */ + if (this.newMessages.has(message.id)) { + this.newMessages.delete(message.id); + } + this.pastMessages.set(message.id, message); + } + } + return [3 /*break*/, 5]; + case 3: + e_4 = _b.sent(); + if (!isNetworkError(e_4) && !isErrorMaybeAppPasswordPermissions(e_4)) { + logger.error('failed to fetch message history', { + safeMessage: e_4.message, + }); + } + this.fetchMessageHistoryError = { + retry: function () { + _this.fetchMessageHistory(); + }, + }; + return [3 /*break*/, 5]; + case 4: + this.isFetchingHistory = false; + this.commit(); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); + }; + Convo.prototype.setupFirehose = function () { + var _this = this; + var _a; + // remove old listeners, if exist + (_a = this.cleanupFirehoseConnection) === null || _a === void 0 ? void 0 : _a.call(this); + // reconnect + this.cleanupFirehoseConnection = this.events.on(function (event) { + switch (event.type) { + case 'connect': { + _this.onFirehoseConnect(); + break; + } + case 'error': { + _this.onFirehoseError(event.error); + break; + } + case 'logs': { + _this.ingestFirehose(event.logs); + break; + } + } + }, + /* + * This is VERY important — we only want events for this convo. + */ + { convoId: this.convoId }); + }; + Convo.prototype.onFirehoseConnect = function () { + this.firehoseError = undefined; + this.batchRetryPendingMessages(); + this.commit(); + }; + Convo.prototype.onFirehoseError = function (error) { + this.firehoseError = error; + this.commit(); + }; + Convo.prototype.ingestFirehose = function (events) { + var needsCommit = false; + for (var _i = 0, events_1 = events; _i < events_1.length; _i++) { + var ev = events_1[_i]; + /* + * If there's a rev, we should handle it. If there's not a rev, we don't + * know what it is. + */ + if ('rev' in ev && typeof ev.rev === 'string') { + var isUninitialized = !this.latestRev; + var isNewEvent = this.latestRev && ev.rev > this.latestRev; + /* + * We received an event prior to fetching any history, so we can safely + * use this as the initial history cursor + */ + if (this.oldestRev === undefined && isUninitialized) { + this.oldestRev = ev.rev; + } + /* + * We only care about new events + */ + if (isNewEvent || isUninitialized) { + /* + * Update rev regardless of if it's a ev type we care about or not + */ + this.latestRev = ev.rev; + if (ChatBskyConvoDefs.isLogCreateMessage(ev) && + ChatBskyConvoDefs.isMessageView(ev.message)) { + /** + * If this message is already in new messages, it was added by our + * sending logic, and is based on client-ordering. When we receive + * the "commited" event from the log, we should replace this + * reference and re-insert in order to respect the order we receied + * from the log. + */ + if (this.newMessages.has(ev.message.id)) { + this.newMessages.delete(ev.message.id); + } + this.newMessages.set(ev.message.id, ev.message); + needsCommit = true; + } + else if (ChatBskyConvoDefs.isLogDeleteMessage(ev) && + ChatBskyConvoDefs.isDeletedMessageView(ev.message)) { + /* + * Update if we have this in state. If we don't, don't worry about it. + */ + if (this.pastMessages.has(ev.message.id) || + this.newMessages.has(ev.message.id)) { + this.pastMessages.delete(ev.message.id); + this.newMessages.delete(ev.message.id); + this.deletedMessages.delete(ev.message.id); + needsCommit = true; + } + } + else if ((ChatBskyConvoDefs.isLogAddReaction(ev) || + ChatBskyConvoDefs.isLogRemoveReaction(ev)) && + ChatBskyConvoDefs.isMessageView(ev.message)) { + /* + * Update if we have this in state - replace message wholesale. If we don't, don't worry about it. + */ + if (this.pastMessages.has(ev.message.id)) { + this.pastMessages.set(ev.message.id, ev.message); + needsCommit = true; + } + if (this.newMessages.has(ev.message.id)) { + this.newMessages.set(ev.message.id, ev.message); + needsCommit = true; + } + } + } + } + } + if (needsCommit) { + this.commit(); + } + }; + Convo.prototype.sendMessage = function (message) { + var _a; + // Ignore empty messages for now since they have no other purpose atm + if (!message.text.trim() && !message.embed) + return; + logger.debug('send message', {}); + var tempId = nanoid(); + this.pendingMessageFailure = null; + this.pendingMessages.set(tempId, { + id: tempId, + message: message, + }); + if (((_a = this.convo) === null || _a === void 0 ? void 0 : _a.status) === 'request') { + this.convo = __assign(__assign({}, this.convo), { status: 'accepted' }); + } + this.commit(); + if (!this.isProcessingPendingMessages && !this.pendingMessageFailure) { + this.processPendingMessages(); + } + }; + Convo.prototype.markConvoAccepted = function () { + if (this.convo) { + this.convo = __assign(__assign({}, this.convo), { status: 'accepted' }); + } + this.commit(); + }; + Convo.prototype.processPendingMessages = function () { + return __awaiter(this, void 0, void 0, function () { + var pendingMessage, id, message, response, res, e_5; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + logger.debug("processing messages (".concat(this.pendingMessages.size, " remaining)"), {}); + pendingMessage = Array.from(this.pendingMessages.values()).shift(); + /* + * If there are no pending messages, we're done. + */ + if (!pendingMessage) { + this.isProcessingPendingMessages = false; + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 4, , 5]); + this.isProcessingPendingMessages = true; + id = pendingMessage.id, message = pendingMessage.message; + return [4 /*yield*/, this.agent.api.chat.bsky.convo.sendMessage({ + convoId: this.convoId, + message: message, + }, { encoding: 'application/json', headers: DM_SERVICE_HEADERS })]; + case 2: + response = _a.sent(); + res = response.data; + // remove from queue + this.pendingMessages.delete(id); + /* + * Insert into `newMessages` as soon as we have a real ID. That way, when + * we get an event log back, we can replace in situ. + */ + this.newMessages.set(res.id, __assign(__assign({}, res), { $type: 'chat.bsky.convo.defs#messageView' })); + // render new message state, prior to firehose + this.commit(); + // continue queue processing + return [4 /*yield*/, this.processPendingMessages()]; + case 3: + // continue queue processing + _a.sent(); + return [3 /*break*/, 5]; + case 4: + e_5 = _a.sent(); + this.handleSendMessageFailure(e_5); + this.isProcessingPendingMessages = false; + return [3 /*break*/, 5]; + case 5: return [2 /*return*/]; + } + }); + }); + }; + Convo.prototype.handleSendMessageFailure = function (e) { + if (e instanceof XRPCError) { + if (NETWORK_FAILURE_STATUSES.includes(e.status)) { + this.pendingMessageFailure = 'recoverable'; + } + else { + this.pendingMessageFailure = 'unrecoverable'; + switch (e.message) { + case 'block between recipient and sender': + this.emitter.emit('event', { + type: 'invalidate-block-state', + accountDids: __spreadArray([ + this.sender.did + ], this.recipients.map(function (r) { return r.did; }), true), + }); + break; + case 'Account is disabled': + this.dispatch({ event: ConvoDispatchEvent.Disable }); + break; + case 'Convo not found': + case 'Account does not exist': + case 'recipient does not exist': + case 'recipient requires incoming messages to come from someone they follow': + case 'recipient has disabled incoming messages': + break; + default: + if (!isNetworkError(e)) { + logger.warn("handleSendMessageFailure could not handle error", { + status: e.status, + message: e.message, + }); + } + break; + } + } + } + else { + this.pendingMessageFailure = 'unrecoverable'; + if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) { + logger.error("handleSendMessageFailure received unknown error", { + safeMessage: e.message, + }); + } + } + this.commit(); + }; + Convo.prototype.batchRetryPendingMessages = function () { + return __awaiter(this, void 0, void 0, function () { + var messageArray, data, items, _i, items_1, item, _a, messageArray_1, pendingMessage, e_6; + var _this = this; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (this.pendingMessageFailure === null) + return [2 /*return*/]; + messageArray = Array.from(this.pendingMessages.values()); + if (messageArray.length === 0) + return [2 /*return*/]; + this.pendingMessageFailure = null; + this.commit(); + logger.debug("batch retrying ".concat(this.pendingMessages.size, " pending messages"), {}); + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, this.agent.api.chat.bsky.convo.sendMessageBatch({ + items: messageArray.map(function (_a) { + var message = _a.message; + return ({ + convoId: _this.convoId, + message: message, + }); + }), + }, { encoding: 'application/json', headers: DM_SERVICE_HEADERS })]; + case 2: + data = (_b.sent()).data; + items = data.items; + /* + * Insert into `newMessages` as soon as we have a real ID. That way, when + * we get an event log back, we can replace in situ. + */ + for (_i = 0, items_1 = items; _i < items_1.length; _i++) { + item = items_1[_i]; + this.newMessages.set(item.id, __assign(__assign({}, item), { $type: 'chat.bsky.convo.defs#messageView' })); + } + for (_a = 0, messageArray_1 = messageArray; _a < messageArray_1.length; _a++) { + pendingMessage = messageArray_1[_a]; + this.pendingMessages.delete(pendingMessage.id); + } + this.commit(); + logger.debug("sent ".concat(this.pendingMessages.size, " pending messages"), {}); + return [3 /*break*/, 4]; + case 3: + e_6 = _b.sent(); + this.handleSendMessageFailure(e_6); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); + }; + Convo.prototype.deleteMessage = function (messageId) { + return __awaiter(this, void 0, void 0, function () { + var e_7; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + logger.debug('delete message', {}); + this.deletedMessages.add(messageId); + this.commit(); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, networkRetry(2, function () { + return _this.agent.api.chat.bsky.convo.deleteMessageForSelf({ + convoId: _this.convoId, + messageId: messageId, + }, { encoding: 'application/json', headers: DM_SERVICE_HEADERS }); + })]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + e_7 = _a.sent(); + if (!isNetworkError(e_7) && !isErrorMaybeAppPasswordPermissions(e_7)) { + logger.error("failed to delete message", { + safeMessage: e_7.message, + }); + } + this.deletedMessages.delete(messageId); + this.commit(); + throw e_7; + case 4: return [2 /*return*/]; + } + }); + }); + }; + Convo.prototype.on = function (handler) { + var _this = this; + this.emitter.on('event', handler); + return function () { + _this.emitter.off('event', handler); + }; + }; + /* + * Items in reverse order, since FlatList inverts + */ + Convo.prototype.getItems = function () { + var _this = this; + var items = []; + this.pastMessages.forEach(function (m) { + if (ChatBskyConvoDefs.isMessageView(m)) { + items.unshift({ + type: 'message', + key: m.id, + message: m, + nextMessage: null, + prevMessage: null, + }); + } + else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { + items.unshift({ + type: 'deleted-message', + key: m.id, + message: m, + nextMessage: null, + prevMessage: null, + }); + } + }); + if (this.fetchMessageHistoryError) { + items.unshift({ + type: 'error', + code: ConvoItemError.HistoryFailed, + key: ConvoItemError.HistoryFailed, + retry: function () { + _this.maybeRecoverFromNetworkError(); + }, + }); + } + this.newMessages.forEach(function (m) { + if (ChatBskyConvoDefs.isMessageView(m)) { + items.push({ + type: 'message', + key: m.id, + message: m, + nextMessage: null, + prevMessage: null, + }); + } + else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { + items.push({ + type: 'deleted-message', + key: m.id, + message: m, + nextMessage: null, + prevMessage: null, + }); + } + }); + this.pendingMessages.forEach(function (m) { + items.push({ + type: 'pending-message', + key: m.id, + message: __assign(__assign({}, m.message), { embed: undefined, $type: 'chat.bsky.convo.defs#messageView', id: nanoid(), rev: '__fake__', sentAt: new Date().toISOString(), + /* + * `getItems` is only run in "active" status states, where + * `this.sender` is defined + */ + sender: { + $type: 'chat.bsky.convo.defs#messageViewSender', + did: _this.sender.did, + } }), + nextMessage: null, + prevMessage: null, + failed: _this.pendingMessageFailure !== null, + retry: _this.pendingMessageFailure === 'recoverable' + ? function () { + _this.maybeRecoverFromNetworkError(); + } + : undefined, + }); + }); + if (this.firehoseError) { + items.push({ + type: 'error', + code: ConvoItemError.FirehoseFailed, + key: ConvoItemError.FirehoseFailed, + retry: function () { + var _a; + (_a = _this.firehoseError) === null || _a === void 0 ? void 0 : _a.retry(); + }, + }); + } + return items + .filter(function (item) { + if (isConvoItemMessage(item)) { + return !_this.deletedMessages.has(item.message.id); + } + return true; + }) + .map(function (item, i, arr) { + var nextMessage = null; + var prevMessage = null; + var isMessage = isConvoItemMessage(item); + if (isMessage) { + if (ChatBskyConvoDefs.isMessageView(item.message) || + ChatBskyConvoDefs.isDeletedMessageView(item.message)) { + var next = arr[i + 1]; + if (isConvoItemMessage(next) && + (ChatBskyConvoDefs.isMessageView(next.message) || + ChatBskyConvoDefs.isDeletedMessageView(next.message))) { + nextMessage = next.message; + } + var prev = arr[i - 1]; + if (isConvoItemMessage(prev) && + (ChatBskyConvoDefs.isMessageView(prev.message) || + ChatBskyConvoDefs.isDeletedMessageView(prev.message))) { + prevMessage = prev.message; + } + } + return __assign(__assign({}, item), { nextMessage: nextMessage, prevMessage: prevMessage }); + } + return item; + }); + }; + /** + * Add an emoji reaction to a message + * + * @param messageId - the id of the message to add the reaction to + * @param emoji - must be one grapheme + */ + Convo.prototype.addReaction = function (messageId, emoji) { + return __awaiter(this, void 0, void 0, function () { + var optimisticReaction, restore, prevMessage_1, prevMessage_2, data, error_1; + var _this = this; + var _a, _b, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + optimisticReaction = { + value: emoji, + sender: { did: this.senderUserDid }, + createdAt: new Date().toISOString(), + }; + restore = null; + if (this.pastMessages.has(messageId)) { + prevMessage_1 = this.pastMessages.get(messageId); + if (ChatBskyConvoDefs.isMessageView(prevMessage_1) && + // skip optimistic update if reaction already exists + !((_a = prevMessage_1.reactions) === null || _a === void 0 ? void 0 : _a.find(function (reaction) { + return reaction.sender.did === _this.senderUserDid && + reaction.value === emoji; + }))) { + if (prevMessage_1.reactions) { + if (prevMessage_1.reactions.filter(function (reaction) { return reaction.sender.did === _this.senderUserDid; }).length >= 5) { + throw new Error('Maximum reactions reached'); + } + } + this.pastMessages.set(messageId, __assign(__assign({}, prevMessage_1), { reactions: __spreadArray(__spreadArray([], ((_b = prevMessage_1.reactions) !== null && _b !== void 0 ? _b : []), true), [optimisticReaction], false) })); + this.commit(); + restore = function () { + _this.pastMessages.set(messageId, prevMessage_1); + _this.commit(); + }; + } + } + else if (this.newMessages.has(messageId)) { + prevMessage_2 = this.newMessages.get(messageId); + if (ChatBskyConvoDefs.isMessageView(prevMessage_2) && + !((_c = prevMessage_2.reactions) === null || _c === void 0 ? void 0 : _c.find(function (reaction) { return reaction.value === emoji; }))) { + if (prevMessage_2.reactions && prevMessage_2.reactions.length >= 5) + throw new Error('Maximum reactions reached'); + this.newMessages.set(messageId, __assign(__assign({}, prevMessage_2), { reactions: __spreadArray(__spreadArray([], ((_d = prevMessage_2.reactions) !== null && _d !== void 0 ? _d : []), true), [optimisticReaction], false) })); + this.commit(); + restore = function () { + _this.newMessages.set(messageId, prevMessage_2); + _this.commit(); + }; + } + } + _e.label = 1; + case 1: + _e.trys.push([1, 3, , 4]); + logger.debug("Adding reaction ".concat(emoji, " to message ").concat(messageId)); + return [4 /*yield*/, this.agent.chat.bsky.convo.addReaction({ messageId: messageId, value: emoji, convoId: this.convoId }, { encoding: 'application/json', headers: DM_SERVICE_HEADERS })]; + case 2: + data = (_e.sent()).data; + if (ChatBskyConvoDefs.isMessageView(data.message)) { + if (this.pastMessages.has(messageId)) { + this.pastMessages.set(messageId, data.message); + this.commit(); + } + else if (this.newMessages.has(messageId)) { + this.newMessages.set(messageId, data.message); + this.commit(); + } + } + return [3 /*break*/, 4]; + case 3: + error_1 = _e.sent(); + if (restore) + restore(); + throw error_1; + case 4: return [2 /*return*/]; + } + }); + }); + }; + /* + * Remove a reaction from a message. + * + * @param messageId - The ID of the message to remove the reaction from. + * @param emoji - The emoji to remove. + */ + Convo.prototype.removeReaction = function (messageId, emoji) { + return __awaiter(this, void 0, void 0, function () { + var restore, prevMessage_3, prevMessage_4, error_2; + var _this = this; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + restore = null; + if (this.pastMessages.has(messageId)) { + prevMessage_3 = this.pastMessages.get(messageId); + if (ChatBskyConvoDefs.isMessageView(prevMessage_3)) { + this.pastMessages.set(messageId, __assign(__assign({}, prevMessage_3), { reactions: (_a = prevMessage_3.reactions) === null || _a === void 0 ? void 0 : _a.filter(function (reaction) { + return reaction.value !== emoji || + reaction.sender.did !== _this.senderUserDid; + }) })); + this.commit(); + restore = function () { + _this.pastMessages.set(messageId, prevMessage_3); + _this.commit(); + }; + } + } + else if (this.newMessages.has(messageId)) { + prevMessage_4 = this.newMessages.get(messageId); + if (ChatBskyConvoDefs.isMessageView(prevMessage_4)) { + this.newMessages.set(messageId, __assign(__assign({}, prevMessage_4), { reactions: (_b = prevMessage_4.reactions) === null || _b === void 0 ? void 0 : _b.filter(function (reaction) { + return reaction.value !== emoji || + reaction.sender.did !== _this.senderUserDid; + }) })); + this.commit(); + restore = function () { + _this.newMessages.set(messageId, prevMessage_4); + _this.commit(); + }; + } + } + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + logger.debug("Removing reaction ".concat(emoji, " from message ").concat(messageId)); + return [4 /*yield*/, this.agent.chat.bsky.convo.removeReaction({ messageId: messageId, value: emoji, convoId: this.convoId }, { encoding: 'application/json', headers: DM_SERVICE_HEADERS })]; + case 2: + _c.sent(); + return [3 /*break*/, 4]; + case 3: + error_2 = _c.sent(); + if (restore) + restore(); + throw error_2; + case 4: return [2 /*return*/]; + } + }); + }); + }; + return Convo; +}()); +export { Convo }; diff --git a/src/state/messages/convo/const.js b/src/state/messages/convo/const.js new file mode 100644 index 0000000000..f0ffec6ae0 --- /dev/null +++ b/src/state/messages/convo/const.js @@ -0,0 +1,7 @@ +export var ACTIVE_POLL_INTERVAL = 4e3; +export var MESSAGE_SCREEN_POLL_INTERVAL = 30e3; +export var BACKGROUND_POLL_INTERVAL = 60e3; +export var INACTIVE_TIMEOUT = 60e3 * 5; +export var NETWORK_FAILURE_STATUSES = [ + 1, 408, 425, 429, 500, 502, 503, 504, 522, 524, +]; diff --git a/src/state/messages/convo/index.js b/src/state/messages/convo/index.js new file mode 100644 index 0000000000..27c3168981 --- /dev/null +++ b/src/state/messages/convo/index.js @@ -0,0 +1,84 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React, { useContext, useState, useSyncExternalStore } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAppState } from '#/lib/appState'; +import { Convo } from '#/state/messages/convo/agent'; +import { isConvoActive } from '#/state/messages/convo/util'; +import { useMessagesEventBus } from '#/state/messages/events'; +import { RQKEY as getConvoKey, useMarkAsReadMutation, } from '#/state/queries/messages/conversation'; +import { RQKEY_ROOT as ListConvosQueryKeyRoot } from '#/state/queries/messages/list-conversations'; +import { RQKEY as createProfileQueryKey } from '#/state/queries/profile'; +import { useAgent } from '#/state/session'; +export * from '#/state/messages/convo/util'; +var ChatContext = React.createContext(null); +ChatContext.displayName = 'ChatContext'; +export function useConvo() { + var ctx = useContext(ChatContext); + if (!ctx) { + throw new Error('useConvo must be used within a ConvoProvider'); + } + return ctx; +} +/** + * This hook should only be used when the Convo is "active", meaning the chat + * is loaded and ready to be used, or its in a suspended or background state, + * and ready for resumption. + */ +export function useConvoActive() { + var ctx = useContext(ChatContext); + if (!ctx) { + throw new Error('useConvo must be used within a ConvoProvider'); + } + if (!isConvoActive(ctx)) { + throw new Error("useConvoActive must only be rendered when the Convo is ready."); + } + return ctx; +} +export function ConvoProvider(_a) { + var children = _a.children, convoId = _a.convoId; + var queryClient = useQueryClient(); + var agent = useAgent(); + var events = useMessagesEventBus(); + var convo = useState(function () { + var placeholder = queryClient.getQueryData(getConvoKey(convoId)); + return new Convo({ + convoId: convoId, + agent: agent, + events: events, + placeholderData: placeholder ? { convo: placeholder } : undefined, + }); + })[0]; + var service = useSyncExternalStore(convo.subscribe, convo.getSnapshot); + var markAsRead = useMarkAsReadMutation().mutate; + var appState = useAppState(); + var isActive = appState === 'active'; + useFocusEffect(React.useCallback(function () { + if (isActive) { + convo.resume(); + markAsRead({ convoId: convoId }); + return function () { + convo.background(); + markAsRead({ convoId: convoId }); + }; + } + }, [isActive, convo, convoId, markAsRead])); + React.useEffect(function () { + return convo.on(function (event) { + switch (event.type) { + case 'invalidate-block-state': { + for (var _i = 0, _a = event.accountDids; _i < _a.length; _i++) { + var did = _a[_i]; + queryClient.invalidateQueries({ + queryKey: createProfileQueryKey(did), + }); + } + queryClient.invalidateQueries({ + queryKey: [ListConvosQueryKeyRoot], + }); + } + } + }); + }, [convo, queryClient]); + return _jsx(ChatContext.Provider, { value: service, children: children }); +} diff --git a/src/state/messages/convo/types.js b/src/state/messages/convo/types.js new file mode 100644 index 0000000000..b3105255bf --- /dev/null +++ b/src/state/messages/convo/types.js @@ -0,0 +1,35 @@ +export var ConvoStatus; +(function (ConvoStatus) { + ConvoStatus["Uninitialized"] = "uninitialized"; + ConvoStatus["Initializing"] = "initializing"; + ConvoStatus["Ready"] = "ready"; + ConvoStatus["Error"] = "error"; + ConvoStatus["Backgrounded"] = "backgrounded"; + ConvoStatus["Suspended"] = "suspended"; + ConvoStatus["Disabled"] = "disabled"; +})(ConvoStatus || (ConvoStatus = {})); +export var ConvoItemError; +(function (ConvoItemError) { + /** + * Error connecting to event firehose + */ + ConvoItemError["FirehoseFailed"] = "firehoseFailed"; + /** + * Error fetching past messages + */ + ConvoItemError["HistoryFailed"] = "historyFailed"; +})(ConvoItemError || (ConvoItemError = {})); +export var ConvoErrorCode; +(function (ConvoErrorCode) { + ConvoErrorCode["InitFailed"] = "initFailed"; +})(ConvoErrorCode || (ConvoErrorCode = {})); +export var ConvoDispatchEvent; +(function (ConvoDispatchEvent) { + ConvoDispatchEvent["Init"] = "init"; + ConvoDispatchEvent["Ready"] = "ready"; + ConvoDispatchEvent["Resume"] = "resume"; + ConvoDispatchEvent["Background"] = "background"; + ConvoDispatchEvent["Suspend"] = "suspend"; + ConvoDispatchEvent["Error"] = "error"; + ConvoDispatchEvent["Disable"] = "disable"; +})(ConvoDispatchEvent || (ConvoDispatchEvent = {})); diff --git a/src/state/messages/convo/util.js b/src/state/messages/convo/util.js new file mode 100644 index 0000000000..3f3b55b2b0 --- /dev/null +++ b/src/state/messages/convo/util.js @@ -0,0 +1,12 @@ +import { ConvoStatus, } from './types'; +/** + * Checks if a `Convo` has a `status` that is "active", meaning the chat is + * loaded and ready to be used, or its in a suspended or background state, and + * ready for resumption. + */ +export function isConvoActive(convo) { + return (convo.status === ConvoStatus.Ready || + convo.status === ConvoStatus.Backgrounded || + convo.status === ConvoStatus.Suspended || + convo.status === ConvoStatus.Disabled); +} diff --git a/src/state/messages/current-convo-id.js b/src/state/messages/current-convo-id.js new file mode 100644 index 0000000000..072ce31d64 --- /dev/null +++ b/src/state/messages/current-convo-id.js @@ -0,0 +1,20 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +var CurrentConvoIdContext = React.createContext({ + currentConvoId: undefined, + setCurrentConvoId: function () { }, +}); +CurrentConvoIdContext.displayName = 'CurrentConvoIdContext'; +export function useCurrentConvoId() { + var ctx = React.useContext(CurrentConvoIdContext); + if (!ctx) { + throw new Error('useCurrentConvoId must be used within a CurrentConvoIdProvider'); + } + return ctx; +} +export function CurrentConvoIdProvider(_a) { + var children = _a.children; + var _b = React.useState(), currentConvoId = _b[0], setCurrentConvoId = _b[1]; + var ctx = React.useMemo(function () { return ({ currentConvoId: currentConvoId, setCurrentConvoId: setCurrentConvoId }); }, [currentConvoId]); + return (_jsx(CurrentConvoIdContext.Provider, { value: ctx, children: children })); +} diff --git a/src/state/messages/events/agent.js b/src/state/messages/events/agent.js new file mode 100644 index 0000000000..e9e3d192d2 --- /dev/null +++ b/src/state/messages/events/agent.js @@ -0,0 +1,423 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import EventEmitter from 'eventemitter3'; +import { nanoid } from 'nanoid/non-secure'; +import { networkRetry } from '#/lib/async/retry'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { isErrorMaybeAppPasswordPermissions, isNetworkError, } from '#/lib/strings/errors'; +import { Logger } from '#/logger'; +import { BACKGROUND_POLL_INTERVAL, DEFAULT_POLL_INTERVAL, } from '#/state/messages/events/const'; +import { MessagesEventBusDispatchEvent, MessagesEventBusErrorCode, MessagesEventBusStatus, } from '#/state/messages/events/types'; +var logger = Logger.create(Logger.Context.DMsAgent); +var MessagesEventBus = /** @class */ (function () { + function MessagesEventBus(params) { + this.emitter = new EventEmitter(); + this.status = MessagesEventBusStatus.Initializing; + this.latestRev = undefined; + this.pollInterval = DEFAULT_POLL_INTERVAL; + this.requestedPollIntervals = new Map(); + /* + * Polling + */ + this.isPolling = false; + this.id = nanoid(3); + this.agent = params.agent; + this.init(); + } + MessagesEventBus.prototype.requestPollInterval = function (interval) { + var _this = this; + var id = nanoid(); + this.requestedPollIntervals.set(id, interval); + this.dispatch({ + event: MessagesEventBusDispatchEvent.UpdatePoll, + }); + return function () { + _this.requestedPollIntervals.delete(id); + _this.dispatch({ + event: MessagesEventBusDispatchEvent.UpdatePoll, + }); + }; + }; + MessagesEventBus.prototype.getLatestRev = function () { + return this.latestRev; + }; + MessagesEventBus.prototype.on = function (handler, options) { + var _this = this; + var handle = function (event) { + if (event.type === 'logs' && options.convoId) { + var filteredLogs = event.logs.filter(function (log) { + if ('convoId' in log && log.convoId === options.convoId) { + return log.convoId === options.convoId; + } + return false; + }); + if (filteredLogs.length > 0) { + handler(__assign(__assign({}, event), { logs: filteredLogs })); + } + } + else { + handler(event); + } + }; + this.emitter.on('event', handle); + return function () { + _this.emitter.off('event', handle); + }; + }; + MessagesEventBus.prototype.background = function () { + logger.debug("background", {}); + this.dispatch({ event: MessagesEventBusDispatchEvent.Background }); + }; + MessagesEventBus.prototype.suspend = function () { + logger.debug("suspend", {}); + this.dispatch({ event: MessagesEventBusDispatchEvent.Suspend }); + }; + MessagesEventBus.prototype.resume = function () { + logger.debug("resume", {}); + this.dispatch({ event: MessagesEventBusDispatchEvent.Resume }); + }; + MessagesEventBus.prototype.dispatch = function (action) { + var prevStatus = this.status; + switch (this.status) { + case MessagesEventBusStatus.Initializing: { + switch (action.event) { + case MessagesEventBusDispatchEvent.Ready: { + this.status = MessagesEventBusStatus.Ready; + this.resetPoll(); + this.emitter.emit('event', { type: 'connect' }); + break; + } + case MessagesEventBusDispatchEvent.Background: { + this.status = MessagesEventBusStatus.Backgrounded; + this.resetPoll(); + this.emitter.emit('event', { type: 'connect' }); + break; + } + case MessagesEventBusDispatchEvent.Suspend: { + this.status = MessagesEventBusStatus.Suspended; + break; + } + case MessagesEventBusDispatchEvent.Error: { + this.status = MessagesEventBusStatus.Error; + this.emitter.emit('event', { type: 'error', error: action.payload }); + break; + } + } + break; + } + case MessagesEventBusStatus.Ready: { + switch (action.event) { + case MessagesEventBusDispatchEvent.Background: { + this.status = MessagesEventBusStatus.Backgrounded; + this.resetPoll(); + break; + } + case MessagesEventBusDispatchEvent.Suspend: { + this.status = MessagesEventBusStatus.Suspended; + this.stopPoll(); + break; + } + case MessagesEventBusDispatchEvent.Error: { + this.status = MessagesEventBusStatus.Error; + this.stopPoll(); + this.emitter.emit('event', { type: 'error', error: action.payload }); + break; + } + case MessagesEventBusDispatchEvent.UpdatePoll: { + this.resetPoll(); + break; + } + } + break; + } + case MessagesEventBusStatus.Backgrounded: { + switch (action.event) { + case MessagesEventBusDispatchEvent.Resume: { + this.status = MessagesEventBusStatus.Ready; + this.resetPoll(); + break; + } + case MessagesEventBusDispatchEvent.Suspend: { + this.status = MessagesEventBusStatus.Suspended; + this.stopPoll(); + break; + } + case MessagesEventBusDispatchEvent.Error: { + this.status = MessagesEventBusStatus.Error; + this.stopPoll(); + this.emitter.emit('event', { type: 'error', error: action.payload }); + break; + } + case MessagesEventBusDispatchEvent.UpdatePoll: { + this.resetPoll(); + break; + } + } + break; + } + case MessagesEventBusStatus.Suspended: { + switch (action.event) { + case MessagesEventBusDispatchEvent.Resume: { + this.status = MessagesEventBusStatus.Ready; + this.resetPoll(); + break; + } + case MessagesEventBusDispatchEvent.Background: { + this.status = MessagesEventBusStatus.Backgrounded; + this.resetPoll(); + break; + } + case MessagesEventBusDispatchEvent.Error: { + this.status = MessagesEventBusStatus.Error; + this.stopPoll(); + this.emitter.emit('event', { type: 'error', error: action.payload }); + break; + } + } + break; + } + case MessagesEventBusStatus.Error: { + switch (action.event) { + case MessagesEventBusDispatchEvent.UpdatePoll: { + // basically reset + this.status = MessagesEventBusStatus.Initializing; + this.latestRev = undefined; + this.init(); + break; + } + case MessagesEventBusDispatchEvent.Resume: { + this.status = MessagesEventBusStatus.Ready; + this.resetPoll(); + this.emitter.emit('event', { type: 'connect' }); + break; + } + } + break; + } + default: + break; + } + logger.debug("dispatch '".concat(action.event, "'"), { + id: this.id, + prev: prevStatus, + next: this.status, + }); + }; + MessagesEventBus.prototype.init = function () { + return __awaiter(this, void 0, void 0, function () { + var response, cursor, e_1; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + logger.debug("init", {}); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, networkRetry(2, function () { + return _this.agent.chat.bsky.convo.getLog({}, { headers: DM_SERVICE_HEADERS }); + }) + // throw new Error('UNCOMMENT TO TEST INIT FAILURE') + ]; + case 2: + response = _a.sent(); + cursor = response.data.cursor; + // should always be defined + if (cursor) { + if (!this.latestRev) { + this.latestRev = cursor; + } + else if (cursor > this.latestRev) { + this.latestRev = cursor; + } + } + this.dispatch({ event: MessagesEventBusDispatchEvent.Ready }); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + if (!isNetworkError(e_1) && !isErrorMaybeAppPasswordPermissions(e_1)) { + logger.error("init failed", { + safeMessage: e_1.message, + }); + } + this.dispatch({ + event: MessagesEventBusDispatchEvent.Error, + payload: { + exception: e_1, + code: MessagesEventBusErrorCode.InitFailed, + retry: function () { + _this.dispatch({ event: MessagesEventBusDispatchEvent.Resume }); + }, + }, + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); + }; + MessagesEventBus.prototype.getPollInterval = function () { + switch (this.status) { + case MessagesEventBusStatus.Ready: { + var requested = Array.from(this.requestedPollIntervals.values()); + var lowest = Math.min.apply(Math, __spreadArray([DEFAULT_POLL_INTERVAL], requested, false)); + return lowest; + } + case MessagesEventBusStatus.Backgrounded: { + return BACKGROUND_POLL_INTERVAL; + } + default: + return DEFAULT_POLL_INTERVAL; + } + }; + MessagesEventBus.prototype.resetPoll = function () { + this.pollInterval = this.getPollInterval(); + this.stopPoll(); + this.startPoll(); + }; + MessagesEventBus.prototype.startPoll = function () { + var _this = this; + if (!this.isPolling) + this.poll(); + this.pollIntervalRef = setInterval(function () { + if (_this.isPolling) + return; + _this.poll(); + }, this.pollInterval); + }; + MessagesEventBus.prototype.stopPoll = function () { + if (this.pollIntervalRef) + clearInterval(this.pollIntervalRef); + }; + MessagesEventBus.prototype.poll = function () { + return __awaiter(this, void 0, void 0, function () { + var response, events, needsEmit, batch, _i, events_1, ev, e_2; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (this.isPolling) + return [2 /*return*/]; + this.isPolling = true; + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, networkRetry(2, function () { + return _this.agent.chat.bsky.convo.getLog({ + cursor: _this.latestRev, + }, { headers: DM_SERVICE_HEADERS }); + }) + // throw new Error('UNCOMMENT TO TEST POLL FAILURE') + ]; + case 2: + response = _a.sent(); + events = response.data.logs; + needsEmit = false; + batch = []; + for (_i = 0, events_1 = events; _i < events_1.length; _i++) { + ev = events_1[_i]; + /* + * If there's a rev, we should handle it. If there's not a rev, we don't + * know what it is. + */ + if ('rev' in ev && typeof ev.rev === 'string') { + /* + * We only care about new events + */ + if (ev.rev > (this.latestRev = this.latestRev || ev.rev)) { + /* + * Update rev regardless of if it's a ev type we care about or not + */ + this.latestRev = ev.rev; + needsEmit = true; + batch.push(ev); + } + } + } + if (needsEmit) { + this.emitter.emit('event', { type: 'logs', logs: batch }); + } + return [3 /*break*/, 5]; + case 3: + e_2 = _a.sent(); + if (!isNetworkError(e_2) && !isErrorMaybeAppPasswordPermissions(e_2)) { + logger.error("poll events failed", { + safeMessage: e_2.message, + }); + } + this.dispatch({ + event: MessagesEventBusDispatchEvent.Error, + payload: { + exception: e_2, + code: MessagesEventBusErrorCode.PollFailed, + retry: function () { + _this.dispatch({ event: MessagesEventBusDispatchEvent.Resume }); + }, + }, + }); + return [3 /*break*/, 5]; + case 4: + this.isPolling = false; + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); + }; + return MessagesEventBus; +}()); +export { MessagesEventBus }; diff --git a/src/state/messages/events/const.js b/src/state/messages/events/const.js new file mode 100644 index 0000000000..7f508dc913 --- /dev/null +++ b/src/state/messages/events/const.js @@ -0,0 +1,2 @@ +export var DEFAULT_POLL_INTERVAL = 60e3; +export var BACKGROUND_POLL_INTERVAL = 60e3 * 5; diff --git a/src/state/messages/events/index.js b/src/state/messages/events/index.js new file mode 100644 index 0000000000..66feea0b9e --- /dev/null +++ b/src/state/messages/events/index.js @@ -0,0 +1,52 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { AppState } from 'react-native'; +import { MessagesEventBus } from '#/state/messages/events/agent'; +import { useAgent, useSession } from '#/state/session'; +var MessagesEventBusContext = React.createContext(null); +MessagesEventBusContext.displayName = 'MessagesEventBusContext'; +export function useMessagesEventBus() { + var ctx = React.useContext(MessagesEventBusContext); + if (!ctx) { + throw new Error('useMessagesEventBus must be used within a MessagesEventBusProvider'); + } + return ctx; +} +export function MessagesEventBusProvider(_a) { + var children = _a.children; + var currentAccount = useSession().currentAccount; + if (!currentAccount) { + return (_jsx(MessagesEventBusContext.Provider, { value: null, children: children })); + } + return (_jsx(MessagesEventBusProviderInner, { children: children })); +} +export function MessagesEventBusProviderInner(_a) { + var children = _a.children; + var agent = useAgent(); + var bus = React.useState(function () { + return new MessagesEventBus({ + agent: agent, + }); + })[0]; + React.useEffect(function () { + bus.resume(); + return function () { + bus.suspend(); + }; + }, [bus]); + React.useEffect(function () { + var handleAppStateChange = function (nextAppState) { + if (nextAppState === 'active') { + bus.resume(); + } + else { + bus.background(); + } + }; + var sub = AppState.addEventListener('change', handleAppStateChange); + return function () { + sub.remove(); + }; + }, [bus]); + return (_jsx(MessagesEventBusContext.Provider, { value: bus, children: children })); +} diff --git a/src/state/messages/events/types.js b/src/state/messages/events/types.js new file mode 100644 index 0000000000..f1f6f90d74 --- /dev/null +++ b/src/state/messages/events/types.js @@ -0,0 +1,23 @@ +export var MessagesEventBusStatus; +(function (MessagesEventBusStatus) { + MessagesEventBusStatus["Initializing"] = "initializing"; + MessagesEventBusStatus["Ready"] = "ready"; + MessagesEventBusStatus["Error"] = "error"; + MessagesEventBusStatus["Backgrounded"] = "backgrounded"; + MessagesEventBusStatus["Suspended"] = "suspended"; +})(MessagesEventBusStatus || (MessagesEventBusStatus = {})); +export var MessagesEventBusDispatchEvent; +(function (MessagesEventBusDispatchEvent) { + MessagesEventBusDispatchEvent["Ready"] = "ready"; + MessagesEventBusDispatchEvent["Error"] = "error"; + MessagesEventBusDispatchEvent["Background"] = "background"; + MessagesEventBusDispatchEvent["Suspend"] = "suspend"; + MessagesEventBusDispatchEvent["Resume"] = "resume"; + MessagesEventBusDispatchEvent["UpdatePoll"] = "updatePoll"; +})(MessagesEventBusDispatchEvent || (MessagesEventBusDispatchEvent = {})); +export var MessagesEventBusErrorCode; +(function (MessagesEventBusErrorCode) { + MessagesEventBusErrorCode["Unknown"] = "unknown"; + MessagesEventBusErrorCode["InitFailed"] = "initFailed"; + MessagesEventBusErrorCode["PollFailed"] = "pollFailed"; +})(MessagesEventBusErrorCode || (MessagesEventBusErrorCode = {})); diff --git a/src/state/messages/index.js b/src/state/messages/index.js new file mode 100644 index 0000000000..ce2159b293 --- /dev/null +++ b/src/state/messages/index.js @@ -0,0 +1,9 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { CurrentConvoIdProvider } from '#/state/messages/current-convo-id'; +import { MessagesEventBusProvider } from '#/state/messages/events'; +import { ListConvosProvider } from '#/state/queries/messages/list-conversations'; +import { MessageDraftsProvider } from './message-drafts'; +export function MessagesProvider(_a) { + var children = _a.children; + return (_jsx(CurrentConvoIdProvider, { children: _jsx(MessageDraftsProvider, { children: _jsx(MessagesEventBusProvider, { children: _jsx(ListConvosProvider, { children: children }) }) }) })); +} diff --git a/src/state/messages/message-drafts.js b/src/state/messages/message-drafts.js new file mode 100644 index 0000000000..bead5f0d96 --- /dev/null +++ b/src/state/messages/message-drafts.js @@ -0,0 +1,71 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React, { useEffect, useMemo, useReducer, useRef } from 'react'; +import { useCurrentConvoId } from './current-convo-id'; +var MessageDraftsContext = React.createContext(null); +MessageDraftsContext.displayName = 'MessageDraftsContext'; +function useMessageDraftsContext() { + var ctx = React.useContext(MessageDraftsContext); + if (!ctx) { + throw new Error('useMessageDrafts must be used within a MessageDraftsContext'); + } + return ctx; +} +export function useMessageDraft() { + var currentConvoId = useCurrentConvoId().currentConvoId; + var _a = useMessageDraftsContext(), state = _a.state, dispatch = _a.dispatch; + return useMemo(function () { return ({ + getDraft: function () { return (currentConvoId && state[currentConvoId]) || ''; }, + clearDraft: function () { + if (currentConvoId) { + dispatch({ type: 'clear', convoId: currentConvoId }); + } + }, + }); }, [state, dispatch, currentConvoId]); +} +export function useSaveMessageDraft(message) { + var currentConvoId = useCurrentConvoId().currentConvoId; + var dispatch = useMessageDraftsContext().dispatch; + var messageRef = useRef(message); + messageRef.current = message; + useEffect(function () { + return function () { + if (currentConvoId) { + dispatch({ + type: 'set', + convoId: currentConvoId, + draft: messageRef.current, + }); + } + }; + }, [currentConvoId, dispatch]); +} +function reducer(state, action) { + var _a, _b; + switch (action.type) { + case 'set': + return __assign(__assign({}, state), (_a = {}, _a[action.convoId] = action.draft, _a)); + case 'clear': + return __assign(__assign({}, state), (_b = {}, _b[action.convoId] = '', _b)); + default: + return state; + } +} +export function MessageDraftsProvider(_a) { + var children = _a.children; + var _b = useReducer(reducer, {}), state = _b[0], dispatch = _b[1]; + var ctx = useMemo(function () { + return { state: state, dispatch: dispatch }; + }, [state]); + return (_jsx(MessageDraftsContext.Provider, { value: ctx, children: children })); +} diff --git a/src/state/modals/index.js b/src/state/modals/index.js new file mode 100644 index 0000000000..963d5235e8 --- /dev/null +++ b/src/state/modals/index.js @@ -0,0 +1,64 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +var ModalContext = React.createContext({ + isModalActive: false, + activeModals: [], +}); +ModalContext.displayName = 'ModalContext'; +var ModalControlContext = React.createContext({ + openModal: function () { }, + closeModal: function () { return false; }, + closeAllModals: function () { return false; }, +}); +ModalControlContext.displayName = 'ModalControlContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState([]), activeModals = _b[0], setActiveModals = _b[1]; + var openModal = useNonReactiveCallback(function (modal) { + setActiveModals(function (modals) { return __spreadArray(__spreadArray([], modals, true), [modal], false); }); + }); + var closeModal = useNonReactiveCallback(function () { + var wasActive = activeModals.length > 0; + setActiveModals(function (modals) { + return modals.slice(0, -1); + }); + return wasActive; + }); + var closeAllModals = useNonReactiveCallback(function () { + var wasActive = activeModals.length > 0; + setActiveModals([]); + return wasActive; + }); + var state = React.useMemo(function () { return ({ + isModalActive: activeModals.length > 0, + activeModals: activeModals, + }); }, [activeModals]); + var methods = React.useMemo(function () { return ({ + openModal: openModal, + closeModal: closeModal, + closeAllModals: closeAllModals, + }); }, [openModal, closeModal, closeAllModals]); + return (_jsx(ModalContext.Provider, { value: state, children: _jsx(ModalControlContext.Provider, { value: methods, children: children }) })); +} +/** + * @deprecated use the dialog system from `#/components/Dialog.tsx` + */ +export function useModals() { + return React.useContext(ModalContext); +} +/** + * @deprecated use the dialog system from `#/components/Dialog.tsx` + */ +export function useModalControls() { + return React.useContext(ModalControlContext); +} diff --git a/src/state/persisted/index.js b/src/state/persisted/index.js new file mode 100644 index 0000000000..d2023ff460 --- /dev/null +++ b/src/state/persisted/index.js @@ -0,0 +1,176 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { logger } from '#/logger'; +import { defaults, tryParse, tryStringify, } from '#/state/persisted/schema'; +import { device } from '#/storage'; +import { normalizeData } from './util'; +export { defaults } from '#/state/persisted/schema'; +var BSKY_STORAGE = 'BSKY_STORAGE'; +var _state = defaults; +export function init() { + return __awaiter(this, void 0, void 0, function () { + var stored; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, readFromStorage()]; + case 1: + stored = _a.sent(); + if (stored) { + _state = stored; + } + return [2 /*return*/]; + } + }); + }); +} +init; +export function get(key) { + return _state[key]; +} +get; +export function write(key, value) { + return __awaiter(this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _state = normalizeData(__assign(__assign({}, _state), (_a = {}, _a[key] = value, _a))); + return [4 /*yield*/, writeToStorage(_state)]; + case 1: + _b.sent(); + return [2 /*return*/]; + } + }); + }); +} +write; +export function onUpdate(_key, _cb) { + return function () { }; +} +onUpdate; +export function clearStorage() { + return __awaiter(this, void 0, void 0, function () { + var e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, AsyncStorage.removeItem(BSKY_STORAGE)]; + case 1: + _a.sent(); + device.removeAll(); + return [3 /*break*/, 3]; + case 2: + e_1 = _a.sent(); + logger.error("persisted store: failed to clear", { message: e_1.toString() }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); +} +clearStorage; +function writeToStorage(value) { + return __awaiter(this, void 0, void 0, function () { + var rawData, e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + rawData = tryStringify(value); + if (!rawData) return [3 /*break*/, 4]; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, AsyncStorage.setItem(BSKY_STORAGE, rawData)]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + e_2 = _a.sent(); + logger.error("persisted state: failed writing root state to storage", { + message: e_2, + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); +} +function readFromStorage() { + return __awaiter(this, void 0, void 0, function () { + var rawData, e_3, parsed; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + rawData = null; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, AsyncStorage.getItem(BSKY_STORAGE)]; + case 2: + rawData = _a.sent(); + return [3 /*break*/, 4]; + case 3: + e_3 = _a.sent(); + logger.error("persisted state: failed reading root state from storage", { + message: e_3, + }); + return [3 /*break*/, 4]; + case 4: + if (rawData) { + parsed = tryParse(rawData); + if (parsed) { + return [2 /*return*/, normalizeData(parsed)]; + } + } + return [2 /*return*/]; + } + }); + }); +} diff --git a/src/state/persisted/index.web.js b/src/state/persisted/index.web.js new file mode 100644 index 0000000000..87ce24284c --- /dev/null +++ b/src/state/persisted/index.web.js @@ -0,0 +1,213 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import EventEmitter from 'eventemitter3'; +import BroadcastChannel from '#/lib/broadcast'; +import { logger } from '#/logger'; +import { defaults, tryParse, tryStringify, } from '#/state/persisted/schema'; +import { normalizeData } from './util'; +export { defaults } from '#/state/persisted/schema'; +var BSKY_STORAGE = 'BSKY_STORAGE'; +var broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL'); +var UPDATE_EVENT = 'BSKY_UPDATE'; +var _state = defaults; +var _emitter = new EventEmitter(); +// async, to match native implementation +// eslint-disable-next-line @typescript-eslint/require-await +export function init() { + return __awaiter(this, void 0, void 0, function () { + var stored; + return __generator(this, function (_a) { + broadcast.onmessage = onBroadcastMessage; + window.onstorage = onStorage; + stored = readFromStorage(); + if (stored) { + _state = stored; + } + return [2 /*return*/]; + }); + }); +} +init; +export function get(key) { + return _state[key]; +} +get; +// eslint-disable-next-line @typescript-eslint/require-await +export function write(key, value) { + return __awaiter(this, void 0, void 0, function () { + var next; + var _a; + return __generator(this, function (_b) { + next = readFromStorage(); + if (next) { + // The storage could have been updated by a different tab before this tab is notified. + // Make sure this write is applied on top of the latest data in the storage as long as it's valid. + _state = next; + // Don't fire the update listeners yet to avoid a loop. + // If there was a change, we'll receive the broadcast event soon enough which will do that. + } + try { + if (JSON.stringify({ v: _state[key] }) === JSON.stringify({ v: value })) { + // Fast path for updates that are guaranteed to be noops. + // This is good mostly because it avoids useless broadcasts to other tabs. + return [2 /*return*/]; + } + } + catch (e) { + // Ignore and go through the normal path. + } + _state = normalizeData(__assign(__assign({}, _state), (_a = {}, _a[key] = value, _a))); + writeToStorage(_state); + broadcast.postMessage({ event: { type: UPDATE_EVENT, key: key } }); + broadcast.postMessage({ event: UPDATE_EVENT }); // Backcompat while upgrading + return [2 /*return*/]; + }); + }); +} +write; +export function onUpdate(key, cb) { + var listener = function () { return cb(get(key)); }; + _emitter.addListener('update', listener); // Backcompat while upgrading + _emitter.addListener('update:' + key, listener); + return function () { + _emitter.removeListener('update', listener); // Backcompat while upgrading + _emitter.removeListener('update:' + key, listener); + }; +} +onUpdate; +// eslint-disable-next-line @typescript-eslint/require-await +export function clearStorage() { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + try { + localStorage.removeItem(BSKY_STORAGE); + } + catch (e) { + // Expected on the web in private mode. + } + return [2 /*return*/]; + }); + }); +} +clearStorage; +function onStorage() { + var next = readFromStorage(); + if (next === _state) { + return; + } + if (next) { + _state = next; + _emitter.emit('update'); + } +} +// eslint-disable-next-line @typescript-eslint/require-await +function onBroadcastMessage(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var next; + var _c; + var data = _b.data; + return __generator(this, function (_d) { + if (typeof data === 'object' && + (data.event === UPDATE_EVENT || // Backcompat while upgrading + ((_c = data.event) === null || _c === void 0 ? void 0 : _c.type) === UPDATE_EVENT)) { + next = readFromStorage(); + if (next === _state) { + return [2 /*return*/]; + } + if (next) { + _state = next; + if (typeof data.event.key === 'string') { + _emitter.emit('update:' + data.event.key); + } + else { + _emitter.emit('update'); // Backcompat while upgrading + } + } + else { + logger.error("persisted state: handled update update from broadcast channel, but found no data"); + } + } + return [2 /*return*/]; + }); + }); +} +function writeToStorage(value) { + var rawData = tryStringify(value); + if (rawData) { + try { + localStorage.setItem(BSKY_STORAGE, rawData); + } + catch (e) { + // Expected on the web in private mode. + } + } +} +var lastRawData; +var lastResult; +function readFromStorage() { + var rawData = null; + try { + rawData = localStorage.getItem(BSKY_STORAGE); + } + catch (e) { + // Expected on the web in private mode. + } + if (rawData) { + if (rawData === lastRawData) { + return lastResult; + } + else { + var result = tryParse(rawData); + if (result) { + lastRawData = rawData; + lastResult = normalizeData(result); + return lastResult; + } + } + } +} diff --git a/src/state/persisted/schema.js b/src/state/persisted/schema.js new file mode 100644 index 0000000000..7a92ac282f --- /dev/null +++ b/src/state/persisted/schema.js @@ -0,0 +1,215 @@ +var _a; +import { z } from 'zod'; +import { deviceLanguageCodes, deviceLocales } from '#/locale/deviceLocales'; +import { findSupportedAppLanguage } from '#/locale/helpers'; +import { logger } from '#/logger'; +import { PlatformInfo } from '../../../modules/expo-bluesky-swiss-army'; +var externalEmbedOptions = ['show', 'hide']; +/** + * A account persisted to storage. Stored in the `accounts[]` array. Contains + * base account info and access tokens. + */ +var accountSchema = z.object({ + service: z.string(), + did: z.string(), + handle: z.string(), + email: z.string().optional(), + emailConfirmed: z.boolean().optional(), + emailAuthFactor: z.boolean().optional(), + refreshJwt: z.string().optional(), // optional because it can expire + accessJwt: z.string().optional(), // optional because it can expire + signupQueued: z.boolean().optional(), + active: z.boolean().optional(), // optional for backwards compat + /** + * Known values: takendown, suspended, deactivated + * @see https://github.com/bluesky-social/atproto/blob/5441fbde9ed3b22463e91481ec80cb095643e141/lexicons/com/atproto/server/getSession.json + */ + status: z.string().optional(), + pdsUrl: z.string().optional(), + isSelfHosted: z.boolean().optional(), +}); +/** + * The current account. Stored in the `currentAccount` field. + * + * In previous versions, this included tokens and other info. Now, it's used + * only to reference the `did` field, and all other fields are marked as + * optional. They should be considered deprecated and not used, but are kept + * here for backwards compat. + */ +var currentAccountSchema = accountSchema.extend({ + service: z.string().optional(), + handle: z.string().optional(), +}); +var schema = z.object({ + colorMode: z.enum(['system', 'light', 'dark']), + darkTheme: z.enum(['dim', 'dark']).optional(), + session: z.object({ + accounts: z.array(accountSchema), + currentAccount: currentAccountSchema.optional(), + }), + reminders: z.object({ + lastEmailConfirm: z.string().optional(), + }), + languagePrefs: z.object({ + /** + * The target language for translating posts. + * + * BCP-47 2-letter language code without region. + */ + primaryLanguage: z.string(), + /** + * The languages the user can read, passed to feeds. + * + * BCP-47 2-letter language codes without region. + */ + contentLanguages: z.array(z.string()), + /** + * The language(s) the user is currently posting in, configured within the + * composer. Multiple languages are separated by commas. + * + * BCP-47 2-letter language code without region. + */ + postLanguage: z.string(), + /** + * The user's post language history, used to pre-populate the post language + * selector in the composer. Within each value, multiple languages are separated + * by commas. + * + * BCP-47 2-letter language codes without region. + */ + postLanguageHistory: z.array(z.string()), + /** + * The language for UI translations in the app. + * + * BCP-47 2-letter language code with or without region, + * to match with {@link AppLanguage}. + */ + appLanguage: z.string(), + }), + requireAltTextEnabled: z.boolean(), // should move to server + largeAltBadgeEnabled: z.boolean().optional(), + externalEmbeds: z + .object({ + giphy: z.enum(externalEmbedOptions).optional(), + tenor: z.enum(externalEmbedOptions).optional(), + youtube: z.enum(externalEmbedOptions).optional(), + youtubeShorts: z.enum(externalEmbedOptions).optional(), + twitch: z.enum(externalEmbedOptions).optional(), + vimeo: z.enum(externalEmbedOptions).optional(), + spotify: z.enum(externalEmbedOptions).optional(), + appleMusic: z.enum(externalEmbedOptions).optional(), + soundcloud: z.enum(externalEmbedOptions).optional(), + flickr: z.enum(externalEmbedOptions).optional(), + bandcamp: z.enum(externalEmbedOptions).optional(), + }) + .optional(), + invites: z.object({ + copiedInvites: z.array(z.string()), + }), + onboarding: z.object({ + step: z.string(), + }), + hiddenPosts: z.array(z.string()).optional(), // should move to server + useInAppBrowser: z.boolean().optional(), + /** @deprecated */ + lastSelectedHomeFeed: z.string().optional(), + pdsAddressHistory: z.array(z.string()).optional(), + disableHaptics: z.boolean().optional(), + disableAutoplay: z.boolean().optional(), + kawaii: z.boolean().optional(), + hasCheckedForStarterPack: z.boolean().optional(), + subtitlesEnabled: z.boolean().optional(), + /** @deprecated */ + mutedThreads: z.array(z.string()), + trendingDisabled: z.boolean().optional(), + trendingVideoDisabled: z.boolean().optional(), +}); +export var defaults = { + colorMode: 'system', + darkTheme: 'dim', + session: { + accounts: [], + currentAccount: undefined, + }, + reminders: { + lastEmailConfirm: undefined, + }, + languagePrefs: { + primaryLanguage: deviceLanguageCodes[0] || 'en', + contentLanguages: deviceLanguageCodes || [], + postLanguage: deviceLanguageCodes[0] || 'en', + postLanguageHistory: (deviceLanguageCodes || []) + .concat(['en', 'ja', 'pt', 'de']) + .slice(0, 6), + // try full language tag first, then fallback to language code + appLanguage: findSupportedAppLanguage([ + (_a = deviceLocales.at(0)) === null || _a === void 0 ? void 0 : _a.languageTag, + deviceLanguageCodes[0], + ]), + }, + requireAltTextEnabled: false, + largeAltBadgeEnabled: false, + externalEmbeds: {}, + mutedThreads: [], + invites: { + copiedInvites: [], + }, + onboarding: { + step: 'Home', + }, + hiddenPosts: [], + useInAppBrowser: undefined, + lastSelectedHomeFeed: undefined, + pdsAddressHistory: [], + disableHaptics: false, + disableAutoplay: PlatformInfo.getIsReducedMotionEnabled(), + kawaii: false, + hasCheckedForStarterPack: false, + subtitlesEnabled: true, + trendingDisabled: false, + trendingVideoDisabled: false, +}; +export function tryParse(rawData) { + var _a, _b; + var objData; + try { + objData = JSON.parse(rawData); + } + catch (e) { + logger.error('persisted state: failed to parse root state from storage', { + message: e, + }); + } + if (!objData) { + return undefined; + } + var parsed = schema.safeParse(objData); + if (parsed.success) { + return objData; + } + else { + var errors = ((_b = (_a = parsed.error) === null || _a === void 0 ? void 0 : _a.errors) === null || _b === void 0 ? void 0 : _b.map(function (e) { + var _a; + return ({ + code: e.code, + // @ts-ignore exists on some types + expected: e === null || e === void 0 ? void 0 : e.expected, + path: (_a = e.path) === null || _a === void 0 ? void 0 : _a.join('.'), + }); + })) || []; + logger.error("persisted store: data failed validation on read", { errors: errors }); + return undefined; + } +} +export function tryStringify(value) { + try { + schema.parse(value); + return JSON.stringify(value); + } + catch (e) { + logger.error("persisted state: failed stringifying root state", { + message: e, + }); + return undefined; + } +} diff --git a/src/state/persisted/types.js b/src/state/persisted/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/state/persisted/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/state/persisted/util.js b/src/state/persisted/util.js new file mode 100644 index 0000000000..ee0a715326 --- /dev/null +++ b/src/state/persisted/util.js @@ -0,0 +1,51 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { parse } from 'bcp-47'; +import { dedupArray } from '#/lib/functions'; +import { logger } from '#/logger'; +export function normalizeData(data) { + var next = __assign({}, data); + /** + * Normalize language prefs to ensure that these values only contain 2-letter + * country codes without region. + */ + try { + var langPrefs = __assign({}, next.languagePrefs); + langPrefs.primaryLanguage = normalizeLanguageTagToTwoLetterCode(langPrefs.primaryLanguage); + langPrefs.contentLanguages = dedupArray(langPrefs.contentLanguages.map(function (lang) { + return normalizeLanguageTagToTwoLetterCode(lang); + })); + langPrefs.postLanguage = langPrefs.postLanguage + .split(',') + .map(function (lang) { return normalizeLanguageTagToTwoLetterCode(lang); }) + .filter(Boolean) + .join(','); + langPrefs.postLanguageHistory = dedupArray(langPrefs.postLanguageHistory.map(function (postLanguage) { + return postLanguage + .split(',') + .map(function (lang) { return normalizeLanguageTagToTwoLetterCode(lang); }) + .filter(Boolean) + .join(','); + })); + next.languagePrefs = langPrefs; + } + catch (e) { + logger.error("persisted state: failed to normalize language prefs", { + safeMessage: e.message, + }); + } + return next; +} +export function normalizeLanguageTagToTwoLetterCode(lang) { + var result = parse(lang).language; + return result !== null && result !== void 0 ? result : lang; +} diff --git a/src/state/preferences/alt-text-required.js b/src/state/preferences/alt-text-required.js new file mode 100644 index 0000000000..393065acd2 --- /dev/null +++ b/src/state/preferences/alt-text-required.js @@ -0,0 +1,27 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(persisted.defaults.requireAltTextEnabled); +stateContext.displayName = 'AltTextRequiredStateContext'; +var setContext = React.createContext(function (_) { }); +setContext.displayName = 'AltTextRequiredSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(persisted.get('requireAltTextEnabled')), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (requireAltTextEnabled) { + setState(requireAltTextEnabled); + persisted.write('requireAltTextEnabled', requireAltTextEnabled); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('requireAltTextEnabled', function (nextRequireAltTextEnabled) { + setState(nextRequireAltTextEnabled); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export function useRequireAltTextEnabled() { + return React.useContext(stateContext); +} +export function useSetRequireAltTextEnabled() { + return React.useContext(setContext); +} diff --git a/src/state/preferences/autoplay.js b/src/state/preferences/autoplay.js new file mode 100644 index 0000000000..57b98af190 --- /dev/null +++ b/src/state/preferences/autoplay.js @@ -0,0 +1,23 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(Boolean(persisted.defaults.disableAutoplay)); +stateContext.displayName = 'AutoplayStateContext'; +var setContext = React.createContext(function (_) { }); +setContext.displayName = 'AutoplaySetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(Boolean(persisted.get('disableAutoplay'))), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (autoplayDisabled) { + setState(Boolean(autoplayDisabled)); + persisted.write('disableAutoplay', autoplayDisabled); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('disableAutoplay', function (nextDisableAutoplay) { + setState(Boolean(nextDisableAutoplay)); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export var useAutoplayDisabled = function () { return React.useContext(stateContext); }; +export var useSetAutoplayDisabled = function () { return React.useContext(setContext); }; diff --git a/src/state/preferences/disable-haptics.js b/src/state/preferences/disable-haptics.js new file mode 100644 index 0000000000..95e264c490 --- /dev/null +++ b/src/state/preferences/disable-haptics.js @@ -0,0 +1,23 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(Boolean(persisted.defaults.disableHaptics)); +stateContext.displayName = 'DisableHapticsStateContext'; +var setContext = React.createContext(function (_) { }); +setContext.displayName = 'DisableHapticsSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(Boolean(persisted.get('disableHaptics'))), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (hapticsEnabled) { + setState(Boolean(hapticsEnabled)); + persisted.write('disableHaptics', hapticsEnabled); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('disableHaptics', function (nextDisableHaptics) { + setState(Boolean(nextDisableHaptics)); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export var useHapticsDisabled = function () { return React.useContext(stateContext); }; +export var useSetHapticsDisabled = function () { return React.useContext(setContext); }; diff --git a/src/state/preferences/external-embeds-prefs.js b/src/state/preferences/external-embeds-prefs.js new file mode 100644 index 0000000000..33ae62b936 --- /dev/null +++ b/src/state/preferences/external-embeds-prefs.js @@ -0,0 +1,41 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(persisted.defaults.externalEmbeds); +stateContext.displayName = 'ExternalEmbedsPrefsStateContext'; +var setContext = React.createContext({}); +setContext.displayName = 'ExternalEmbedsPrefsSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(persisted.get('externalEmbeds')), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (source, value) { + setState(function (prev) { + var _a, _b; + persisted.write('externalEmbeds', __assign(__assign({}, prev), (_a = {}, _a[source] = value, _a))); + return __assign(__assign({}, prev), (_b = {}, _b[source] = value, _b)); + }); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('externalEmbeds', function (nextExternalEmbeds) { + setState(nextExternalEmbeds); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export function useExternalEmbedsPrefs() { + return React.useContext(stateContext); +} +export function useSetExternalEmbedPref() { + return React.useContext(setContext); +} diff --git a/src/state/preferences/feed-tuners.js b/src/state/preferences/feed-tuners.js new file mode 100644 index 0000000000..ade1be7322 --- /dev/null +++ b/src/state/preferences/feed-tuners.js @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { FeedTuner } from '#/lib/api/feed-manip'; +import { usePreferencesQuery } from '../queries/preferences'; +import { useSession } from '../session'; +import { useLanguagePrefs } from './languages'; +export function useFeedTuners(feedDesc) { + var langPrefs = useLanguagePrefs(); + var preferences = usePreferencesQuery().data; + var currentAccount = useSession().currentAccount; + return useMemo(function () { + if (feedDesc.startsWith('author')) { + if (feedDesc.endsWith('|posts_with_replies')) { + // TODO: Do this on the server instead. + return [FeedTuner.removeReposts]; + } + } + if (feedDesc.startsWith('feedgen')) { + return [ + FeedTuner.preferredLangOnly(langPrefs.contentLanguages), + FeedTuner.removeMutedThreads, + ]; + } + if (feedDesc === 'following' || feedDesc.startsWith('list')) { + var feedTuners = [FeedTuner.removeOrphans]; + if (preferences === null || preferences === void 0 ? void 0 : preferences.feedViewPrefs.hideReposts) { + feedTuners.push(FeedTuner.removeReposts); + } + if (preferences === null || preferences === void 0 ? void 0 : preferences.feedViewPrefs.hideReplies) { + feedTuners.push(FeedTuner.removeReplies); + } + else { + feedTuners.push(FeedTuner.followedRepliesOnly({ + userDid: (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) || '', + })); + } + if (preferences === null || preferences === void 0 ? void 0 : preferences.feedViewPrefs.hideQuotePosts) { + feedTuners.push(FeedTuner.removeQuotePosts); + } + feedTuners.push(FeedTuner.dedupThreads); + feedTuners.push(FeedTuner.removeMutedThreads); + return feedTuners; + } + return []; + }, [feedDesc, currentAccount, preferences, langPrefs]); +} diff --git a/src/state/preferences/hidden-posts.js b/src/state/preferences/hidden-posts.js new file mode 100644 index 0000000000..ffbe47efb1 --- /dev/null +++ b/src/state/preferences/hidden-posts.js @@ -0,0 +1,50 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(persisted.defaults.hiddenPosts); +stateContext.displayName = 'HiddenPostsStateContext'; +var apiContext = React.createContext({ + hidePost: function () { }, + unhidePost: function () { }, +}); +apiContext.displayName = 'HiddenPostsApiContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(persisted.get('hiddenPosts')), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (fn) { + var s = fn(persisted.get('hiddenPosts')); + setState(s); + persisted.write('hiddenPosts', s); + }, [setState]); + var api = React.useMemo(function () { return ({ + hidePost: function (_a) { + var uri = _a.uri; + setStateWrapped(function (s) { return __spreadArray(__spreadArray([], (s || []), true), [uri], false); }); + }, + unhidePost: function (_a) { + var uri = _a.uri; + setStateWrapped(function (s) { return (s || []).filter(function (u) { return u !== uri; }); }); + }, + }); }, [setStateWrapped]); + React.useEffect(function () { + return persisted.onUpdate('hiddenPosts', function (nextHiddenPosts) { + setState(nextHiddenPosts); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(apiContext.Provider, { value: api, children: children }) })); +} +export function useHiddenPosts() { + return React.useContext(stateContext); +} +export function useHiddenPostsApi() { + return React.useContext(apiContext); +} diff --git a/src/state/preferences/in-app-browser.js b/src/state/preferences/in-app-browser.js new file mode 100644 index 0000000000..384b96e6a8 --- /dev/null +++ b/src/state/preferences/in-app-browser.js @@ -0,0 +1,27 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(persisted.defaults.useInAppBrowser); +stateContext.displayName = 'InAppBrowserStateContext'; +var setContext = React.createContext(function (_) { }); +setContext.displayName = 'InAppBrowserSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(persisted.get('useInAppBrowser')), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (inAppBrowser) { + setState(inAppBrowser); + persisted.write('useInAppBrowser', inAppBrowser); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('useInAppBrowser', function (nextUseInAppBrowser) { + setState(nextUseInAppBrowser); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export function useInAppBrowser() { + return React.useContext(stateContext); +} +export function useSetInAppBrowser() { + return React.useContext(setContext); +} diff --git a/src/state/preferences/index.js b/src/state/preferences/index.js new file mode 100644 index 0000000000..369760bd20 --- /dev/null +++ b/src/state/preferences/index.js @@ -0,0 +1,25 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { Provider as AltTextRequiredProvider } from './alt-text-required'; +import { Provider as AutoplayProvider } from './autoplay'; +import { Provider as DisableHapticsProvider } from './disable-haptics'; +import { Provider as ExternalEmbedsProvider } from './external-embeds-prefs'; +import { Provider as HiddenPostsProvider } from './hidden-posts'; +import { Provider as InAppBrowserProvider } from './in-app-browser'; +import { Provider as KawaiiProvider } from './kawaii'; +import { Provider as LanguagesProvider } from './languages'; +import { Provider as LargeAltBadgeProvider } from './large-alt-badge'; +import { Provider as SubtitlesProvider } from './subtitles'; +import { Provider as TrendingSettingsProvider } from './trending'; +import { Provider as UsedStarterPacksProvider } from './used-starter-packs'; +export { useRequireAltTextEnabled, useSetRequireAltTextEnabled, } from './alt-text-required'; +export { useAutoplayDisabled, useSetAutoplayDisabled } from './autoplay'; +export { useHapticsDisabled, useSetHapticsDisabled } from './disable-haptics'; +export { useExternalEmbedsPrefs, useSetExternalEmbedPref, } from './external-embeds-prefs'; +export { useHiddenPosts, useHiddenPostsApi } from './hidden-posts'; +export { useLabelDefinitions } from './label-defs'; +export { useLanguagePrefs, useLanguagePrefsApi } from './languages'; +export { useSetSubtitlesEnabled, useSubtitlesEnabled } from './subtitles'; +export function Provider(_a) { + var children = _a.children; + return (_jsx(LanguagesProvider, { children: _jsx(AltTextRequiredProvider, { children: _jsx(LargeAltBadgeProvider, { children: _jsx(ExternalEmbedsProvider, { children: _jsx(HiddenPostsProvider, { children: _jsx(InAppBrowserProvider, { children: _jsx(DisableHapticsProvider, { children: _jsx(AutoplayProvider, { children: _jsx(UsedStarterPacksProvider, { children: _jsx(SubtitlesProvider, { children: _jsx(TrendingSettingsProvider, { children: _jsx(KawaiiProvider, { children: children }) }) }) }) }) }) }) }) }) }) }) })); +} diff --git a/src/state/preferences/kawaii.js b/src/state/preferences/kawaii.js new file mode 100644 index 0000000000..cadd159369 --- /dev/null +++ b/src/state/preferences/kawaii.js @@ -0,0 +1,37 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +import { IS_WEB } from '#/env'; +var stateContext = React.createContext(persisted.defaults.kawaii); +stateContext.displayName = 'KawaiiStateContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(persisted.get('kawaii')), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (kawaii) { + setState(kawaii); + persisted.write('kawaii', kawaii); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('kawaii', function (nextKawaii) { + setState(nextKawaii); + }); + }, [setStateWrapped]); + React.useEffect(function () { + // dumb and stupid but it's web only so just refresh the page if you want to change it + if (IS_WEB) { + var kawaii = new URLSearchParams(window.location.search).get('kawaii'); + switch (kawaii) { + case 'true': + setStateWrapped(true); + break; + case 'false': + setStateWrapped(false); + break; + } + } + }, [setStateWrapped]); + return _jsx(stateContext.Provider, { value: state, children: children }); +} +export function useKawaiiMode() { + return React.useContext(stateContext); +} diff --git a/src/state/preferences/label-defs.js b/src/state/preferences/label-defs.js new file mode 100644 index 0000000000..e989f4246b --- /dev/null +++ b/src/state/preferences/label-defs.js @@ -0,0 +1,16 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { useLabelDefinitionsQuery } from '../queries/preferences'; +var stateContext = React.createContext({ + labelDefs: {}, + labelers: [], +}); +stateContext.displayName = 'LabelDefsStateContext'; +export function Provider(_a) { + var children = _a.children; + var state = useLabelDefinitionsQuery(); + return _jsx(stateContext.Provider, { value: state, children: children }); +} +export function useLabelDefinitions() { + return React.useContext(stateContext); +} diff --git a/src/state/preferences/languages.js b/src/state/preferences/languages.js new file mode 100644 index 0000000000..436a74b112 --- /dev/null +++ b/src/state/preferences/languages.js @@ -0,0 +1,100 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +import { AnalyticsContext, utils } from '#/analytics'; +var stateContext = React.createContext(persisted.defaults.languagePrefs); +stateContext.displayName = 'LanguagePrefsStateContext'; +var apiContext = React.createContext({ + setPrimaryLanguage: function (_) { }, + setPostLanguage: function (_) { }, + setContentLanguages: function (_) { }, + savePostLanguageToHistory: function () { }, + setAppLanguage: function (_) { }, +}); +apiContext.displayName = 'LanguagePrefsApiContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(function () { return persisted.get('languagePrefs'); }), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (fn) { + var s = fn(persisted.get('languagePrefs')); + setState(s); + persisted.write('languagePrefs', s); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('languagePrefs', function (nextLanguagePrefs) { + setState(nextLanguagePrefs); + }); + }, [setStateWrapped]); + var api = React.useMemo(function () { return ({ + setPrimaryLanguage: function (code2) { + setStateWrapped(function (s) { return (__assign(__assign({}, s), { primaryLanguage: code2 })); }); + }, + setPostLanguage: function (commaSeparatedLangCodes) { + setStateWrapped(function (s) { return (__assign(__assign({}, s), { postLanguage: commaSeparatedLangCodes })); }); + }, + setContentLanguages: function (code2s) { + setStateWrapped(function (s) { return (__assign(__assign({}, s), { contentLanguages: code2s })); }); + }, + /** + * Saves whatever language codes are currently selected into a history array, + * which is then used to populate the language selector menu. + */ + savePostLanguageToHistory: function () { + // filter out duplicate `this.postLanguage` if exists, and prepend + // value to start of array + setStateWrapped(function (s) { return (__assign(__assign({}, s), { postLanguageHistory: [s.postLanguage] + .concat(s.postLanguageHistory.filter(function (commaSeparatedLangCodes) { + return commaSeparatedLangCodes !== s.postLanguage; + })) + .slice(0, 6) })); }); + }, + setAppLanguage: function (code2) { + setStateWrapped(function (s) { return (__assign(__assign({}, s), { appLanguage: code2 })); }); + }, + }); }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(apiContext.Provider, { value: api, children: _jsx(AnalyticsContext, { metadata: utils.useMeta({ + preferences: { + appLanguage: state.appLanguage, + contentLanguages: state.contentLanguages, + }, + }), children: children }) }) })); +} +export function useLanguagePrefs() { + return React.useContext(stateContext); +} +export function useLanguagePrefsApi() { + return React.useContext(apiContext); +} +export function getContentLanguages() { + return persisted.get('languagePrefs').contentLanguages; +} +/** + * Be careful with this. It's used for the PWI home screen so that users can + * select a UI language and have it apply to the fetched Discover feed. + * + * We only support BCP-47 two-letter codes here, hence the split. + */ +export function getAppLanguageAsContentLanguage() { + return persisted.get('languagePrefs').appLanguage.split('-')[0]; +} +export function toPostLanguages(postLanguage) { + // filter out empty strings if exist + return postLanguage.split(',').filter(Boolean); +} +export function fromPostLanguages(languages) { + return languages.filter(Boolean).join(','); +} +export function hasPostLanguage(postLanguage, code2) { + return toPostLanguages(postLanguage).includes(code2); +} diff --git a/src/state/preferences/large-alt-badge.js b/src/state/preferences/large-alt-badge.js new file mode 100644 index 0000000000..54d4709c3d --- /dev/null +++ b/src/state/preferences/large-alt-badge.js @@ -0,0 +1,27 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(persisted.defaults.largeAltBadgeEnabled); +stateContext.displayName = 'LargeAltBadgeStateContext'; +var setContext = React.createContext(function (_) { }); +setContext.displayName = 'LargeAltBadgeSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(persisted.get('largeAltBadgeEnabled')), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (largeAltBadgeEnabled) { + setState(largeAltBadgeEnabled); + persisted.write('largeAltBadgeEnabled', largeAltBadgeEnabled); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('largeAltBadgeEnabled', function (nextLargeAltBadgeEnabled) { + setState(nextLargeAltBadgeEnabled); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export function useLargeAltBadgeEnabled() { + return React.useContext(stateContext); +} +export function useSetLargeAltBadgeEnabled() { + return React.useContext(setContext); +} diff --git a/src/state/preferences/moderation-opts.js b/src/state/preferences/moderation-opts.js new file mode 100644 index 0000000000..b072ee553d --- /dev/null +++ b/src/state/preferences/moderation-opts.js @@ -0,0 +1,56 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { BskyAgent } from '@atproto/api'; +import { useHiddenPosts, useLabelDefinitions } from '#/state/preferences'; +import { DEFAULT_LOGGED_OUT_LABEL_PREFERENCES } from '#/state/queries/preferences/moderation'; +import { useSession } from '#/state/session'; +import { usePreferencesQuery } from '../queries/preferences'; +export var moderationOptsContext = createContext(undefined); +moderationOptsContext.displayName = 'ModerationOptsContext'; +// used in the moderation state devtool +export var moderationOptsOverrideContext = createContext(undefined); +moderationOptsOverrideContext.displayName = 'ModerationOptsOverrideContext'; +export function useModerationOpts() { + return useContext(moderationOptsContext); +} +export function Provider(_a) { + var _b; + var children = _a.children; + var override = useContext(moderationOptsOverrideContext); + var currentAccount = useSession().currentAccount; + var prefs = usePreferencesQuery(); + var labelDefs = useLabelDefinitions().labelDefs; + var hiddenPosts = useHiddenPosts(); // TODO move this into pds-stored prefs + var userDid = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did; + var moderationPrefs = (_b = prefs.data) === null || _b === void 0 ? void 0 : _b.moderationPrefs; + var value = useMemo(function () { + if (override) { + return override; + } + if (!moderationPrefs) { + return undefined; + } + return { + userDid: userDid, + prefs: __assign(__assign({}, moderationPrefs), { labelers: moderationPrefs.labelers.length + ? moderationPrefs.labelers + : BskyAgent.appLabelers.map(function (did) { return ({ + did: did, + labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, + }); }), hiddenPosts: hiddenPosts || [] }), + labelDefs: labelDefs, + }; + }, [override, userDid, labelDefs, moderationPrefs, hiddenPosts]); + return (_jsx(moderationOptsContext.Provider, { value: value, children: children })); +} diff --git a/src/state/preferences/subtitles.js b/src/state/preferences/subtitles.js new file mode 100644 index 0000000000..e3ff77e010 --- /dev/null +++ b/src/state/preferences/subtitles.js @@ -0,0 +1,23 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(Boolean(persisted.defaults.subtitlesEnabled)); +stateContext.displayName = 'SubtitlesStateContext'; +var setContext = React.createContext(function (_) { }); +setContext.displayName = 'SubtitlesSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(Boolean(persisted.get('subtitlesEnabled'))), state = _b[0], setState = _b[1]; + var setStateWrapped = React.useCallback(function (subtitlesEnabled) { + setState(Boolean(subtitlesEnabled)); + persisted.write('subtitlesEnabled', subtitlesEnabled); + }, [setState]); + React.useEffect(function () { + return persisted.onUpdate('subtitlesEnabled', function (nextSubtitlesEnabled) { + setState(Boolean(nextSubtitlesEnabled)); + }); + }, [setStateWrapped]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export var useSubtitlesEnabled = function () { return React.useContext(stateContext); }; +export var useSetSubtitlesEnabled = function () { return React.useContext(setContext); }; diff --git a/src/state/preferences/trending.js b/src/state/preferences/trending.js new file mode 100644 index 0000000000..519eca7b30 --- /dev/null +++ b/src/state/preferences/trending.js @@ -0,0 +1,45 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var StateContext = React.createContext({ + trendingDisabled: Boolean(persisted.defaults.trendingDisabled), + trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled), +}); +StateContext.displayName = 'TrendingStateContext'; +var ApiContext = React.createContext({ + setTrendingDisabled: function () { }, + setTrendingVideoDisabled: function () { }, +}); +ApiContext.displayName = 'TrendingApiContext'; +function usePersistedBooleanValue(key) { + var _a = React.useState(function () { + return Boolean(persisted.get(key)); + }), value = _a[0], _set = _a[1]; + var set = React.useCallback(function (hidden) { + _set(Boolean(hidden)); + persisted.write(key, hidden); + }, [key, _set]); + React.useEffect(function () { + return persisted.onUpdate(key, function (hidden) { + _set(Boolean(hidden)); + }); + }, [key, _set]); + return [value, set]; +} +export function Provider(_a) { + var children = _a.children; + var _b = usePersistedBooleanValue('trendingDisabled'), trendingDisabled = _b[0], setTrendingDisabled = _b[1]; + var _c = usePersistedBooleanValue('trendingVideoDisabled'), trendingVideoDisabled = _c[0], setTrendingVideoDisabled = _c[1]; + /* + * Context + */ + var state = React.useMemo(function () { return ({ trendingDisabled: trendingDisabled, trendingVideoDisabled: trendingVideoDisabled }); }, [trendingDisabled, trendingVideoDisabled]); + var api = React.useMemo(function () { return ({ setTrendingDisabled: setTrendingDisabled, setTrendingVideoDisabled: setTrendingVideoDisabled }); }, [setTrendingDisabled, setTrendingVideoDisabled]); + return (_jsx(StateContext.Provider, { value: state, children: _jsx(ApiContext.Provider, { value: api, children: children }) })); +} +export function useTrendingSettings() { + return React.useContext(StateContext); +} +export function useTrendingSettingsApi() { + return React.useContext(ApiContext); +} diff --git a/src/state/preferences/used-starter-packs.js b/src/state/preferences/used-starter-packs.js new file mode 100644 index 0000000000..1342ab4df0 --- /dev/null +++ b/src/state/preferences/used-starter-packs.js @@ -0,0 +1,25 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext(false); +stateContext.displayName = 'UsedStarterPacksStateContext'; +var setContext = React.createContext(function (_) { }); +setContext.displayName = 'UsedStarterPacksSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(function () { + return persisted.get('hasCheckedForStarterPack'); + }), state = _b[0], setState = _b[1]; + var setStateWrapped = function (v) { + setState(v); + persisted.write('hasCheckedForStarterPack', v); + }; + React.useEffect(function () { + return persisted.onUpdate('hasCheckedForStarterPack', function (nextHasCheckedForStarterPack) { + setState(nextHasCheckedForStarterPack); + }); + }, []); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(setContext.Provider, { value: setStateWrapped, children: children }) })); +} +export var useHasCheckedForStarterPack = function () { return React.useContext(stateContext); }; +export var useSetHasCheckedForStarterPack = function () { return React.useContext(setContext); }; diff --git a/src/state/queries/activity-subscriptions.js b/src/state/queries/activity-subscriptions.js new file mode 100644 index 0000000000..fd0312ad60 --- /dev/null +++ b/src/state/queries/activity-subscriptions.js @@ -0,0 +1,191 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { t } from '@lingui/core/macro'; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; +import { useAgent, useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +export var RQKEY_getActivitySubscriptions = ['activity-subscriptions']; +export var RQKEY_getNotificationDeclaration = ['notification-declaration']; +export function useActivitySubscriptionsQuery() { + var _this = this; + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY_getActivitySubscriptions, + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var response; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.notification.listActivitySubscriptions({ + cursor: pageParam, + })]; + case 1: + response = _c.sent(); + return [2 /*return*/, response.data]; + } + }); + }); }, + initialPageParam: undefined, + getNextPageParam: function (prev) { return prev.cursor; }, + }); +} +export function useNotificationDeclarationQuery() { + var _this = this; + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + return useQuery({ + queryKey: RQKEY_getNotificationDeclaration, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var response, err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + return [4 /*yield*/, agent.app.bsky.notification.declaration.get({ + repo: currentAccount.did, + rkey: 'self', + })]; + case 1: + response = _a.sent(); + return [2 /*return*/, response]; + case 2: + err_1 = _a.sent(); + if (err_1 instanceof Error && + err_1.message.startsWith('Could not locate record')) { + return [2 /*return*/, { + value: { + $type: 'app.bsky.notification.declaration', + allowSubscriptions: 'followers', + }, + }]; + } + else { + throw err_1; + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useNotificationDeclarationMutation() { + var _this = this; + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (record) { return __awaiter(_this, void 0, void 0, function () { + var response; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.notification.declaration.put({ + repo: currentAccount.did, + rkey: 'self', + }, record)]; + case 1: + response = _a.sent(); + return [2 /*return*/, response]; + } + }); + }); }, + onMutate: function (value) { + queryClient.setQueryData(RQKEY_getNotificationDeclaration, function (old) { + if (!old) + return old; + return { + value: value, + }; + }); + }, + onError: function () { + Toast.show(t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to update notification declaration"], ["Failed to update notification declaration"])))); + queryClient.invalidateQueries({ + queryKey: RQKEY_getNotificationDeclaration, + }); + }, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, subscription; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: RQKEY_getActivitySubscriptions, + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.subscriptions; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + subscription = _e[_d]; + if (!(subscription.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, subscription]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} +var templateObject_1; diff --git a/src/state/queries/actor-autocomplete.js b/src/state/queries/actor-autocomplete.js new file mode 100644 index 0000000000..bd5a7fb65d --- /dev/null +++ b/src/state/queries/actor-autocomplete.js @@ -0,0 +1,160 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { moderateProfile, } from '@atproto/api'; +import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'; +import { isJustAMute, moduiContainsHideableOffense } from '#/lib/moderation'; +import { logger } from '#/logger'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +import { useModerationOpts } from '../preferences/moderation-opts'; +import { DEFAULT_LOGGED_OUT_PREFERENCES } from './preferences'; +var DEFAULT_MOD_OPTS = { + userDid: undefined, + prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, +}; +var RQKEY_ROOT = 'actor-autocomplete'; +export var RQKEY = function (prefix) { return [RQKEY_ROOT, prefix]; }; +export function useActorAutocompleteQuery(prefix, maintainData, limit) { + var moderationOpts = useModerationOpts(); + var agent = useAgent(); + prefix = prefix.toLowerCase().trim(); + if (prefix.endsWith('.')) { + // Going from "foo" to "foo." should not clear matches. + prefix = prefix.slice(0, -1); + } + return useQuery({ + staleTime: STALE.MINUTES.ONE, + queryKey: RQKEY(prefix || ''), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var res, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!prefix) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.searchActorsTypeahead({ + q: prefix, + limit: limit || 8, + })]; + case 1: + _a = _b.sent(); + return [3 /*break*/, 3]; + case 2: + _a = undefined; + _b.label = 3; + case 3: + res = _a; + return [2 /*return*/, (res === null || res === void 0 ? void 0 : res.data.actors) || []]; + } + }); + }); + }, + select: React.useCallback(function (data) { + return computeSuggestions({ + q: prefix, + searched: data, + moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, + }); + }, [prefix, moderationOpts]), + placeholderData: maintainData ? keepPreviousData : undefined, + }); +} +export function useActorAutocompleteFn() { + var _this = this; + var queryClient = useQueryClient(); + var moderationOpts = useModerationOpts(); + var agent = useAgent(); + return React.useCallback(function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res, e_1; + var query = _b.query, _c = _b.limit, limit = _c === void 0 ? 8 : _c; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + query = query.toLowerCase(); + if (!query) return [3 /*break*/, 4]; + _d.label = 1; + case 1: + _d.trys.push([1, 3, , 4]); + return [4 /*yield*/, queryClient.fetchQuery({ + staleTime: STALE.MINUTES.ONE, + queryKey: RQKEY(query || ''), + queryFn: function () { + return agent.searchActorsTypeahead({ + q: query, + limit: limit, + }); + }, + })]; + case 2: + res = _d.sent(); + return [3 /*break*/, 4]; + case 3: + e_1 = _d.sent(); + logger.error('useActorSearch: searchActorsTypeahead failed', { + message: e_1, + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/, computeSuggestions({ + q: query, + searched: res === null || res === void 0 ? void 0 : res.data.actors, + moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, + })]; + } + }); + }); }, [queryClient, moderationOpts, agent]); +} +function computeSuggestions(_a) { + var q = _a.q, _b = _a.searched, searched = _b === void 0 ? [] : _b, moderationOpts = _a.moderationOpts; + var items = []; + var _loop_1 = function (item) { + if (!items.find(function (item2) { return item2.handle === item.handle; })) { + items.push(item); + } + }; + for (var _i = 0, searched_1 = searched; _i < searched_1.length; _i++) { + var item = searched_1[_i]; + _loop_1(item); + } + return items.filter(function (profile) { + var modui = moderateProfile(profile, moderationOpts).ui('profileList'); + var isExactMatch = q && profile.handle.toLowerCase() === q; + return ((isExactMatch && !moduiContainsHideableOffense(modui)) || + !modui.filter || + isJustAMute(modui)); + }); +} diff --git a/src/state/queries/actor-search.js b/src/state/queries/actor-search.js new file mode 100644 index 0000000000..cf6c989c34 --- /dev/null +++ b/src/state/queries/actor-search.js @@ -0,0 +1,135 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { keepPreviousData, useInfiniteQuery, } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +export var RQKEY_ROOT = 'actor-search'; +export var RQKEY = function (query, limit) { return [ + RQKEY_ROOT, + query, + limit, +]; }; +export function useActorSearch(_a) { + var _this = this; + var query = _a.query, enabled = _a.enabled, maintainData = _a.maintainData, _b = _a.limit, limit = _b === void 0 ? 25 : _b; + var agent = useAgent(); + return useInfiniteQuery({ + staleTime: STALE.MINUTES.FIVE, + queryKey: RQKEY(query, limit), + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.searchActors({ + q: query, + limit: limit, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + enabled: enabled && !!query, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + placeholderData: maintainData ? keepPreviousData : undefined, + select: select, + }); +} +function select(data) { + // enforce uniqueness + var dids = new Set(); + return __assign(__assign({}, data), { pages: data.pages.map(function (page) { return ({ + actors: page.actors.filter(function (actor) { + if (dids.has(actor.did)) { + return false; + } + dids.add(actor.did); + return true; + }), + }); }) }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, actor; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _d.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 6]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!queryData) { + return [3 /*break*/, 5]; + } + _b = 0, _c = queryData.pages.flatMap(function (page) { return page.actors; }); + _d.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 5]; + actor = _c[_b]; + if (!(actor.did === did)) return [3 /*break*/, 4]; + return [4 /*yield*/, actor]; + case 3: + _d.sent(); + _d.label = 4; + case 4: + _b++; + return [3 /*break*/, 2]; + case 5: + _i++; + return [3 /*break*/, 1]; + case 6: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/actor-starter-packs.js b/src/state/queries/actor-starter-packs.js new file mode 100644 index 0000000000..9a400adcd6 --- /dev/null +++ b/src/state/queries/actor-starter-packs.js @@ -0,0 +1,125 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +export var RQKEY_ROOT = 'actor-starter-packs'; +export var RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export var RQKEY_WITH_MEMBERSHIP = function (did) { return [ + RQKEY_WITH_MEMBERSHIP_ROOT, + did, +]; }; +export function useActorStarterPacksQuery(_a) { + var _this = this; + var did = _a.did, _b = _a.enabled, enabled = _b === void 0 ? true : _b; + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(did), + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getActorStarterPacks({ + actor: did, + limit: 10, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + enabled: Boolean(did) && enabled, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} +export function useActorStarterPacksWithMembershipsQuery(_a) { + var _this = this; + var did = _a.did, _b = _a.enabled, enabled = _b === void 0 ? true : _b; + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY_WITH_MEMBERSHIP(did), + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getStarterPacksWithMembership({ + actor: did, + limit: 10, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + enabled: Boolean(did) && enabled, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} +export function invalidateActorStarterPacksQuery(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var queryClient = _b.queryClient, did = _b.did; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, queryClient.invalidateQueries({ queryKey: RQKEY(did) })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function invalidateActorStarterPacksWithMembershipQuery(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var queryClient = _b.queryClient, did = _b.did; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, queryClient.invalidateQueries({ queryKey: RQKEY_WITH_MEMBERSHIP(did) })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} diff --git a/src/state/queries/app-passwords.js b/src/state/queries/app-passwords.js new file mode 100644 index 0000000000..4648f75c86 --- /dev/null +++ b/src/state/queries/app-passwords.js @@ -0,0 +1,109 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '../session'; +var RQKEY_ROOT = 'app-passwords'; +export var RQKEY = function () { return [RQKEY_ROOT]; }; +export function useAppPasswordsQuery() { + var _this = this; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.MINUTES.FIVE, + queryKey: RQKEY(), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.com.atproto.server.listAppPasswords({})]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.passwords]; + } + }); + }); }, + }); +} +export function useAppPasswordCreateMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var name = _b.name, privileged = _b.privileged; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.com.atproto.server.createAppPassword({ + name: name, + privileged: privileged, + })]; + case 1: return [2 /*return*/, (_c.sent()).data]; + } + }); + }); }, + onSuccess: function () { + queryClient.invalidateQueries({ + queryKey: RQKEY(), + }); + }, + }); +} +export function useAppPasswordDeleteMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var name = _b.name; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.com.atproto.server.revokeAppPassword({ + name: name, + })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { + queryClient.invalidateQueries({ + queryKey: RQKEY(), + }); + }, + }); +} diff --git a/src/state/queries/bookmarks/useBookmarkMutation.js b/src/state/queries/bookmarks/useBookmarkMutation.js new file mode 100644 index 0000000000..763274974c --- /dev/null +++ b/src/state/queries/bookmarks/useBookmarkMutation.js @@ -0,0 +1,98 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { updatePostShadow } from '#/state/cache/post-shadow'; +import { optimisticallyDeleteBookmark, optimisticallySaveBookmark, } from '#/state/queries/bookmarks/useBookmarksQuery'; +import { useAgent } from '#/state/session'; +export function useBookmarkMutation() { + var qc = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (args) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(args.action === 'create')) return [3 /*break*/, 2]; + updatePostShadow(qc, args.post.uri, { bookmarked: true }); + return [4 /*yield*/, agent.app.bsky.bookmark.createBookmark({ + uri: args.post.uri, + cid: args.post.cid, + })]; + case 1: + _a.sent(); + return [3 /*break*/, 4]; + case 2: + if (!(args.action === 'delete')) return [3 /*break*/, 4]; + updatePostShadow(qc, args.uri, { bookmarked: false }); + return [4 /*yield*/, agent.app.bsky.bookmark.deleteBookmark({ + uri: args.uri, + })]; + case 3: + _a.sent(); + _a.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); + }, + onSuccess: function (_, args) { + if (args.action === 'create') { + optimisticallySaveBookmark(qc, args.post); + } + else if (args.action === 'delete') { + optimisticallyDeleteBookmark(qc, { uri: args.uri }); + } + }, + onError: function (e, args) { + if (args.action === 'create') { + updatePostShadow(qc, args.post.uri, { bookmarked: false }); + } + else if (args.action === 'delete') { + updatePostShadow(qc, args.uri, { bookmarked: true }); + } + if (!isNetworkError(e)) { + logger.error('bookmark mutation failed', { + bookmarkAction: args.action, + safeMessage: e, + }); + } + }, + }); +} diff --git a/src/state/queries/bookmarks/useBookmarksQuery.js b/src/state/queries/bookmarks/useBookmarksQuery.js new file mode 100644 index 0000000000..940f470930 --- /dev/null +++ b/src/state/queries/bookmarks/useBookmarksQuery.js @@ -0,0 +1,203 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { AppBskyFeedDefs, AtUri, } from '@atproto/api'; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { didOrHandleUriMatches, embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util'; +import { useAgent } from '#/state/session'; +import * as bsky from '#/types/bsky'; +export var bookmarksQueryKeyRoot = 'bookmarks'; +export var createBookmarksQueryKey = function () { return [bookmarksQueryKeyRoot]; }; +export function useBookmarksQuery() { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: createBookmarksQueryKey(), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.bookmark.getBookmarks({ + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} +export function truncateAndInvalidate(qc) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + qc.setQueriesData({ queryKey: [bookmarksQueryKeyRoot] }, function (data) { + if (data) { + return { + pageParams: data.pageParams.slice(0, 1), + pages: data.pages.slice(0, 1), + }; + } + return data; + }); + return [2 /*return*/, qc.invalidateQueries({ queryKey: [bookmarksQueryKeyRoot] })]; + }); + }); +} +export function optimisticallySaveBookmark(qc, post) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + qc.setQueriesData({ + queryKey: [bookmarksQueryKeyRoot], + }, function (data) { + if (!data) + return data; + return __assign(__assign({}, data), { pages: data.pages.map(function (page, index) { + if (index === 0) { + post.$type = 'app.bsky.feed.defs#postView'; + return __assign(__assign({}, page), { bookmarks: __spreadArray([ + { + createdAt: new Date().toISOString(), + subject: { + uri: post.uri, + cid: post.cid, + }, + item: post, + } + ], page.bookmarks, true) }); + } + return page; + }) }); + }); + return [2 /*return*/]; + }); + }); +} +export function optimisticallyDeleteBookmark(qc_1, _a) { + return __awaiter(this, arguments, void 0, function (qc, _b) { + var uri = _b.uri; + return __generator(this, function (_c) { + qc.setQueriesData({ + queryKey: [bookmarksQueryKeyRoot], + }, function (data) { + if (!data) + return data; + return __assign(__assign({}, data), { pages: data.pages.map(function (page) { + return __assign(__assign({}, page), { bookmarks: page.bookmarks.filter(function (b) { return b.subject.uri !== uri; }) }); + }) }); + }); + return [2 /*return*/]; + }); + }); +} +export function findAllPostsInQueryData(queryClient, uri) { + var queryDatas, atUri, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, bookmark, quotedPost; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [bookmarksQueryKeyRoot], + }); + atUri = new AtUri(uri); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 10]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + _d = 0, _e = page.bookmarks; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + bookmark = _e[_d]; + if (!bsky.dangerousIsType(bookmark.item, AppBskyFeedDefs.isPostView)) + return [3 /*break*/, 7]; + if (!didOrHandleUriMatches(atUri, bookmark.item)) return [3 /*break*/, 5]; + return [4 /*yield*/, bookmark.item]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + quotedPost = getEmbeddedPost(bookmark.item.embed); + if (!(quotedPost && didOrHandleUriMatches(atUri, quotedPost))) return [3 /*break*/, 7]; + return [4 /*yield*/, embedViewRecordToPostView(quotedPost)]; + case 6: + _f.sent(); + _f.label = 7; + case 7: + _d++; + return [3 /*break*/, 3]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/explore-feed-previews.js b/src/state/queries/explore-feed-previews.js new file mode 100644 index 0000000000..4abcc6345e --- /dev/null +++ b/src/state/queries/explore-feed-previews.js @@ -0,0 +1,465 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useMemo, useRef } from 'react'; +import { AppBskyFeedDefs, AtUri, moderatePost, } from '@atproto/api'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { CustomFeedAPI } from '#/lib/api/feed/custom'; +import { aggregateUserInterests } from '#/lib/api/feed/utils'; +import { FeedTuner } from '#/lib/api/feed-manip'; +import { cleanError } from '#/lib/strings/errors'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { didOrHandleUriMatches, embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util'; +import { useAgent } from '#/state/session'; +var RQKEY_ROOT = 'feed-previews'; +var RQKEY = function (feeds) { return [RQKEY_ROOT, feeds]; }; +var LIMIT = 8; // sliced to 6, overfetch to account for moderation +var PINNED_POST_URIS = { + // 📰 News + 'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b': true, + // Gardening + 'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27': true, + // Web Development Trending + 'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22': true, + // Anime & Manga EN + 'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y': true, + // 📽️ Film + 'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g': true, + // PopSky + 'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g': true, + // Science + 'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s': true, + // Birds! 🦉 + 'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d': true, + // Astronomy + 'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w': true, + // What's Cooking 🍽️ + 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q': true, + // BookSky 💙📚 #booksky + 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e': true, +}; +export function useFeedPreviews(feedsMaybeWithDuplicates, isEnabled) { + var _this = this; + if (isEnabled === void 0) { isEnabled = true; } + var feeds = useMemo(function () { + return feedsMaybeWithDuplicates.filter(function (f, i, a) { return i === a.findIndex(function (f2) { return f.uri === f2.uri; }); }); + }, [feedsMaybeWithDuplicates]); + var uris = feeds.map(function (feed) { return feed.uri; }); + var _ = useLingui()._; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + var userInterests = aggregateUserInterests(preferences); + var moderationOpts = useModerationOpts(); + var enabled = feeds.length > 0 && isEnabled; + var processedPageCache = useRef(new Map()); + var query = useInfiniteQuery({ + enabled: enabled, + queryKey: RQKEY(uris), + queryFn: function (_b) { return __awaiter(_this, [_b], void 0, function (_c) { + var feed, api, data; + var pageParam = _c.pageParam; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + feed = feeds[pageParam]; + api = new CustomFeedAPI({ + agent: agent, + feedParams: { feed: feed.uri }, + userInterests: userInterests, + }); + return [4 /*yield*/, api.fetch({ cursor: undefined, limit: LIMIT })]; + case 1: + data = _d.sent(); + return [2 /*return*/, { + feed: feed, + posts: data.feed, + }]; + } + }); + }); }, + initialPageParam: 0, + getNextPageParam: function (_p, _a, count) { + return count < feeds.length ? count + 1 : undefined; + }, + }); + var data = query.data, isFetched = query.isFetched, isError = query.isError, isPending = query.isPending, error = query.error; + return { + query: query, + data: useMemo(function () { + var _b, _c; + var items = []; + if (!enabled) + return items; + items.push({ + type: 'preview:spacer', + key: 'spacer', + }); + var isEmpty = !isPending && !((_b = data === null || data === void 0 ? void 0 : data.pages) === null || _b === void 0 ? void 0 : _b.some(function (page) { return page.posts.length; })); + if (isFetched) { + if (isError && isEmpty) { + items.push({ + type: 'preview:error', + key: 'error', + message: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["An error occurred while fetching the feed."], ["An error occurred while fetching the feed."])))), + error: cleanError(error), + }); + } + else if (isEmpty) { + items.push({ + type: 'preview:empty', + key: 'empty', + }); + } + else if (data) { + for (var pageIndex = 0; pageIndex < data.pages.length; pageIndex++) { + var page = data.pages[pageIndex]; + var cachedPage = processedPageCache.current.get(page); + if (cachedPage) { + items.push.apply(items, cachedPage); + continue; + } + // default feed tuner - we just want it to slice up the feed + var tuner = new FeedTuner([]); + var slices = []; + var rowIndex = 0; + var _loop_1 = function (item) { + if (item.isFallbackMarker) + return "continue"; + var moderations = item.items.map(function (item) { + return moderatePost(item.post, moderationOpts); + }); + // apply moderation filters + item.items = item.items.filter(function (_, i) { + var _b; + return !((_b = moderations[i]) === null || _b === void 0 ? void 0 : _b.ui('contentList').filter); + }); + var slice = { + _reactKey: page.feed.uri + item._reactKey, + _isFeedPostSlice: true, + isFallbackMarker: false, + isIncompleteThread: item.isIncompleteThread, + feedContext: item.feedContext, + reqId: item.reqId, + reason: item.reason, + feedPostUri: item.feedPostUri, + items: item.items + .slice(0, 6) + .filter(function (subItem) { + return !PINNED_POST_URIS[subItem.post.uri]; + }) + .map(function (subItem, i) { + var feedPostSliceItem = { + _reactKey: "".concat(item._reactKey, "-").concat(i, "-").concat(subItem.post.uri), + uri: subItem.post.uri, + post: subItem.post, + record: subItem.record, + moderation: moderations[i], + parentAuthor: subItem.parentAuthor, + isParentBlocked: subItem.isParentBlocked, + isParentNotFound: subItem.isParentNotFound, + }; + return feedPostSliceItem; + }), + }; + if (slice.isIncompleteThread && slice.items.length >= 3) { + var beforeLast = slice.items.length - 2; + var last = slice.items.length - 1; + slices.push({ + type: 'preview:sliceItem', + key: slice.items[0]._reactKey, + slice: slice, + indexInSlice: 0, + feed: page.feed, + showReplyTo: false, + hideTopBorder: rowIndex === 0, + }); + slices.push({ + type: 'preview:sliceViewFullThread', + key: slice._reactKey + '-viewFullThread', + uri: slice.items[0].uri, + }); + slices.push({ + type: 'preview:sliceItem', + key: slice.items[beforeLast]._reactKey, + slice: slice, + indexInSlice: beforeLast, + feed: page.feed, + showReplyTo: ((_c = slice.items[beforeLast].parentAuthor) === null || _c === void 0 ? void 0 : _c.did) !== + slice.items[beforeLast].post.author.did, + hideTopBorder: false, + }); + slices.push({ + type: 'preview:sliceItem', + key: slice.items[last]._reactKey, + slice: slice, + indexInSlice: last, + feed: page.feed, + showReplyTo: false, + hideTopBorder: false, + }); + } + else { + for (var i = 0; i < slice.items.length; i++) { + slices.push({ + type: 'preview:sliceItem', + key: slice.items[i]._reactKey, + slice: slice, + indexInSlice: i, + feed: page.feed, + showReplyTo: i === 0, + hideTopBorder: i === 0 && rowIndex === 0, + }); + } + } + rowIndex++; + }; + for (var _i = 0, _d = tuner.tune(page.posts); _i < _d.length; _i++) { + var item = _d[_i]; + _loop_1(item); + } + var processedPage = void 0; + if (slices.length > 0) { + processedPage = __spreadArray(__spreadArray([ + { + type: 'preview:header', + key: "header-".concat(page.feed.uri), + feed: page.feed, + } + ], slices, true), [ + { + type: 'preview:footer', + key: "footer-".concat(page.feed.uri), + }, + ], false); + } + else { + processedPage = []; + } + processedPageCache.current.set(page, processedPage); + items.push.apply(items, processedPage); + } + } + else if (isError && !isEmpty) { + items.push({ + type: 'preview:loadMoreError', + key: 'loadMoreError', + }); + } + } + else { + items.push({ + type: 'preview:loading', + key: 'loading', + }); + } + return items; + }, [ + enabled, + data, + isFetched, + isError, + isPending, + moderationOpts, + _, + error, + ]), + }; +} +export function findAllPostsInQueryData(queryClient, uri) { + var atUri, queryDatas, _i, queryDatas_1, _b, _queryKey, queryData, _c, _d, page, _e, _f, item, quotedPost, parentQuotedPost, rootQuotedPost; + var _g, _h; + return __generator(this, function (_j) { + switch (_j.label) { + case 0: + atUri = new AtUri(uri); + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _j.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 18]; + _b = queryDatas_1[_i], _queryKey = _b[0], queryData = _b[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 17]; + } + _c = 0, _d = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _j.label = 2; + case 2: + if (!(_c < _d.length)) return [3 /*break*/, 17]; + page = _d[_c]; + _e = 0, _f = page.posts; + _j.label = 3; + case 3: + if (!(_e < _f.length)) return [3 /*break*/, 16]; + item = _f[_e]; + if (!didOrHandleUriMatches(atUri, item.post)) return [3 /*break*/, 5]; + return [4 /*yield*/, item.post]; + case 4: + _j.sent(); + _j.label = 5; + case 5: + quotedPost = getEmbeddedPost(item.post.embed); + if (!(quotedPost && didOrHandleUriMatches(atUri, quotedPost))) return [3 /*break*/, 7]; + return [4 /*yield*/, embedViewRecordToPostView(quotedPost)]; + case 6: + _j.sent(); + _j.label = 7; + case 7: + if (!AppBskyFeedDefs.isPostView((_g = item.reply) === null || _g === void 0 ? void 0 : _g.parent)) return [3 /*break*/, 11]; + if (!didOrHandleUriMatches(atUri, item.reply.parent)) return [3 /*break*/, 9]; + return [4 /*yield*/, item.reply.parent]; + case 8: + _j.sent(); + _j.label = 9; + case 9: + parentQuotedPost = getEmbeddedPost(item.reply.parent.embed); + if (!(parentQuotedPost && + didOrHandleUriMatches(atUri, parentQuotedPost))) return [3 /*break*/, 11]; + return [4 /*yield*/, embedViewRecordToPostView(parentQuotedPost)]; + case 10: + _j.sent(); + _j.label = 11; + case 11: + if (!AppBskyFeedDefs.isPostView((_h = item.reply) === null || _h === void 0 ? void 0 : _h.root)) return [3 /*break*/, 15]; + if (!didOrHandleUriMatches(atUri, item.reply.root)) return [3 /*break*/, 13]; + return [4 /*yield*/, item.reply.root]; + case 12: + _j.sent(); + _j.label = 13; + case 13: + rootQuotedPost = getEmbeddedPost(item.reply.root.embed); + if (!(rootQuotedPost && didOrHandleUriMatches(atUri, rootQuotedPost))) return [3 /*break*/, 15]; + return [4 /*yield*/, embedViewRecordToPostView(rootQuotedPost)]; + case 14: + _j.sent(); + _j.label = 15; + case 15: + _e++; + return [3 /*break*/, 3]; + case 16: + _c++; + return [3 /*break*/, 2]; + case 17: + _i++; + return [3 /*break*/, 1]; + case 18: return [2 /*return*/]; + } + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_2, _b, _queryKey, queryData, _c, _d, page, _e, _f, item, quotedPost; + var _g, _h, _j, _k, _l, _m; + return __generator(this, function (_o) { + switch (_o.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_2 = queryDatas; + _o.label = 1; + case 1: + if (!(_i < queryDatas_2.length)) return [3 /*break*/, 14]; + _b = queryDatas_2[_i], _queryKey = _b[0], queryData = _b[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 13]; + } + _c = 0, _d = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _o.label = 2; + case 2: + if (!(_c < _d.length)) return [3 /*break*/, 13]; + page = _d[_c]; + _e = 0, _f = page.posts; + _o.label = 3; + case 3: + if (!(_e < _f.length)) return [3 /*break*/, 12]; + item = _f[_e]; + if (!(item.post.author.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, item.post.author]; + case 4: + _o.sent(); + _o.label = 5; + case 5: + quotedPost = getEmbeddedPost(item.post.embed); + if (!((quotedPost === null || quotedPost === void 0 ? void 0 : quotedPost.author.did) === did)) return [3 /*break*/, 7]; + return [4 /*yield*/, quotedPost.author]; + case 6: + _o.sent(); + _o.label = 7; + case 7: + if (!(AppBskyFeedDefs.isPostView((_g = item.reply) === null || _g === void 0 ? void 0 : _g.parent) && + ((_j = (_h = item.reply) === null || _h === void 0 ? void 0 : _h.parent) === null || _j === void 0 ? void 0 : _j.author.did) === did)) return [3 /*break*/, 9]; + return [4 /*yield*/, item.reply.parent.author]; + case 8: + _o.sent(); + _o.label = 9; + case 9: + if (!(AppBskyFeedDefs.isPostView((_k = item.reply) === null || _k === void 0 ? void 0 : _k.root) && + ((_m = (_l = item.reply) === null || _l === void 0 ? void 0 : _l.root) === null || _m === void 0 ? void 0 : _m.author.did) === did)) return [3 /*break*/, 11]; + return [4 /*yield*/, item.reply.root.author]; + case 10: + _o.sent(); + _o.label = 11; + case 11: + _e++; + return [3 /*break*/, 3]; + case 12: + _c++; + return [3 /*break*/, 2]; + case 13: + _i++; + return [3 /*break*/, 1]; + case 14: return [2 /*return*/]; + } + }); +} +var templateObject_1; diff --git a/src/state/queries/feed.js b/src/state/queries/feed.js new file mode 100644 index 0000000000..2e7238aaf3 --- /dev/null +++ b/src/state/queries/feed.js @@ -0,0 +1,613 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { AtUri, moderateFeedGenerator, RichText, } from '@atproto/api'; +import { t } from '@lingui/core/macro'; +import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; +import { DISCOVER_FEED_URI, DISCOVER_SAVED_FEED } from '#/lib/constants'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { PERSISTED_QUERY_GCTIME, PERSISTED_QUERY_ROOT, STALE, } from '#/state/queries'; +import { RQKEY as listQueryKey } from '#/state/queries/list'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent, useSession } from '#/state/session'; +import { router } from '#/routes'; +import { useModerationOpts } from '../preferences/moderation-opts'; +import { precacheResolvedUri } from './resolve-uri'; +export function isFeedSourceFeedInfo(feed) { + return feed.type === 'feed'; +} +var feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo'; +export var feedSourceInfoQueryKey = function (_a) { + var uri = _a.uri; + return [ + feedSourceInfoQueryKeyRoot, + uri, + ]; +}; +var feedSourceNSIDs = { + feed: 'app.bsky.feed.generator', + list: 'app.bsky.graph.list', +}; +export function hydrateFeedGenerator(view) { + var _a, _b; + var urip = new AtUri(view.uri); + var collection = urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists'; + var href = "/profile/".concat(urip.hostname, "/").concat(collection, "/").concat(urip.rkey); + var route = router.matchPath(href); + return { + type: 'feed', + view: view, + uri: view.uri, + feedDescriptor: "feedgen|".concat(view.uri), + cid: view.cid, + route: { + href: href, + name: route[0], + params: route[1], + }, + avatar: view.avatar, + displayName: view.displayName + ? sanitizeDisplayName(view.displayName) + : t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Feed by ", ""], ["Feed by ", ""])), sanitizeHandle(view.creator.handle, '@')), + description: new RichText({ + text: view.description || '', + facets: (_a = (view.descriptionFacets || [])) === null || _a === void 0 ? void 0 : _a.slice(), + }), + creatorDid: view.creator.did, + creatorHandle: view.creator.handle, + likeCount: view.likeCount, + acceptsInteractions: view.acceptsInteractions, + likeUri: (_b = view.viewer) === null || _b === void 0 ? void 0 : _b.like, + contentMode: view.contentMode, + }; +} +export function hydrateList(view) { + var _a; + var urip = new AtUri(view.uri); + var collection = urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists'; + var href = "/profile/".concat(urip.hostname, "/").concat(collection, "/").concat(urip.rkey); + var route = router.matchPath(href); + return { + type: 'list', + view: view, + uri: view.uri, + feedDescriptor: "list|".concat(view.uri), + route: { + href: href, + name: route[0], + params: route[1], + }, + cid: view.cid, + avatar: view.avatar, + description: new RichText({ + text: view.description || '', + facets: (_a = (view.descriptionFacets || [])) === null || _a === void 0 ? void 0 : _a.slice(), + }), + creatorDid: view.creator.did, + creatorHandle: view.creator.handle, + displayName: view.name + ? sanitizeDisplayName(view.name) + : t(templateObject_2 || (templateObject_2 = __makeTemplateObject(["User List by ", ""], ["User List by ", ""])), sanitizeHandle(view.creator.handle, '@')), + contentMode: undefined, + }; +} +export function getFeedTypeFromUri(uri) { + var pathname = new AtUri(uri).pathname; + return pathname.includes(feedSourceNSIDs.feed) ? 'feed' : 'list'; +} +export function getAvatarTypeFromUri(uri) { + return getFeedTypeFromUri(uri) === 'feed' ? 'algo' : 'list'; +} +export function useFeedSourceInfoQuery(_a) { + var _this = this; + var uri = _a.uri; + var type = getFeedTypeFromUri(uri); + var agent = useAgent(); + return useQuery({ + staleTime: STALE.INFINITY, + queryKey: feedSourceInfoQueryKey({ uri: uri }), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var view, res, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(type === 'feed')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.app.bsky.feed.getFeedGenerator({ feed: uri })]; + case 1: + res = _a.sent(); + view = hydrateFeedGenerator(res.data.view); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, agent.app.bsky.graph.getList({ + list: uri, + limit: 1, + })]; + case 3: + res = _a.sent(); + view = hydrateList(res.data.list); + _a.label = 4; + case 4: return [2 /*return*/, view]; + } + }); + }); }, + }); +} +// HACK +// the protocol doesn't yet tell us which feeds are personalized +// this list is used to filter out feed recommendations from logged out users +// for the ones we know need it +// -prf +export var KNOWN_AUTHED_ONLY_FEEDS = [ + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed + 'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz + 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why +]; +export function createGetPopularFeedsQueryKey(options) { + return ['getPopularFeeds', options === null || options === void 0 ? void 0 : options.limit]; +} +export function useGetPopularFeedsQuery(options) { + var _this = this; + var hasSession = useSession().hasSession; + var agent = useAgent(); + var limit = (options === null || options === void 0 ? void 0 : options.limit) || 10; + var preferences = usePreferencesQuery().data; + var queryClient = useQueryClient(); + var moderationOpts = useModerationOpts(); + // Make sure this doesn't invalidate unless really needed. + var selectArgs = useMemo(function () { return ({ + hasSession: hasSession, + savedFeeds: (preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds) || [], + moderationOpts: moderationOpts, + }); }, [hasSession, preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds, moderationOpts]); + var lastPageCountRef = useRef(0); + var query = useInfiniteQuery({ + enabled: Boolean(moderationOpts) && (options === null || options === void 0 ? void 0 : options.enabled) !== false, + queryKey: createGetPopularFeedsQueryKey(options), + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res, _i, _c, feed, hydratedFeed; + var pageParam = _b.pageParam; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getPopularFeedGenerators({ + limit: limit, + cursor: pageParam, + }) + // precache feeds + ]; + case 1: + res = _d.sent(); + // precache feeds + for (_i = 0, _c = res.data.feeds; _i < _c.length; _i++) { + feed = _c[_i]; + hydratedFeed = hydrateFeedGenerator(feed); + precacheFeed(queryClient, hydratedFeed); + } + return [2 /*return*/, res.data]; + } + }); + }); }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + select: useCallback(function (data) { + var savedFeeds = selectArgs.savedFeeds, hasSessionInner = selectArgs.hasSession, moderationOpts = selectArgs.moderationOpts; + return __assign(__assign({}, data), { pages: data.pages.map(function (page) { + return __assign(__assign({}, page), { feeds: page.feeds.filter(function (feed) { + if (!hasSessionInner && + KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri)) { + return false; + } + var alreadySaved = Boolean(savedFeeds === null || savedFeeds === void 0 ? void 0 : savedFeeds.find(function (f) { + return f.value === feed.uri; + })); + var decision = moderateFeedGenerator(feed, moderationOpts); + return !alreadySaved && !decision.ui('contentList').filter; + }) }); + }) }); + }, [selectArgs /* Don't change. Everything needs to go into selectArgs. */]), + }); + useEffect(function () { + var _a, _b; + var isFetching = query.isFetching, hasNextPage = query.hasNextPage, data = query.data; + if (isFetching || !hasNextPage) { + return; + } + // avoid double-fires of fetchNextPage() + if (lastPageCountRef.current !== 0 && + lastPageCountRef.current === ((_a = data === null || data === void 0 ? void 0 : data.pages) === null || _a === void 0 ? void 0 : _a.length)) { + return; + } + // fetch next page if we haven't gotten a full page of content + var count = 0; + for (var _i = 0, _c = (data === null || data === void 0 ? void 0 : data.pages) || []; _i < _c.length; _i++) { + var page = _c[_i]; + count += page.feeds.length; + } + if (count < limit && ((data === null || data === void 0 ? void 0 : data.pages.length) || 0) < 6) { + query.fetchNextPage(); + lastPageCountRef.current = ((_b = data === null || data === void 0 ? void 0 : data.pages) === null || _b === void 0 ? void 0 : _b.length) || 0; + } + }, [query, limit]); + return query; +} +export function useSearchPopularFeedsMutation() { + var _this = this; + var agent = useAgent(); + var moderationOpts = useModerationOpts(); + return useMutation({ + mutationFn: function (query) { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getPopularFeedGenerators({ + limit: 10, + query: query, + })]; + case 1: + res = _a.sent(); + if (moderationOpts) { + return [2 /*return*/, res.data.feeds.filter(function (feed) { + var decision = moderateFeedGenerator(feed, moderationOpts); + return !decision.ui('contentMedia').blur; + })]; + } + return [2 /*return*/, res.data.feeds]; + } + }); + }); }, + }); +} +var popularFeedsSearchQueryKeyRoot = 'popularFeedsSearch'; +export var createPopularFeedsSearchQueryKey = function (query) { return [ + popularFeedsSearchQueryKeyRoot, + query, +]; }; +export function usePopularFeedsSearch(_a) { + var _this = this; + var query = _a.query, enabled = _a.enabled; + var agent = useAgent(); + var moderationOpts = useModerationOpts(); + var enabledInner = enabled !== null && enabled !== void 0 ? enabled : Boolean(moderationOpts); + return useQuery({ + enabled: enabledInner, + queryKey: createPopularFeedsSearchQueryKey(query), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getPopularFeedGenerators({ + limit: 15, + query: query, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.feeds]; + } + }); + }); }, + placeholderData: keepPreviousData, + select: function (data) { + return data.filter(function (feed) { + var decision = moderateFeedGenerator(feed, moderationOpts); + return !decision.ui('contentMedia').blur; + }); + }, + }); +} +var PWI_DISCOVER_FEED_STUB = { + type: 'feed', + displayName: 'Discover', + uri: DISCOVER_FEED_URI, + feedDescriptor: "feedgen|".concat(DISCOVER_FEED_URI), + route: { + href: '/', + name: 'Home', + params: {}, + }, + cid: '', + avatar: '', + description: new RichText({ text: '' }), + creatorDid: '', + creatorHandle: '', + likeCount: 0, + likeUri: '', + // --- + savedFeed: __assign({ id: 'pwi-discover' }, DISCOVER_SAVED_FEED), + contentMode: undefined, +}; +var createPinnedFeedInfosQueryKeyRoot = function (kind, feedUris) { return [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris]; }; +export function usePinnedFeedsInfos() { + var _this = this; + var _a; + var hasSession = useSession().hasSession; + var agent = useAgent(); + var _b = usePreferencesQuery(), preferences = _b.data, isLoadingPrefs = _b.isLoading; + var pinnedItems = (_a = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds.filter(function (feed) { return feed.pinned; })) !== null && _a !== void 0 ? _a : []; + return useQuery({ + queryKey: createPinnedFeedInfosQueryKeyRoot('pinned', pinnedItems.map(function (f) { return f.value; })), + gcTime: PERSISTED_QUERY_GCTIME, + staleTime: STALE.INFINITY, + enabled: !isLoadingPrefs, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var resolved, pinnedFeeds, feedsPromise, pinnedLists, listsPromises, result, _i, pinnedItems_1, pinnedItem, feedInfo; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!hasSession) { + return [2 /*return*/, [PWI_DISCOVER_FEED_STUB]]; + } + resolved = new Map(); + pinnedFeeds = pinnedItems.filter(function (feed) { return feed.type === 'feed'; }); + feedsPromise = Promise.resolve(); + if (pinnedFeeds.length > 0) { + feedsPromise = agent.app.bsky.feed + .getFeedGenerators({ + feeds: pinnedFeeds.map(function (f) { return f.value; }), + }) + .then(function (res) { + for (var i = 0; i < res.data.feeds.length; i++) { + var feedView = res.data.feeds[i]; + resolved.set(feedView.uri, hydrateFeedGenerator(feedView)); + } + }); + } + pinnedLists = pinnedItems.filter(function (feed) { return feed.type === 'list'; }); + listsPromises = pinnedLists.map(function (list) { + return agent.app.bsky.graph + .getList({ + list: list.value, + limit: 1, + }) + .then(function (res) { + var listView = res.data.list; + resolved.set(listView.uri, hydrateList(listView)); + }); + }); + return [4 /*yield*/, feedsPromise]; // Fail the whole query if it fails. + case 1: + _a.sent(); // Fail the whole query if it fails. + return [4 /*yield*/, Promise.allSettled(listsPromises) + // order the feeds/lists in the order they were pinned + ]; // Ignore individual failing ones. + case 2: + _a.sent(); // Ignore individual failing ones. + result = []; + for (_i = 0, pinnedItems_1 = pinnedItems; _i < pinnedItems_1.length; _i++) { + pinnedItem = pinnedItems_1[_i]; + feedInfo = resolved.get(pinnedItem.value); + if (feedInfo) { + result.push(__assign(__assign({}, feedInfo), { savedFeed: pinnedItem })); + } + else if (pinnedItem.type === 'timeline') { + result.push({ + type: 'feed', + displayName: 'Following', + uri: pinnedItem.value, + feedDescriptor: 'following', + route: { + href: '/', + name: 'Home', + params: {}, + }, + cid: '', + avatar: '', + description: new RichText({ text: '' }), + creatorDid: '', + creatorHandle: '', + likeCount: 0, + likeUri: '', + savedFeed: pinnedItem, + contentMode: undefined, + }); + } + } + return [2 /*return*/, result]; + } + }); + }); }, + }); +} +export function useSavedFeeds() { + var _this = this; + var _a; + var agent = useAgent(); + var _b = usePreferencesQuery(), preferences = _b.data, isLoadingPrefs = _b.isLoading; + var savedItems = (_a = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds) !== null && _a !== void 0 ? _a : []; + var queryClient = useQueryClient(); + return useQuery({ + queryKey: createPinnedFeedInfosQueryKeyRoot('saved', savedItems.map(function (f) { return f.value; })), + gcTime: PERSISTED_QUERY_GCTIME, + staleTime: STALE.INFINITY, + enabled: !isLoadingPrefs, + placeholderData: function (previousData) { + return (previousData || { + // The likely count before we try to resolve them. + count: savedItems.length, + feeds: [], + }); + }, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var resolvedFeeds, resolvedLists, savedFeeds, savedLists, feedsPromise, listsPromises, result, _i, savedItems_1, savedItem, resolvedFeed, resolvedList; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + resolvedFeeds = new Map(); + resolvedLists = new Map(); + savedFeeds = savedItems.filter(function (feed) { return feed.type === 'feed'; }); + savedLists = savedItems.filter(function (feed) { return feed.type === 'list'; }); + feedsPromise = Promise.resolve(); + if (savedFeeds.length > 0) { + feedsPromise = agent.app.bsky.feed + .getFeedGenerators({ + feeds: savedFeeds.map(function (f) { return f.value; }), + }) + .then(function (res) { + res.data.feeds.forEach(function (f) { + resolvedFeeds.set(f.uri, f); + }); + }); + } + listsPromises = savedLists.map(function (list) { + return agent.app.bsky.graph + .getList({ + list: list.value, + limit: 1, + }) + .then(function (res) { + var listView = res.data.list; + resolvedLists.set(listView.uri, listView); + }); + }); + return [4 /*yield*/, Promise.allSettled(__spreadArray([feedsPromise], listsPromises, true))]; + case 1: + _a.sent(); + resolvedFeeds.forEach(function (feed) { + var hydratedFeed = hydrateFeedGenerator(feed); + precacheFeed(queryClient, hydratedFeed); + }); + resolvedLists.forEach(function (list) { + precacheList(queryClient, list); + }); + result = []; + for (_i = 0, savedItems_1 = savedItems; _i < savedItems_1.length; _i++) { + savedItem = savedItems_1[_i]; + if (savedItem.type === 'timeline') { + result.push({ + type: 'timeline', + config: savedItem, + view: undefined, + }); + } + else if (savedItem.type === 'feed') { + resolvedFeed = resolvedFeeds.get(savedItem.value); + if (resolvedFeed) { + result.push({ + type: 'feed', + config: savedItem, + view: resolvedFeed, + }); + } + } + else if (savedItem.type === 'list') { + resolvedList = resolvedLists.get(savedItem.value); + if (resolvedList) { + result.push({ + type: 'list', + config: savedItem, + view: resolvedList, + }); + } + } + } + return [2 /*return*/, { + // By this point we know the real count. + count: result.length, + feeds: result, + }]; + } + }); + }); }, + }); +} +var feedInfoQueryKeyRoot = 'feedInfo'; +export function useFeedInfo(feedUri) { + var _this = this; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.INFINITY, + queryKey: [feedInfoQueryKeyRoot, feedUri], + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res, feedSourceInfo; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!feedUri) { + return [2 /*return*/, null]; + } + return [4 /*yield*/, agent.app.bsky.feed.getFeedGenerator({ + feed: feedUri, + })]; + case 1: + res = _a.sent(); + feedSourceInfo = hydrateFeedGenerator(res.data.view); + return [2 /*return*/, feedSourceInfo]; + } + }); + }); }, + }); +} +function precacheFeed(queryClient, hydratedFeed) { + precacheResolvedUri(queryClient, hydratedFeed.creatorHandle, hydratedFeed.creatorDid); + queryClient.setQueryData(feedSourceInfoQueryKey({ uri: hydratedFeed.uri }), hydratedFeed); +} +export function precacheList(queryClient, list) { + precacheResolvedUri(queryClient, list.creator.handle, list.creator.did); + queryClient.setQueryData(listQueryKey(list.uri), list); +} +export function precacheFeedFromGeneratorView(queryClient, view) { + var hydratedFeed = hydrateFeedGenerator(view); + precacheFeed(queryClient, hydratedFeed); +} +var templateObject_1, templateObject_2; diff --git a/src/state/queries/find-contacts.js b/src/state/queries/find-contacts.js new file mode 100644 index 0000000000..e44625e016 --- /dev/null +++ b/src/state/queries/find-contacts.js @@ -0,0 +1,200 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, useQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +import { STALE } from '.'; +var RQ_KEY_ROOT = 'find-contacts'; +export var findContactsStatusQueryKey = [RQ_KEY_ROOT, 'sync-status']; +export function useContactsSyncStatusQuery() { + var _this = this; + var agent = useAgent(); + return useQuery({ + queryKey: findContactsStatusQueryKey, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var status; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.contact.getSyncStatus()]; + case 1: + status = _a.sent(); + return [2 /*return*/, status.data]; + } + }); + }); }, + staleTime: STALE.SECONDS.THIRTY, + }); +} +export var findContactsGetMatchesQueryKey = [RQ_KEY_ROOT, 'matches']; +export function useContactsMatchesQuery() { + var _this = this; + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: findContactsGetMatchesQueryKey, + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var matches; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.contact.getMatches({ + cursor: pageParam, + })]; + case 1: + matches = _c.sent(); + return [2 /*return*/, matches.data]; + } + }); + }); }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + staleTime: STALE.MINUTES.ONE, + }); +} +export function optimisticRemoveMatch(queryClient, did) { + queryClient.setQueryData(findContactsGetMatchesQueryKey, function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { return (__assign(__assign({}, page), { matches: page.matches.filter(function (match) { return match.did !== did; }) })); }) }); + }); +} +export var findContactsMatchesPassthroughQueryKey = function (dids) { return [ + RQ_KEY_ROOT, + 'passthrough', + dids, +]; }; +/** + * DIRTY HACK WARNING! + * + * The only way to get shadow state to work is to put it into React Query. + * However, when we get the matches it's via a POST, not a GET, so we use a mutation, + * which means we can't use shadowing! + * + * In lieu of any better ideas, I'm just going to take the contacts we have and + * "launder" them through a dummy query. This will then return "shadow-able" profiles. + */ +export function useMatchesPassthroughQuery(matches) { + var dids = matches.map(function (match) { return match.profile.did; }); + var data = useQuery({ + queryKey: findContactsMatchesPassthroughQueryKey(dids), + queryFn: function () { + return matches; + }, + }).data; + return data !== null && data !== void 0 ? data : matches; +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, match, passthroughQueryDatas, _f, passthroughQueryDatas_1, _g, _queryKey, queryData, _h, queryData_1, match; + return __generator(this, function (_j) { + switch (_j.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: findContactsGetMatchesQueryKey, + }); + _i = 0, queryDatas_1 = queryDatas; + _j.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _j.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.matches; + _j.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + match = _e[_d]; + if (!(match.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, match]; + case 4: + _j.sent(); + _j.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: + passthroughQueryDatas = queryClient.getQueriesData({ + queryKey: [RQ_KEY_ROOT, 'passthrough'], + }); + _f = 0, passthroughQueryDatas_1 = passthroughQueryDatas; + _j.label = 9; + case 9: + if (!(_f < passthroughQueryDatas_1.length)) return [3 /*break*/, 14]; + _g = passthroughQueryDatas_1[_f], _queryKey = _g[0], queryData = _g[1]; + if (!queryData) { + return [3 /*break*/, 13]; + } + _h = 0, queryData_1 = queryData; + _j.label = 10; + case 10: + if (!(_h < queryData_1.length)) return [3 /*break*/, 13]; + match = queryData_1[_h]; + if (!(match.profile.did === did)) return [3 /*break*/, 12]; + return [4 /*yield*/, match.profile]; + case 11: + _j.sent(); + _j.label = 12; + case 12: + _h++; + return [3 /*break*/, 10]; + case 13: + _f++; + return [3 /*break*/, 9]; + case 14: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/handle-availability.js b/src/state/queries/handle-availability.js new file mode 100644 index 0000000000..b08208993a --- /dev/null +++ b/src/state/queries/handle-availability.js @@ -0,0 +1,135 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { ComAtprotoTempCheckHandleAvailability } from '@atproto/api'; +import { useQuery } from '@tanstack/react-query'; +import { BSKY_SERVICE, BSKY_SERVICE_DID, PUBLIC_BSKY_SERVICE, } from '#/lib/constants'; +import { useDebouncedValue } from '#/lib/hooks/useDebouncedValue'; +import { createFullHandle } from '#/lib/strings/handles'; +import { useAnalytics } from '#/analytics'; +import * as bsky from '#/types/bsky'; +import { Agent } from '../session/agent'; +export var RQKEY_handleAvailability = function (handle, domain, serviceDid) { return ['handle-availability', { handle: handle, domain: domain, serviceDid: serviceDid }]; }; +export function useHandleAvailabilityQuery(_a, debounceDelayMs) { + var _this = this; + var username = _a.username, serviceDomain = _a.serviceDomain, serviceDid = _a.serviceDid, enabled = _a.enabled, birthDate = _a.birthDate, email = _a.email; + if (debounceDelayMs === void 0) { debounceDelayMs = 500; } + var ax = useAnalytics(); + var name = username.trim(); + var debouncedHandle = useDebouncedValue(name, debounceDelayMs); + return { + debouncedUsername: debouncedHandle, + enabled: enabled && name === debouncedHandle, + query: useQuery({ + enabled: enabled && name === debouncedHandle, + queryKey: RQKEY_handleAvailability(debouncedHandle, serviceDomain, serviceDid), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var handle, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + handle = createFullHandle(name, serviceDomain); + return [4 /*yield*/, checkHandleAvailability(handle, serviceDid, { + email: email, + birthDate: birthDate, + })]; + case 1: + res = _a.sent(); + if (res.available) { + ax.metric('signup:handleAvailable', { typeahead: true }); + } + else { + ax.metric('signup:handleTaken', { typeahead: true }); + } + return [2 /*return*/, res]; + } + }); + }); }, + }), + }; +} +export function checkHandleAvailability(handle_1, serviceDid_1, _a) { + return __awaiter(this, arguments, void 0, function (handle, serviceDid, _b) { + var agent, data, agent, res, _c; + var email = _b.email, birthDate = _b.birthDate; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + if (!(serviceDid === BSKY_SERVICE_DID)) return [3 /*break*/, 2]; + agent = new Agent(null, { service: BSKY_SERVICE }); + return [4 /*yield*/, agent.com.atproto.temp.checkHandleAvailability({ + handle: handle, + birthDate: birthDate, + email: email, + })]; + case 1: + data = (_d.sent()).data; + if (bsky.dangerousIsType(data.result, ComAtprotoTempCheckHandleAvailability.isResultAvailable)) { + return [2 /*return*/, { available: true }]; + } + else if (bsky.dangerousIsType(data.result, ComAtprotoTempCheckHandleAvailability.isResultUnavailable)) { + return [2 /*return*/, { + available: false, + suggestions: data.result.suggestions, + }]; + } + else { + throw new Error("Unexpected result of `checkHandleAvailability`: ".concat(JSON.stringify(data.result))); + } + return [3 /*break*/, 7]; + case 2: + agent = new Agent(null, { service: PUBLIC_BSKY_SERVICE }); + _d.label = 3; + case 3: + _d.trys.push([3, 5, , 6]); + return [4 /*yield*/, agent.resolveHandle({ + handle: handle, + })]; + case 4: + res = _d.sent(); + if (res.data.did) { + return [2 /*return*/, { available: false }]; + } + return [3 /*break*/, 6]; + case 5: + _c = _d.sent(); + return [3 /*break*/, 6]; + case 6: return [2 /*return*/, { available: true }]; + case 7: return [2 /*return*/]; + } + }); + }); +} diff --git a/src/state/queries/handle.js b/src/state/queries/handle.js new file mode 100644 index 0000000000..c4e501ddbe --- /dev/null +++ b/src/state/queries/handle.js @@ -0,0 +1,125 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +var handleQueryKeyRoot = 'handle'; +var fetchHandleQueryKey = function (handleOrDid) { return [ + handleQueryKeyRoot, + handleOrDid, +]; }; +var didQueryKeyRoot = 'did'; +var fetchDidQueryKey = function (handleOrDid) { return [didQueryKeyRoot, handleOrDid]; }; +export function useFetchHandle() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return React.useCallback(function (handleOrDid) { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!handleOrDid.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, queryClient.fetchQuery({ + staleTime: STALE.MINUTES.FIVE, + queryKey: fetchHandleQueryKey(handleOrDid), + queryFn: function () { return agent.getProfile({ actor: handleOrDid }); }, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.handle]; + case 2: return [2 /*return*/, handleOrDid]; + } + }); + }); }, [queryClient, agent]); +} +export function useUpdateHandleMutation(opts) { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var handle = _b.handle; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.updateHandle({ handle: handle })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_data, variables) { + var _a; + (_a = opts === null || opts === void 0 ? void 0 : opts.onSuccess) === null || _a === void 0 ? void 0 : _a.call(opts, variables.handle); + queryClient.invalidateQueries({ + queryKey: fetchHandleQueryKey(variables.handle), + }); + }, + }); +} +export function useFetchDid() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return React.useCallback(function (handleOrDid) { return __awaiter(_this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + return [2 /*return*/, queryClient.fetchQuery({ + staleTime: STALE.INFINITY, + queryKey: fetchDidQueryKey(handleOrDid), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var identifier, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + identifier = handleOrDid; + if (!!identifier.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.resolveHandle({ handle: identifier })]; + case 1: + res = _a.sent(); + identifier = res.data.did; + _a.label = 2; + case 2: return [2 /*return*/, identifier]; + } + }); + }); }, + })]; + }); + }); }, [queryClient, agent]); +} diff --git a/src/state/queries/index.js b/src/state/queries/index.js new file mode 100644 index 0000000000..0634f08e16 --- /dev/null +++ b/src/state/queries/index.js @@ -0,0 +1,38 @@ +var SECOND = 1e3; +var MINUTE = SECOND * 60; +var HOUR = MINUTE * 60; +export var STALE = { + SECONDS: { + FIFTEEN: 15 * SECOND, + THIRTY: 30 * SECOND, + }, + MINUTES: { + ONE: MINUTE, + THREE: 3 * MINUTE, + FIVE: 5 * MINUTE, + THIRTY: 30 * MINUTE, + }, + HOURS: { + ONE: HOUR, + }, + INFINITY: Infinity, +}; +/** + * Root key for persisted queries. + * + * If the `querykey` of your query uses this at index 0, it will be + * persisted automatically by the `PersistQueryClientProvider` in + * `#/lib/react-query.tsx`. + * + * Be careful when using this, since it will change the query key and may + * break any cases where we call `invalidateQueries` or `refetchQueries` + * with the old key. + * + * Also, only use this for queries that are safe to persist between + * app launches (like user preferences). + * + * Note that for queries that are persisted, it is recommended to extend + * the `gcTime` to a longer duration, otherwise it'll get busted + */ +export var PERSISTED_QUERY_ROOT = 'PERSISTED'; +export var PERSISTED_QUERY_GCTIME = Infinity; diff --git a/src/state/queries/known-followers.js b/src/state/queries/known-followers.js new file mode 100644 index 0000000000..7a6d4a88f5 --- /dev/null +++ b/src/state/queries/known-followers.js @@ -0,0 +1,112 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +var PAGE_SIZE = 50; +var RQKEY_ROOT = 'profile-known-followers'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export function useProfileKnownFollowersQuery(did) { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(did || ''), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getKnownFollowers({ + actor: did, + limit: PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: !!did, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, follow; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.followers; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + follow = _e[_d]; + if (!(follow.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, follow]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/labeler.js b/src/state/queries/labeler.js new file mode 100644 index 0000000000..d7984e293a --- /dev/null +++ b/src/state/queries/labeler.js @@ -0,0 +1,243 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { z } from 'zod'; +import { MAX_LABELERS } from '#/lib/constants'; +import { PERSISTED_QUERY_GCTIME, PERSISTED_QUERY_ROOT, STALE, } from '#/state/queries'; +import { preferencesQueryKey, usePreferencesQuery, } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +var labelerInfoQueryKeyRoot = 'labeler-info'; +export var labelerInfoQueryKey = function (did) { return [ + labelerInfoQueryKeyRoot, + did, +]; }; +var labelersInfoQueryKeyRoot = 'labelers-info'; +export var labelersInfoQueryKey = function (dids) { return [ + labelersInfoQueryKeyRoot, + dids.slice().sort(), +]; }; +var persistedLabelersDetailedInfoQueryKey = function (dids) { return [ + PERSISTED_QUERY_ROOT, + 'labelers-detailed-info', + dids, +]; }; +export function useLabelerInfoQuery(_a) { + var _this = this; + var did = _a.did, enabled = _a.enabled; + var agent = useAgent(); + return useQuery({ + enabled: !!did && enabled !== false, + queryKey: labelerInfoQueryKey(did), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.labeler.getServices({ + dids: [did], + detailed: true, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.views[0]]; + } + }); + }); }, + }); +} +export function useLabelersInfoQuery(_a) { + var _this = this; + var dids = _a.dids; + var agent = useAgent(); + return useQuery({ + enabled: !!dids.length, + queryKey: labelersInfoQueryKey(dids), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.labeler.getServices({ dids: dids })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.views]; + } + }); + }); }, + }); +} +export function useLabelersDetailedInfoQuery(_a) { + var _this = this; + var dids = _a.dids; + var agent = useAgent(); + return useQuery({ + enabled: !!dids.length, + queryKey: persistedLabelersDetailedInfoQueryKey(dids), + gcTime: PERSISTED_QUERY_GCTIME, + staleTime: STALE.MINUTES.ONE, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.labeler.getServices({ + dids: dids, + detailed: true, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.views]; + } + }); + }); }, + }); +} +export function useRemoveLabelersMutation() { + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var dids = _b.dids; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, Promise.all(dids.map(function (did) { return agent.removeLabeler(did); }))]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); + }, + onSuccess: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, + }); +} +export function useLabelerSubscriptionMutation() { + var queryClient = useQueryClient(); + var agent = useAgent(); + var preferences = usePreferencesQuery(); + return useMutation({ + mutationFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var labelerDids, invalidLabelers, profiles, _loop_1, _i, labelerDids_1, did_1, labelerCount; + var _c, _d, _e; + var did = _b.did, subscribe = _b.subscribe; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + // TODO + z.object({ + did: z.string(), + subscribe: z.boolean(), + }).parse({ did: did, subscribe: subscribe }); + labelerDids = ((_e = (_d = (_c = preferences.data) === null || _c === void 0 ? void 0 : _c.moderationPrefs) === null || _d === void 0 ? void 0 : _d.labelers) !== null && _e !== void 0 ? _e : []).map(function (l) { return l.did; }); + invalidLabelers = []; + if (!labelerDids.length) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.getProfiles({ actors: labelerDids })]; + case 1: + profiles = _f.sent(); + if (profiles.data) { + _loop_1 = function (did_1) { + var exists = profiles.data.profiles.find(function (p) { return p.did === did_1; }); + if (exists) { + // profile came back but it's not a valid labeler + if (exists.associated && !exists.associated.labeler) { + invalidLabelers.push(did_1); + } + } + else { + // no response came back, might be deactivated or takendown + invalidLabelers.push(did_1); + } + }; + for (_i = 0, labelerDids_1 = labelerDids; _i < labelerDids_1.length; _i++) { + did_1 = labelerDids_1[_i]; + _loop_1(did_1); + } + } + _f.label = 2; + case 2: + if (!invalidLabelers.length) return [3 /*break*/, 4]; + return [4 /*yield*/, Promise.all(invalidLabelers.map(function (did) { return agent.removeLabeler(did); }))]; + case 3: + _f.sent(); + _f.label = 4; + case 4: + if (!subscribe) return [3 /*break*/, 6]; + labelerCount = labelerDids.length - invalidLabelers.length; + if (labelerCount >= MAX_LABELERS) { + throw new Error('MAX_LABELERS'); + } + return [4 /*yield*/, agent.addLabeler(did)]; + case 5: + _f.sent(); + return [3 /*break*/, 8]; + case 6: return [4 /*yield*/, agent.removeLabeler(did)]; + case 7: + _f.sent(); + _f.label = 8; + case 8: return [2 /*return*/]; + } + }); + }); + }, + onSuccess: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, + }); +} diff --git a/src/state/queries/like.js b/src/state/queries/like.js new file mode 100644 index 0000000000..7bb5221c0f --- /dev/null +++ b/src/state/queries/like.js @@ -0,0 +1,73 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +export function useLikeMutation() { + var _this = this; + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res; + var uri = _b.uri, cid = _b.cid; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.like(uri, cid)]; + case 1: + res = _c.sent(); + return [2 /*return*/, { uri: res.uri }]; + } + }); + }); }, + }); +} +export function useUnlikeMutation() { + var _this = this; + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var uri = _b.uri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.deleteLike(uri)]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/list-members.js b/src/state/queries/list-members.js new file mode 100644 index 0000000000..e231814ff0 --- /dev/null +++ b/src/state/queries/list-members.js @@ -0,0 +1,207 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, useQuery, } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +var PAGE_SIZE = 30; +var RQKEY_ROOT = 'list-members'; +var RQKEY_ROOT_ALL = 'list-members-all'; +export var RQKEY = function (uri) { return [RQKEY_ROOT, uri]; }; +export var RQKEY_ALL = function (uri) { return [RQKEY_ROOT_ALL, uri]; }; +export function useListMembersQuery(uri, limit) { + if (limit === void 0) { limit = PAGE_SIZE; } + var agent = useAgent(); + return useInfiniteQuery({ + staleTime: STALE.MINUTES.ONE, + queryKey: RQKEY(uri !== null && uri !== void 0 ? uri : ''), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getList({ + list: uri, // the enabled flag will prevent this from running until uri is set + limit: limit, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: Boolean(uri), + }); +} +export function useAllListMembersQuery(uri) { + var _this = this; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.MINUTES.ONE, + queryKey: RQKEY_ALL(uri !== null && uri !== void 0 ? uri : ''), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, getAllListMembers(agent, uri)]; + }); + }); }, + enabled: Boolean(uri), + }); +} +export function getAllListMembers(agent, uri) { + return __awaiter(this, void 0, void 0, function () { + var hasMore, cursor, listItems, i, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + hasMore = true; + listItems = []; + i = 0; + _a.label = 1; + case 1: + if (!(hasMore && i < 6)) return [3 /*break*/, 3]; + return [4 /*yield*/, agent.app.bsky.graph.getList({ + list: uri, + limit: 50, + cursor: cursor, + })]; + case 2: + res = _a.sent(); + listItems.push.apply(listItems, res.data.items); + hasMore = Boolean(res.data.cursor); + cursor = res.data.cursor; + i++; + return [3 /*break*/, 1]; + case 3: return [2 /*return*/, listItems]; + } + }); + }); +} +export function invalidateListMembersQuery(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var queryClient = _b.queryClient, uri = _b.uri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, queryClient.invalidateQueries({ queryKey: RQKEY(uri) })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, item, allQueryData, _f, allQueryData_1, _g, _queryKey, queryData, _h, queryData_1, item; + return __generator(this, function (_j) { + switch (_j.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _j.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 10]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _j.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + if (!(page.list.creator.did === did)) return [3 /*break*/, 4]; + return [4 /*yield*/, page.list.creator]; + case 3: + _j.sent(); + _j.label = 4; + case 4: + _d = 0, _e = page.items; + _j.label = 5; + case 5: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + item = _e[_d]; + if (!(item.subject.did === did)) return [3 /*break*/, 7]; + return [4 /*yield*/, item.subject]; + case 6: + _j.sent(); + _j.label = 7; + case 7: + _d++; + return [3 /*break*/, 5]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: + allQueryData = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT_ALL], + }); + _f = 0, allQueryData_1 = allQueryData; + _j.label = 11; + case 11: + if (!(_f < allQueryData_1.length)) return [3 /*break*/, 16]; + _g = allQueryData_1[_f], _queryKey = _g[0], queryData = _g[1]; + if (!queryData) { + return [3 /*break*/, 15]; + } + _h = 0, queryData_1 = queryData; + _j.label = 12; + case 12: + if (!(_h < queryData_1.length)) return [3 /*break*/, 15]; + item = queryData_1[_h]; + if (!(item.subject.did === did)) return [3 /*break*/, 14]; + return [4 /*yield*/, item.subject]; + case 13: + _j.sent(); + _j.label = 14; + case 14: + _h++; + return [3 /*break*/, 12]; + case 15: + _f++; + return [3 /*break*/, 11]; + case 16: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/list-memberships.js b/src/state/queries/list-memberships.js new file mode 100644 index 0000000000..6a51072dae --- /dev/null +++ b/src/state/queries/list-memberships.js @@ -0,0 +1,295 @@ +/** + * NOTE + * + * This query is a temporary solution to our lack of server API for + * querying user membership in an API. It is extremely inefficient. + * + * THIS SHOULD ONLY BE USED IN MODALS FOR MODIFYING A USER'S LIST MEMBERSHIP! + * Use the list-members query for rendering a list's members. + * + * It works by fetching *all* of the user's list item records and querying + * or manipulating that cache. For users with large lists, it will fall + * down completely, so be very conservative about how you use it. + * + * -prf + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { AtUri, } from '@atproto/api'; +import { useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { RQKEY as LIST_MEMBERS_RQKEY } from '#/state/queries/list-members'; +import { useAgent, useSession } from '#/state/session'; +import { RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY } from './actor-starter-packs'; +// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records +var SANITY_PAGE_LIMIT = 1000; +var PAGE_SIZE = 100; +// ...which comes 100,000k list members +var RQKEY_ROOT = 'list-memberships'; +export var RQKEY = function () { return [RQKEY_ROOT]; }; +/** + * This API is dangerous! Read the note above! + */ +export function useDangerousListMembershipsQuery() { + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.MINUTES.FIVE, + queryKey: RQKEY(), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var cursor, arr, i, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!currentAccount) { + return [2 /*return*/, []]; + } + arr = []; + i = 0; + _a.label = 1; + case 1: + if (!(i < SANITY_PAGE_LIMIT)) return [3 /*break*/, 4]; + return [4 /*yield*/, agent.app.bsky.graph.listitem.list({ + repo: currentAccount.did, + limit: PAGE_SIZE, + cursor: cursor, + })]; + case 2: + res = _a.sent(); + arr = arr.concat(res.records.map(function (r) { return ({ + membershipUri: r.uri, + listUri: r.value.list, + actorDid: r.value.subject, + }); })); + cursor = res.cursor; + if (!cursor) { + return [3 /*break*/, 4]; + } + _a.label = 3; + case 3: + i++; + return [3 /*break*/, 1]; + case 4: return [2 /*return*/, arr]; + } + }); + }); + }, + }); +} +/** + * Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record) + */ +export function getMembership(memberships, list, actor) { + if (!memberships) { + return undefined; + } + var membership = memberships.find(function (m) { return m.listUri === list && m.actorDid === actor; }); + return membership ? membership.membershipUri : false; +} +export function useListMembershipAddMutation(_a) { + var _this = this; + var _b = _a === void 0 ? {} : _a, subject = _b.subject, onSuccess = _b.onSuccess, onError = _b.onError; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res; + var listUri = _b.listUri, actorDid = _b.actorDid; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) { + throw new Error('Not signed in'); + } + return [4 /*yield*/, agent.app.bsky.graph.listitem.create({ repo: currentAccount.did }, { + subject: actorDid, + list: listUri, + createdAt: new Date().toISOString(), + }) + // TODO + // we need to wait for appview to update, but there's not an efficient + // query for that, so we use a timeout below + // -prf + ]; + case 1: + res = _c.sent(); + // TODO + // we need to wait for appview to update, but there's not an efficient + // query for that, so we use a timeout below + // -prf + return [2 /*return*/, res]; + } + }); + }); }, + onSuccess: function (data, variables) { + // manually update the cache; a refetch is too expensive + var memberships = queryClient.getQueryData(RQKEY()); + if (memberships) { + memberships = memberships + // avoid dups + .filter(function (m) { + return !(m.actorDid === variables.actorDid && + m.listUri === variables.listUri); + }) + .concat([ + __assign(__assign({}, variables), { membershipUri: data.uri }), + ]); + queryClient.setQueryData(RQKEY(), memberships); + } + // invalidate the members queries (used for rendering the listings) + // use a timeout to wait for the appview (see above) + setTimeout(function () { + queryClient.invalidateQueries({ + queryKey: LIST_MEMBERS_RQKEY(variables.listUri), + }); + }, 1e3); + // update WITH_MEMBERSHIPS query + if (subject) { + queryClient.setQueryData(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { return (__assign(__assign({}, page), { starterPacksWithMembership: page.starterPacksWithMembership.map(function (spWithMembership) { + var _a, _b, _c, _d; + if (spWithMembership.starterPack.list && + ((_a = spWithMembership.starterPack.list) === null || _a === void 0 ? void 0 : _a.uri) === variables.listUri) { + return __assign(__assign({}, spWithMembership), { starterPack: __assign(__assign({}, spWithMembership.starterPack), { listItemsSample: __spreadArray([ + { + uri: data.uri, + subject: subject, + } + ], ((_c = (_b = spWithMembership.starterPack.listItemsSample) === null || _b === void 0 ? void 0 : _b.filter(function (item) { return item.subject.did !== variables.actorDid; })) !== null && _c !== void 0 ? _c : []), true), list: __assign(__assign({}, spWithMembership.starterPack.list), { listItemCount: ((_d = spWithMembership.starterPack.list.listItemCount) !== null && _d !== void 0 ? _d : 0) + 1 }) }), listItem: { + uri: data.uri, + subject: subject, + } }); + } + return spWithMembership; + }) })); }) }); + }); + } + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(data); + }, + onError: onError, + }); +} +export function useListMembershipRemoveMutation(_a) { + var _this = this; + var _b = _a === void 0 ? {} : _a, onSuccess = _b.onSuccess, onError = _b.onError; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var membershipUrip; + var membershipUri = _b.membershipUri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) { + throw new Error('Not signed in'); + } + membershipUrip = new AtUri(membershipUri); + return [4 /*yield*/, agent.app.bsky.graph.listitem.delete({ + repo: currentAccount.did, + rkey: membershipUrip.rkey, + }) + // TODO + // we need to wait for appview to update, but there's not an efficient + // query for that, so we use a timeout below + // -prf + ]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (data, variables) { + // manually update the cache; a refetch is too expensive + var memberships = queryClient.getQueryData(RQKEY()); + if (memberships) { + memberships = memberships.filter(function (m) { + return !(m.actorDid === variables.actorDid && + m.listUri === variables.listUri); + }); + queryClient.setQueryData(RQKEY(), memberships); + } + // invalidate the members queries (used for rendering the listings) + // use a timeout to wait for the appview (see above) + setTimeout(function () { + queryClient.invalidateQueries({ + queryKey: LIST_MEMBERS_RQKEY(variables.listUri), + }); + }, 1e3); + queryClient.setQueryData(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { return (__assign(__assign({}, page), { starterPacksWithMembership: page.starterPacksWithMembership.map(function (spWithMembership) { + var _a, _b; + if (spWithMembership.starterPack.list && + spWithMembership.starterPack.list.uri === variables.listUri) { + return __assign(__assign({}, spWithMembership), { starterPack: __assign(__assign({}, spWithMembership.starterPack), { listItemsSample: (_a = spWithMembership.starterPack.listItemsSample) === null || _a === void 0 ? void 0 : _a.filter(function (item) { return item.subject.did !== variables.actorDid; }), list: __assign(__assign({}, spWithMembership.starterPack.list), { listItemCount: Math.max(0, ((_b = spWithMembership.starterPack.list.listItemCount) !== null && _b !== void 0 ? _b : 1) - 1) }) }), listItem: undefined }); + } + return spWithMembership; + }) })); }) }); + }); + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(data); + }, + onError: onError, + }); +} diff --git a/src/state/queries/list.js b/src/state/queries/list.js new file mode 100644 index 0000000000..72cb1d65e0 --- /dev/null +++ b/src/state/queries/list.js @@ -0,0 +1,389 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AtUri, } from '@atproto/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import chunk from 'lodash.chunk'; +import { uploadBlob } from '#/lib/api'; +import { until } from '#/lib/async/until'; +import { STALE } from '#/state/queries'; +import { useAgent, useSession } from '#/state/session'; +import { invalidate as invalidateMyLists } from './my-lists'; +import { RQKEY as PROFILE_LISTS_RQKEY } from './profile-lists'; +export var RQKEY_ROOT = 'list'; +export var RQKEY = function (uri) { return [RQKEY_ROOT, uri]; }; +export function useListQuery(uri) { + var agent = useAgent(); + return useQuery({ + staleTime: STALE.MINUTES.ONE, + queryKey: RQKEY(uri || ''), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!uri) { + throw new Error('URI not provided'); + } + return [4 /*yield*/, agent.app.bsky.graph.getList({ + list: uri, + limit: 1, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.list]; + } + }); + }); + }, + enabled: !!uri, + }); +} +export function useListCreateMutation() { + var currentAccount = useSession().currentAccount; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var record, blobRes, res; + var purpose = _b.purpose, name = _b.name, description = _b.description, descriptionFacets = _b.descriptionFacets, avatar = _b.avatar; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) { + throw new Error('Not signed in'); + } + if (purpose !== 'app.bsky.graph.defs#curatelist' && + purpose !== 'app.bsky.graph.defs#modlist') { + throw new Error('Invalid list purpose: must be curatelist or modlist'); + } + record = { + purpose: purpose, + name: name, + description: description, + descriptionFacets: descriptionFacets, + avatar: undefined, + createdAt: new Date().toISOString(), + }; + if (!avatar) return [3 /*break*/, 2]; + return [4 /*yield*/, uploadBlob(agent, avatar.path, avatar.mime)]; + case 1: + blobRes = _c.sent(); + record.avatar = blobRes.data.blob; + _c.label = 2; + case 2: return [4 /*yield*/, agent.app.bsky.graph.list.create({ + repo: currentAccount.did, + }, record) + // wait for the appview to update + ]; + case 3: + res = _c.sent(); + // wait for the appview to update + return [4 /*yield*/, whenAppViewReady(agent, res.uri, function (v) { + var _a; + return typeof ((_a = v === null || v === void 0 ? void 0 : v.data) === null || _a === void 0 ? void 0 : _a.list.uri) === 'string'; + })]; + case 4: + // wait for the appview to update + _c.sent(); + return [2 /*return*/, res]; + } + }); + }); + }, + onSuccess: function () { + invalidateMyLists(queryClient); + queryClient.invalidateQueries({ + queryKey: PROFILE_LISTS_RQKEY(currentAccount.did), + }); + }, + }); +} +export function useListMetadataMutation() { + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var _c, hostname, rkey, record, blobRes, res; + var uri = _b.uri, name = _b.name, description = _b.description, descriptionFacets = _b.descriptionFacets, avatar = _b.avatar; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + _c = new AtUri(uri), hostname = _c.hostname, rkey = _c.rkey; + if (!currentAccount) { + throw new Error('Not signed in'); + } + if (currentAccount.did !== hostname) { + throw new Error('You do not own this list'); + } + return [4 /*yield*/, agent.app.bsky.graph.list.get({ + repo: currentAccount.did, + rkey: rkey, + }) + // update the fields + ]; + case 1: + record = (_d.sent()).value; + // update the fields + record.name = name; + record.description = description; + record.descriptionFacets = descriptionFacets; + if (!avatar) return [3 /*break*/, 3]; + return [4 /*yield*/, uploadBlob(agent, avatar.path, avatar.mime)]; + case 2: + blobRes = _d.sent(); + record.avatar = blobRes.data.blob; + return [3 /*break*/, 4]; + case 3: + if (avatar === null) { + record.avatar = undefined; + } + _d.label = 4; + case 4: return [4 /*yield*/, agent.com.atproto.repo.putRecord({ + repo: currentAccount.did, + collection: 'app.bsky.graph.list', + rkey: rkey, + record: record, + })]; + case 5: + res = (_d.sent()).data; + // wait for the appview to update + return [4 /*yield*/, whenAppViewReady(agent, res.uri, function (v) { + var list = v.data.list; + return (list.name === record.name && list.description === record.description); + })]; + case 6: + // wait for the appview to update + _d.sent(); + return [2 /*return*/, res]; + } + }); + }); + }, + onSuccess: function (data, variables) { + invalidateMyLists(queryClient); + queryClient.invalidateQueries({ + queryKey: PROFILE_LISTS_RQKEY(currentAccount.did), + }); + queryClient.invalidateQueries({ + queryKey: RQKEY(variables.uri), + }); + }, + }); +} +export function useListDeleteMutation() { + var _this = this; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var cursor, listitemRecordUris, i, res, createDel, writes, _i, _c, writesChunk; + var uri = _b.uri; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + if (!currentAccount) { + return [2 /*return*/]; + } + listitemRecordUris = []; + i = 0; + _d.label = 1; + case 1: + if (!(i < 100)) return [3 /*break*/, 4]; + return [4 /*yield*/, agent.app.bsky.graph.listitem.list({ + repo: currentAccount.did, + cursor: cursor, + limit: 100, + })]; + case 2: + res = _d.sent(); + listitemRecordUris = listitemRecordUris.concat(res.records + .filter(function (record) { return record.value.list === uri; }) + .map(function (record) { return record.uri; })); + cursor = res.cursor; + if (!cursor) { + return [3 /*break*/, 4]; + } + _d.label = 3; + case 3: + i++; + return [3 /*break*/, 1]; + case 4: + createDel = function (uri) { + var urip = new AtUri(uri); + return { + $type: 'com.atproto.repo.applyWrites#delete', + collection: urip.collection, + rkey: urip.rkey, + }; + }; + writes = listitemRecordUris + .map(function (uri) { return createDel(uri); }) + .concat([createDel(uri)]); + _i = 0, _c = chunk(writes, 10); + _d.label = 5; + case 5: + if (!(_i < _c.length)) return [3 /*break*/, 8]; + writesChunk = _c[_i]; + return [4 /*yield*/, agent.com.atproto.repo.applyWrites({ + repo: currentAccount.did, + writes: writesChunk, + })]; + case 6: + _d.sent(); + _d.label = 7; + case 7: + _i++; + return [3 /*break*/, 5]; + case 8: + // wait for the appview to update + return [4 /*yield*/, whenAppViewReady(agent, uri, function (v) { + return !(v === null || v === void 0 ? void 0 : v.success); + })]; + case 9: + // wait for the appview to update + _d.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { + invalidateMyLists(queryClient); + queryClient.invalidateQueries({ + queryKey: PROFILE_LISTS_RQKEY(currentAccount.did), + }); + // TODO!! /* dont await */ this.rootStore.preferences.removeSavedFeed(this.uri) + }, + }); +} +export function useListMuteMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var uri = _b.uri, mute = _b.mute; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!mute) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.muteModList(uri)]; + case 1: + _c.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, agent.unmuteModList(uri)]; + case 3: + _c.sent(); + _c.label = 4; + case 4: return [4 /*yield*/, whenAppViewReady(agent, uri, function (v) { + var _a; + return Boolean((_a = v === null || v === void 0 ? void 0 : v.data.list.viewer) === null || _a === void 0 ? void 0 : _a.muted) === mute; + })]; + case 5: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (data, variables) { + queryClient.invalidateQueries({ + queryKey: RQKEY(variables.uri), + }); + }, + }); +} +export function useListBlockMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var uri = _b.uri, block = _b.block; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!block) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.blockModList(uri)]; + case 1: + _c.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, agent.unblockModList(uri)]; + case 3: + _c.sent(); + _c.label = 4; + case 4: return [4 /*yield*/, whenAppViewReady(agent, uri, function (v) { + var _a, _b; + return block + ? typeof ((_a = v === null || v === void 0 ? void 0 : v.data.list.viewer) === null || _a === void 0 ? void 0 : _a.blocked) === 'string' + : !((_b = v === null || v === void 0 ? void 0 : v.data.list.viewer) === null || _b === void 0 ? void 0 : _b.blocked); + })]; + case 5: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (data, variables) { + queryClient.invalidateQueries({ + queryKey: RQKEY(variables.uri), + }); + }, + }); +} +function whenAppViewReady(agent, uri, fn) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, until(5, // 5 tries + 1e3, // 1s delay between tries + fn, function () { + return agent.app.bsky.graph.getList({ + list: uri, + limit: 1, + }); + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} diff --git a/src/state/queries/messages/accept-conversation.js b/src/state/queries/messages/accept-conversation.js new file mode 100644 index 0000000000..6360b3a9da --- /dev/null +++ b/src/state/queries/messages/accept-conversation.js @@ -0,0 +1,134 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { RQKEY as CONVO_LIST_KEY, RQKEY_ROOT as CONVO_LIST_ROOT_KEY, } from './list-conversations'; +export function useAcceptConversation(convoId, _a) { + var _this = this; + var onSuccess = _a.onSuccess, onMutate = _a.onMutate, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.chat.bsky.convo.acceptConvo({ convoId: convoId }, { headers: DM_SERVICE_HEADERS })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + onMutate: function () { + var prevAcceptedPages = []; + var prevInboxPages = []; + var convoBeingAccepted; + queryClient.setQueryData(CONVO_LIST_KEY('request'), function (old) { + if (!old) + return old; + prevInboxPages = old.pages; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { + var found = page.convos.find(function (convo) { return convo.id === convoId; }); + if (found) { + convoBeingAccepted = found; + return __assign(__assign({}, page), { convos: page.convos.filter(function (convo) { return convo.id !== convoId; }) }); + } + return page; + }) }); + }); + queryClient.setQueryData(CONVO_LIST_KEY('accepted'), function (old) { + if (!old) + return old; + prevAcceptedPages = old.pages; + if (convoBeingAccepted) { + return __assign(__assign({}, old), { pages: __spreadArray([ + __assign(__assign({}, old.pages[0]), { convos: __spreadArray([ + __assign(__assign({}, convoBeingAccepted), { status: 'accepted' }) + ], old.pages[0].convos, true) }) + ], old.pages.slice(1), true) }); + } + else { + return old; + } + }); + onMutate === null || onMutate === void 0 ? void 0 : onMutate(); + return { prevAcceptedPages: prevAcceptedPages, prevInboxPages: prevInboxPages }; + }, + onSuccess: function (data) { + queryClient.invalidateQueries({ queryKey: [CONVO_LIST_KEY] }); + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(data); + }, + onError: function (error, _, context) { + logger.error(error); + queryClient.setQueryData(CONVO_LIST_KEY('accepted'), function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: (context === null || context === void 0 ? void 0 : context.prevAcceptedPages) || old.pages }); + }); + queryClient.setQueryData(CONVO_LIST_KEY('request'), function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: (context === null || context === void 0 ? void 0 : context.prevInboxPages) || old.pages }); + }); + queryClient.invalidateQueries({ queryKey: [CONVO_LIST_ROOT_KEY] }); + onError === null || onError === void 0 ? void 0 : onError(error); + }, + }); +} diff --git a/src/state/queries/messages/actor-declaration.js b/src/state/queries/messages/actor-declaration.js new file mode 100644 index 0000000000..884d3808cc --- /dev/null +++ b/src/state/queries/messages/actor-declaration.js @@ -0,0 +1,129 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { logger } from '#/logger'; +import { useAgent, useSession } from '#/state/session'; +import { RQKEY as PROFILE_RKEY } from '../profile'; +export function useUpdateActorDeclaration(_a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var queryClient = useQueryClient(); + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + return useMutation({ + mutationFn: function (allowIncoming) { return __awaiter(_this, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!currentAccount) + throw new Error('Not signed in'); + return [4 /*yield*/, agent.com.atproto.repo.putRecord({ + repo: currentAccount.did, + collection: 'chat.bsky.actor.declaration', + rkey: 'self', + record: { + $type: 'chat.bsky.actor.declaration', + allowIncoming: allowIncoming, + }, + })]; + case 1: + result = _a.sent(); + return [2 /*return*/, result]; + } + }); + }); }, + onMutate: function (allowIncoming) { + if (!currentAccount) + return; + queryClient.setQueryData(PROFILE_RKEY(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did), function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { associated: __assign(__assign({}, old.associated), { chat: { + allowIncoming: allowIncoming, + } }) }); + }); + }, + onSuccess: onSuccess, + onError: function (error) { + logger.error(error); + if (currentAccount) { + queryClient.invalidateQueries({ + queryKey: PROFILE_RKEY(currentAccount.did), + }); + } + onError === null || onError === void 0 ? void 0 : onError(error); + }, + }); +} +// for use in the settings screen for testing +export function useDeleteActorDeclaration() { + var _this = this; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + return useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!currentAccount) + throw new Error('Not signed in'); + return [4 /*yield*/, agent.api.com.atproto.repo.deleteRecord({ + repo: currentAccount.did, + collection: 'chat.bsky.actor.declaration', + rkey: 'self', + })]; + case 1: + result = _a.sent(); + return [2 /*return*/, result]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/messages/conversation.js b/src/state/queries/messages/conversation.js new file mode 100644 index 0000000000..533fe1b7b9 --- /dev/null +++ b/src/state/queries/messages/conversation.js @@ -0,0 +1,136 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { STALE } from '#/state/queries'; +import { useOnMarkAsRead } from '#/state/queries/messages/list-conversations'; +import { useAgent } from '#/state/session'; +import { getConvoFromQueryData, RQKEY_ROOT as LIST_CONVOS_KEY, } from './list-conversations'; +var RQKEY_ROOT = 'convo'; +export var RQKEY = function (convoId) { return [RQKEY_ROOT, convoId]; }; +export function useConvoQuery(convo) { + var _this = this; + var agent = useAgent(); + return useQuery({ + queryKey: RQKEY(convo.id), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.chat.bsky.convo.getConvo({ convoId: convo.id }, { headers: DM_SERVICE_HEADERS })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data.convo]; + } + }); + }); }, + initialData: convo, + staleTime: STALE.INFINITY, + }); +} +export function precacheConvoQuery(queryClient, convo) { + queryClient.setQueryData(RQKEY(convo.id), convo); +} +export function useMarkAsReadMutation() { + var _this = this; + var optimisticUpdate = useOnMarkAsRead(); + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var convoId = _b.convoId, messageId = _b.messageId; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!convoId) + throw new Error('No convoId provided'); + return [4 /*yield*/, agent.api.chat.bsky.convo.updateRead({ + convoId: convoId, + messageId: messageId, + }, { + encoding: 'application/json', + headers: DM_SERVICE_HEADERS, + })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onMutate: function (_a) { + var convoId = _a.convoId; + if (!convoId) + throw new Error('No convoId provided'); + optimisticUpdate(convoId); + }, + onSuccess: function (_, _a) { + var convoId = _a.convoId; + if (!convoId) + return; + queryClient.setQueriesData({ queryKey: [LIST_CONVOS_KEY] }, function (old) { + if (!old) + return old; + var existingConvo = getConvoFromQueryData(convoId, old); + if (existingConvo) { + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { + return __assign(__assign({}, page), { convos: page.convos.map(function (convo) { + if (convo.id === convoId) { + return __assign(__assign({}, convo), { unreadCount: 0 }); + } + return convo; + }) }); + }) }); + } + else { + // If we somehow marked a convo as read that doesn't exist in the + // list, then we don't need to do anything. + } + }); + }, + }); +} diff --git a/src/state/queries/messages/get-convo-availability.js b/src/state/queries/messages/get-convo-availability.js new file mode 100644 index 0000000000..4fe0aa198d --- /dev/null +++ b/src/state/queries/messages/get-convo-availability.js @@ -0,0 +1,61 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { useAgent } from '#/state/session'; +import { STALE } from '..'; +var RQKEY_ROOT = 'convo-availability'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export function useGetConvoAvailabilityQuery(did) { + var _this = this; + var agent = useAgent(); + return useQuery({ + queryKey: RQKEY(did), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.chat.bsky.convo.getConvoAvailability({ members: [did] }, { headers: DM_SERVICE_HEADERS })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + staleTime: STALE.INFINITY, + }); +} diff --git a/src/state/queries/messages/get-convo-for-members.js b/src/state/queries/messages/get-convo-for-members.js new file mode 100644 index 0000000000..9499eae0f9 --- /dev/null +++ b/src/state/queries/messages/get-convo-for-members.js @@ -0,0 +1,68 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { precacheConvoQuery } from './conversation'; +export function useGetConvoForMembers(_a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (members) { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.chat.bsky.convo.getConvoForMembers({ members: members }, { headers: DM_SERVICE_HEADERS })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + onSuccess: function (data) { + precacheConvoQuery(queryClient, data.convo); + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(data); + }, + onError: function (error) { + logger.error(error); + onError === null || onError === void 0 ? void 0 : onError(error); + }, + }); +} diff --git a/src/state/queries/messages/leave-conversation.js b/src/state/queries/messages/leave-conversation.js new file mode 100644 index 0000000000..adbbccbbc2 --- /dev/null +++ b/src/state/queries/messages/leave-conversation.js @@ -0,0 +1,132 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useMemo } from 'react'; +import { useMutation, useMutationState, useQueryClient, } from '@tanstack/react-query'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { RQKEY_ROOT as CONVO_LIST_KEY } from './list-conversations'; +var RQKEY_ROOT = 'leave-convo'; +export function RQKEY(convoId) { + return [RQKEY_ROOT, convoId]; +} +export function useLeaveConvo(convoId, _a) { + var _this = this; + var onSuccess = _a.onSuccess, onMutate = _a.onMutate, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationKey: RQKEY(convoId), + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!convoId) + throw new Error('No convoId provided'); + return [4 /*yield*/, agent.chat.bsky.convo.leaveConvo({ convoId: convoId }, { headers: DM_SERVICE_HEADERS, encoding: 'application/json' })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + onMutate: function () { + var prevPages = []; + queryClient.setQueryData([CONVO_LIST_KEY], function (old) { + if (!old) + return old; + prevPages = old.pages; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { + return __assign(__assign({}, page), { convos: page.convos.filter(function (convo) { return convo.id !== convoId; }) }); + }) }); + }); + onMutate === null || onMutate === void 0 ? void 0 : onMutate(); + return { prevPages: prevPages }; + }, + onSuccess: function (data) { + queryClient.invalidateQueries({ queryKey: [CONVO_LIST_KEY] }); + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(data); + }, + onError: function (error, _, context) { + logger.error(error); + queryClient.setQueryData([CONVO_LIST_KEY], function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: (context === null || context === void 0 ? void 0 : context.prevPages) || old.pages }); + }); + queryClient.invalidateQueries({ queryKey: [CONVO_LIST_KEY] }); + onError === null || onError === void 0 ? void 0 : onError(error); + }, + }); +} +/** + * Gets currently pending and successful leave convo mutations + * + * @returns Array of `convoId` + */ +export function useLeftConvos() { + var pending = useMutationState({ + filters: { mutationKey: [RQKEY_ROOT], status: 'pending' }, + select: function (mutation) { var _a; return (_a = mutation.options.mutationKey) === null || _a === void 0 ? void 0 : _a[1]; }, + }); + var success = useMutationState({ + filters: { mutationKey: [RQKEY_ROOT], status: 'success' }, + select: function (mutation) { var _a; return (_a = mutation.options.mutationKey) === null || _a === void 0 ? void 0 : _a[1]; }, + }); + return useMemo(function () { return __spreadArray(__spreadArray([], pending, true), success, true).filter(function (id) { return id !== undefined; }); }, [pending, success]); +} diff --git a/src/state/queries/messages/list-conversations.js b/src/state/queries/messages/list-conversations.js new file mode 100644 index 0000000000..16fee91475 --- /dev/null +++ b/src/state/queries/messages/list-conversations.js @@ -0,0 +1,475 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; +import { ChatBskyConvoDefs, moderateProfile, } from '@atproto/api'; +import { useInfiniteQuery, useQueryClient, } from '@tanstack/react-query'; +import throttle from 'lodash.throttle'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { useCurrentConvoId } from '#/state/messages/current-convo-id'; +import { useMessagesEventBus } from '#/state/messages/events'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useAgent, useSession } from '#/state/session'; +import { useLeftConvos } from './leave-conversation'; +export var RQKEY_ROOT = 'convo-list'; +export var RQKEY = function (status, readState) { + if (readState === void 0) { readState = 'all'; } + return [RQKEY_ROOT, status, readState]; +}; +export function useListConvosQuery(_a) { + var _this = this; + var _b = _a === void 0 ? {} : _a, enabled = _b.enabled, status = _b.status, _c = _b.readState, readState = _c === void 0 ? 'all' : _c; + var agent = useAgent(); + return useInfiniteQuery({ + enabled: enabled, + queryKey: RQKEY(status !== null && status !== void 0 ? status : 'all', readState), + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var data; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.chat.bsky.convo.listConvos({ + limit: 20, + cursor: pageParam, + readState: readState === 'unread' ? 'unread' : undefined, + status: status, + }, { headers: DM_SERVICE_HEADERS })]; + case 1: + data = (_c.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} +var ListConvosContext = createContext(null); +ListConvosContext.displayName = 'ListConvosContext'; +export function useListConvos() { + var ctx = useContext(ListConvosContext); + if (!ctx) { + throw new Error('useListConvos must be used within a ListConvosProvider'); + } + return ctx; +} +var empty = { accepted: [], request: [] }; +export function ListConvosProvider(_a) { + var children = _a.children; + var hasSession = useSession().hasSession; + if (!hasSession) { + return (_jsx(ListConvosContext.Provider, { value: empty, children: children })); + } + return _jsx(ListConvosProviderInner, { children: children }); +} +export function ListConvosProviderInner(_a) { + var children = _a.children; + var _b = useListConvosQuery({ readState: 'unread' }), refetch = _b.refetch, data = _b.data; + var messagesBus = useMessagesEventBus(); + var queryClient = useQueryClient(); + var currentConvoId = useCurrentConvoId().currentConvoId; + var currentAccount = useSession().currentAccount; + var leftConvos = useLeftConvos(); + var debouncedRefetch = useMemo(function () { + var refetchAndInvalidate = function () { + refetch(); + queryClient.invalidateQueries({ queryKey: [RQKEY_ROOT] }); + }; + return throttle(refetchAndInvalidate, 500, { + leading: true, + trailing: true, + }); + }, [refetch, queryClient]); + useEffect(function () { + var unsub = messagesBus.on(function (events) { + if (events.type !== 'logs') + return; + var _loop_1 = function (log) { + if (ChatBskyConvoDefs.isLogBeginConvo(log)) { + debouncedRefetch(); + } + else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) { + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { return optimisticDelete(log.convoId, old); }); + } + else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) { + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { + return optimisticUpdate(log.convoId, old, function (convo) { + if ((ChatBskyConvoDefs.isDeletedMessageView(log.message) || + ChatBskyConvoDefs.isMessageView(log.message)) && + (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage) || + ChatBskyConvoDefs.isMessageView(convo.lastMessage))) { + return log.message.id === convo.lastMessage.id + ? __assign(__assign({}, convo), { rev: log.rev, lastMessage: log.message }) : convo; + } + else { + return convo; + } + }); + }); + } + else if (ChatBskyConvoDefs.isLogCreateMessage(log)) { + // Store in a new var to avoid TS errors due to closures. + var logRef_1 = log; + // Get all matching queries + var queries = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + // Check if convo exists in any query + var foundConvo = null; + for (var _b = 0, queries_1 = queries; _b < queries_1.length; _b++) { + var _c = queries_1[_b], _key = _c[0], query = _c[1]; + if (!query) + continue; + var convo = getConvoFromQueryData(logRef_1.convoId, query); + if (convo) { + foundConvo = convo; + break; + } + } + if (!foundConvo) { + // Convo not found, trigger refetch + debouncedRefetch(); + return { value: void 0 }; + } + // Update the convo + var updatedConvo_1 = __assign(__assign({}, foundConvo), { rev: logRef_1.rev, lastMessage: logRef_1.message, unreadCount: foundConvo.id !== currentConvoId + ? (ChatBskyConvoDefs.isMessageView(logRef_1.message) || + ChatBskyConvoDefs.isDeletedMessageView(logRef_1.message)) && + logRef_1.message.sender.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) + ? foundConvo.unreadCount + 1 + : foundConvo.unreadCount + : 0 }); + function filterConvoFromPage(convo) { + return convo.filter(function (c) { return c.id !== logRef_1.convoId; }); + } + // Update all matching queries + function updateFn(old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page, i) { + if (i === 0) { + return __assign(__assign({}, page), { convos: __spreadArray([ + updatedConvo_1 + ], filterConvoFromPage(page.convos), true) }); + } + return __assign(__assign({}, page), { convos: filterConvoFromPage(page.convos) }); + }) }); + } + // always update the unread one + queryClient.setQueriesData({ queryKey: RQKEY('all', 'unread') }, function (old) { + return old + ? updateFn(old) + : { + pageParams: [undefined], + pages: [{ convos: [updatedConvo_1], cursor: undefined }], + }; + }); + // update the other ones based on status of the incoming message + if (updatedConvo_1.status === 'accepted') { + queryClient.setQueriesData({ queryKey: RQKEY('accepted') }, updateFn); + } + else if (updatedConvo_1.status === 'request') { + queryClient.setQueriesData({ queryKey: RQKEY('request') }, updateFn); + } + } + else if (ChatBskyConvoDefs.isLogReadMessage(log)) { + var logRef_2 = log; + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { + return optimisticUpdate(logRef_2.convoId, old, function (convo) { return (__assign(__assign({}, convo), { unreadCount: 0, rev: logRef_2.rev })); }); + }); + } + else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) { + var logRef_3 = log; + var requests = queryClient.getQueryData(RQKEY('request')); + if (!requests) { + debouncedRefetch(); + return { value: void 0 }; + } + var acceptedConvo_1 = getConvoFromQueryData(log.convoId, requests); + if (!acceptedConvo_1) { + debouncedRefetch(); + return { value: void 0 }; + } + queryClient.setQueryData(RQKEY('request'), function (old) { + return optimisticDelete(logRef_3.convoId, old); + }); + queryClient.setQueriesData({ queryKey: RQKEY('accepted') }, function (old) { + if (!old) { + debouncedRefetch(); + return old; + } + return __assign(__assign({}, old), { pages: old.pages.map(function (page, i) { + if (i === 0) { + return __assign(__assign({}, page), { convos: __spreadArray([ + __assign(__assign({}, acceptedConvo_1), { status: 'accepted' }) + ], page.convos, true) }); + } + return page; + }) }); + }); + } + else if (ChatBskyConvoDefs.isLogMuteConvo(log)) { + var logRef_4 = log; + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { + return optimisticUpdate(logRef_4.convoId, old, function (convo) { return (__assign(__assign({}, convo), { muted: true, rev: logRef_4.rev })); }); + }); + } + else if (ChatBskyConvoDefs.isLogUnmuteConvo(log)) { + var logRef_5 = log; + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { + return optimisticUpdate(logRef_5.convoId, old, function (convo) { return (__assign(__assign({}, convo), { muted: false, rev: logRef_5.rev })); }); + }); + } + else if (ChatBskyConvoDefs.isLogAddReaction(log)) { + var logRef_6 = log; + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { + return optimisticUpdate(logRef_6.convoId, old, function (convo) { return (__assign(__assign({}, convo), { lastReaction: { + $type: 'chat.bsky.convo.defs#messageAndReactionView', + reaction: logRef_6.reaction, + message: logRef_6.message, + }, rev: logRef_6.rev })); }); + }); + } + else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) { + var logRef_7 = log; + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { + return optimisticUpdate(logRef_7.convoId, old, function (convo) { + if ( + // if the convo is the same + logRef_7.convoId === convo.id && + ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction) && + ChatBskyConvoDefs.isMessageView(logRef_7.message) && + // ...and the message is the same + convo.lastReaction.message.id === logRef_7.message.id && + // ...and the reaction is the same + convo.lastReaction.reaction.sender.did === + logRef_7.reaction.sender.did && + convo.lastReaction.reaction.value === logRef_7.reaction.value) { + return __assign(__assign({}, convo), { + // ...remove the reaction. hopefully they didn't react twice in a row! + lastReaction: undefined, rev: logRef_7.rev }); + } + else { + return convo; + } + }); + }); + } + }; + for (var _i = 0, _a = events.logs; _i < _a.length; _i++) { + var log = _a[_i]; + var state_1 = _loop_1(log); + if (typeof state_1 === "object") + return state_1.value; + } + }, { + // get events for all chats + convoId: undefined, + }); + return function () { return unsub(); }; + }, [ + messagesBus, + currentConvoId, + queryClient, + currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + debouncedRefetch, + ]); + var ctx = useMemo(function () { + var _a; + var convos = (_a = data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { return page.convos; }).filter(function (convo) { return !leftConvos.includes(convo.id); })) !== null && _a !== void 0 ? _a : []; + return { + accepted: convos.filter(function (conv) { return conv.status === 'accepted'; }), + request: convos.filter(function (conv) { return conv.status === 'request'; }), + }; + }, [data, leftConvos]); + return (_jsx(ListConvosContext.Provider, { value: ctx, children: children })); +} +export function useUnreadMessageCount() { + var currentConvoId = useCurrentConvoId().currentConvoId; + var currentAccount = useSession().currentAccount; + var _a = useListConvos(), accepted = _a.accepted, request = _a.request; + var moderationOpts = useModerationOpts(); + return useMemo(function () { + var acceptedCount = calculateCount(accepted, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, currentConvoId, moderationOpts); + var requestCount = calculateCount(request, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, currentConvoId, moderationOpts); + if (acceptedCount > 0) { + var total = acceptedCount + Math.min(requestCount, 1); + return { + count: total, + numUnread: total > 10 ? '10+' : String(total), + // only needed when numUnread is undefined + hasNew: false, + }; + } + else if (requestCount > 0) { + return { + count: 1, + numUnread: undefined, + hasNew: true, + }; + } + else { + return { + count: 0, + numUnread: undefined, + hasNew: false, + }; + } + }, [accepted, request, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, currentConvoId, moderationOpts]); +} +function calculateCount(convos, currentAccountDid, currentConvoId, moderationOpts) { + var _a; + return ((_a = convos + .filter(function (convo) { return convo.id !== currentConvoId; }) + .reduce(function (acc, convo) { + var otherMember = convo.members.find(function (member) { return member.did !== currentAccountDid; }); + if (!otherMember || !moderationOpts) + return acc; + var moderation = moderateProfile(otherMember, moderationOpts); + var shouldIgnore = convo.muted || + moderation.blocked || + otherMember.handle === 'missing.invalid'; + var unreadCount = !shouldIgnore && convo.unreadCount > 0 ? 1 : 0; + return acc + unreadCount; + }, 0)) !== null && _a !== void 0 ? _a : 0); +} +export function useOnMarkAsRead() { + var queryClient = useQueryClient(); + return useCallback(function (chatId) { + queryClient.setQueriesData({ queryKey: [RQKEY_ROOT] }, function (old) { + if (!old) + return old; + return optimisticUpdate(chatId, old, function (convo) { return (__assign(__assign({}, convo), { unreadCount: 0 })); }); + }); + }, [queryClient]); +} +function optimisticUpdate(chatId, old, updateFn) { + if (!old || !updateFn) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { return (__assign(__assign({}, page), { convos: page.convos.map(function (convo) { + return chatId === convo.id ? updateFn(convo) : convo; + }) })); }) }); +} +function optimisticDelete(chatId, old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { return (__assign(__assign({}, page), { convos: page.convos.filter(function (convo) { return chatId !== convo.id; }) })); }) }); +} +export function getConvoFromQueryData(chatId, old) { + for (var _i = 0, _a = old.pages; _i < _a.length; _i++) { + var page = _a[_i]; + for (var _b = 0, _c = page.convos; _b < _c.length; _b++) { + var convo = _c[_b]; + if (convo.id === chatId) { + return convo; + } + } + } + return null; +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, convo, _f, _g, member; + return __generator(this, function (_h) { + switch (_h.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _h.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 10]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData.pages; + _h.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + _d = 0, _e = page.convos; + _h.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + convo = _e[_d]; + _f = 0, _g = convo.members; + _h.label = 4; + case 4: + if (!(_f < _g.length)) return [3 /*break*/, 7]; + member = _g[_f]; + if (!(member.did === did)) return [3 /*break*/, 6]; + return [4 /*yield*/, member]; + case 5: + _h.sent(); + _h.label = 6; + case 6: + _f++; + return [3 /*break*/, 4]; + case 7: + _d++; + return [3 /*break*/, 3]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/messages/mute-conversation.js b/src/state/queries/messages/mute-conversation.js new file mode 100644 index 0000000000..4cc454ee52 --- /dev/null +++ b/src/state/queries/messages/mute-conversation.js @@ -0,0 +1,100 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQueryClient, } from '@tanstack/react-query'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { useAgent } from '#/state/session'; +import { RQKEY as CONVO_KEY } from './conversation'; +import { RQKEY_ROOT as CONVO_LIST_KEY } from './list-conversations'; +export function useMuteConvo(convoId, _a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var data, data; + var mute = _b.mute; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!convoId) + throw new Error('No convoId provided'); + if (!mute) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.api.chat.bsky.convo.muteConvo({ convoId: convoId }, { headers: DM_SERVICE_HEADERS, encoding: 'application/json' })]; + case 1: + data = (_c.sent()).data; + return [2 /*return*/, data]; + case 2: return [4 /*yield*/, agent.api.chat.bsky.convo.unmuteConvo({ convoId: convoId }, { headers: DM_SERVICE_HEADERS, encoding: 'application/json' })]; + case 3: + data = (_c.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + onSuccess: function (data, params) { + queryClient.setQueryData(CONVO_KEY(data.convo.id), function (prev) { + if (!prev) + return; + return __assign(__assign({}, prev), { muted: params.mute }); + }); + queryClient.setQueryData([CONVO_LIST_KEY], function (prev) { + if (!(prev === null || prev === void 0 ? void 0 : prev.pages)) + return; + return __assign(__assign({}, prev), { pages: prev.pages.map(function (page) { return (__assign(__assign({}, page), { convos: page.convos.map(function (convo) { + if (convo.id !== data.convo.id) + return convo; + return __assign(__assign({}, convo), { muted: params.mute }); + }) })); }) }); + }); + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(data); + }, + onError: function (e) { + onError === null || onError === void 0 ? void 0 : onError(e); + }, + }); +} diff --git a/src/state/queries/messages/update-all-read.js b/src/state/queries/messages/update-all-read.js new file mode 100644 index 0000000000..016c71eedd --- /dev/null +++ b/src/state/queries/messages/update-all-read.js @@ -0,0 +1,109 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { DM_SERVICE_HEADERS } from '#/lib/constants'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { RQKEY as CONVO_LIST_KEY } from './list-conversations'; +export function useUpdateAllRead(status, _a) { + var _this = this; + var onSuccess = _a.onSuccess, onMutate = _a.onMutate, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.chat.bsky.convo.updateAllRead({ status: status }, { headers: DM_SERVICE_HEADERS, encoding: 'application/json' })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + onMutate: function () { + var prevPages = []; + queryClient.setQueryData(CONVO_LIST_KEY(status), function (old) { + if (!old) + return old; + prevPages = old.pages; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { + return __assign(__assign({}, page), { convos: page.convos.map(function (convo) { + return __assign(__assign({}, convo), { unreadCount: 0 }); + }) }); + }) }); + }); + // remove unread convos from the badge query + queryClient.setQueryData(CONVO_LIST_KEY('all', 'unread'), function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { + return __assign(__assign({}, page), { convos: page.convos.filter(function (convo) { return convo.status !== status; }) }); + }) }); + }); + onMutate === null || onMutate === void 0 ? void 0 : onMutate(); + return { prevPages: prevPages }; + }, + onSuccess: function () { + queryClient.invalidateQueries({ queryKey: CONVO_LIST_KEY(status) }); + onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(); + }, + onError: function (error, _, context) { + logger.error(error); + queryClient.setQueryData(CONVO_LIST_KEY(status), function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: (context === null || context === void 0 ? void 0 : context.prevPages) || old.pages }); + }); + queryClient.invalidateQueries({ queryKey: CONVO_LIST_KEY(status) }); + queryClient.invalidateQueries({ queryKey: CONVO_LIST_KEY('all', 'unread') }); + onError === null || onError === void 0 ? void 0 : onError(error); + }, + }); +} diff --git a/src/state/queries/my-blocked-accounts.js b/src/state/queries/my-blocked-accounts.js new file mode 100644 index 0000000000..ef97c9ddf8 --- /dev/null +++ b/src/state/queries/my-blocked-accounts.js @@ -0,0 +1,109 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +var RQKEY_ROOT = 'my-blocked-accounts'; +export var RQKEY = function () { return [RQKEY_ROOT]; }; +export function useMyBlockedAccountsQuery() { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getBlocks({ + limit: 30, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, block; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.blocks; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + block = _e[_d]; + if (!(block.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, block]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/my-lists.js b/src/state/queries/my-lists.js new file mode 100644 index 0000000000..4b8e320a54 --- /dev/null +++ b/src/state/queries/my-lists.js @@ -0,0 +1,132 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { accumulate } from '#/lib/async/accumulate'; +import { STALE } from '#/state/queries'; +import { useAgent, useSession } from '#/state/session'; +var RQKEY_ROOT = 'my-lists'; +export var RQKEY = function (filter) { return [RQKEY_ROOT, filter]; }; +export function useMyListsQuery(filter) { + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.MINUTES.ONE, + queryKey: RQKEY(filter), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var lists, promises, resultset, _i, resultset_1, res, _loop_1, _a, res_1, list; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + lists = []; + promises = [ + accumulate(function (cursor) { + return agent.app.bsky.graph + .getLists({ + actor: currentAccount.did, + cursor: cursor, + limit: 50, + }) + .then(function (res) { return ({ + cursor: res.data.cursor, + items: res.data.lists, + }); }); + }), + ]; + if (filter === 'all-including-subscribed' || filter === 'mod') { + promises.push(accumulate(function (cursor) { + return agent.app.bsky.graph + .getListMutes({ + cursor: cursor, + limit: 50, + }) + .then(function (res) { return ({ + cursor: res.data.cursor, + items: res.data.lists, + }); }); + })); + promises.push(accumulate(function (cursor) { + return agent.app.bsky.graph + .getListBlocks({ + cursor: cursor, + limit: 50, + }) + .then(function (res) { return ({ + cursor: res.data.cursor, + items: res.data.lists, + }); }); + })); + } + return [4 /*yield*/, Promise.all(promises)]; + case 1: + resultset = _b.sent(); + for (_i = 0, resultset_1 = resultset; _i < resultset_1.length; _i++) { + res = resultset_1[_i]; + _loop_1 = function (list) { + if (filter === 'curate' && + list.purpose !== 'app.bsky.graph.defs#curatelist') { + return "continue"; + } + if (filter === 'mod' && + list.purpose !== 'app.bsky.graph.defs#modlist') { + return "continue"; + } + if (!lists.find(function (l) { return l.uri === list.uri; })) { + lists.push(list); + } + }; + for (_a = 0, res_1 = res; _a < res_1.length; _a++) { + list = res_1[_a]; + _loop_1(list); + } + } + return [2 /*return*/, lists]; + } + }); + }); + }, + enabled: !!currentAccount, + }); +} +export function invalidate(qc, filter) { + if (filter) { + qc.invalidateQueries({ queryKey: RQKEY(filter) }); + } + else { + qc.invalidateQueries({ queryKey: [RQKEY_ROOT] }); + } +} diff --git a/src/state/queries/my-muted-accounts.js b/src/state/queries/my-muted-accounts.js new file mode 100644 index 0000000000..dd06163d54 --- /dev/null +++ b/src/state/queries/my-muted-accounts.js @@ -0,0 +1,109 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +var RQKEY_ROOT = 'my-muted-accounts'; +export var RQKEY = function () { return [RQKEY_ROOT]; }; +export function useMyMutedAccountsQuery() { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getMutes({ + limit: 30, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, mute; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.mutes; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + mute = _e[_d]; + if (!(mute.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, mute]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/notifications/feed.js b/src/state/queries/notifications/feed.js new file mode 100644 index 0000000000..e453e13e21 --- /dev/null +++ b/src/state/queries/notifications/feed.js @@ -0,0 +1,393 @@ +/** + * NOTE + * The ./unread.ts API: + * + * - Provides a `checkUnread()` function to sync with the server, + * - Periodically calls `checkUnread()`, and + * - Caches the first page of notifications. + * + * IMPORTANT: This query uses ./unread.ts's cache as its first page, + * IMPORTANT: which means the cache-freshness of this query is driven by the unread API. + * + * Follow these rules: + * + * 1. Call `checkUnread()` if you want to fetch latest in the background. + * 2. Call `checkUnread({invalidate: true})` if you want latest to sync into this query's results immediately. + * 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead. + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { AppBskyFeedDefs, AppBskyFeedPost, AtUri, moderatePost, } from '@atproto/api'; +import { useInfiniteQuery, useQueryClient, } from '@tanstack/react-query'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +import { useThreadgateHiddenReplyUris } from '#/state/threadgate-hidden-replies'; +import { didOrHandleUriMatches, embedViewRecordToPostView, getEmbeddedPost, } from '../util'; +import { useUnreadNotificationsApi } from './unread'; +import { fetchPage } from './util'; +var PAGE_SIZE = 30; +var RQKEY_ROOT = 'notification-feed'; +export function RQKEY(filter) { + return [RQKEY_ROOT, filter]; +} +export function useNotificationFeedQuery(opts) { + var agent = useAgent(); + var queryClient = useQueryClient(); + var moderationOpts = useModerationOpts(); + var unreads = useUnreadNotificationsApi(); + var enabled = opts.enabled !== false; + var filter = opts.filter; + var hiddenReplyUris = useThreadgateHiddenReplyUris().uris; + var selectArgs = useMemo(function () { + return { + moderationOpts: moderationOpts, + hiddenReplyUris: hiddenReplyUris, + }; + }, [moderationOpts, hiddenReplyUris]); + var lastRun = useRef(null); + var query = useInfiniteQuery({ + staleTime: STALE.INFINITY, + queryKey: RQKEY(filter), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var page, reasons, fetchedPage; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (filter === 'all' && !pageParam) { + // for the first page, we check the cached page held by the unread-checker first + page = unreads.getCachedUnreadPage(); + } + if (!!page) return [3 /*break*/, 2]; + reasons = []; + if (filter === 'mentions') { + reasons = [ + // Anything that's a post + 'mention', + 'reply', + 'quote', + ]; + } + return [4 /*yield*/, fetchPage({ + agent: agent, + limit: PAGE_SIZE, + cursor: pageParam, + queryClient: queryClient, + moderationOpts: moderationOpts, + fetchAdditionalData: true, + reasons: reasons, + })]; + case 1: + fetchedPage = (_c.sent()).page; + page = fetchedPage; + _c.label = 2; + case 2: + if (filter === 'all' && !pageParam) { + // if the first page has an unread, mark all read + unreads.markAllRead(); + } + return [2 /*return*/, page]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: enabled, + select: useCallback(function (data) { + var _a; + var moderationOpts = selectArgs.moderationOpts, hiddenReplyUris = selectArgs.hiddenReplyUris; + // Keep track of the last run and whether we can reuse + // some already selected pages from there. + var reusedPages = []; + if (lastRun.current) { + var _b = lastRun.current, lastData = _b.data, lastArgs = _b.args, lastResult = _b.result; + var canReuse = true; + for (var key in selectArgs) { + if (selectArgs.hasOwnProperty(key)) { + if (selectArgs[key] !== lastArgs[key]) { + // Can't do reuse anything if any input has changed. + canReuse = false; + break; + } + } + } + if (canReuse) { + for (var i = 0; i < data.pages.length; i++) { + if (data.pages[i] && lastData.pages[i] === data.pages[i]) { + reusedPages.push(lastResult.pages[i]); + continue; + } + // Stop as soon as pages stop matching up. + break; + } + } + } + // override 'isRead' using the first page's returned seenAt + // we do this because the `markAllRead()` call above will + // mark subsequent pages as read prematurely + var seenAt = ((_a = data.pages[0]) === null || _a === void 0 ? void 0 : _a.seenAt) || new Date(); + for (var _i = 0, _c = data.pages; _i < _c.length; _i++) { + var page = _c[_i]; + for (var _d = 0, _e = page.items; _d < _e.length; _d++) { + var item = _e[_d]; + item.notification.isRead = + seenAt > new Date(item.notification.indexedAt); + } + } + var result = __assign(__assign({}, data), { pages: __spreadArray(__spreadArray([], reusedPages, true), data.pages.slice(reusedPages.length).map(function (page) { + return __assign(__assign({}, page), { items: page.items + .filter(function (item) { + var isHiddenReply = item.type === 'reply' && + item.subjectUri && + hiddenReplyUris.has(item.subjectUri); + return !isHiddenReply; + }) + .filter(function (item) { + var _a; + if (item.type === 'reply' || + item.type === 'mention' || + item.type === 'quote') { + /* + * The `isPostView` check will fail here bc we don't have + * a `$type` field on the `subject`. But if the nested + * `record` is a post, we know it's a post view. + */ + if (AppBskyFeedPost.isRecord((_a = item.subject) === null || _a === void 0 ? void 0 : _a.record)) { + var mod = moderatePost(item.subject, moderationOpts); + if (mod.ui('contentList').filter) { + return false; + } + } + } + return true; + }) }); + }), true) }); + lastRun.current = { data: data, result: result, args: selectArgs }; + return result; + }, [selectArgs]), + }); + // The server may end up returning an empty page, a page with too few items, + // or a page with items that end up getting filtered out. When we fetch pages, + // we'll keep track of how many items we actually hope to see. If the server + // doesn't return enough items, we're going to continue asking for more items. + var lastItemCount = useRef(0); + var wantedItemCount = useRef(0); + var autoPaginationAttemptCount = useRef(0); + useEffect(function () { + var data = query.data, isLoading = query.isLoading, isRefetching = query.isRefetching, isFetchingNextPage = query.isFetchingNextPage, hasNextPage = query.hasNextPage; + // Count the items that we already have. + var itemCount = 0; + for (var _i = 0, _a = (data === null || data === void 0 ? void 0 : data.pages) || []; _i < _a.length; _i++) { + var page = _a[_i]; + itemCount += page.items.length; + } + // If items got truncated, reset the state we're tracking below. + if (itemCount !== lastItemCount.current) { + if (itemCount < lastItemCount.current) { + wantedItemCount.current = itemCount; + } + lastItemCount.current = itemCount; + } + // Now track how many items we really want, and fetch more if needed. + if (isLoading || isRefetching) { + // During the initial fetch, we want to get an entire page's worth of items. + wantedItemCount.current = PAGE_SIZE; + } + else if (isFetchingNextPage) { + if (itemCount > wantedItemCount.current) { + // We have more items than wantedItemCount, so wantedItemCount must be out of date. + // Some other code must have called fetchNextPage(), for example, from onEndReached. + // Adjust the wantedItemCount to reflect that we want one more full page of items. + wantedItemCount.current = itemCount + PAGE_SIZE; + } + } + else if (hasNextPage) { + // At this point we're not fetching anymore, so it's time to make a decision. + // If we didn't receive enough items from the server, paginate again until we do. + if (itemCount < wantedItemCount.current) { + autoPaginationAttemptCount.current++; + if (autoPaginationAttemptCount.current < 50 /* failsafe */) { + query.fetchNextPage(); + } + } + else { + autoPaginationAttemptCount.current = 0; + } + } + }, [query]); + return query; +} +export function findAllPostsInQueryData(queryClient, uri) { + var atUri, queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, item, quotedPost; + var _f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + atUri = new AtUri(uri); + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _g.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 10]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _g.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + _d = 0, _e = page.items; + _g.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + item = _e[_d]; + if (!(item.type !== 'starterpack-joined')) return [3 /*break*/, 5]; + if (!(item.subject && didOrHandleUriMatches(atUri, item.subject))) return [3 /*break*/, 5]; + return [4 /*yield*/, item.subject]; + case 4: + _g.sent(); + _g.label = 5; + case 5: + if (!AppBskyFeedDefs.isPostView(item.subject)) return [3 /*break*/, 7]; + quotedPost = getEmbeddedPost((_f = item.subject) === null || _f === void 0 ? void 0 : _f.embed); + if (!(quotedPost && didOrHandleUriMatches(atUri, quotedPost))) return [3 /*break*/, 7]; + return [4 /*yield*/, embedViewRecordToPostView(quotedPost)]; + case 6: + _g.sent(); + _g.label = 7; + case 7: + _d++; + return [3 /*break*/, 3]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: return [2 /*return*/]; + } + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_2, _a, _queryKey, queryData, _b, _c, page, _d, _e, item, quotedPost; + var _f, _g; + return __generator(this, function (_h) { + switch (_h.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_2 = queryDatas; + _h.label = 1; + case 1: + if (!(_i < queryDatas_2.length)) return [3 /*break*/, 12]; + _a = queryDatas_2[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 11]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _h.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 11]; + page = _c[_b]; + _d = 0, _e = page.items; + _h.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 10]; + item = _e[_d]; + if (!((item.type === 'follow' || item.type === 'contact-match') && + item.notification.author.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, item.notification.author]; + case 4: + _h.sent(); + return [3 /*break*/, 7]; + case 5: + if (!(item.type !== 'starterpack-joined' && + ((_f = item.subject) === null || _f === void 0 ? void 0 : _f.author.did) === did)) return [3 /*break*/, 7]; + return [4 /*yield*/, item.subject.author]; + case 6: + _h.sent(); + _h.label = 7; + case 7: + if (!AppBskyFeedDefs.isPostView(item.subject)) return [3 /*break*/, 9]; + quotedPost = getEmbeddedPost((_g = item.subject) === null || _g === void 0 ? void 0 : _g.embed); + if (!((quotedPost === null || quotedPost === void 0 ? void 0 : quotedPost.author.did) === did)) return [3 /*break*/, 9]; + return [4 /*yield*/, quotedPost.author]; + case 8: + _h.sent(); + _h.label = 9; + case 9: + _d++; + return [3 /*break*/, 3]; + case 10: + _b++; + return [3 /*break*/, 2]; + case 11: + _i++; + return [3 /*break*/, 1]; + case 12: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/notifications/settings.js b/src/state/queries/notifications/settings.js new file mode 100644 index 0000000000..174c25ca6b --- /dev/null +++ b/src/state/queries/notifications/settings.js @@ -0,0 +1,112 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { t } from '@lingui/core/macro'; +import { useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +var RQKEY_ROOT = 'notification-settings'; +var RQKEY = [RQKEY_ROOT]; +export function useNotificationSettingsQuery(_a) { + var _this = this; + var _b = _a === void 0 ? {} : _a, enabled = _b.enabled; + var agent = useAgent(); + return useQuery({ + queryKey: RQKEY, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var response; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.notification.getPreferences()]; + case 1: + response = _a.sent(); + return [2 /*return*/, response.data.preferences]; + } + }); + }); }, + enabled: enabled, + }); +} +export function useNotificationSettingsUpdateMutation() { + var _this = this; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (update) { return __awaiter(_this, void 0, void 0, function () { + var response; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.notification.putPreferencesV2(update)]; + case 1: + response = _a.sent(); + return [2 /*return*/, response.data.preferences]; + } + }); + }); }, + onMutate: function (update) { + optimisticUpdateNotificationSettings(queryClient, update); + }, + onError: function (e) { + logger.error('Could not update notification settings', { message: e }); + queryClient.invalidateQueries({ queryKey: RQKEY }); + Toast.show(t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Could not update notification settings"], ["Could not update notification settings"]))), 'xmark'); + }, + }); +} +function optimisticUpdateNotificationSettings(queryClient, update) { + queryClient.setQueryData(RQKEY, function (old) { + if (!old) + return old; + return __assign(__assign({}, old), update); + }); +} +var templateObject_1; diff --git a/src/state/queries/notifications/types.js b/src/state/queries/notifications/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/state/queries/notifications/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/state/queries/notifications/unread.js b/src/state/queries/notifications/unread.js new file mode 100644 index 0000000000..d213fe6228 --- /dev/null +++ b/src/state/queries/notifications/unread.js @@ -0,0 +1,259 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { jsx as _jsx } from "react/jsx-runtime"; +/** + * A kind of companion API to ./feed.ts. See that file for more info. + */ +import React, { useRef } from 'react'; +import { AppState } from 'react-native'; +import { useQueryClient } from '@tanstack/react-query'; +import EventEmitter from 'eventemitter3'; +import BroadcastChannel from '#/lib/broadcast'; +import { resetBadgeCount } from '#/lib/notifications/notifications'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { truncateAndInvalidate } from '#/state/queries/util'; +import { useAgent, useSession } from '#/state/session'; +import { RQKEY as RQKEY_NOTIFS } from './feed'; +import { fetchPage } from './util'; +var UPDATE_INTERVAL = 30 * 1e3; // 30sec +var broadcast = new BroadcastChannel('NOTIFS_BROADCAST_CHANNEL'); +var emitter = new EventEmitter(); +var stateContext = React.createContext(''); +stateContext.displayName = 'NotificationsUnreadStateContext'; +var apiContext = React.createContext({ + markAllRead: function () { + return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); + }, + checkUnread: function () { + return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); + }, + getCachedUnreadPage: function () { return undefined; }, +}); +apiContext.displayName = 'NotificationsUnreadApiContext'; +export function Provider(_a) { + var children = _a.children; + var hasSession = useSession().hasSession; + var agent = useAgent(); + var queryClient = useQueryClient(); + var moderationOpts = useModerationOpts(); + var _b = React.useState(''), numUnread = _b[0], setNumUnread = _b[1]; + var checkUnreadRef = React.useRef(null); + var cacheRef = React.useRef({ + usableInFeed: false, + syncedAt: new Date(), + data: undefined, + unreadCount: 0, + }); + React.useEffect(function () { + function markAsUnusable() { + if (cacheRef.current) { + cacheRef.current.usableInFeed = false; + } + } + emitter.addListener('invalidate', markAsUnusable); + return function () { + emitter.removeListener('invalidate', markAsUnusable); + }; + }, []); + // periodic sync + React.useEffect(function () { + if (!hasSession || !checkUnreadRef.current) { + return; + } + checkUnreadRef.current(); // fire on init + var interval = setInterval(function () { var _a; return (_a = checkUnreadRef.current) === null || _a === void 0 ? void 0 : _a.call(checkUnreadRef, { isPoll: true }); }, UPDATE_INTERVAL); + return function () { return clearInterval(interval); }; + }, [hasSession]); + // listen for broadcasts + React.useEffect(function () { + var listener = function (_a) { + var data = _a.data; + cacheRef.current = { + usableInFeed: false, + syncedAt: new Date(), + data: undefined, + unreadCount: data.event === '30+' + ? 30 + : data.event === '' + ? 0 + : parseInt(data.event, 10) || 1, + }; + setNumUnread(data.event); + }; + broadcast.addEventListener('message', listener); + return function () { + broadcast.removeEventListener('message', listener); + }; + }, [setNumUnread]); + var isFetchingRef = useRef(false); + // create API + var api = React.useMemo(function () { + return { + markAllRead: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // update server + return [4 /*yield*/, agent.updateSeenNotifications(cacheRef.current.syncedAt.toISOString()) + // update & broadcast + ]; + case 1: + // update server + _a.sent(); + // update & broadcast + setNumUnread(''); + broadcast.postMessage({ event: '' }); + resetBadgeCount(); + return [2 /*return*/]; + } + }); + }); + }, + checkUnread: function () { + return __awaiter(this, arguments, void 0, function (_a) { + var _b, page, lastIndexed, unreadCount, unreadCountStr, now, lastIndexedDate; + var _c, _d; + var _e = _a === void 0 ? {} : _a, invalidate = _e.invalidate, isPoll = _e.isPoll; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + _f.trys.push([0, , 2, 3]); + if (!agent.session) + return [2 /*return*/]; + if (AppState.currentState !== 'active') { + return [2 /*return*/]; + } + // reduce polling if unread count is set + if (isPoll && ((_c = cacheRef.current) === null || _c === void 0 ? void 0 : _c.unreadCount) !== 0) { + // if hit 30+ then don't poll, otherwise reduce polling by 50% + if (((_d = cacheRef.current) === null || _d === void 0 ? void 0 : _d.unreadCount) >= 30 || Math.random() >= 0.5) { + return [2 /*return*/]; + } + } + if (isFetchingRef.current) { + return [2 /*return*/]; + } + // Do not move this without ensuring it gets a symmetrical reset in the finally block. + isFetchingRef.current = true; + return [4 /*yield*/, fetchPage({ + agent: agent, + cursor: undefined, + limit: 40, + queryClient: queryClient, + moderationOpts: moderationOpts, + reasons: [], + // only fetch subjects when the page is going to be used + // in the notifications query, otherwise skip it + fetchAdditionalData: !!invalidate, + })]; + case 1: + _b = _f.sent(), page = _b.page, lastIndexed = _b.indexedAt; + unreadCount = countUnread(page); + unreadCountStr = unreadCount >= 30 + ? '30+' + : unreadCount === 0 + ? '' + : String(unreadCount); + now = new Date(); + lastIndexedDate = lastIndexed + ? new Date(lastIndexed) + : undefined; + cacheRef.current = { + usableInFeed: !!invalidate, // will be used immediately + data: page, + syncedAt: !lastIndexedDate || now > lastIndexedDate ? now : lastIndexedDate, + unreadCount: unreadCount, + }; + // update & broadcast + setNumUnread(unreadCountStr); + if (invalidate) { + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')); + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')); + } + broadcast.postMessage({ event: unreadCountStr }); + return [3 /*break*/, 3]; + case 2: + isFetchingRef.current = false; + return [7 /*endfinally*/]; + case 3: return [2 /*return*/]; + } + }); + }); + }, + getCachedUnreadPage: function () { + // return cached page if it's marked as fresh enough + if (cacheRef.current.usableInFeed) { + return cacheRef.current.data; + } + }, + }; + }, [setNumUnread, queryClient, moderationOpts, agent]); + checkUnreadRef.current = api.checkUnread; + return (_jsx(stateContext.Provider, { value: numUnread, children: _jsx(apiContext.Provider, { value: api, children: children }) })); +} +export function useUnreadNotifications() { + return React.useContext(stateContext); +} +export function useUnreadNotificationsApi() { + return React.useContext(apiContext); +} +function countUnread(page) { + var num = 0; + for (var _i = 0, _a = page.items; _i < _a.length; _i++) { + var item = _a[_i]; + if (!item.notification.isRead) { + num++; + } + if (item.additional) { + for (var _b = 0, _c = item.additional; _b < _c.length; _b++) { + var item2 = _c[_b]; + if (!item2.isRead) { + num++; + } + } + } + } + return num; +} +export function invalidateCachedUnreadPage() { + emitter.emit('invalidate'); +} diff --git a/src/state/queries/notifications/util.js b/src/state/queries/notifications/util.js new file mode 100644 index 0000000000..2bfd44083f --- /dev/null +++ b/src/state/queries/notifications/util.js @@ -0,0 +1,288 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AppBskyFeedLike, AppBskyFeedPost, AppBskyFeedRepost, AppBskyGraphStarterpack, hasMutedWord, moderateNotification, } from '@atproto/api'; +import chunk from 'lodash.chunk'; +import { labelIsHideableOffense } from '#/lib/moderation'; +import * as bsky from '#/types/bsky'; +import { precacheProfile } from '../profile'; +var GROUPABLE_REASONS = [ + 'like', + 'repost', + 'follow', + 'like-via-repost', + 'repost-via-repost', + 'subscribed-post', +]; +var MS_1HR = 1e3 * 60 * 60; +var MS_2DAY = MS_1HR * 48; +// exported api +// = +export function fetchPage(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res, indexedAt, notifs, notifsGrouped, subjects, _i, notifsGrouped_1, notif, seenAt; + var _c, _d; + var agent = _b.agent, cursor = _b.cursor, limit = _b.limit, queryClient = _b.queryClient, moderationOpts = _b.moderationOpts, fetchAdditionalData = _b.fetchAdditionalData, reasons = _b.reasons; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: return [4 /*yield*/, agent.listNotifications({ + limit: limit, + cursor: cursor, + reasons: reasons, + })]; + case 1: + res = _e.sent(); + indexedAt = (_c = res.data.notifications[0]) === null || _c === void 0 ? void 0 : _c.indexedAt; + notifs = res.data.notifications.filter(function (notif) { return !shouldFilterNotif(notif, moderationOpts); }); + notifsGrouped = groupNotifications(notifs); + if (!fetchAdditionalData) return [3 /*break*/, 3]; + return [4 /*yield*/, fetchSubjects(agent, notifsGrouped)]; + case 2: + subjects = _e.sent(); + for (_i = 0, notifsGrouped_1 = notifsGrouped; _i < notifsGrouped_1.length; _i++) { + notif = notifsGrouped_1[_i]; + if (notif.subjectUri) { + if (notif.type === 'starterpack-joined' && + notif.notification.reasonSubject) { + notif.subject = subjects.starterPacks.get(notif.notification.reasonSubject); + } + else { + notif.subject = subjects.posts.get(notif.subjectUri); + if (notif.subject) { + precacheProfile(queryClient, notif.subject.author); + } + } + } + } + _e.label = 3; + case 3: + seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date(); + if (Number.isNaN(seenAt.getTime())) { + seenAt = new Date(); + } + return [2 /*return*/, { + page: { + cursor: res.data.cursor, + seenAt: seenAt, + items: notifsGrouped, + priority: (_d = res.data.priority) !== null && _d !== void 0 ? _d : false, + }, + indexedAt: indexedAt, + }]; + } + }); + }); +} +// internal methods +// = +export function shouldFilterNotif(notif, moderationOpts) { + var _a, _b; + var containsImperative = !!((_a = notif.author.labels) === null || _a === void 0 ? void 0 : _a.some(labelIsHideableOffense)); + if (containsImperative) { + return true; + } + if (!moderationOpts) { + return false; + } + if (notif.reason === 'subscribed-post' && + bsky.dangerousIsType(notif.record, AppBskyFeedPost.isRecord) && + hasMutedWord({ + mutedWords: moderationOpts.prefs.mutedWords, + text: notif.record.text, + facets: notif.record.facets, + outlineTags: notif.record.tags, + languages: notif.record.langs, + actor: notif.author, + })) { + return true; + } + if ((_b = notif.author.viewer) === null || _b === void 0 ? void 0 : _b.following) { + return false; + } + return moderateNotification(notif, moderationOpts).ui('contentList').filter; +} +export function groupNotifications(notifs) { + var _a, _b; + var groupedNotifs = []; + for (var _i = 0, notifs_1 = notifs; _i < notifs_1.length; _i++) { + var notif = notifs_1[_i]; + var ts = +new Date(notif.indexedAt); + var grouped = false; + if (GROUPABLE_REASONS.includes(notif.reason)) { + for (var _c = 0, groupedNotifs_1 = groupedNotifs; _c < groupedNotifs_1.length; _c++) { + var groupedNotif = groupedNotifs_1[_c]; + var ts2 = +new Date(groupedNotif.notification.indexedAt); + if (Math.abs(ts2 - ts) < MS_2DAY && + notif.reason === groupedNotif.notification.reason && + notif.reasonSubject === groupedNotif.notification.reasonSubject && + (notif.author.did !== groupedNotif.notification.author.did || + notif.reason === 'subscribed-post')) { + var nextIsFollowBack = notif.reason === 'follow' && ((_a = notif.author.viewer) === null || _a === void 0 ? void 0 : _a.following); + var prevIsFollowBack = groupedNotif.notification.reason === 'follow' && + ((_b = groupedNotif.notification.author.viewer) === null || _b === void 0 ? void 0 : _b.following); + var shouldUngroup = nextIsFollowBack || prevIsFollowBack; + if (!shouldUngroup) { + groupedNotif.additional = groupedNotif.additional || []; + groupedNotif.additional.push(notif); + grouped = true; + break; + } + } + } + } + if (!grouped) { + var type = toKnownType(notif); + if (type !== 'starterpack-joined') { + groupedNotifs.push({ + _reactKey: "notif-".concat(notif.uri, "-").concat(notif.reason), + type: type, + notification: notif, + subjectUri: getSubjectUri(type, notif), + }); + } + else { + groupedNotifs.push({ + _reactKey: "notif-".concat(notif.uri, "-").concat(notif.reason), + type: 'starterpack-joined', + notification: notif, + subjectUri: notif.uri, + }); + } + } + } + return groupedNotifs; +} +function fetchSubjects(agent, groupedNotifs) { + return __awaiter(this, void 0, void 0, function () { + var postUris, packUris, _i, groupedNotifs_2, notif, postUriChunks, packUriChunks, postsChunks, packsChunks, postsMap, packsMap, _a, _b, post, _c, _d, pack; + var _e, _f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + postUris = new Set(); + packUris = new Set(); + for (_i = 0, groupedNotifs_2 = groupedNotifs; _i < groupedNotifs_2.length; _i++) { + notif = groupedNotifs_2[_i]; + if ((_e = notif.subjectUri) === null || _e === void 0 ? void 0 : _e.includes('app.bsky.feed.post')) { + postUris.add(notif.subjectUri); + } + else if ((_f = notif.notification.reasonSubject) === null || _f === void 0 ? void 0 : _f.includes('app.bsky.graph.starterpack')) { + packUris.add(notif.notification.reasonSubject); + } + } + postUriChunks = chunk(Array.from(postUris), 25); + packUriChunks = chunk(Array.from(packUris), 25); + return [4 /*yield*/, Promise.all(postUriChunks.map(function (uris) { + return agent.app.bsky.feed.getPosts({ uris: uris }).then(function (res) { return res.data.posts; }); + }))]; + case 1: + postsChunks = _g.sent(); + return [4 /*yield*/, Promise.all(packUriChunks.map(function (uris) { + return agent.app.bsky.graph + .getStarterPacks({ uris: uris }) + .then(function (res) { return res.data.starterPacks; }); + }))]; + case 2: + packsChunks = _g.sent(); + postsMap = new Map(); + packsMap = new Map(); + for (_a = 0, _b = postsChunks.flat(); _a < _b.length; _a++) { + post = _b[_a]; + if (AppBskyFeedPost.isRecord(post.record)) { + postsMap.set(post.uri, post); + } + } + for (_c = 0, _d = packsChunks.flat(); _c < _d.length; _c++) { + pack = _d[_c]; + if (AppBskyGraphStarterpack.isRecord(pack.record)) { + packsMap.set(pack.uri, pack); + } + } + return [2 /*return*/, { + posts: postsMap, + starterPacks: packsMap, + }]; + } + }); + }); +} +function toKnownType(notif) { + var _a; + if (notif.reason === 'like') { + if ((_a = notif.reasonSubject) === null || _a === void 0 ? void 0 : _a.includes('feed.generator')) { + return 'feedgen-like'; + } + return 'post-like'; + } + if (notif.reason === 'repost' || + notif.reason === 'mention' || + notif.reason === 'reply' || + notif.reason === 'quote' || + notif.reason === 'follow' || + notif.reason === 'starterpack-joined' || + notif.reason === 'verified' || + notif.reason === 'unverified' || + notif.reason === 'like-via-repost' || + notif.reason === 'repost-via-repost' || + notif.reason === 'subscribed-post' || + notif.reason === 'contact-match') { + return notif.reason; + } + return 'unknown'; +} +function getSubjectUri(type, notif) { + var _a, _b; + if (type === 'reply' || + type === 'quote' || + type === 'mention' || + type === 'subscribed-post') { + return notif.uri; + } + else if (type === 'post-like' || + type === 'repost' || + type === 'like-via-repost' || + type === 'repost-via-repost') { + if (bsky.dangerousIsType(notif.record, AppBskyFeedRepost.isRecord) || + bsky.dangerousIsType(notif.record, AppBskyFeedLike.isRecord)) { + return typeof ((_a = notif.record.subject) === null || _a === void 0 ? void 0 : _a.uri) === 'string' + ? (_b = notif.record.subject) === null || _b === void 0 ? void 0 : _b.uri + : undefined; + } + } + else if (type === 'feedgen-like') { + return notif.reasonSubject; + } +} diff --git a/src/state/queries/nuxs/__mocks__/index.js b/src/state/queries/nuxs/__mocks__/index.js new file mode 100644 index 0000000000..80f801a002 --- /dev/null +++ b/src/state/queries/nuxs/__mocks__/index.js @@ -0,0 +1,20 @@ +import { jest } from '@jest/globals'; +export { Nux } from '#/state/queries/nuxs/definitions'; +export var useNuxs = jest.fn(function () { + return { + nuxs: undefined, + status: 'loading', + }; +}); +export var useNux = jest.fn(function (id) { + return { + nux: undefined, + status: 'loading', + }; +}); +export var useSaveNux = jest.fn(function () { + return {}; +}); +export var useResetNuxs = jest.fn(function () { + return {}; +}); diff --git a/src/state/queries/nuxs/definitions.js b/src/state/queries/nuxs/definitions.js new file mode 100644 index 0000000000..cad9f9f0bf --- /dev/null +++ b/src/state/queries/nuxs/definitions.js @@ -0,0 +1,36 @@ +var _a; +export var Nux; +(function (Nux) { + Nux["NeueTypography"] = "NeueTypography"; + Nux["ExploreInterestsCard"] = "ExploreInterestsCard"; + Nux["InitialVerificationAnnouncement"] = "InitialVerificationAnnouncement"; + Nux["ActivitySubscriptions"] = "ActivitySubscriptions"; + Nux["AgeAssuranceDismissibleNotice"] = "AgeAssuranceDismissibleNotice"; + Nux["AgeAssuranceDismissibleFeedBanner"] = "AgeAssuranceDismissibleFeedBanner"; + Nux["BookmarksAnnouncement"] = "BookmarksAnnouncement"; + Nux["FindContactsAnnouncement"] = "FindContactsAnnouncement"; + Nux["FindContactsDismissibleBanner"] = "FindContactsDismissibleBanner"; + Nux["LiveNowBetaDialog"] = "LiveNowBetaDialog"; + Nux["LiveNowBetaNudge"] = "LiveNowBetaNudge"; + Nux["DraftsAnnouncement"] = "DraftsAnnouncement"; + /* + * Blocking announcements. New IDs are required for each new announcement. + */ + Nux["PolicyUpdate202508"] = "PolicyUpdate202508"; +})(Nux || (Nux = {})); +export var nuxNames = new Set(Object.values(Nux)); +export var NuxSchemas = (_a = {}, + _a[Nux.NeueTypography] = undefined, + _a[Nux.ExploreInterestsCard] = undefined, + _a[Nux.InitialVerificationAnnouncement] = undefined, + _a[Nux.ActivitySubscriptions] = undefined, + _a[Nux.AgeAssuranceDismissibleNotice] = undefined, + _a[Nux.AgeAssuranceDismissibleFeedBanner] = undefined, + _a[Nux.PolicyUpdate202508] = undefined, + _a[Nux.BookmarksAnnouncement] = undefined, + _a[Nux.FindContactsAnnouncement] = undefined, + _a[Nux.FindContactsDismissibleBanner] = undefined, + _a[Nux.LiveNowBetaDialog] = undefined, + _a[Nux.LiveNowBetaNudge] = undefined, + _a[Nux.DraftsAnnouncement] = undefined, + _a); diff --git a/src/state/queries/nuxs/index.js b/src/state/queries/nuxs/index.js new file mode 100644 index 0000000000..0a5c178dd8 --- /dev/null +++ b/src/state/queries/nuxs/index.js @@ -0,0 +1,153 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { parseAppNux, serializeAppNux } from '#/state/queries/nuxs/util'; +import { preferencesQueryKey, usePreferencesQuery, } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export { Nux } from '#/state/queries/nuxs/definitions'; +export function useNuxs() { + var _a, _b, _c; + var _d = usePreferencesQuery(), data = _d.data, isSuccess = _d.isSuccess, isError = _d.isError; + var status = isSuccess ? 'ready' : isError ? 'error' : 'loading'; + if (status === 'ready') { + var nuxs = (_c = (_b = (_a = data === null || data === void 0 ? void 0 : data.bskyAppState) === null || _a === void 0 ? void 0 : _a.nuxs) === null || _b === void 0 ? void 0 : _b.map(parseAppNux)) === null || _c === void 0 ? void 0 : _c.filter(Boolean); + if (nuxs) { + return { + nuxs: nuxs, + status: status, + }; + } + else { + return { + nuxs: [], + status: status, + }; + } + } + // if (__DEV__) { + // const queryClient = useQueryClient() + // const agent = useAgent() + // // @ts-ignore + // window.clearNux = async (ids: string[]) => { + // await agent.bskyAppRemoveNuxs(ids) + // // triggers a refetch + // await queryClient.invalidateQueries({ + // queryKey: preferencesQueryKey, + // }) + // } + // } + return { + nuxs: undefined, + status: status, + }; +} +export function useNux(id) { + var _a = useNuxs(), nuxs = _a.nuxs, status = _a.status; + if (status === 'ready') { + var nux = nuxs.find(function (nux) { return nux.id === id; }); + if (nux) { + return { + nux: nux, + status: status, + }; + } + else { + return { + nux: undefined, + status: status, + }; + } + } + return { + nux: undefined, + status: status, + }; +} +export function useSaveNux() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + retry: 3, + mutationFn: function (nux) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.bskyAppUpsertNux(serializeAppNux(nux)) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useResetNuxs() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + retry: 3, + mutationFn: function (ids) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.bskyAppRemoveNuxs(ids) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/nuxs/types.js b/src/state/queries/nuxs/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/state/queries/nuxs/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/state/queries/nuxs/util.js b/src/state/queries/nuxs/util.js new file mode 100644 index 0000000000..d533b14405 --- /dev/null +++ b/src/state/queries/nuxs/util.js @@ -0,0 +1,50 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import { nuxSchema } from '@atproto/api'; +import { nuxNames, NuxSchemas, } from '#/state/queries/nuxs/definitions'; +export function parseAppNux(nux) { + if (!nuxNames.has(nux.id)) + return; + if (!nuxSchema.safeParse(nux).success) + return; + var data = nux.data, rest = __rest(nux, ["data"]); + var schema = NuxSchemas[nux.id]; + if (schema && data) { + var parsedData = JSON.parse(data); + if (!schema.safeParse(parsedData).success) + return; + return __assign(__assign({}, rest), { data: parsedData }); + } + return __assign(__assign({}, rest), { data: undefined }); +} +export function serializeAppNux(nux) { + var data = nux.data, rest = __rest(nux, ["data"]); + var schema = NuxSchemas[nux.id]; + var result = __assign(__assign({}, rest), { data: undefined }); + if (schema) { + schema.parse(data); + result.data = JSON.stringify(data); + } + nuxSchema.parse(result); + return result; +} diff --git a/src/state/queries/pinned-post.js b/src/state/queries/pinned-post.js new file mode 100644 index 0000000000..205b4946a9 --- /dev/null +++ b/src/state/queries/pinned-post.js @@ -0,0 +1,130 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { logger } from '#/logger'; +import { RQKEY as FEED_RQKEY } from '#/state/queries/post-feed'; +import * as Toast from '#/view/com/util/Toast'; +import { updatePostShadow } from '../cache/post-shadow'; +import { useAgent, useSession } from '../session'; +import { useProfileUpdateMutation } from './profile'; +export function usePinnedPostMutation() { + var _this = this; + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + var profileUpdateMutate = useProfileUpdateMutation().mutateAsync; + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var pinCurrentPost, prevPinnedPost, profile, e_1; + var _c; + var postUri = _b.postUri, postCid = _b.postCid, action = _b.action; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + pinCurrentPost = action === 'pin'; + _d.label = 1; + case 1: + _d.trys.push([1, 4, , 5]); + updatePostShadow(queryClient, postUri, { pinned: pinCurrentPost }); + // get the currently pinned post so we can optimistically remove the pin from it + if (!currentAccount) + throw new Error('Not signed in'); + return [4 /*yield*/, agent.getProfile({ + actor: currentAccount.did, + })]; + case 2: + profile = (_d.sent()).data; + prevPinnedPost = (_c = profile.pinnedPost) === null || _c === void 0 ? void 0 : _c.uri; + if (prevPinnedPost && prevPinnedPost !== postUri) { + updatePostShadow(queryClient, prevPinnedPost, { pinned: false }); + } + return [4 /*yield*/, profileUpdateMutate({ + profile: profile, + updates: function (existing) { + existing.pinnedPost = pinCurrentPost + ? { uri: postUri, cid: postCid } + : undefined; + return existing; + }, + checkCommitted: function (res) { + var _a; + return pinCurrentPost + ? ((_a = res.data.pinnedPost) === null || _a === void 0 ? void 0 : _a.uri) === postUri + : !res.data.pinnedPost; + }, + })]; + case 3: + _d.sent(); + if (pinCurrentPost) { + Toast.show(_(msg({ message: 'Post pinned', context: 'toast' }))); + } + else { + Toast.show(_(msg({ message: 'Post unpinned', context: 'toast' }))); + } + queryClient.invalidateQueries({ + queryKey: FEED_RQKEY("author|".concat(currentAccount.did, "|posts_and_author_threads")), + }); + queryClient.invalidateQueries({ + queryKey: FEED_RQKEY("author|".concat(currentAccount.did, "|posts_with_replies")), + }); + return [3 /*break*/, 5]; + case 4: + e_1 = _d.sent(); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to pin post"], ["Failed to pin post"]))))); + logger.error('Failed to pin post', { message: String(e_1) }); + // revert optimistic update + updatePostShadow(queryClient, postUri, { + pinned: !pinCurrentPost, + }); + if (prevPinnedPost && prevPinnedPost !== postUri) { + updatePostShadow(queryClient, prevPinnedPost, { pinned: true }); + } + return [3 /*break*/, 5]; + case 5: return [2 /*return*/]; + } + }); + }); }, + }); +} +var templateObject_1; diff --git a/src/state/queries/post-feed.js b/src/state/queries/post-feed.js new file mode 100644 index 0000000000..d776a8acbd --- /dev/null +++ b/src/state/queries/post-feed.js @@ -0,0 +1,611 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import React, { useCallback, useEffect, useRef } from 'react'; +import { AppState } from 'react-native'; +import { AppBskyFeedDefs, AtUri, moderatePost, } from '@atproto/api'; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { AuthorFeedAPI } from '#/lib/api/feed/author'; +import { CustomFeedAPI } from '#/lib/api/feed/custom'; +import { DemoFeedAPI } from '#/lib/api/feed/demo'; +import { FollowingFeedAPI } from '#/lib/api/feed/following'; +import { HomeFeedAPI } from '#/lib/api/feed/home'; +import { LikesFeedAPI } from '#/lib/api/feed/likes'; +import { ListFeedAPI } from '#/lib/api/feed/list'; +import { MergeFeedAPI } from '#/lib/api/feed/merge'; +import { PostListFeedAPI } from '#/lib/api/feed/posts'; +import { aggregateUserInterests } from '#/lib/api/feed/utils'; +import { FeedTuner } from '#/lib/api/feed-manip'; +import { DISCOVER_FEED_URI } from '#/lib/constants'; +import { logger } from '#/logger'; +import { STALE } from '#/state/queries'; +import { DEFAULT_LOGGED_OUT_PREFERENCES } from '#/state/queries/preferences/const'; +import { useAgent } from '#/state/session'; +import * as userActionHistory from '#/state/userActionHistory'; +import { KnownError } from '#/view/com/posts/PostFeedErrorMessage'; +import { useFeedTuners } from '../preferences/feed-tuners'; +import { useModerationOpts } from '../preferences/moderation-opts'; +import { usePreferencesQuery } from './preferences'; +import { didOrHandleUriMatches, embedViewRecordToPostView, getEmbeddedPost, } from './util'; +export var RQKEY_ROOT = 'post-feed'; +export function RQKEY(feedDesc, params) { + return [RQKEY_ROOT, feedDesc, params || {}]; +} +/** + * The minimum number of posts we want in a single "page" of results. Since we + * filter out unwanted content, we may fetch more than this number to ensure + * that we get _at least_ this number. + */ +var MIN_POSTS = 30; +export function usePostFeedQuery(feedDesc, params, opts) { + var _a, _b; + var feedTuners = useFeedTuners(feedDesc); + var moderationOpts = useModerationOpts(); + var preferences = usePreferencesQuery().data; + /** + * Load bearing: we need to await AA state or risk FOUC. This marginally + * delays feeds, but AA state is fetched immediately on load and is then + * available for the remainder of the session, so this delay only affects cold + * loads. -esb + */ + var enabled = (opts === null || opts === void 0 ? void 0 : opts.enabled) !== false && Boolean(moderationOpts) && Boolean(preferences); + var userInterests = aggregateUserInterests(preferences); + var followingPinnedIndex = (_b = (_a = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds) === null || _a === void 0 ? void 0 : _a.findIndex(function (f) { return f.pinned && f.value === 'following'; })) !== null && _b !== void 0 ? _b : -1; + var enableFollowingToDiscoverFallback = followingPinnedIndex === 0; + var agent = useAgent(); + var lastRun = useRef(null); + var isDiscover = feedDesc.includes(DISCOVER_FEED_URI); + /** + * The number of posts to fetch in a single request. Because we filter + * unwanted content, we may over-fetch here to try and fill pages by + * `MIN_POSTS`. But if you're doing this, ask @why if it's ok first. + */ + var fetchLimit = MIN_POSTS; + // Make sure this doesn't invalidate unless really needed. + var selectArgs = React.useMemo(function () { return ({ + feedTuners: feedTuners, + moderationOpts: moderationOpts, + ignoreFilterFor: opts === null || opts === void 0 ? void 0 : opts.ignoreFilterFor, + isDiscover: isDiscover, + }); }, [feedTuners, moderationOpts, opts === null || opts === void 0 ? void 0 : opts.ignoreFilterFor, isDiscover]); + var query = useInfiniteQuery({ + enabled: enabled, + staleTime: STALE.INFINITY, + queryKey: RQKEY(feedDesc, params), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var _c, api, cursor, res; + var pageParam = _b.pageParam; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + logger.debug('usePostFeedQuery', { feedDesc: feedDesc, cursor: pageParam === null || pageParam === void 0 ? void 0 : pageParam.cursor }); + _c = pageParam + ? pageParam + : { + api: createApi({ + feedDesc: feedDesc, + feedParams: params || {}, + feedTuners: feedTuners, + agent: agent, + // Not in the query key because they don't change: + userInterests: userInterests, + // Not in the query key. Reacting to it switching isn't important: + enableFollowingToDiscoverFallback: enableFollowingToDiscoverFallback, + }), + cursor: undefined, + }, api = _c.api, cursor = _c.cursor; + return [4 /*yield*/, api.fetch({ cursor: cursor, limit: fetchLimit }) + /* + * If this is a public view, we need to check if posts fail moderation. + * If all fail, we throw an error. If only some fail, we continue and let + * moderations happen later, which results in some posts being shown and + * some not. + */ + ]; + case 1: + res = _d.sent(); + /* + * If this is a public view, we need to check if posts fail moderation. + * If all fail, we throw an error. If only some fail, we continue and let + * moderations happen later, which results in some posts being shown and + * some not. + */ + if (!agent.session) { + assertSomePostsPassModeration(res.feed, (preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs) || + DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs); + } + return [2 /*return*/, { + api: api, + cursor: res.cursor, + feed: res.feed, + fetchedAt: Date.now(), + }]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { + return lastPage.cursor + ? { + api: lastPage.api, + cursor: lastPage.cursor, + } + : undefined; + }, + select: useCallback(function (data) { + // If the selection depends on some data, that data should + // be included in the selectArgs object and read here. + var feedTuners = selectArgs.feedTuners, moderationOpts = selectArgs.moderationOpts, ignoreFilterFor = selectArgs.ignoreFilterFor, isDiscover = selectArgs.isDiscover; + var tuner = new FeedTuner(feedTuners); + // Keep track of the last run and whether we can reuse + // some already selected pages from there. + var reusedPages = []; + if (lastRun.current) { + var _a = lastRun.current, lastData = _a.data, lastArgs = _a.args, lastResult = _a.result; + var canReuse = true; + for (var key in selectArgs) { + if (selectArgs.hasOwnProperty(key)) { + if (selectArgs[key] !== lastArgs[key]) { + // Can't do reuse anything if any input has changed. + canReuse = false; + break; + } + } + } + if (canReuse) { + for (var i = 0; i < data.pages.length; i++) { + if (data.pages[i] && lastData.pages[i] === data.pages[i]) { + reusedPages.push(lastResult.pages[i]); + // Keep the tuner in sync so that the end result is deterministic. + tuner.tune(lastData.pages[i].feed); + continue; + } + // Stop as soon as pages stop matching up. + break; + } + } + } + var result = { + pageParams: data.pageParams, + pages: __spreadArray(__spreadArray([], reusedPages, true), data.pages.slice(reusedPages.length).map(function (page) { return ({ + api: page.api, + tuner: tuner, + cursor: page.cursor, + fetchedAt: page.fetchedAt, + slices: tuner + .tune(page.feed) + .map(function (slice) { + var _a; + var moderations = slice.items.map(function (item) { + return moderatePost(item.post, moderationOpts); + }); + // apply moderation filter + for (var i = 0; i < slice.items.length; i++) { + var ignoreFilter = slice.items[i].post.author.did === ignoreFilterFor; + if (ignoreFilter) { + // remove mutes to avoid confused UIs + moderations[i].causes = moderations[i].causes.filter(function (cause) { return cause.type !== 'muted'; }); + } + if (!ignoreFilter && + ((_a = moderations[i]) === null || _a === void 0 ? void 0 : _a.ui('contentList').filter)) { + return undefined; + } + } + if (isDiscover) { + userActionHistory.seen(slice.items.map(function (item) { + var _a, _b, _c, _d; + return ({ + feedContext: slice.feedContext, + reqId: slice.reqId, + likeCount: (_a = item.post.likeCount) !== null && _a !== void 0 ? _a : 0, + repostCount: (_b = item.post.repostCount) !== null && _b !== void 0 ? _b : 0, + replyCount: (_c = item.post.replyCount) !== null && _c !== void 0 ? _c : 0, + isFollowedBy: Boolean((_d = item.post.author.viewer) === null || _d === void 0 ? void 0 : _d.followedBy), + uri: item.post.uri, + }); + })); + } + var feedPostSlice = { + _reactKey: slice._reactKey, + _isFeedPostSlice: true, + isIncompleteThread: slice.isIncompleteThread, + isFallbackMarker: slice.isFallbackMarker, + feedContext: slice.feedContext, + reqId: slice.reqId, + reason: slice.reason, + feedPostUri: slice.feedPostUri, + items: slice.items.map(function (item, i) { + var feedPostSliceItem = { + _reactKey: "".concat(slice._reactKey, "-").concat(i, "-").concat(item.post.uri), + uri: item.post.uri, + post: item.post, + record: item.record, + moderation: moderations[i], + parentAuthor: item.parentAuthor, + isParentBlocked: item.isParentBlocked, + isParentNotFound: item.isParentNotFound, + }; + return feedPostSliceItem; + }), + }; + return feedPostSlice; + }) + .filter(function (n) { return !!n; }), + }); }), true), + }; + // Save for memoization. + lastRun.current = { data: data, result: result, args: selectArgs }; + return result; + }, [selectArgs /* Don't change. Everything needs to go into selectArgs. */]), + }); + // The server may end up returning an empty page, a page with too few items, + // or a page with items that end up getting filtered out. When we fetch pages, + // we'll keep track of how many items we actually hope to see. If the server + // doesn't return enough items, we're going to continue asking for more items. + var lastItemCount = useRef(0); + var wantedItemCount = useRef(0); + var autoPaginationAttemptCount = useRef(0); + useEffect(function () { + var data = query.data, isLoading = query.isLoading, isRefetching = query.isRefetching, isFetchingNextPage = query.isFetchingNextPage, hasNextPage = query.hasNextPage; + // Count the items that we already have. + var itemCount = 0; + for (var _i = 0, _a = (data === null || data === void 0 ? void 0 : data.pages) || []; _i < _a.length; _i++) { + var page = _a[_i]; + for (var _b = 0, _c = page.slices; _b < _c.length; _b++) { + var slice = _c[_b]; + itemCount += slice.items.length; + } + } + // If items got truncated, reset the state we're tracking below. + if (itemCount !== lastItemCount.current) { + if (itemCount < lastItemCount.current) { + wantedItemCount.current = itemCount; + } + lastItemCount.current = itemCount; + } + // Now track how many items we really want, and fetch more if needed. + if (isLoading || isRefetching) { + // During the initial fetch, we want to get an entire page's worth of items. + wantedItemCount.current = MIN_POSTS; + } + else if (isFetchingNextPage) { + if (itemCount > wantedItemCount.current) { + // We have more items than wantedItemCount, so wantedItemCount must be out of date. + // Some other code must have called fetchNextPage(), for example, from onEndReached. + // Adjust the wantedItemCount to reflect that we want one more full page of items. + wantedItemCount.current = itemCount + MIN_POSTS; + } + } + else if (hasNextPage) { + // At this point we're not fetching anymore, so it's time to make a decision. + // If we didn't receive enough items from the server, paginate again until we do. + if (itemCount < wantedItemCount.current) { + autoPaginationAttemptCount.current++; + if (autoPaginationAttemptCount.current < 50 /* failsafe */) { + query.fetchNextPage(); + } + } + else { + autoPaginationAttemptCount.current = 0; + } + } + }, [query]); + return query; +} +export function pollLatest(page) { + return __awaiter(this, void 0, void 0, function () { + var post, slices; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!page) { + return [2 /*return*/, false]; + } + if (AppState.currentState !== 'active') { + return [2 /*return*/]; + } + logger.debug('usePostFeedQuery: pollLatest'); + return [4 /*yield*/, page.api.peekLatest()]; + case 1: + post = _a.sent(); + if (post) { + slices = page.tuner.tune([post], { + dryRun: true, + }); + if (slices[0]) { + return [2 /*return*/, true]; + } + } + return [2 /*return*/, false]; + } + }); + }); +} +function createApi(_a) { + var feedDesc = _a.feedDesc, feedParams = _a.feedParams, feedTuners = _a.feedTuners, userInterests = _a.userInterests, agent = _a.agent, enableFollowingToDiscoverFallback = _a.enableFollowingToDiscoverFallback; + if (feedDesc === 'following') { + if (feedParams.mergeFeedEnabled) { + return new MergeFeedAPI({ + agent: agent, + feedParams: feedParams, + feedTuners: feedTuners, + userInterests: userInterests, + }); + } + else { + if (enableFollowingToDiscoverFallback) { + return new HomeFeedAPI({ agent: agent, userInterests: userInterests }); + } + else { + return new FollowingFeedAPI({ agent: agent }); + } + } + } + else if (feedDesc.startsWith('author')) { + var _b = feedDesc.split('|'), __ = _b[0], actor = _b[1], filter = _b[2]; + return new AuthorFeedAPI({ agent: agent, feedParams: { actor: actor, filter: filter } }); + } + else if (feedDesc.startsWith('likes')) { + var _c = feedDesc.split('|'), __ = _c[0], actor = _c[1]; + return new LikesFeedAPI({ agent: agent, feedParams: { actor: actor } }); + } + else if (feedDesc.startsWith('feedgen')) { + var _d = feedDesc.split('|'), __ = _d[0], feed = _d[1]; + return new CustomFeedAPI({ + agent: agent, + feedParams: { feed: feed }, + userInterests: userInterests, + }); + } + else if (feedDesc.startsWith('list')) { + var _e = feedDesc.split('|'), __ = _e[0], list = _e[1]; + return new ListFeedAPI({ agent: agent, feedParams: { list: list } }); + } + else if (feedDesc.startsWith('posts')) { + var _f = feedDesc.split('|'), __ = _f[0], uriList = _f[1]; + return new PostListFeedAPI({ agent: agent, feedParams: { uris: uriList.split(',') } }); + } + else if (feedDesc === 'demo') { + return new DemoFeedAPI({ agent: agent }); + } + else { + // shouldnt happen + return new FollowingFeedAPI({ agent: agent }); + } +} +export function findAllPostsInQueryData(queryClient, uri) { + var atUri, queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, item, quotedPost, parentQuotedPost, rootQuotedPost; + var _f, _g; + return __generator(this, function (_h) { + switch (_h.label) { + case 0: + atUri = new AtUri(uri); + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _h.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 18]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 17]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _h.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 17]; + page = _c[_b]; + _d = 0, _e = page.feed; + _h.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 16]; + item = _e[_d]; + if (!didOrHandleUriMatches(atUri, item.post)) return [3 /*break*/, 5]; + return [4 /*yield*/, item.post]; + case 4: + _h.sent(); + _h.label = 5; + case 5: + quotedPost = getEmbeddedPost(item.post.embed); + if (!(quotedPost && didOrHandleUriMatches(atUri, quotedPost))) return [3 /*break*/, 7]; + return [4 /*yield*/, embedViewRecordToPostView(quotedPost)]; + case 6: + _h.sent(); + _h.label = 7; + case 7: + if (!AppBskyFeedDefs.isPostView((_f = item.reply) === null || _f === void 0 ? void 0 : _f.parent)) return [3 /*break*/, 11]; + if (!didOrHandleUriMatches(atUri, item.reply.parent)) return [3 /*break*/, 9]; + return [4 /*yield*/, item.reply.parent]; + case 8: + _h.sent(); + _h.label = 9; + case 9: + parentQuotedPost = getEmbeddedPost(item.reply.parent.embed); + if (!(parentQuotedPost && + didOrHandleUriMatches(atUri, parentQuotedPost))) return [3 /*break*/, 11]; + return [4 /*yield*/, embedViewRecordToPostView(parentQuotedPost)]; + case 10: + _h.sent(); + _h.label = 11; + case 11: + if (!AppBskyFeedDefs.isPostView((_g = item.reply) === null || _g === void 0 ? void 0 : _g.root)) return [3 /*break*/, 15]; + if (!didOrHandleUriMatches(atUri, item.reply.root)) return [3 /*break*/, 13]; + return [4 /*yield*/, item.reply.root]; + case 12: + _h.sent(); + _h.label = 13; + case 13: + rootQuotedPost = getEmbeddedPost(item.reply.root.embed); + if (!(rootQuotedPost && didOrHandleUriMatches(atUri, rootQuotedPost))) return [3 /*break*/, 15]; + return [4 /*yield*/, embedViewRecordToPostView(rootQuotedPost)]; + case 14: + _h.sent(); + _h.label = 15; + case 15: + _d++; + return [3 /*break*/, 3]; + case 16: + _b++; + return [3 /*break*/, 2]; + case 17: + _i++; + return [3 /*break*/, 1]; + case 18: return [2 /*return*/]; + } + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_2, _a, _queryKey, queryData, _b, _c, page, _d, _e, item, quotedPost; + var _f, _g, _h, _j, _k, _l; + return __generator(this, function (_m) { + switch (_m.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_2 = queryDatas; + _m.label = 1; + case 1: + if (!(_i < queryDatas_2.length)) return [3 /*break*/, 14]; + _a = queryDatas_2[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 13]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _m.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 13]; + page = _c[_b]; + _d = 0, _e = page.feed; + _m.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 12]; + item = _e[_d]; + if (!(item.post.author.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, item.post.author]; + case 4: + _m.sent(); + _m.label = 5; + case 5: + quotedPost = getEmbeddedPost(item.post.embed); + if (!((quotedPost === null || quotedPost === void 0 ? void 0 : quotedPost.author.did) === did)) return [3 /*break*/, 7]; + return [4 /*yield*/, quotedPost.author]; + case 6: + _m.sent(); + _m.label = 7; + case 7: + if (!(AppBskyFeedDefs.isPostView((_f = item.reply) === null || _f === void 0 ? void 0 : _f.parent) && + ((_h = (_g = item.reply) === null || _g === void 0 ? void 0 : _g.parent) === null || _h === void 0 ? void 0 : _h.author.did) === did)) return [3 /*break*/, 9]; + return [4 /*yield*/, item.reply.parent.author]; + case 8: + _m.sent(); + _m.label = 9; + case 9: + if (!(AppBskyFeedDefs.isPostView((_j = item.reply) === null || _j === void 0 ? void 0 : _j.root) && + ((_l = (_k = item.reply) === null || _k === void 0 ? void 0 : _k.root) === null || _l === void 0 ? void 0 : _l.author.did) === did)) return [3 /*break*/, 11]; + return [4 /*yield*/, item.reply.root.author]; + case 10: + _m.sent(); + _m.label = 11; + case 11: + _d++; + return [3 /*break*/, 3]; + case 12: + _b++; + return [3 /*break*/, 2]; + case 13: + _i++; + return [3 /*break*/, 1]; + case 14: return [2 /*return*/]; + } + }); +} +function assertSomePostsPassModeration(feed, moderationPrefs) { + // no posts in this feed + if (feed.length === 0) + return true; + // assume false + var somePostsPassModeration = false; + for (var _i = 0, feed_1 = feed; _i < feed_1.length; _i++) { + var item = feed_1[_i]; + var moderation = moderatePost(item.post, { + userDid: undefined, + prefs: moderationPrefs, + }); + if (!moderation.ui('contentList').filter) { + // we have a sfw post + somePostsPassModeration = true; + } + } + if (!somePostsPassModeration) { + throw new Error(KnownError.FeedSignedInOnly); + } +} +export function resetPostsFeedQueries(queryClient, timeout) { + if (timeout === void 0) { timeout = 0; } + setTimeout(function () { + queryClient.resetQueries({ + predicate: function (query) { return query.queryKey[0] === RQKEY_ROOT; }, + }); + }, timeout); +} +export function resetProfilePostsQueries(queryClient, did, timeout) { + if (timeout === void 0) { timeout = 0; } + setTimeout(function () { + queryClient.resetQueries({ + predicate: function (query) { + var _a; + return !!(query.queryKey[0] === RQKEY_ROOT && + ((_a = query.queryKey[1]) === null || _a === void 0 ? void 0 : _a.includes(did))); + }, + }); + }, timeout); +} +export function isFeedPostSlice(v) { + return (v && typeof v === 'object' && '_isFeedPostSlice' in v && v._isFeedPostSlice); +} diff --git a/src/state/queries/post-interaction-settings.js b/src/state/queries/post-interaction-settings.js new file mode 100644 index 0000000000..0265f1ea85 --- /dev/null +++ b/src/state/queries/post-interaction-settings.js @@ -0,0 +1,74 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { preferencesQueryKey } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export function usePostInteractionSettingsMutation(_a) { + var _b = _a === void 0 ? {} : _a, onError = _b.onError, onSettled = _b.onSettled; + var qc = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (props) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.setPostInteractionSettings(props)]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, + onSuccess: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, qc.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, + onError: onError, + onSettled: onSettled, + }); +} diff --git a/src/state/queries/post-liked-by.js b/src/state/queries/post-liked-by.js new file mode 100644 index 0000000000..c26e595b82 --- /dev/null +++ b/src/state/queries/post-liked-by.js @@ -0,0 +1,113 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +var PAGE_SIZE = 30; +// TODO refactor invalidate on mutate? +var RQKEY_ROOT = 'liked-by'; +export var RQKEY = function (resolvedUri) { return [RQKEY_ROOT, resolvedUri]; }; +export function useLikedByQuery(resolvedUri) { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(resolvedUri || ''), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.getLikes({ + uri: resolvedUri || '', + limit: PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: !!resolvedUri, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, like; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.likes; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + like = _e[_d]; + if (!(like.actor.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, like.actor]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/post-quotes.js b/src/state/queries/post-quotes.js new file mode 100644 index 0000000000..284f6f6bc9 --- /dev/null +++ b/src/state/queries/post-quotes.js @@ -0,0 +1,196 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AppBskyEmbedRecord, AtUri, } from '@atproto/api'; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +import { didOrHandleUriMatches, embedViewRecordToPostView, getEmbeddedPost, } from './util'; +var PAGE_SIZE = 30; +var RQKEY_ROOT = 'post-quotes'; +export var RQKEY = function (resolvedUri) { return [RQKEY_ROOT, resolvedUri]; }; +export function usePostQuotesQuery(resolvedUri) { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(resolvedUri || ''), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.api.app.bsky.feed.getQuotes({ + uri: resolvedUri || '', + limit: PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: !!resolvedUri, + select: function (data) { + return __assign(__assign({}, data), { pages: data.pages.map(function (page) { + return __assign(__assign({}, page), { posts: page.posts.filter(function (post) { + if (post.embed && AppBskyEmbedRecord.isView(post.embed)) { + if (AppBskyEmbedRecord.isViewDetached(post.embed.record)) { + return false; + } + } + return true; + }) }); + }) }); + }, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, item, quotedPost; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 10]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + _d = 0, _e = page.posts; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + item = _e[_d]; + if (!(item.author.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, item.author]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + quotedPost = getEmbeddedPost(item.embed); + if (!((quotedPost === null || quotedPost === void 0 ? void 0 : quotedPost.author.did) === did)) return [3 /*break*/, 7]; + return [4 /*yield*/, quotedPost.author]; + case 6: + _f.sent(); + _f.label = 7; + case 7: + _d++; + return [3 /*break*/, 3]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: return [2 /*return*/]; + } + }); +} +export function findAllPostsInQueryData(queryClient, uri) { + var queryDatas, atUri, _i, queryDatas_2, _a, _queryKey, queryData, _b, _c, page, _d, _e, post, quotedPost; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + atUri = new AtUri(uri); + _i = 0, queryDatas_2 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_2.length)) return [3 /*break*/, 10]; + _a = queryDatas_2[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + _d = 0, _e = page.posts; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + post = _e[_d]; + if (!didOrHandleUriMatches(atUri, post)) return [3 /*break*/, 5]; + return [4 /*yield*/, post]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + quotedPost = getEmbeddedPost(post.embed); + if (!(quotedPost && didOrHandleUriMatches(atUri, quotedPost))) return [3 /*break*/, 7]; + return [4 /*yield*/, embedViewRecordToPostView(quotedPost)]; + case 6: + _f.sent(); + _f.label = 7; + case 7: + _d++; + return [3 /*break*/, 3]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/post-reposted-by.js b/src/state/queries/post-reposted-by.js new file mode 100644 index 0000000000..1845dda5b1 --- /dev/null +++ b/src/state/queries/post-reposted-by.js @@ -0,0 +1,113 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +var PAGE_SIZE = 30; +// TODO refactor invalidate on mutate? +var RQKEY_ROOT = 'post-reposted-by'; +export var RQKEY = function (resolvedUri) { return [RQKEY_ROOT, resolvedUri]; }; +export function usePostRepostedByQuery(resolvedUri) { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(resolvedUri || ''), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.getRepostedBy({ + uri: resolvedUri || '', + limit: PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: !!resolvedUri, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, repostedBy; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.repostedBy; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + repostedBy = _e[_d]; + if (!(repostedBy.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, repostedBy]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/post.js b/src/state/queries/post.js new file mode 100644 index 0000000000..843a292eb3 --- /dev/null +++ b/src/state/queries/post.js @@ -0,0 +1,456 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import { AtUri } from '@atproto/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useToggleMutationQueue } from '#/lib/hooks/useToggleMutationQueue'; +import { updatePostShadow } from '#/state/cache/post-shadow'; +import { useAgent, useSession } from '#/state/session'; +import * as userActionHistory from '#/state/userActionHistory'; +import { useAnalytics } from '#/analytics'; +import { toClout } from '#/analytics/metrics'; +import { useIsThreadMuted, useSetThreadMute } from '../cache/thread-mutes'; +import { findProfileQueryData } from './profile'; +var RQKEY_ROOT = 'post'; +export var RQKEY = function (postUri) { return [RQKEY_ROOT, postUri]; }; +export function usePostQuery(uri) { + var _this = this; + var agent = useAgent(); + return useQuery({ + queryKey: RQKEY(uri || ''), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var urip, res_1, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!uri) + throw new Error('[unreachable] No URI provided'); + urip = new AtUri(uri); + if (!!urip.host.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.resolveHandle({ + handle: urip.host, + }) + // @ts-expect-error TODO new-sdk-migration + ]; + case 1: + res_1 = _a.sent(); + // @ts-expect-error TODO new-sdk-migration + urip.host = res_1.data.did; + _a.label = 2; + case 2: return [4 /*yield*/, agent.getPosts({ uris: [urip.toString()] })]; + case 3: + res = _a.sent(); + if (res.success && res.data.posts[0]) { + return [2 /*return*/, res.data.posts[0]]; + } + throw new Error('No data'); + } + }); + }); }, + enabled: !!uri, + }); +} +export function useGetPost() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useCallback(function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var uri = _b.uri; + return __generator(this, function (_c) { + return [2 /*return*/, queryClient.fetchQuery({ + queryKey: RQKEY(uri || ''), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var urip, res_2, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + urip = new AtUri(uri); + if (!!urip.host.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.resolveHandle({ + handle: urip.host, + }) + // @ts-expect-error TODO new-sdk-migration + ]; + case 1: + res_2 = _a.sent(); + // @ts-expect-error TODO new-sdk-migration + urip.host = res_2.data.did; + _a.label = 2; + case 2: return [4 /*yield*/, agent.getPosts({ + uris: [urip.toString()], + })]; + case 3: + res = _a.sent(); + if (res.success && res.data.posts[0]) { + return [2 /*return*/, res.data.posts[0]]; + } + throw new Error('useGetPost: post not found'); + } + }); + }); + }, + })]; + }); + }); }, [queryClient, agent]); +} +export function useGetPosts() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useCallback(function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var uris = _b.uris; + return __generator(this, function (_c) { + return [2 /*return*/, queryClient.fetchQuery({ + queryKey: RQKEY(uris.join(',') || ''), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.getPosts({ + uris: uris, + })]; + case 1: + res = _a.sent(); + if (res.success) { + return [2 /*return*/, res.data.posts]; + } + else { + throw new Error('useGetPosts failed'); + } + return [2 /*return*/]; + } + }); + }); + }, + })]; + }); + }); }, [queryClient, agent]); +} +export function usePostLikeMutationQueue(post, viaRepost, feedDescriptor, logContext) { + var _this = this; + var _a; + var queryClient = useQueryClient(); + var postUri = post.uri; + var postCid = post.cid; + var initialLikeUri = (_a = post.viewer) === null || _a === void 0 ? void 0 : _a.like; + var likeMutation = usePostLikeMutation(feedDescriptor, logContext, post); + var unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext, post); + var queueToggle = useToggleMutationQueue({ + initialState: initialLikeUri, + runMutation: function (prevLikeUri, shouldLike) { return __awaiter(_this, void 0, void 0, function () { + var likeUri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!shouldLike) return [3 /*break*/, 2]; + return [4 /*yield*/, likeMutation.mutateAsync({ + uri: postUri, + cid: postCid, + via: viaRepost, + })]; + case 1: + likeUri = (_a.sent()).uri; + userActionHistory.like([postUri]); + return [2 /*return*/, likeUri]; + case 2: + if (!prevLikeUri) return [3 /*break*/, 4]; + return [4 /*yield*/, unlikeMutation.mutateAsync({ + postUri: postUri, + likeUri: prevLikeUri, + })]; + case 3: + _a.sent(); + userActionHistory.unlike([postUri]); + _a.label = 4; + case 4: return [2 /*return*/, undefined]; + } + }); + }); }, + onSuccess: function (finalLikeUri) { + // finalize + updatePostShadow(queryClient, postUri, { + likeUri: finalLikeUri, + }); + }, + }); + var queueLike = useCallback(function () { + // optimistically update + updatePostShadow(queryClient, postUri, { + likeUri: 'pending', + }); + return queueToggle(true); + }, [queryClient, postUri, queueToggle]); + var queueUnlike = useCallback(function () { + // optimistically update + updatePostShadow(queryClient, postUri, { + likeUri: undefined, + }); + return queueToggle(false); + }, [queryClient, postUri, queueToggle]); + return [queueLike, queueUnlike]; +} +function usePostLikeMutation(feedDescriptor, logContext, post) { + var currentAccount = useSession().currentAccount; + var queryClient = useQueryClient(); + var postAuthor = post.author; + var agent = useAgent(); + var ax = useAnalytics(); + return useMutation({ + mutationFn: function (_a) { + var uri = _a.uri, cid = _a.cid, via = _a.via; + var ownProfile; + if (currentAccount) { + ownProfile = findProfileQueryData(queryClient, currentAccount.did); + } + ax.metric('post:like', { + uri: uri, + authorDid: postAuthor.did, + logContext: logContext, + doesPosterFollowLiker: postAuthor.viewer + ? Boolean(postAuthor.viewer.followedBy) + : undefined, + doesLikerFollowPoster: postAuthor.viewer + ? Boolean(postAuthor.viewer.following) + : undefined, + likerClout: toClout(ownProfile === null || ownProfile === void 0 ? void 0 : ownProfile.followersCount), + postClout: post.likeCount != null && + post.repostCount != null && + post.replyCount != null + ? toClout(post.likeCount + post.repostCount + post.replyCount) + : undefined, + feedDescriptor: feedDescriptor, + }); + return agent.like(uri, cid, via); + }, + }); +} +function usePostUnlikeMutation(feedDescriptor, logContext, post) { + var agent = useAgent(); + var ax = useAnalytics(); + return useMutation({ + mutationFn: function (_a) { + var postUri = _a.postUri, likeUri = _a.likeUri; + ax.metric('post:unlike', { + uri: postUri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + }); + return agent.deleteLike(likeUri); + }, + }); +} +export function usePostRepostMutationQueue(post, viaRepost, feedDescriptor, logContext) { + var _this = this; + var _a; + var queryClient = useQueryClient(); + var postUri = post.uri; + var postCid = post.cid; + var initialRepostUri = (_a = post.viewer) === null || _a === void 0 ? void 0 : _a.repost; + var repostMutation = usePostRepostMutation(feedDescriptor, logContext, post); + var unrepostMutation = usePostUnrepostMutation(feedDescriptor, logContext, post); + var queueToggle = useToggleMutationQueue({ + initialState: initialRepostUri, + runMutation: function (prevRepostUri, shouldRepost) { return __awaiter(_this, void 0, void 0, function () { + var repostUri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!shouldRepost) return [3 /*break*/, 2]; + return [4 /*yield*/, repostMutation.mutateAsync({ + uri: postUri, + cid: postCid, + via: viaRepost, + })]; + case 1: + repostUri = (_a.sent()).uri; + return [2 /*return*/, repostUri]; + case 2: + if (!prevRepostUri) return [3 /*break*/, 4]; + return [4 /*yield*/, unrepostMutation.mutateAsync({ + postUri: postUri, + repostUri: prevRepostUri, + })]; + case 3: + _a.sent(); + _a.label = 4; + case 4: return [2 /*return*/, undefined]; + } + }); + }); }, + onSuccess: function (finalRepostUri) { + // finalize + updatePostShadow(queryClient, postUri, { + repostUri: finalRepostUri, + }); + }, + }); + var queueRepost = useCallback(function () { + // optimistically update + updatePostShadow(queryClient, postUri, { + repostUri: 'pending', + }); + return queueToggle(true); + }, [queryClient, postUri, queueToggle]); + var queueUnrepost = useCallback(function () { + // optimistically update + updatePostShadow(queryClient, postUri, { + repostUri: undefined, + }); + return queueToggle(false); + }, [queryClient, postUri, queueToggle]); + return [queueRepost, queueUnrepost]; +} +function usePostRepostMutation(feedDescriptor, logContext, post) { + var agent = useAgent(); + var ax = useAnalytics(); + return useMutation({ + mutationFn: function (_a) { + var uri = _a.uri, cid = _a.cid, via = _a.via; + ax.metric('post:repost', { + uri: uri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + }); + return agent.repost(uri, cid, via); + }, + }); +} +function usePostUnrepostMutation(feedDescriptor, logContext, post) { + var agent = useAgent(); + var ax = useAnalytics(); + return useMutation({ + mutationFn: function (_a) { + var postUri = _a.postUri, repostUri = _a.repostUri; + ax.metric('post:unrepost', { + uri: postUri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + }); + return agent.deleteRepost(repostUri); + }, + }); +} +export function usePostDeleteMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var uri = _b.uri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.deletePost(uri)]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_, variables) { + updatePostShadow(queryClient, variables.uri, { isDeleted: true }); + }, + }); +} +export function useThreadMuteMutationQueue(post, rootUri) { + var _this = this; + var _a; + var threadMuteMutation = useThreadMuteMutation(); + var threadUnmuteMutation = useThreadUnmuteMutation(); + var isThreadMuted = useIsThreadMuted(rootUri, (_a = post.viewer) === null || _a === void 0 ? void 0 : _a.threadMuted); + var setThreadMute = useSetThreadMute(); + var queueToggle = useToggleMutationQueue({ + initialState: isThreadMuted, + runMutation: function (_prev, shouldMute) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!shouldMute) return [3 /*break*/, 2]; + return [4 /*yield*/, threadMuteMutation.mutateAsync({ + uri: rootUri, + })]; + case 1: + _a.sent(); + return [2 /*return*/, true]; + case 2: return [4 /*yield*/, threadUnmuteMutation.mutateAsync({ + uri: rootUri, + })]; + case 3: + _a.sent(); + return [2 /*return*/, false]; + } + }); + }); }, + onSuccess: function (finalIsMuted) { + // finalize + setThreadMute(rootUri, finalIsMuted); + }, + }); + var queueMuteThread = useCallback(function () { + // optimistically update + setThreadMute(rootUri, true); + return queueToggle(true); + }, [setThreadMute, rootUri, queueToggle]); + var queueUnmuteThread = useCallback(function () { + // optimistically update + setThreadMute(rootUri, false); + return queueToggle(false); + }, [rootUri, setThreadMute, queueToggle]); + return [isThreadMuted, queueMuteThread, queueUnmuteThread]; +} +function useThreadMuteMutation() { + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { + var uri = _a.uri; + return agent.api.app.bsky.graph.muteThread({ root: uri }); + }, + }); +} +function useThreadUnmuteMutation() { + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { + var uri = _a.uri; + return agent.api.app.bsky.graph.unmuteThread({ root: uri }); + }, + }); +} diff --git a/src/state/queries/postgate/index.js b/src/state/queries/postgate/index.js new file mode 100644 index 0000000000..b8094b3eb5 --- /dev/null +++ b/src/state/queries/postgate/index.js @@ -0,0 +1,370 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedPostgate, AtUri, } from '@atproto/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { networkRetry, retry } from '#/lib/async/retry'; +import { logger } from '#/logger'; +import { updatePostShadow } from '#/state/cache/post-shadow'; +import { STALE } from '#/state/queries'; +import { useGetPosts } from '#/state/queries/post'; +import { createMaybeDetachedQuoteEmbed, createPostgateRecord, mergePostgateRecords, POSTGATE_COLLECTION, } from '#/state/queries/postgate/util'; +import { useAgent } from '#/state/session'; +import * as bsky from '#/types/bsky'; +export function getPostgateRecord(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var urip, res, data, e_1; + var agent = _b.agent, postUri = _b.postUri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + urip = new AtUri(postUri); + if (!!urip.host.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.resolveHandle({ + handle: urip.host, + }) + // @ts-expect-error TODO new-sdk-migration + ]; + case 1: + res = _c.sent(); + // @ts-expect-error TODO new-sdk-migration + urip.host = res.data.did; + _c.label = 2; + case 2: + _c.trys.push([2, 4, , 5]); + return [4 /*yield*/, retry(2, function (e) { + /* + * If the record doesn't exist, we want to return null instead of + * throwing an error. NB: This will also catch reference errors, such as + * a typo in the URI. + */ + if (e.message.includes("Could not locate record:")) { + return false; + } + return true; + }, function () { + return agent.api.com.atproto.repo.getRecord({ + repo: urip.host, + collection: POSTGATE_COLLECTION, + rkey: urip.rkey, + }); + })]; + case 3: + data = (_c.sent()).data; + if (data.value && + bsky.validate(data.value, AppBskyFeedPostgate.validateRecord)) { + return [2 /*return*/, data.value]; + } + else { + return [2 /*return*/, undefined]; + } + return [3 /*break*/, 5]; + case 4: + e_1 = _c.sent(); + /* + * If the record doesn't exist, we want to return null instead of + * throwing an error. NB: This will also catch reference errors, such as + * a typo in the URI. + */ + if (e_1.message.includes("Could not locate record:")) { + return [2 /*return*/, undefined]; + } + else { + throw e_1; + } + return [3 /*break*/, 5]; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function writePostgateRecord(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var postUrip; + var agent = _b.agent, postUri = _b.postUri, postgate = _b.postgate; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + postUrip = new AtUri(postUri); + return [4 /*yield*/, networkRetry(2, function () { + return agent.api.com.atproto.repo.putRecord({ + repo: agent.session.did, + collection: POSTGATE_COLLECTION, + rkey: postUrip.rkey, + record: postgate, + }); + })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function upsertPostgate(_a, callback_1) { + return __awaiter(this, arguments, void 0, function (_b, callback) { + var prev, next; + var agent = _b.agent, postUri = _b.postUri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, getPostgateRecord({ + agent: agent, + postUri: postUri, + })]; + case 1: + prev = _c.sent(); + return [4 /*yield*/, callback(prev)]; + case 2: + next = _c.sent(); + if (!next) + return [2 /*return*/]; + return [4 /*yield*/, writePostgateRecord({ + agent: agent, + postUri: postUri, + postgate: next, + })]; + case 3: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +export var createPostgateQueryKey = function (postUri) { return [ + 'postgate-record', + postUri, +]; }; +export function usePostgateQuery(_a) { + var postUri = _a.postUri; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.SECONDS.THIRTY, + queryKey: createPostgateQueryKey(postUri), + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getPostgateRecord({ agent: agent, postUri: postUri }).then(function (res) { return res !== null && res !== void 0 ? res : null; })]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }, + }); +} +export function useWritePostgateMutation() { + var _this = this; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var postUri = _b.postUri, postgate = _b.postgate; + return __generator(this, function (_c) { + return [2 /*return*/, writePostgateRecord({ + agent: agent, + postUri: postUri, + postgate: postgate, + })]; + }); + }); }, + onSuccess: function (_, _a) { + var postUri = _a.postUri; + queryClient.invalidateQueries({ + queryKey: createPostgateQueryKey(postUri), + }); + }, + }); +} +export function useToggleQuoteDetachmentMutation() { + var _this = this; + var agent = useAgent(); + var queryClient = useQueryClient(); + var getPosts = useGetPosts(); + var prevEmbed = React.useRef(undefined); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var _this = this; + var post = _b.post, quoteUri = _b.quoteUri, action = _b.action; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + // cache here since post shadow mutates original object + prevEmbed.current = post.embed; + if (action === 'detach') { + updatePostShadow(queryClient, post.uri, { + embed: createMaybeDetachedQuoteEmbed({ + post: post, + quote: undefined, + quoteUri: quoteUri, + detached: true, + }), + }); + } + return [4 /*yield*/, upsertPostgate({ agent: agent, postUri: quoteUri }, function (prev) { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + if (prev) { + if (action === 'detach') { + return [2 /*return*/, mergePostgateRecords(prev, { + detachedEmbeddingUris: [post.uri], + })]; + } + else if (action === 'reattach') { + return [2 /*return*/, __assign(__assign({}, prev), { detachedEmbeddingUris: ((_a = prev.detachedEmbeddingUris) === null || _a === void 0 ? void 0 : _a.filter(function (uri) { return uri !== post.uri; })) || + [] })]; + } + } + else { + if (action === 'detach') { + return [2 /*return*/, createPostgateRecord({ + post: quoteUri, + detachedEmbeddingUris: [post.uri], + })]; + } + } + return [2 /*return*/]; + }); + }); })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_data_1, _a) { + return __awaiter(this, arguments, void 0, function (_data, _b) { + var quote, e_2; + var post = _b.post, quoteUri = _b.quoteUri, action = _b.action; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!(action === 'reattach')) return [3 /*break*/, 4]; + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, getPosts({ uris: [quoteUri] })]; + case 2: + quote = (_c.sent())[0]; + updatePostShadow(queryClient, post.uri, { + embed: createMaybeDetachedQuoteEmbed({ + post: post, + quote: quote, + quoteUri: undefined, + detached: false, + }), + }); + return [3 /*break*/, 4]; + case 3: + e_2 = _c.sent(); + // ok if this fails, it's just optimistic UI + logger.error("Postgate: failed to get quote post for re-attachment", { + safeMessage: e_2.message, + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); + }, + onError: function (_, _a) { + var post = _a.post, action = _a.action; + if (action === 'detach' && prevEmbed.current) { + // detach failed, add the embed back + if (AppBskyEmbedRecord.isView(prevEmbed.current) || + AppBskyEmbedRecordWithMedia.isView(prevEmbed.current)) { + updatePostShadow(queryClient, post.uri, { + embed: prevEmbed.current, + }); + } + } + }, + onSettled: function () { + prevEmbed.current = undefined; + }, + }); +} +export function useToggleQuotepostEnabledMutation() { + var _this = this; + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var _this = this; + var postUri = _b.postUri, action = _b.action; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, upsertPostgate({ agent: agent, postUri: postUri }, function (prev) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (prev) { + if (action === 'disable') { + return [2 /*return*/, mergePostgateRecords(prev, { + embeddingRules: [{ $type: 'app.bsky.feed.postgate#disableRule' }], + })]; + } + else if (action === 'enable') { + return [2 /*return*/, __assign(__assign({}, prev), { embeddingRules: [] })]; + } + } + else { + if (action === 'disable') { + return [2 /*return*/, createPostgateRecord({ + post: postUri, + embeddingRules: [{ $type: 'app.bsky.feed.postgate#disableRule' }], + })]; + } + } + return [2 /*return*/]; + }); + }); })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/postgate/util.js b/src/state/queries/postgate/util.js new file mode 100644 index 0000000000..8c84c6baaa --- /dev/null +++ b/src/state/queries/postgate/util.js @@ -0,0 +1,152 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AtUri, } from '@atproto/api'; +export var POSTGATE_COLLECTION = 'app.bsky.feed.postgate'; +export function createPostgateRecord(postgate) { + return { + $type: POSTGATE_COLLECTION, + createdAt: new Date().toISOString(), + post: postgate.post, + detachedEmbeddingUris: postgate.detachedEmbeddingUris || [], + embeddingRules: postgate.embeddingRules || [], + }; +} +export function mergePostgateRecords(prev, next) { + var detachedEmbeddingUris = Array.from(new Set(__spreadArray(__spreadArray([], (prev.detachedEmbeddingUris || []), true), (next.detachedEmbeddingUris || []), true))); + var embeddingRules = __spreadArray(__spreadArray([], (prev.embeddingRules || []), true), (next.embeddingRules || []), true).filter(function (rule, i, all) { return all.findIndex(function (_rule) { return _rule.$type === rule.$type; }) === i; }); + return createPostgateRecord({ + post: prev.post, + detachedEmbeddingUris: detachedEmbeddingUris, + embeddingRules: embeddingRules, + }); +} +export function createEmbedViewDetachedRecord(_a) { + var uri = _a.uri; + var record = { + $type: 'app.bsky.embed.record#viewDetached', + uri: uri, + detached: true, + }; + return { + $type: 'app.bsky.embed.record#view', + record: record, + }; +} +export function createMaybeDetachedQuoteEmbed(_a) { + var post = _a.post, quote = _a.quote, quoteUri = _a.quoteUri, detached = _a.detached; + if (AppBskyEmbedRecord.isView(post.embed)) { + if (detached) { + return createEmbedViewDetachedRecord({ uri: quoteUri }); + } + else { + return createEmbedRecordView({ post: quote }); + } + } + else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) { + if (detached) { + return __assign(__assign({}, post.embed), { record: createEmbedViewDetachedRecord({ uri: quoteUri }) }); + } + else { + return createEmbedRecordWithMediaView({ post: post, quote: quote }); + } + } +} +export function createEmbedViewRecordFromPost(post) { + return { + $type: 'app.bsky.embed.record#viewRecord', + uri: post.uri, + cid: post.cid, + author: post.author, + value: post.record, + labels: post.labels, + replyCount: post.replyCount, + repostCount: post.repostCount, + likeCount: post.likeCount, + quoteCount: post.quoteCount, + indexedAt: post.indexedAt, + embeds: post.embed ? [post.embed] : [], + }; +} +export function createEmbedRecordView(_a) { + var post = _a.post; + return { + $type: 'app.bsky.embed.record#view', + record: createEmbedViewRecordFromPost(post), + }; +} +export function createEmbedRecordWithMediaView(_a) { + var post = _a.post, quote = _a.quote; + if (!AppBskyEmbedRecordWithMedia.isView(post.embed)) + return; + return __assign(__assign({}, (post.embed || {})), { record: { + record: createEmbedViewRecordFromPost(quote), + } }); +} +export function getMaybeDetachedQuoteEmbed(_a) { + var viewerDid = _a.viewerDid, post = _a.post; + if (AppBskyEmbedRecord.isView(post.embed)) { + // detached + if (AppBskyEmbedRecord.isViewDetached(post.embed.record)) { + var urip = new AtUri(post.embed.record.uri); + return { + embed: post.embed, + uri: urip.toString(), + isOwnedByViewer: urip.host === viewerDid, + isDetached: true, + }; + } + // post + if (AppBskyEmbedRecord.isViewRecord(post.embed.record)) { + var urip = new AtUri(post.embed.record.uri); + return { + embed: post.embed, + uri: urip.toString(), + isOwnedByViewer: urip.host === viewerDid, + isDetached: false, + }; + } + } + else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) { + // detached + if (AppBskyEmbedRecord.isViewDetached(post.embed.record.record)) { + var urip = new AtUri(post.embed.record.record.uri); + return { + embed: post.embed, + uri: urip.toString(), + isOwnedByViewer: urip.host === viewerDid, + isDetached: true, + }; + } + // post + if (AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) { + var urip = new AtUri(post.embed.record.record.uri); + return { + embed: post.embed, + uri: urip.toString(), + isOwnedByViewer: urip.host === viewerDid, + isDetached: false, + }; + } + } +} +export var embeddingRules = { + disableRule: { $type: 'app.bsky.feed.postgate#disableRule' }, +}; diff --git a/src/state/queries/preferences/const.js b/src/state/queries/preferences/const.js new file mode 100644 index 0000000000..31140149a6 --- /dev/null +++ b/src/state/queries/preferences/const.js @@ -0,0 +1,44 @@ +import { DEFAULT_LOGGED_OUT_LABEL_PREFERENCES } from '#/state/queries/preferences/moderation'; +export var DEFAULT_HOME_FEED_PREFS = { + hideReplies: false, + hideRepliesByUnfollowed: true, // Legacy, ignored + hideRepliesByLikeCount: 0, // Legacy, ignored + hideReposts: false, + hideQuotePosts: false, + lab_mergeFeedEnabled: false, // experimental +}; +export var DEFAULT_THREAD_VIEW_PREFS = { + sort: 'hotness', + lab_treeViewEnabled: false, +}; +export var DEFAULT_LOGGED_OUT_PREFERENCES = { + birthDate: new Date('2022-11-17'), // TODO(pwi) + moderationPrefs: { + adultContentEnabled: false, + labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, + labelers: [], + mutedWords: [], + hiddenPosts: [], + }, + feedViewPrefs: DEFAULT_HOME_FEED_PREFS, + threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS, + userAge: 13, // TODO(pwi) + interests: { tags: [] }, + savedFeeds: [], + bskyAppState: { + queuedNudges: [], + activeProgressGuide: undefined, + nuxs: [], + }, + postInteractionSettings: { + threadgateAllowRules: undefined, + postgateEmbeddingRules: [], + }, + verificationPrefs: { + hideBadges: false, + }, + liveEventPreferences: { + hideAllFeeds: false, + hiddenFeedIds: [], + }, +}; diff --git a/src/state/queries/preferences/index.js b/src/state/queries/preferences/index.js new file mode 100644 index 0000000000..04ca6b01dc --- /dev/null +++ b/src/state/queries/preferences/index.js @@ -0,0 +1,653 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { PROD_DEFAULT_FEED } from '#/lib/constants'; +import { replaceEqualDeep } from '#/lib/functions'; +import { getAge } from '#/lib/strings/time'; +import { PERSISTED_QUERY_GCTIME, PERSISTED_QUERY_ROOT, STALE, } from '#/state/queries'; +import { DEFAULT_HOME_FEED_PREFS, DEFAULT_LOGGED_OUT_PREFERENCES, DEFAULT_THREAD_VIEW_PREFS, } from '#/state/queries/preferences/const'; +import { useAgent } from '#/state/session'; +import { saveLabelers } from '#/state/session/agent-config'; +import { useAgeAssurance } from '#/ageAssurance'; +import { makeAgeRestrictedModerationPrefs } from '#/ageAssurance/util'; +import { useAnalytics } from '#/analytics'; +export * from '#/state/queries/preferences/const'; +export * from '#/state/queries/preferences/moderation'; +export * from '#/state/queries/preferences/types'; +export var preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences']; +export function usePreferencesQuery() { + var _this = this; + var _a; + var agent = useAgent(); + var aa = useAgeAssurance(); + var query = useQuery({ + staleTime: STALE.SECONDS.FIFTEEN, + structuralSharing: replaceEqualDeep, + refetchOnWindowFocus: true, + queryKey: preferencesQueryKey, + gcTime: PERSISTED_QUERY_GCTIME, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res, preferences; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!!agent.did) return [3 /*break*/, 1]; + return [2 /*return*/, DEFAULT_LOGGED_OUT_PREFERENCES]; + case 1: return [4 /*yield*/, agent.getPreferences() + // save to local storage to ensure there are labels on initial requests + ]; + case 2: + res = _b.sent(); + // save to local storage to ensure there are labels on initial requests + saveLabelers(agent.did, res.moderationPrefs.labelers.map(function (l) { return l.did; })); + preferences = __assign(__assign({}, res), { savedFeeds: res.savedFeeds.filter(function (f) { return f.type !== 'unknown'; }), + /** + * Special preference, only used for following feed, previously + * called `home` + */ + feedViewPrefs: __assign(__assign({}, DEFAULT_HOME_FEED_PREFS), (res.feedViewPrefs.home || {})), threadViewPrefs: __assign(__assign({}, DEFAULT_THREAD_VIEW_PREFS), ((_a = res.threadViewPrefs) !== null && _a !== void 0 ? _a : {})), userAge: res.birthDate ? getAge(res.birthDate) : undefined }); + return [2 /*return*/, preferences]; + } + }); + }); }, + select: useCallback(function (data) { + /** + * Prefs are all downstream of age assurance now. For logged-out + * users, we override moderation prefs based on AA state. + */ + if (aa.state.access !== aa.Access.Full) { + data = __assign(__assign({}, data), { moderationPrefs: makeAgeRestrictedModerationPrefs(data.moderationPrefs) }); + } + return data; + }, [aa]), + }); + if ((_a = query.data) === null || _a === void 0 ? void 0 : _a.birthDate) { + /** + * The persisted query cache stores dates as strings, but our code expects a `Date`. + */ + query.data.birthDate = new Date(query.data.birthDate); + } + return query; +} +export function useClearPreferencesMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.actor.putPreferences({ preferences: [] }) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function usePreferencesSetContentLabelMutation() { + var _this = this; + var ax = useAnalytics(); + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var label = _b.label, visibility = _b.visibility, labelerDid = _b.labelerDid; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.setContentLabelPref(label, visibility, labelerDid)]; + case 1: + _c.sent(); + ax.metric('moderation:changeLabelPreference', { preference: visibility }); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useSetContentLabelMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var label = _b.label, visibility = _b.visibility, labelerDid = _b.labelerDid; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.setContentLabelPref(label, visibility, labelerDid) + // triggers a refetch + ]; + case 1: + _c.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function usePreferencesSetAdultContentMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var enabled = _b.enabled; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.setAdultContentEnabled(enabled) + // triggers a refetch + ]; + case 1: + _c.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useSetFeedViewPreferencesMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (prefs) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + /* + * special handling here, merged into `feedViewPrefs` above, since + * following was previously called `home` + */ + return [4 /*yield*/, agent.setFeedViewPrefs('home', prefs) + // triggers a refetch + ]; + case 1: + /* + * special handling here, merged into `feedViewPrefs` above, since + * following was previously called `home` + */ + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useSetThreadViewPreferencesMutation(_a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (prefs) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.setThreadViewPrefs(prefs) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: onSuccess, + onError: onError, + }); +} +export function useOverwriteSavedFeedsMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (savedFeeds) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.overwriteSavedFeeds(savedFeeds) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useAddSavedFeedsMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (savedFeeds) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.addSavedFeeds(savedFeeds) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useRemoveFeedMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (savedFeed) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.removeSavedFeeds([savedFeed.id]) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useReplaceForYouWithDiscoverFeedMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var forYouFeedConfig = _b.forYouFeedConfig, discoverFeedConfig = _b.discoverFeedConfig; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!forYouFeedConfig) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.removeSavedFeeds([forYouFeedConfig.id])]; + case 1: + _c.sent(); + _c.label = 2; + case 2: + if (!!discoverFeedConfig) return [3 /*break*/, 4]; + return [4 /*yield*/, agent.addSavedFeeds([ + { + type: 'feed', + value: PROD_DEFAULT_FEED('whats-hot'), + pinned: true, + }, + ])]; + case 3: + _c.sent(); + return [3 /*break*/, 6]; + case 4: return [4 /*yield*/, agent.updateSavedFeeds([ + __assign(__assign({}, discoverFeedConfig), { pinned: true }), + ])]; + case 5: + _c.sent(); + _c.label = 6; + case 6: + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 7: + // triggers a refetch + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useUpdateSavedFeedsMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (feeds) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.updateSavedFeeds(feeds) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useUpsertMutedWordsMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (mutedWords) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.upsertMutedWords(mutedWords) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useUpdateMutedWordMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (mutedWord) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.updateMutedWord(mutedWord) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useRemoveMutedWordMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (mutedWord) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.removeMutedWord(mutedWord) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useRemoveMutedWordsMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (mutedWords) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.removeMutedWords(mutedWords) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useQueueNudgesMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (nudges) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.bskyAppQueueNudges(nudges) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useDismissNudgesMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (nudges) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.bskyAppDismissNudges(nudges) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useSetActiveProgressGuideMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (guide) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.bskyAppSetActiveProgressGuide(guide) + // triggers a refetch + ]; + case 1: + _a.sent(); + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} +export function useSetVerificationPrefsMutation() { + var _this = this; + var ax = useAnalytics(); + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (prefs) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.setVerificationPrefs(prefs)]; + case 1: + _a.sent(); + if (prefs.hideBadges) { + ax.metric('verification:settings:hideBadges', {}); + } + else { + ax.metric('verification:settings:unHideBadges', {}); + } + // triggers a refetch + return [4 /*yield*/, queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + })]; + case 2: + // triggers a refetch + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/preferences/moderation.js b/src/state/queries/preferences/moderation.js new file mode 100644 index 0000000000..baf2194049 --- /dev/null +++ b/src/state/queries/preferences/moderation.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { BskyAgent, DEFAULT_LABEL_SETTINGS, interpretLabelValueDefinitions, } from '@atproto/api'; +import { isNonConfigurableModerationAuthority } from '#/state/session/additional-moderation-authorities'; +import { useLabelersDetailedInfoQuery } from '../labeler'; +import { usePreferencesQuery } from './index'; +/** + * More strict than our default settings for logged in users. + */ +export var DEFAULT_LOGGED_OUT_LABEL_PREFERENCES = Object.fromEntries(Object.entries(DEFAULT_LABEL_SETTINGS).map(function (_a) { + var key = _a[0], _pref = _a[1]; + return [key, 'hide']; +})); +export function useMyLabelersQuery(_a) { + var _b; + var _c = _a === void 0 ? {} : _a, _d = _c.excludeNonConfigurableLabelers, excludeNonConfigurableLabelers = _d === void 0 ? false : _d; + var prefs = usePreferencesQuery(); + var dids = Array.from(new Set(BskyAgent.appLabelers.concat(((_b = prefs.data) === null || _b === void 0 ? void 0 : _b.moderationPrefs.labelers.map(function (l) { return l.did; })) || []))); + if (excludeNonConfigurableLabelers) { + dids = dids.filter(function (did) { return !isNonConfigurableModerationAuthority(did); }); + } + var labelers = useLabelersDetailedInfoQuery({ dids: dids }); + var isLoading = prefs.isLoading || labelers.isLoading; + var error = prefs.error || labelers.error; + return React.useMemo(function () { + return { + isLoading: isLoading, + error: error, + data: labelers.data, + refetch: labelers.refetch, + }; + }, [labelers, isLoading, error]); +} +export function useLabelDefinitionsQuery() { + var labelers = useMyLabelersQuery(); + return React.useMemo(function () { + return { + labelDefs: Object.fromEntries((labelers.data || []).map(function (labeler) { return [ + labeler.creator.did, + interpretLabelValueDefinitions(labeler), + ]; })), + labelers: labelers.data || [], + }; + }, [labelers]); +} diff --git a/src/state/queries/preferences/types.js b/src/state/queries/preferences/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/state/queries/preferences/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/state/queries/preferences/useThreadPreferences.js b/src/state/queries/preferences/useThreadPreferences.js new file mode 100644 index 0000000000..1507e70061 --- /dev/null +++ b/src/state/queries/preferences/useThreadPreferences.js @@ -0,0 +1,109 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; +import debounce from 'lodash.debounce'; +import { useCallOnce } from '#/lib/once'; +import { usePreferencesQuery, useSetThreadViewPreferencesMutation, } from '#/state/queries/preferences'; +import { useAnalytics } from '#/analytics'; +export function useThreadPreferences(_a) { + var _b = _a === void 0 ? {} : _a, save = _b.save; + var ax = useAnalytics(); + var preferences = usePreferencesQuery().data; + var serverPrefs = preferences === null || preferences === void 0 ? void 0 : preferences.threadViewPrefs; + var once = useCallOnce(); + /* + * Create local state representations of server state + */ + var _c = useState(normalizeSort((serverPrefs === null || serverPrefs === void 0 ? void 0 : serverPrefs.sort) || 'top')), sort = _c[0], setSort = _c[1]; + var _d = useState(normalizeView({ + treeViewEnabled: !!(serverPrefs === null || serverPrefs === void 0 ? void 0 : serverPrefs.lab_treeViewEnabled), + })), view = _d[0], setView = _d[1]; + /** + * If we get a server update, update local state + */ + var _e = useState(serverPrefs), prevServerPrefs = _e[0], setPrevServerPrefs = _e[1]; + var isLoaded = !!prevServerPrefs; + if (serverPrefs && prevServerPrefs !== serverPrefs) { + setPrevServerPrefs(serverPrefs); + /* + * Update + */ + setSort(normalizeSort(serverPrefs.sort)); + setView(normalizeView({ + treeViewEnabled: !!serverPrefs.lab_treeViewEnabled, + })); + once(function () { + ax.metric('thread:preferences:load', { + sort: serverPrefs.sort, + view: serverPrefs.lab_treeViewEnabled ? 'tree' : 'linear', + }); + }); + } + var userUpdatedPrefs = useRef(false); + var _f = useSetThreadViewPreferencesMutation({ + onSuccess: function (_data, prefs) { + ax.metric('thread:preferences:update', { + sort: prefs.sort, + view: prefs.lab_treeViewEnabled ? 'tree' : 'linear', + }); + }, + onError: function (err) { + ax.logger.error('useThreadPreferences failed to save', { + safeMessage: err, + }); + }, + }), mutate = _f.mutate, isSaving = _f.isPending; + var savePrefs = useMemo(function () { + return debounce(function (prefs) { + mutate(prefs); + }, 2e3, { leading: true, trailing: true }); + }, [mutate]); + // flush on leave screen + useFocusEffect(useCallback(function () { + return function () { + void savePrefs.flush(); + }; + }, [savePrefs])); + if (save && userUpdatedPrefs.current) { + savePrefs({ + sort: sort, + lab_treeViewEnabled: view === 'tree', + }); + userUpdatedPrefs.current = false; + } + var setSortWrapped = useCallback(function (next) { + userUpdatedPrefs.current = true; + setSort(normalizeSort(next)); + }, [setSort]); + var setViewWrapped = useCallback(function (next) { + userUpdatedPrefs.current = true; + setView(next); + }, [setView]); + return useMemo(function () { return ({ + isLoaded: isLoaded, + isSaving: isSaving, + sort: sort, + setSort: setSortWrapped, + view: view, + setView: setViewWrapped, + }); }, [isLoaded, isSaving, sort, setSortWrapped, view, setViewWrapped]); +} +/** + * Migrates user thread preferences from the old sort values to V2 + */ +export function normalizeSort(sort) { + switch (sort) { + case 'oldest': + return 'oldest'; + case 'newest': + return 'newest'; + default: + return 'top'; + } +} +/** + * Transforms existing treeViewEnabled preference into a ThreadViewOption + */ +export function normalizeView(_a) { + var treeViewEnabled = _a.treeViewEnabled; + return treeViewEnabled ? 'tree' : 'linear'; +} diff --git a/src/state/queries/profile-feedgens.js b/src/state/queries/profile-feedgens.js new file mode 100644 index 0000000000..05ac206a12 --- /dev/null +++ b/src/state/queries/profile-feedgens.js @@ -0,0 +1,97 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { moderateFeedGenerator, } from '@atproto/api'; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +import { useModerationOpts } from '../preferences/moderation-opts'; +var PAGE_SIZE = 50; +// TODO refactor invalidate on mutate? +export var RQKEY_ROOT = 'profile-feedgens'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export function useProfileFeedgensQuery(did, opts) { + var moderationOpts = useModerationOpts(); + var enabled = (opts === null || opts === void 0 ? void 0 : opts.enabled) !== false && Boolean(moderationOpts); + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(did), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.feed.getActorFeeds({ + actor: did, + limit: PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + res.data.feeds.sort(function (a, b) { + return (b.likeCount || 0) - (a.likeCount || 0); + }); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: enabled, + select: function (data) { + return __assign(__assign({}, data), { pages: data.pages.map(function (page) { + return __assign(__assign({}, page), { feeds: page.feeds + // filter by labels + .filter(function (list) { + var decision = moderateFeedGenerator(list, moderationOpts); + return !decision.ui('contentList').filter; + }) }); + }) }); + }, + }); +} diff --git a/src/state/queries/profile-followers.js b/src/state/queries/profile-followers.js new file mode 100644 index 0000000000..fd989566bf --- /dev/null +++ b/src/state/queries/profile-followers.js @@ -0,0 +1,112 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +var PAGE_SIZE = 30; +var RQKEY_ROOT = 'profile-followers'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export function useProfileFollowersQuery(did) { + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(did || ''), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getFollowers({ + actor: did || '', + limit: PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: !!did, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, follower; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.followers; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + follower = _e[_d]; + if (!(follower.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, follower]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/profile-follows.js b/src/state/queries/profile-follows.js new file mode 100644 index 0000000000..8aa0943570 --- /dev/null +++ b/src/state/queries/profile-follows.js @@ -0,0 +1,118 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +var PAGE_SIZE = 30; +// TODO refactor invalidate on mutate? +var RQKEY_ROOT = 'profile-follows'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export function useProfileFollowsQuery(did, _a) { + var _b = _a === void 0 ? { + limit: PAGE_SIZE, + } : _a, limit = _b.limit; + var agent = useAgent(); + return useInfiniteQuery({ + staleTime: STALE.MINUTES.ONE, + queryKey: RQKEY(did || ''), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getFollows({ + actor: did || '', + limit: limit || PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: !!did, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, follow; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.follows; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + follow = _e[_d]; + if (!(follow.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, follow]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/profile-lists.js b/src/state/queries/profile-lists.js new file mode 100644 index 0000000000..cfc3979272 --- /dev/null +++ b/src/state/queries/profile-lists.js @@ -0,0 +1,91 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { moderateUserList } from '@atproto/api'; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useAgent } from '#/state/session'; +import { useModerationOpts } from '../preferences/moderation-opts'; +var PAGE_SIZE = 30; +export var RQKEY_ROOT = 'profile-lists'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export function useProfileListsQuery(did, opts) { + var moderationOpts = useModerationOpts(); + var enabled = (opts === null || opts === void 0 ? void 0 : opts.enabled) !== false && Boolean(moderationOpts); + var agent = useAgent(); + return useInfiniteQuery({ + queryKey: RQKEY(did), + queryFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getLists({ + actor: did, + limit: PAGE_SIZE, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: enabled, + select: function (data) { + return __assign(__assign({}, data), { pages: data.pages.map(function (page) { + return __assign(__assign({}, page), { lists: page.lists.filter(function (list) { + var decision = moderateUserList(list, moderationOpts); + return !decision.ui('contentList').filter; + }) }); + }) }); + }, + }); +} diff --git a/src/state/queries/profile.js b/src/state/queries/profile.js new file mode 100644 index 0000000000..0c247938f7 --- /dev/null +++ b/src/state/queries/profile.js @@ -0,0 +1,729 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useCallback } from 'react'; +import { AtUri, } from '@atproto/api'; +import { keepPreviousData, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; +import { uploadBlob } from '#/lib/api'; +import { until } from '#/lib/async/until'; +import { useToggleMutationQueue } from '#/lib/hooks/useToggleMutationQueue'; +import { updateProfileShadow } from '#/state/cache/profile-shadow'; +import { STALE } from '#/state/queries'; +import { resetProfilePostsQueries } from '#/state/queries/post-feed'; +import { RQKEY as PROFILE_FOLLOWS_RQKEY } from '#/state/queries/profile-follows'; +import { unstableCacheProfileView, useUnstableProfileViewCache, } from '#/state/queries/unstable-profile-cache'; +import { useUpdateProfileVerificationCache } from '#/state/queries/verification/useUpdateProfileVerificationCache'; +import { useAgent, useSession } from '#/state/session'; +import * as userActionHistory from '#/state/userActionHistory'; +import { useAnalytics } from '#/analytics'; +import { toClout } from '#/analytics/metrics'; +import { ProgressGuideAction, useProgressGuideControls, } from '../shell/progress-guide'; +import { RQKEY_ROOT as RQKEY_LIST_CONVOS } from './messages/list-conversations'; +import { RQKEY as RQKEY_MY_BLOCKED } from './my-blocked-accounts'; +import { RQKEY as RQKEY_MY_MUTED } from './my-muted-accounts'; +export * from '#/state/queries/unstable-profile-cache'; +/** + * @deprecated use {@link unstableCacheProfileView} instead + */ +export var precacheProfile = unstableCacheProfileView; +var RQKEY_ROOT = 'profile'; +export var RQKEY = function (did) { return [RQKEY_ROOT, did]; }; +export var profilesQueryKeyRoot = 'profiles'; +export var profilesQueryKey = function (handles) { return [ + profilesQueryKeyRoot, + handles, +]; }; +export function useProfileQuery(_a) { + var _this = this; + var did = _a.did, _b = _a.staleTime, staleTime = _b === void 0 ? STALE.SECONDS.FIFTEEN : _b; + var agent = useAgent(); + var getUnstableProfile = useUnstableProfileViewCache().getUnstableProfile; + return useQuery({ + // WARNING + // this staleTime is load-bearing + // if you remove it, the UI infinite-loops + // -prf + staleTime: staleTime, + refetchOnWindowFocus: true, + queryKey: RQKEY(did !== null && did !== void 0 ? did : ''), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.getProfile({ actor: did !== null && did !== void 0 ? did : '' })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + placeholderData: function () { + if (!did) + return; + return getUnstableProfile(did); + }, + enabled: !!did, + }); +} +export function useProfilesQuery(_a) { + var _this = this; + var handles = _a.handles, maintainData = _a.maintainData; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.MINUTES.FIVE, + queryKey: profilesQueryKey(handles), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.getProfiles({ actors: handles })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + placeholderData: maintainData ? keepPreviousData : undefined, + }); +} +export function usePrefetchProfileQuery() { + var _this = this; + var agent = useAgent(); + var queryClient = useQueryClient(); + var prefetchProfileQuery = useCallback(function (did) { return __awaiter(_this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, queryClient.prefetchQuery({ + staleTime: STALE.SECONDS.THIRTY, + queryKey: RQKEY(did), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.getProfile({ actor: did || '' })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, [queryClient, agent]); + return prefetchProfileQuery; +} +export function useProfileUpdateMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + var updateProfileVerificationCache = useUpdateProfileVerificationCache(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var newUserAvatarPromise, newUserBannerPromise; + var _this = this; + var profile = _b.profile, updates = _b.updates, newUserAvatar = _b.newUserAvatar, newUserBanner = _b.newUserBanner, checkCommitted = _b.checkCommitted; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (newUserAvatar) { + newUserAvatarPromise = uploadBlob(agent, newUserAvatar.path, newUserAvatar.mime); + } + if (newUserBanner) { + newUserBannerPromise = uploadBlob(agent, newUserBanner.path, newUserBanner.mime); + } + return [4 /*yield*/, agent.upsertProfile(function (existing) { return __awaiter(_this, void 0, void 0, function () { + var next, res, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + next = existing || {}; + if (typeof updates === 'function') { + next = updates(next); + } + else { + next.displayName = updates.displayName; + next.description = updates.description; + if ('pinnedPost' in updates) { + next.pinnedPost = updates.pinnedPost; + } + } + if (!newUserAvatarPromise) return [3 /*break*/, 2]; + return [4 /*yield*/, newUserAvatarPromise]; + case 1: + res = _a.sent(); + next.avatar = res.data.blob; + return [3 /*break*/, 3]; + case 2: + if (newUserAvatar === null) { + next.avatar = undefined; + } + _a.label = 3; + case 3: + if (!newUserBannerPromise) return [3 /*break*/, 5]; + return [4 /*yield*/, newUserBannerPromise]; + case 4: + res = _a.sent(); + next.banner = res.data.blob; + return [3 /*break*/, 6]; + case 5: + if (newUserBanner === null) { + next.banner = undefined; + } + _a.label = 6; + case 6: return [2 /*return*/, next]; + } + }); + }); })]; + case 1: + _c.sent(); + return [4 /*yield*/, whenAppViewReady(agent, profile.did, checkCommitted || + (function (res) { + if (typeof newUserAvatar !== 'undefined') { + if (newUserAvatar === null && res.data.avatar) { + // url hasnt cleared yet + return false; + } + else if (res.data.avatar === profile.avatar) { + // url hasnt changed yet + return false; + } + } + if (typeof newUserBanner !== 'undefined') { + if (newUserBanner === null && res.data.banner) { + // url hasnt cleared yet + return false; + } + else if (res.data.banner === profile.banner) { + // url hasnt changed yet + return false; + } + } + if (typeof updates === 'function') { + return true; + } + return (res.data.displayName === updates.displayName && + res.data.description === updates.description); + }))]; + case 2: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_, variables) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // invalidate cache + queryClient.invalidateQueries({ + queryKey: RQKEY(variables.profile.did), + }); + queryClient.invalidateQueries({ + queryKey: [profilesQueryKeyRoot, [variables.profile.did]], + }); + return [4 /*yield*/, updateProfileVerificationCache({ profile: variables.profile })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, + }); +} +export function useProfileFollowMutationQueue(profile, logContext, position, contextProfileDid) { + var _this = this; + var _a; + var agent = useAgent(); + var queryClient = useQueryClient(); + var currentAccount = useSession().currentAccount; + var did = profile.did; + var initialFollowingUri = (_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following; + var followMutation = useProfileFollowMutation(logContext, profile, position, contextProfileDid); + var unfollowMutation = useProfileUnfollowMutation(logContext); + var queueToggle = useToggleMutationQueue({ + initialState: initialFollowingUri, + runMutation: function (prevFollowingUri, shouldFollow) { return __awaiter(_this, void 0, void 0, function () { + var uri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!shouldFollow) return [3 /*break*/, 2]; + return [4 /*yield*/, followMutation.mutateAsync({ + did: did, + })]; + case 1: + uri = (_a.sent()).uri; + userActionHistory.follow([did]); + return [2 /*return*/, uri]; + case 2: + if (!prevFollowingUri) return [3 /*break*/, 4]; + return [4 /*yield*/, unfollowMutation.mutateAsync({ + did: did, + followUri: prevFollowingUri, + })]; + case 3: + _a.sent(); + userActionHistory.unfollow([did]); + _a.label = 4; + case 4: return [2 /*return*/, undefined]; + } + }); + }); }, + onSuccess: function (finalFollowingUri) { + // finalize + updateProfileShadow(queryClient, did, { + followingUri: finalFollowingUri, + }); + // Optimistically update profile follows cache for avatar displays + if (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) { + queryClient.setQueryData(PROFILE_FOLLOWS_RQKEY(currentAccount.did), function (old) { + var _a; + if (!((_a = old === null || old === void 0 ? void 0 : old.pages) === null || _a === void 0 ? void 0 : _a[0])) + return old; + if (finalFollowingUri) { + // Add the followed profile to the beginning + var alreadyExists = old.pages[0].follows.some(function (f) { return f.did === profile.did; }); + if (alreadyExists) + return old; + return __assign(__assign({}, old), { pages: __spreadArray([ + __assign(__assign({}, old.pages[0]), { follows: __spreadArray([ + profile + ], old.pages[0].follows, true) }) + ], old.pages.slice(1), true) }); + } + else { + // Remove the unfollowed profile + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { return (__assign(__assign({}, page), { follows: page.follows.filter(function (f) { return f.did !== profile.did; }) })); }) }); + } + }); + } + if (finalFollowingUri) { + agent.app.bsky.graph + .getSuggestedFollowsByActor({ + actor: did, + }) + .then(function (res) { + var dids = res.data.suggestions + .filter(function (a) { var _a; return !((_a = a.viewer) === null || _a === void 0 ? void 0 : _a.following); }) + .map(function (a) { return a.did; }) + .slice(0, 8); + userActionHistory.followSuggestion(dids); + }); + } + }, + }); + var queueFollow = useCallback(function () { + // optimistically update + updateProfileShadow(queryClient, did, { + followingUri: 'pending', + }); + return queueToggle(true); + }, [queryClient, did, queueToggle]); + var queueUnfollow = useCallback(function () { + // optimistically update + updateProfileShadow(queryClient, did, { + followingUri: undefined, + }); + return queueToggle(false); + }, [queryClient, did, queueToggle]); + return [queueFollow, queueUnfollow]; +} +function useProfileFollowMutation(logContext, profile, position, contextProfileDid) { + var _this = this; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + var captureAction = useProgressGuideControls().captureAction; + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var ownProfile; + var did = _b.did; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (currentAccount) { + ownProfile = findProfileQueryData(queryClient, currentAccount.did); + } + captureAction(ProgressGuideAction.Follow); + ax.metric('profile:follow', { + logContext: logContext, + didBecomeMutual: profile.viewer + ? Boolean(profile.viewer.followedBy) + : undefined, + followeeClout: 'followersCount' in profile + ? toClout(profile.followersCount) + : undefined, + followeeDid: did, + followerClout: toClout(ownProfile === null || ownProfile === void 0 ? void 0 : ownProfile.followersCount), + position: position, + contextProfileDid: contextProfileDid, + }); + return [4 /*yield*/, agent.follow(did)]; + case 1: return [2 /*return*/, _c.sent()]; + } + }); + }); }, + }); +} +function useProfileUnfollowMutation(logContext) { + var _this = this; + var ax = useAnalytics(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var followUri = _b.followUri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + ax.metric('profile:unfollow', { logContext: logContext }); + return [4 /*yield*/, agent.deleteFollow(followUri)]; + case 1: return [2 /*return*/, _c.sent()]; + } + }); + }); }, + }); +} +export function useProfileMuteMutationQueue(profile) { + var _this = this; + var _a; + var queryClient = useQueryClient(); + var did = profile.did; + var initialMuted = (_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.muted; + var muteMutation = useProfileMuteMutation(); + var unmuteMutation = useProfileUnmuteMutation(); + var queueToggle = useToggleMutationQueue({ + initialState: initialMuted, + runMutation: function (_prevMuted, shouldMute) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!shouldMute) return [3 /*break*/, 2]; + return [4 /*yield*/, muteMutation.mutateAsync({ + did: did, + })]; + case 1: + _a.sent(); + return [2 /*return*/, true]; + case 2: return [4 /*yield*/, unmuteMutation.mutateAsync({ + did: did, + })]; + case 3: + _a.sent(); + return [2 /*return*/, false]; + } + }); + }); }, + onSuccess: function (finalMuted) { + // finalize + updateProfileShadow(queryClient, did, { muted: finalMuted }); + }, + }); + var queueMute = useCallback(function () { + // optimistically update + updateProfileShadow(queryClient, did, { + muted: true, + }); + return queueToggle(true); + }, [queryClient, did, queueToggle]); + var queueUnmute = useCallback(function () { + // optimistically update + updateProfileShadow(queryClient, did, { + muted: false, + }); + return queueToggle(false); + }, [queryClient, did, queueToggle]); + return [queueMute, queueUnmute]; +} +function useProfileMuteMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var did = _b.did; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.mute(did)]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { + queryClient.invalidateQueries({ queryKey: RQKEY_MY_MUTED() }); + }, + }); +} +function useProfileUnmuteMutation() { + var _this = this; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var did = _b.did; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.unmute(did)]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { + queryClient.invalidateQueries({ queryKey: RQKEY_MY_MUTED() }); + }, + }); +} +export function useProfileBlockMutationQueue(profile) { + var _this = this; + var _a; + var queryClient = useQueryClient(); + var did = profile.did; + var initialBlockingUri = (_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.blocking; + var blockMutation = useProfileBlockMutation(); + var unblockMutation = useProfileUnblockMutation(); + var queueToggle = useToggleMutationQueue({ + initialState: initialBlockingUri, + runMutation: function (prevBlockUri, shouldFollow) { return __awaiter(_this, void 0, void 0, function () { + var uri; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!shouldFollow) return [3 /*break*/, 2]; + return [4 /*yield*/, blockMutation.mutateAsync({ + did: did, + })]; + case 1: + uri = (_a.sent()).uri; + return [2 /*return*/, uri]; + case 2: + if (!prevBlockUri) return [3 /*break*/, 4]; + return [4 /*yield*/, unblockMutation.mutateAsync({ + did: did, + blockUri: prevBlockUri, + })]; + case 3: + _a.sent(); + _a.label = 4; + case 4: return [2 /*return*/, undefined]; + } + }); + }); }, + onSuccess: function (finalBlockingUri) { + // finalize + updateProfileShadow(queryClient, did, { + blockingUri: finalBlockingUri, + }); + queryClient.invalidateQueries({ queryKey: [RQKEY_LIST_CONVOS] }); + }, + }); + var queueBlock = useCallback(function () { + // optimistically update + updateProfileShadow(queryClient, did, { + blockingUri: 'pending', + }); + return queueToggle(true); + }, [queryClient, did, queueToggle]); + var queueUnblock = useCallback(function () { + // optimistically update + updateProfileShadow(queryClient, did, { + blockingUri: undefined, + }); + return queueToggle(false); + }, [queryClient, did, queueToggle]); + return [queueBlock, queueUnblock]; +} +function useProfileBlockMutation() { + var _this = this; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var did = _b.did; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) { + throw new Error('Not signed in'); + } + return [4 /*yield*/, agent.app.bsky.graph.block.create({ repo: currentAccount.did }, { subject: did, createdAt: new Date().toISOString() })]; + case 1: return [2 /*return*/, _c.sent()]; + } + }); + }); }, + onSuccess: function (_, _a) { + var did = _a.did; + queryClient.invalidateQueries({ queryKey: RQKEY_MY_BLOCKED() }); + resetProfilePostsQueries(queryClient, did, 1000); + }, + }); +} +function useProfileUnblockMutation() { + var _this = this; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var rkey; + var blockUri = _b.blockUri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) { + throw new Error('Not signed in'); + } + rkey = new AtUri(blockUri).rkey; + return [4 /*yield*/, agent.app.bsky.graph.block.delete({ + repo: currentAccount.did, + rkey: rkey, + })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_, _a) { + var did = _a.did; + resetProfilePostsQueries(queryClient, did, 1000); + }, + }); +} +function whenAppViewReady(agent, actor, fn) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, until(5, // 5 tries + 1e3, // 1s delay between tries + fn, function () { return agent.app.bsky.actor.getProfile({ actor: actor }); })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var profileQueryDatas, _i, profileQueryDatas_1, _a, _queryKey, queryData, profilesQueryDatas, _b, profilesQueryDatas_1, _c, _queryKey, queryData, _d, _e, profile; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + profileQueryDatas = queryClient.getQueriesData({ + queryKey: [RQKEY_ROOT], + }); + _i = 0, profileQueryDatas_1 = profileQueryDatas; + _f.label = 1; + case 1: + if (!(_i < profileQueryDatas_1.length)) return [3 /*break*/, 4]; + _a = profileQueryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!queryData) { + return [3 /*break*/, 3]; + } + if (!(queryData.did === did)) return [3 /*break*/, 3]; + return [4 /*yield*/, queryData]; + case 2: + _f.sent(); + _f.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: + profilesQueryDatas = queryClient.getQueriesData({ + queryKey: [profilesQueryKeyRoot], + }); + _b = 0, profilesQueryDatas_1 = profilesQueryDatas; + _f.label = 5; + case 5: + if (!(_b < profilesQueryDatas_1.length)) return [3 /*break*/, 10]; + _c = profilesQueryDatas_1[_b], _queryKey = _c[0], queryData = _c[1]; + if (!queryData) { + return [3 /*break*/, 9]; + } + _d = 0, _e = queryData.profiles; + _f.label = 6; + case 6: + if (!(_d < _e.length)) return [3 /*break*/, 9]; + profile = _e[_d]; + if (!(profile.did === did)) return [3 /*break*/, 8]; + return [4 /*yield*/, profile]; + case 7: + _f.sent(); + _f.label = 8; + case 8: + _d++; + return [3 /*break*/, 6]; + case 9: + _b++; + return [3 /*break*/, 5]; + case 10: return [2 /*return*/]; + } + }); +} +export function findProfileQueryData(queryClient, did) { + return queryClient.getQueryData(RQKEY(did)); +} diff --git a/src/state/queries/resolve-link.js b/src/state/queries/resolve-link.js new file mode 100644 index 0000000000..22fd3a1300 --- /dev/null +++ b/src/state/queries/resolve-link.js @@ -0,0 +1,109 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { resolveGif, resolveLink } from '#/lib/api/resolve'; +import { STALE } from '#/state/queries/index'; +import { useAgent } from '#/state/session'; +export var RQKEY_LINK_ROOT = 'resolve-link'; +export var RQKEY_LINK = function (url) { return [RQKEY_LINK_ROOT, url]; }; +export var RQKEY_GIF_ROOT = 'resolve-gif'; +export var RQKEY_GIF = function (url) { return [RQKEY_GIF_ROOT, url]; }; +export function useResolveLinkQuery(url) { + var _this = this; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_LINK(url), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, resolveLink(agent, url)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }); +} +export function fetchResolveLinkQuery(queryClient, agent, url) { + var _this = this; + return queryClient.fetchQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_LINK(url), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, resolveLink(agent, url)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }); +} +export function precacheResolveLinkQuery(queryClient, url, resolvedLink) { + queryClient.setQueryData(RQKEY_LINK(url), resolvedLink); +} +export function useResolveGifQuery(gif) { + var _this = this; + var agent = useAgent(); + return useQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_GIF(gif.url), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, resolveGif(agent, gif)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }); +} +export function fetchResolveGifQuery(queryClient, agent, gif) { + var _this = this; + return queryClient.fetchQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_GIF(gif.url), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, resolveGif(agent, gif)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/resolve-short-link.js b/src/state/queries/resolve-short-link.js new file mode 100644 index 0000000000..8177afe385 --- /dev/null +++ b/src/state/queries/resolve-short-link.js @@ -0,0 +1,66 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { resolveShortLink } from '#/lib/link-meta/resolve-short-link'; +import { parseStarterPackUri } from '#/lib/strings/starter-pack'; +import { STALE } from '#/state/queries/index'; +var ROOT_URI = 'https://go.bsky.app/'; +var RQKEY_ROOT = 'resolved-short-link'; +export var RQKEY = function (code) { return [RQKEY_ROOT, code]; }; +export function useResolvedStarterPackShortLink(_a) { + var _this = this; + var code = _a.code; + return useQuery({ + queryKey: RQKEY(code), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var uri, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + uri = "".concat(ROOT_URI).concat(code); + return [4 /*yield*/, resolveShortLink(uri)]; + case 1: + res = _a.sent(); + return [2 /*return*/, parseStarterPackUri(res)]; + } + }); + }); }, + retry: 1, + enabled: Boolean(code), + staleTime: STALE.HOURS.ONE, + }); +} diff --git a/src/state/queries/resolve-uri.js b/src/state/queries/resolve-uri.js new file mode 100644 index 0000000000..a22b246867 --- /dev/null +++ b/src/state/queries/resolve-uri.js @@ -0,0 +1,103 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AtUri } from '@atproto/api'; +import { useQuery, } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +import { useUnstableProfileViewCache } from './profile'; +var RQKEY_ROOT = 'resolved-did'; +export var RQKEY = function (didOrHandle) { return [RQKEY_ROOT, didOrHandle]; }; +export function useResolveUriQuery(uri) { + var urip = new AtUri(uri || ''); + var res = useResolveDidQuery(urip.host); + if (res.data) { + // @ts-expect-error TODO new-sdk-migration + urip.host = res.data; + return __assign(__assign({}, res), { data: { did: urip.host, uri: urip.toString() } }); + } + return res; +} +export function useResolveDidQuery(didOrHandle) { + var _this = this; + var agent = useAgent(); + var getUnstableProfile = useUnstableProfileViewCache().getUnstableProfile; + return useQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY(didOrHandle !== null && didOrHandle !== void 0 ? didOrHandle : ''), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!didOrHandle) + return [2 /*return*/, '' + // Just return the did if it's already one + ]; + // Just return the did if it's already one + if (didOrHandle.startsWith('did:')) + return [2 /*return*/, didOrHandle]; + return [4 /*yield*/, agent.resolveHandle({ handle: didOrHandle })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.did]; + } + }); + }); }, + initialData: function () { + // Return undefined if no did or handle + if (!didOrHandle) + return; + var profile = getUnstableProfile(didOrHandle); + return profile === null || profile === void 0 ? void 0 : profile.did; + }, + enabled: !!didOrHandle, + }); +} +export function precacheResolvedUri(queryClient, handle, did) { + queryClient.setQueryData(RQKEY(handle), did); +} diff --git a/src/state/queries/search-posts.js b/src/state/queries/search-posts.js new file mode 100644 index 0000000000..71a8b329a4 --- /dev/null +++ b/src/state/queries/search-posts.js @@ -0,0 +1,253 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import React from 'react'; +import { AtUri, moderatePost, } from '@atproto/api'; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useAgent } from '#/state/session'; +import { didOrHandleUriMatches, embedViewRecordToPostView, getEmbeddedPost, } from './util'; +var searchPostsQueryKeyRoot = 'search-posts'; +var searchPostsQueryKey = function (_a) { + var query = _a.query, sort = _a.sort; + return [ + searchPostsQueryKeyRoot, + query, + sort, + ]; +}; +export function useSearchPostsQuery(_a) { + var _this = this; + var query = _a.query, sort = _a.sort, enabled = _a.enabled; + var agent = useAgent(); + var moderationOpts = useModerationOpts(); + var selectArgs = React.useMemo(function () { return ({ + isSearchingSpecificUser: /from:(\w+)/.test(query), + moderationOpts: moderationOpts, + }); }, [query, moderationOpts]); + var lastRun = React.useRef(null); + return useInfiniteQuery({ + queryKey: searchPostsQueryKey({ query: query, sort: sort }), + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.feed.searchPosts({ + q: query, + limit: 25, + cursor: pageParam, + sort: sort, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + enabled: enabled !== null && enabled !== void 0 ? enabled : !!moderationOpts, + select: React.useCallback(function (data) { + var moderationOpts = selectArgs.moderationOpts, isSearchingSpecificUser = selectArgs.isSearchingSpecificUser; + /* + * If a user applies the `from:` filter, don't apply any + * moderation. Note that if we add any more filtering logic below, we + * may need to adjust this. + */ + if (isSearchingSpecificUser) { + return data; + } + // Keep track of the last run and whether we can reuse + // some already selected pages from there. + var reusedPages = []; + if (lastRun.current) { + var _a = lastRun.current, lastData = _a.data, lastArgs = _a.args, lastResult = _a.result; + var canReuse = true; + for (var key in selectArgs) { + if (selectArgs.hasOwnProperty(key)) { + if (selectArgs[key] !== lastArgs[key]) { + // Can't do reuse anything if any input has changed. + canReuse = false; + break; + } + } + } + if (canReuse) { + for (var i = 0; i < data.pages.length; i++) { + if (data.pages[i] && lastData.pages[i] === data.pages[i]) { + reusedPages.push(lastResult.pages[i]); + continue; + } + // Stop as soon as pages stop matching up. + break; + } + } + } + var result = __assign(__assign({}, data), { pages: __spreadArray(__spreadArray([], reusedPages, true), data.pages.slice(reusedPages.length).map(function (page) { + return __assign(__assign({}, page), { posts: page.posts.filter(function (post) { + var mod = moderatePost(post, moderationOpts); + return !mod.ui('contentList').filter; + }) }); + }), true) }); + lastRun.current = { data: data, result: result, args: selectArgs }; + return result; + }, [selectArgs]), + }); +} +export function findAllPostsInQueryData(queryClient, uri) { + var queryDatas, atUri, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, post, quotedPost; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [searchPostsQueryKeyRoot], + }); + atUri = new AtUri(uri); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 10]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + _d = 0, _e = page.posts; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + post = _e[_d]; + if (!didOrHandleUriMatches(atUri, post)) return [3 /*break*/, 5]; + return [4 /*yield*/, post]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + quotedPost = getEmbeddedPost(post.embed); + if (!(quotedPost && didOrHandleUriMatches(atUri, quotedPost))) return [3 /*break*/, 7]; + return [4 /*yield*/, embedViewRecordToPostView(quotedPost)]; + case 6: + _f.sent(); + _f.label = 7; + case 7: + _d++; + return [3 /*break*/, 3]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: return [2 /*return*/]; + } + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_2, _a, _queryKey, queryData, _b, _c, page, _d, _e, post, quotedPost; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [searchPostsQueryKeyRoot], + }); + _i = 0, queryDatas_2 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_2.length)) return [3 /*break*/, 10]; + _a = queryDatas_2[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 9]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 9]; + page = _c[_b]; + _d = 0, _e = page.posts; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 8]; + post = _e[_d]; + if (!(post.author.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, post.author]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + quotedPost = getEmbeddedPost(post.embed); + if (!((quotedPost === null || quotedPost === void 0 ? void 0 : quotedPost.author.did) === did)) return [3 /*break*/, 7]; + return [4 /*yield*/, quotedPost.author]; + case 6: + _f.sent(); + _f.label = 7; + case 7: + _d++; + return [3 /*break*/, 3]; + case 8: + _b++; + return [3 /*break*/, 2]; + case 9: + _i++; + return [3 /*break*/, 1]; + case 10: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/service-config.js b/src/state/queries/service-config.js new file mode 100644 index 0000000000..b7ef92fd91 --- /dev/null +++ b/src/state/queries/service-config.js @@ -0,0 +1,75 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +export function useServiceConfigQuery() { + var _this = this; + var agent = useAgent(); + return useQuery({ + refetchOnWindowFocus: true, + staleTime: STALE.MINUTES.FIVE, + queryKey: ['service-config'], + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data, e_1; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, agent.api.app.bsky.unspecced.getConfig()]; + case 1: + data = (_b.sent()).data; + return [2 /*return*/, { + checkEmailConfirmed: Boolean(data.checkEmailConfirmed), + // @ts-expect-error not included in types atm + topicsEnabled: Boolean(data.topicsEnabled), + liveNow: (_a = data.liveNow) !== null && _a !== void 0 ? _a : [], + }]; + case 2: + e_1 = _b.sent(); + return [2 /*return*/, { + checkEmailConfirmed: false, + topicsEnabled: false, + liveNow: [], + }]; + case 3: return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/service.js b/src/state/queries/service.js new file mode 100644 index 0000000000..0d04b4e99e --- /dev/null +++ b/src/state/queries/service.js @@ -0,0 +1,70 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { Agent } from '../session/agent'; +var RQKEY_ROOT = 'service'; +export var RQKEY = function (serviceUrl) { return [RQKEY_ROOT, serviceUrl]; }; +export function useServiceQuery(serviceUrl) { + var _this = this; + return useQuery({ + queryKey: RQKEY(serviceUrl), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var agent, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + agent = new Agent(null, { service: serviceUrl }); + return [4 /*yield*/, agent.com.atproto.server.describeServer()]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + enabled: isValidUrl(serviceUrl), + }); +} +function isValidUrl(url) { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + var urlp = new URL(url); + return true; + } + catch (_a) { + return false; + } +} diff --git a/src/state/queries/shorten-link.js b/src/state/queries/shorten-link.js new file mode 100644 index 0000000000..d7038e7354 --- /dev/null +++ b/src/state/queries/shorten-link.js @@ -0,0 +1,65 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { logger } from '#/logger'; +export function useShortenLink() { + var _this = this; + return function (inputUrl) { return __awaiter(_this, void 0, void 0, function () { + var url, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + url = new URL(inputUrl); + return [4 /*yield*/, fetch('https://go.bsky.app/link', { + method: 'POST', + body: JSON.stringify({ + path: url.pathname, + }), + headers: { + 'Content-Type': 'application/json', + }, + })]; + case 1: + res = _a.sent(); + if (!res.ok) { + logger.error('Failed to shorten link', { safeMessage: res.status }); + return [2 /*return*/, { url: inputUrl }]; + } + return [2 /*return*/, res.json()]; + } + }); + }); }; +} diff --git a/src/state/queries/starter-packs.js b/src/state/queries/starter-packs.js new file mode 100644 index 0000000000..1a8b96ce96 --- /dev/null +++ b/src/state/queries/starter-packs.js @@ -0,0 +1,455 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AppBskyFeedDefs, AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri, RichText, } from '@atproto/api'; +import { useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; +import chunk from 'lodash.chunk'; +import { until } from '#/lib/async/until'; +import { createStarterPackList } from '#/lib/generate-starterpack'; +import { createStarterPackUri, httpStarterPackUriToAtUri, parseStarterPackUri, } from '#/lib/strings/starter-pack'; +import { invalidateActorStarterPacksQuery } from '#/state/queries/actor-starter-packs'; +import { STALE } from '#/state/queries/index'; +import { invalidateListMembersQuery } from '#/state/queries/list-members'; +import { useAgent } from '#/state/session'; +import * as bsky from '#/types/bsky'; +var RQKEY_ROOT = 'starter-pack'; +var RQKEY = function (_a) { + var uri = _a.uri, did = _a.did, rkey = _a.rkey; + if ((uri === null || uri === void 0 ? void 0 : uri.startsWith('https://')) || (uri === null || uri === void 0 ? void 0 : uri.startsWith('at://'))) { + var parsed = parseStarterPackUri(uri); + return [RQKEY_ROOT, parsed === null || parsed === void 0 ? void 0 : parsed.name, parsed === null || parsed === void 0 ? void 0 : parsed.rkey]; + } + else { + return [RQKEY_ROOT, did, rkey]; + } +}; +export function useStarterPackQuery(_a) { + var _this = this; + var uri = _a.uri, did = _a.did, rkey = _a.rkey; + var agent = useAgent(); + return useQuery({ + queryKey: RQKEY(uri ? { uri: uri } : { did: did, rkey: rkey }), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!uri) { + uri = "at://".concat(did, "/app.bsky.graph.starterpack/").concat(rkey); + } + else if (uri && !uri.startsWith('at://')) { + uri = httpStarterPackUriToAtUri(uri); + } + return [4 /*yield*/, agent.app.bsky.graph.getStarterPack({ + starterPack: uri, + })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.starterPack]; + } + }); + }); }, + enabled: Boolean(uri) || Boolean(did && rkey), + staleTime: STALE.MINUTES.FIVE, + }); +} +export function invalidateStarterPack(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var queryClient = _b.queryClient, did = _b.did, rkey = _b.rkey; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, queryClient.invalidateQueries({ queryKey: RQKEY({ did: did, rkey: rkey }) })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function useCreateStarterPackMutation(_a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var descriptionFacets, rt, listRes; + var name = _b.name, description = _b.description, feeds = _b.feeds, profiles = _b.profiles; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!description) return [3 /*break*/, 2]; + rt = new RichText({ text: description }); + return [4 /*yield*/, rt.detectFacets(agent)]; + case 1: + _c.sent(); + descriptionFacets = rt.facets; + _c.label = 2; + case 2: return [4 /*yield*/, createStarterPackList({ + name: name, + description: description, + profiles: profiles, + descriptionFacets: descriptionFacets, + agent: agent, + })]; + case 3: + listRes = _c.sent(); + return [4 /*yield*/, agent.app.bsky.graph.starterpack.create({ + repo: agent.assertDid, + }, { + name: name, + description: description, + descriptionFacets: descriptionFacets, + list: listRes === null || listRes === void 0 ? void 0 : listRes.uri, + feeds: feeds === null || feeds === void 0 ? void 0 : feeds.map(function (f) { return ({ uri: f.uri }); }), + createdAt: new Date().toISOString(), + })]; + case 4: return [2 /*return*/, _c.sent()]; + } + }); + }); }, + onSuccess: function (data) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, whenAppViewReady(agent, data.uri, function (v) { + return typeof (v === null || v === void 0 ? void 0 : v.data.starterPack.uri) === 'string'; + })]; + case 1: + _a.sent(); + return [4 /*yield*/, invalidateActorStarterPacksQuery({ + queryClient: queryClient, + did: agent.session.did, + })]; + case 2: + _a.sent(); + onSuccess(data); + return [2 /*return*/]; + } + }); + }); }, + onError: function (error) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + onError(error); + return [2 /*return*/]; + }); + }); }, + }); +} +export function useEditStarterPackMutation(_a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var queryClient = useQueryClient(); + var agent = useAgent(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var descriptionFacets, rt, removedItems, chunks, _i, chunks_1, chunk_1, addedProfiles, chunks, _c, chunks_2, chunk_2, rkey; + var _d; + var name = _b.name, description = _b.description, feeds = _b.feeds, profiles = _b.profiles, currentStarterPack = _b.currentStarterPack, currentListItems = _b.currentListItems; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + if (!description) return [3 /*break*/, 2]; + rt = new RichText({ text: description }); + return [4 /*yield*/, rt.detectFacets(agent)]; + case 1: + _e.sent(); + descriptionFacets = rt.facets; + _e.label = 2; + case 2: + if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) { + throw new Error('Invalid starter pack'); + } + removedItems = currentListItems.filter(function (i) { + var _a; + return i.subject.did !== ((_a = agent.session) === null || _a === void 0 ? void 0 : _a.did) && + !profiles.find(function (p) { return p.did === i.subject.did && p.did; }); + }); + if (!(removedItems.length !== 0)) return [3 /*break*/, 6]; + chunks = chunk(removedItems, 50); + _i = 0, chunks_1 = chunks; + _e.label = 3; + case 3: + if (!(_i < chunks_1.length)) return [3 /*break*/, 6]; + chunk_1 = chunks_1[_i]; + return [4 /*yield*/, agent.com.atproto.repo.applyWrites({ + repo: agent.session.did, + writes: chunk_1.map(function (i) { return ({ + $type: 'com.atproto.repo.applyWrites#delete', + collection: 'app.bsky.graph.listitem', + rkey: new AtUri(i.uri).rkey, + }); }), + })]; + case 4: + _e.sent(); + _e.label = 5; + case 5: + _i++; + return [3 /*break*/, 3]; + case 6: + addedProfiles = profiles.filter(function (p) { return !currentListItems.find(function (i) { return i.subject.did === p.did; }); }); + if (!(addedProfiles.length > 0)) return [3 /*break*/, 10]; + chunks = chunk(addedProfiles, 50); + _c = 0, chunks_2 = chunks; + _e.label = 7; + case 7: + if (!(_c < chunks_2.length)) return [3 /*break*/, 10]; + chunk_2 = chunks_2[_c]; + return [4 /*yield*/, agent.com.atproto.repo.applyWrites({ + repo: agent.session.did, + writes: chunk_2.map(function (p) { + var _a; + return ({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + value: { + $type: 'app.bsky.graph.listitem', + subject: p.did, + list: (_a = currentStarterPack.list) === null || _a === void 0 ? void 0 : _a.uri, + createdAt: new Date().toISOString(), + }, + }); + }), + })]; + case 8: + _e.sent(); + _e.label = 9; + case 9: + _c++; + return [3 /*break*/, 7]; + case 10: + rkey = parseStarterPackUri(currentStarterPack.uri).rkey; + return [4 /*yield*/, agent.com.atproto.repo.putRecord({ + repo: agent.session.did, + collection: 'app.bsky.graph.starterpack', + rkey: rkey, + record: { + name: name, + description: description, + descriptionFacets: descriptionFacets, + list: (_d = currentStarterPack.list) === null || _d === void 0 ? void 0 : _d.uri, + feeds: feeds, + createdAt: currentStarterPack.record.createdAt, + updatedAt: new Date().toISOString(), + }, + })]; + case 11: + _e.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_1, _a) { return __awaiter(_this, [_1, _a], void 0, function (_, _b) { + var parsed; + var currentStarterPack = _b.currentStarterPack; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + parsed = parseStarterPackUri(currentStarterPack.uri); + return [4 /*yield*/, whenAppViewReady(agent, currentStarterPack.uri, function (v) { + return currentStarterPack.cid !== (v === null || v === void 0 ? void 0 : v.data.starterPack.cid); + })]; + case 1: + _c.sent(); + return [4 /*yield*/, invalidateActorStarterPacksQuery({ + queryClient: queryClient, + did: agent.session.did, + })]; + case 2: + _c.sent(); + if (!currentStarterPack.list) return [3 /*break*/, 4]; + return [4 /*yield*/, invalidateListMembersQuery({ + queryClient: queryClient, + uri: currentStarterPack.list.uri, + })]; + case 3: + _c.sent(); + _c.label = 4; + case 4: return [4 /*yield*/, invalidateStarterPack({ + queryClient: queryClient, + did: agent.session.did, + rkey: parsed.rkey, + })]; + case 5: + _c.sent(); + onSuccess(); + return [2 /*return*/]; + } + }); + }); }, + onError: function (error) { + onError(error); + }, + }); +} +export function useDeleteStarterPackMutation(_a) { + var _this = this; + var onSuccess = _a.onSuccess, onError = _a.onError; + var agent = useAgent(); + var queryClient = useQueryClient(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var listUri = _b.listUri, rkey = _b.rkey; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!agent.session) { + throw new Error("Requires signed in user"); + } + if (!listUri) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.app.bsky.graph.list.delete({ + repo: agent.session.did, + rkey: new AtUri(listUri).rkey, + })]; + case 1: + _c.sent(); + _c.label = 2; + case 2: return [4 /*yield*/, agent.app.bsky.graph.starterpack.delete({ + repo: agent.session.did, + rkey: rkey, + })]; + case 3: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_1, _a) { return __awaiter(_this, [_1, _a], void 0, function (_, _b) { + var uri; + var listUri = _b.listUri, rkey = _b.rkey; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + uri = createStarterPackUri({ + did: agent.session.did, + rkey: rkey, + }); + if (!uri) return [3 /*break*/, 2]; + return [4 /*yield*/, whenAppViewReady(agent, uri, function (v) { + var _a; + return Boolean((_a = v === null || v === void 0 ? void 0 : v.data) === null || _a === void 0 ? void 0 : _a.starterPack) === false; + })]; + case 1: + _c.sent(); + _c.label = 2; + case 2: + if (!listUri) return [3 /*break*/, 4]; + return [4 /*yield*/, invalidateListMembersQuery({ queryClient: queryClient, uri: listUri })]; + case 3: + _c.sent(); + _c.label = 4; + case 4: return [4 /*yield*/, invalidateActorStarterPacksQuery({ + queryClient: queryClient, + did: agent.session.did, + })]; + case 5: + _c.sent(); + return [4 /*yield*/, invalidateStarterPack({ + queryClient: queryClient, + did: agent.session.did, + rkey: rkey, + })]; + case 6: + _c.sent(); + onSuccess(); + return [2 /*return*/]; + } + }); + }); }, + onError: function (error) { + onError(error); + }, + }); +} +function whenAppViewReady(agent, uri, fn) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, until(5, // 5 tries + 1e3, // 1s delay between tries + fn, function () { return agent.app.bsky.graph.getStarterPack({ starterPack: uri }); })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function precacheStarterPack(queryClient, starterPack) { + if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) { + return; + } + var starterPackView; + if (AppBskyGraphDefs.isStarterPackView(starterPack)) { + starterPackView = starterPack; + } + else if (AppBskyGraphDefs.isStarterPackViewBasic(starterPack) && + bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)) { + var feeds = void 0; + if (starterPack.record.feeds) { + feeds = []; + for (var _i = 0, _a = starterPack.record.feeds; _i < _a.length; _i++) { + var feed = _a[_i]; + // note: types are wrong? claims to be `FeedItem`, but we actually + // get un$typed `GeneratorView` objects here -sfn + if (bsky.validate(feed, AppBskyFeedDefs.validateGeneratorView)) { + feeds.push(feed); + } + } + } + var listView = { + uri: starterPack.record.list, + // This will be populated once the data from server is fetched + cid: '', + name: starterPack.record.name, + purpose: 'app.bsky.graph.defs#referencelist', + }; + starterPackView = __assign(__assign({}, starterPack), { $type: 'app.bsky.graph.defs#starterPackView', list: listView, feeds: feeds }); + } + if (starterPackView) { + queryClient.setQueryData(RQKEY({ uri: starterPack.uri }), starterPackView); + } +} diff --git a/src/state/queries/suggested-feeds.js b/src/state/queries/suggested-feeds.js new file mode 100644 index 0000000000..9138b10768 --- /dev/null +++ b/src/state/queries/suggested-feeds.js @@ -0,0 +1,66 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useInfiniteQuery, } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { useAgent } from '#/state/session'; +var suggestedFeedsQueryKeyRoot = 'suggestedFeeds'; +export var suggestedFeedsQueryKey = [suggestedFeedsQueryKeyRoot]; +export function useSuggestedFeedsQuery() { + var _this = this; + var agent = useAgent(); + return useInfiniteQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: suggestedFeedsQueryKey, + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.feed.getSuggestedFeeds({ + limit: 10, + cursor: pageParam, + })]; + case 1: + res = _c.sent(); + return [2 /*return*/, res.data]; + } + }); + }); }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} diff --git a/src/state/queries/suggested-follows.js b/src/state/queries/suggested-follows.js new file mode 100644 index 0000000000..6eeeeab85b --- /dev/null +++ b/src/state/queries/suggested-follows.js @@ -0,0 +1,252 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +import { moderateProfile, } from '@atproto/api'; +import { useInfiniteQuery, useQuery, } from '@tanstack/react-query'; +import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent, useSession } from '#/state/session'; +import { useModerationOpts } from '../preferences/moderation-opts'; +var suggestedFollowsQueryKeyRoot = 'suggested-follows'; +var suggestedFollowsQueryKey = function (options) { return [ + suggestedFollowsQueryKeyRoot, + options, +]; }; +var suggestedFollowsByActorQueryKeyRoot = 'suggested-follows-by-actor'; +var suggestedFollowsByActorQueryKey = function (did) { return [ + suggestedFollowsByActorQueryKeyRoot, + did, +]; }; +export function useSuggestedFollowsQuery(options) { + var _this = this; + var currentAccount = useSession().currentAccount; + var agent = useAgent(); + var moderationOpts = useModerationOpts(); + var preferences = usePreferencesQuery().data; + var limit = (options === null || options === void 0 ? void 0 : options.limit) || 25; + return useInfiniteQuery({ + enabled: !!moderationOpts && !!preferences, + staleTime: STALE.HOURS.ONE, + queryKey: suggestedFollowsQueryKey(options), + queryFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var contentLangs, maybeDifferentLimit, res; + var pageParam = _b.pageParam; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + contentLangs = getContentLanguages().join(','); + maybeDifferentLimit = (options === null || options === void 0 ? void 0 : options.subsequentPageLimit) && pageParam + ? options.subsequentPageLimit + : limit; + return [4 /*yield*/, agent.app.bsky.actor.getSuggestions({ + limit: maybeDifferentLimit, + cursor: pageParam, + }, { + headers: __assign(__assign({}, createBskyTopicsHeader(aggregateUserInterests(preferences))), { 'Accept-Language': contentLangs }), + })]; + case 1: + res = _c.sent(); + res.data.actors = res.data.actors + .filter(function (actor) { + return !moderateProfile(actor, moderationOpts).ui('profileList').filter; + }) + .filter(function (actor) { + var viewer = actor.viewer; + if (viewer) { + if (viewer.following || + viewer.muted || + viewer.mutedByList || + viewer.blockedBy || + viewer.blocking) { + return false; + } + } + if (actor.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) { + return false; + } + return true; + }); + return [2 /*return*/, res.data]; + } + }); + }); }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.cursor; }, + }); +} +export function useSuggestedFollowsByActorQuery(_a) { + var _this = this; + var did = _a.did, enabled = _a.enabled, _b = _a.staleTime, staleTime = _b === void 0 ? STALE.MINUTES.FIVE : _b; + var agent = useAgent(); + return useQuery({ + staleTime: staleTime, + queryKey: suggestedFollowsByActorQueryKey(did), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var res, suggestions; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.graph.getSuggestedFollowsByActor({ + actor: did, + })]; + case 1: + res = _a.sent(); + suggestions = res.data.isFallback + ? [] + : res.data.suggestions.filter(function (profile) { var _a; return !((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following); }); + return [2 /*return*/, { suggestions: suggestions, recId: res.data.recId }]; + } + }); + }); }, + enabled: enabled, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [5 /*yield**/, __values(findAllProfilesInSuggestedFollowsQueryData(queryClient, did))]; + case 1: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInSuggestedFollowsByActorQueryData(queryClient, did))]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); +} +function findAllProfilesInSuggestedFollowsQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, _b, _c, page, _d, _e, actor; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [suggestedFollowsQueryKeyRoot], + }); + _i = 0, queryDatas_1 = queryDatas; + _f.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!(queryData === null || queryData === void 0 ? void 0 : queryData.pages)) { + return [3 /*break*/, 7]; + } + _b = 0, _c = queryData === null || queryData === void 0 ? void 0 : queryData.pages; + _f.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 7]; + page = _c[_b]; + _d = 0, _e = page.actors; + _f.label = 3; + case 3: + if (!(_d < _e.length)) return [3 /*break*/, 6]; + actor = _e[_d]; + if (!(actor.did === did)) return [3 /*break*/, 5]; + return [4 /*yield*/, actor]; + case 4: + _f.sent(); + _f.label = 5; + case 5: + _d++; + return [3 /*break*/, 3]; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} +function findAllProfilesInSuggestedFollowsByActorQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_2, _a, _queryKey, queryData, _b, _c, suggestion; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [suggestedFollowsByActorQueryKeyRoot], + }); + _i = 0, queryDatas_2 = queryDatas; + _d.label = 1; + case 1: + if (!(_i < queryDatas_2.length)) return [3 /*break*/, 6]; + _a = queryDatas_2[_i], _queryKey = _a[0], queryData = _a[1]; + if (!queryData) { + return [3 /*break*/, 5]; + } + _b = 0, _c = queryData.suggestions; + _d.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 5]; + suggestion = _c[_b]; + if (!(suggestion.did === did)) return [3 /*break*/, 4]; + return [4 /*yield*/, suggestion]; + case 3: + _d.sent(); + _d.label = 4; + case 4: + _b++; + return [3 /*break*/, 2]; + case 5: + _i++; + return [3 /*break*/, 1]; + case 6: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/tenor.js b/src/state/queries/tenor.js new file mode 100644 index 0000000000..bda7fcac89 --- /dev/null +++ b/src/state/queries/tenor.js @@ -0,0 +1,135 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { Platform } from 'react-native'; +import { getLocales } from 'expo-localization'; +import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'; +import { GIF_FEATURED, GIF_SEARCH } from '#/lib/constants'; +import { logger } from '#/logger'; +export var RQKEY_ROOT = 'gif-service'; +export var RQKEY_FEATURED = [RQKEY_ROOT, 'featured']; +export var RQKEY_SEARCH = function (query) { return [RQKEY_ROOT, 'search', query]; }; +var getTrendingGifs = createTenorApi(GIF_FEATURED); +var searchGifs = createTenorApi(GIF_SEARCH); +export function useFeaturedGifsQuery() { + return useInfiniteQuery({ + queryKey: RQKEY_FEATURED, + queryFn: function (_a) { + var pageParam = _a.pageParam; + return getTrendingGifs({ pos: pageParam }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.next; }, + }); +} +export function useGifSearchQuery(query) { + return useInfiniteQuery({ + queryKey: RQKEY_SEARCH(query), + queryFn: function (_a) { + var pageParam = _a.pageParam; + return searchGifs({ q: query, pos: pageParam }); + }, + initialPageParam: undefined, + getNextPageParam: function (lastPage) { return lastPage.next; }, + enabled: !!query, + placeholderData: keepPreviousData, + }); +} +function createTenorApi(urlFn) { + var _this = this; + return function (input) { return __awaiter(_this, void 0, void 0, function () { + var params, locale, _i, _a, _b, key, value, res; + var _c; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + params = new URLSearchParams(); + // set client key based on platform + params.set('client_key', Platform.select({ + ios: 'bluesky-ios', + android: 'bluesky-android', + default: 'bluesky-web', + })); + // 30 is divisible by 2 and 3, so both 2 and 3 column layouts can be used + params.set('limit', '30'); + params.set('contentfilter', 'high'); + params.set('media_filter', ['preview', 'gif', 'tinygif'].join(',')); + locale = (_c = getLocales === null || getLocales === void 0 ? void 0 : getLocales()) === null || _c === void 0 ? void 0 : _c[0]; + if (locale) { + params.set('locale', locale.languageTag.replace('-', '_')); + } + for (_i = 0, _a = Object.entries(input); _i < _a.length; _i++) { + _b = _a[_i], key = _b[0], value = _b[1]; + if (value !== undefined) { + params.set(key, String(value)); + } + } + return [4 /*yield*/, fetch(urlFn(params.toString()), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + })]; + case 1: + res = _d.sent(); + if (!res.ok) { + throw new Error('Failed to fetch Tenor API'); + } + return [2 /*return*/, res.json()]; + } + }); + }); }; +} +export function tenorUrlToBskyGifUrl(tenorUrl) { + var url; + try { + url = new URL(tenorUrl); + } + catch (e) { + logger.debug('invalid url passed to tenorUrlToBskyGifUrl()'); + return ''; + } + url.hostname = 't.gifs.bsky.app'; + return url.href; +} +// | 'nanogif' +// | 'mp4' +// | 'loopedmp4' +// | 'tinymp4' +// | 'nanomp4' +// | 'webm' +// | 'tinywebm' +// | 'nanowebm' diff --git a/src/state/queries/threadgate/index.js b/src/state/queries/threadgate/index.js new file mode 100644 index 0000000000..f7b5bfa92d --- /dev/null +++ b/src/state/queries/threadgate/index.js @@ -0,0 +1,451 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AppBskyFeedThreadgate, AtUri, } from '@atproto/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { networkRetry, retry } from '#/lib/async/retry'; +import { STALE } from '#/state/queries'; +import { useGetPost } from '#/state/queries/post'; +import { createThreadgateRecord, mergeThreadgateRecords, threadgateAllowUISettingToAllowRecordValue, threadgateViewToAllowUISetting, } from '#/state/queries/threadgate/util'; +import { useUpdatePostThreadThreadgateQueryCache } from '#/state/queries/usePostThread'; +import { useAgent } from '#/state/session'; +import { useThreadgateHiddenReplyUrisAPI } from '#/state/threadgate-hidden-replies'; +import * as bsky from '#/types/bsky'; +export * from '#/state/queries/threadgate/types'; +export * from '#/state/queries/threadgate/util'; +/** + * Must match the threadgate lexicon record definition. + */ +export var MAX_HIDDEN_REPLIES = 300; +export var threadgateRecordQueryKeyRoot = 'threadgate-record'; +export var createThreadgateRecordQueryKey = function (uri) { return [ + threadgateRecordQueryKeyRoot, + uri, +]; }; +export function useThreadgateRecordQuery(_a) { + var _b = _a === void 0 ? {} : _a, postUri = _b.postUri, initialData = _b.initialData; + var agent = useAgent(); + return useQuery({ + enabled: !!postUri, + queryKey: createThreadgateRecordQueryKey(postUri || ''), + placeholderData: initialData, + staleTime: STALE.MINUTES.ONE, + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, getThreadgateRecord({ + agent: agent, + postUri: postUri, + })]; + }); + }); + }, + }); +} +export var threadgateViewQueryKeyRoot = 'threadgate-view'; +export var createThreadgateViewQueryKey = function (uri) { return [ + threadgateViewQueryKeyRoot, + uri, +]; }; +export function useThreadgateViewQuery(_a) { + var _b = _a === void 0 ? {} : _a, postUri = _b.postUri, initialData = _b.initialData; + var getPost = useGetPost(); + return useQuery({ + enabled: !!postUri, + queryKey: createThreadgateViewQueryKey(postUri || ''), + placeholderData: initialData, + staleTime: STALE.MINUTES.ONE, + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var post; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, getPost({ uri: postUri })]; + case 1: + post = _b.sent(); + return [2 /*return*/, (_a = post.threadgate) !== null && _a !== void 0 ? _a : null]; + } + }); + }); + }, + }); +} +export function getThreadgateRecord(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var urip, res, data, e_1; + var agent = _b.agent, postUri = _b.postUri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + urip = new AtUri(postUri); + if (!!urip.host.startsWith('did:')) return [3 /*break*/, 2]; + return [4 /*yield*/, agent.resolveHandle({ + handle: urip.host, + }) + // @ts-expect-error TODO new-sdk-migration + ]; + case 1: + res = _c.sent(); + // @ts-expect-error TODO new-sdk-migration + urip.host = res.data.did; + _c.label = 2; + case 2: + _c.trys.push([2, 4, , 5]); + return [4 /*yield*/, retry(2, function (e) { + /* + * If the record doesn't exist, we want to return null instead of + * throwing an error. NB: This will also catch reference errors, such as + * a typo in the URI. + */ + if (e.message.includes("Could not locate record:")) { + return false; + } + return true; + }, function () { + return agent.api.com.atproto.repo.getRecord({ + repo: urip.host, + collection: 'app.bsky.feed.threadgate', + rkey: urip.rkey, + }); + })]; + case 3: + data = (_c.sent()).data; + if (data.value && + bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord)) { + return [2 /*return*/, data.value]; + } + else { + return [2 /*return*/, null]; + } + return [3 /*break*/, 5]; + case 4: + e_1 = _c.sent(); + /* + * If the record doesn't exist, we want to return null instead of + * throwing an error. NB: This will also catch reference errors, such as + * a typo in the URI. + */ + if (e_1.message.includes("Could not locate record:")) { + return [2 /*return*/, null]; + } + else { + throw e_1; + } + return [3 /*break*/, 5]; + case 5: return [2 /*return*/]; + } + }); + }); +} +export function writeThreadgateRecord(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var postUrip, record; + var agent = _b.agent, postUri = _b.postUri, threadgate = _b.threadgate; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + postUrip = new AtUri(postUri); + record = createThreadgateRecord({ + post: postUri, + allow: threadgate.allow, // can/should be undefined! + hiddenReplies: threadgate.hiddenReplies || [], + }); + return [4 /*yield*/, networkRetry(2, function () { + return agent.api.com.atproto.repo.putRecord({ + repo: agent.session.did, + collection: 'app.bsky.feed.threadgate', + rkey: postUrip.rkey, + record: record, + }); + })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function upsertThreadgate(_a, callback_1) { + return __awaiter(this, arguments, void 0, function (_b, callback) { + var prev, next; + var agent = _b.agent, postUri = _b.postUri; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, getThreadgateRecord({ + agent: agent, + postUri: postUri, + })]; + case 1: + prev = _c.sent(); + return [4 /*yield*/, callback(prev)]; + case 2: + next = _c.sent(); + if (!next) + return [2 /*return*/]; + validateThreadgateRecordOrThrow(next); + return [4 /*yield*/, writeThreadgateRecord({ + agent: agent, + postUri: postUri, + threadgate: next, + })]; + case 3: + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +/** + * Update the allow list for a threadgate record. + */ +export function updateThreadgateAllow(_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var _this = this; + var agent = _b.agent, postUri = _b.postUri, allow = _b.allow; + return __generator(this, function (_c) { + return [2 /*return*/, upsertThreadgate({ agent: agent, postUri: postUri }, function (prev) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (prev) { + return [2 /*return*/, __assign(__assign({}, prev), { allow: threadgateAllowUISettingToAllowRecordValue(allow) })]; + } + else { + return [2 /*return*/, createThreadgateRecord({ + post: postUri, + allow: threadgateAllowUISettingToAllowRecordValue(allow), + })]; + } + return [2 /*return*/]; + }); + }); })]; + }); + }); +} +export function useSetThreadgateAllowMutation() { + var _this = this; + var agent = useAgent(); + var queryClient = useQueryClient(); + var getPost = useGetPost(); + var updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var _this = this; + var postUri = _b.postUri, allow = _b.allow; + return __generator(this, function (_c) { + return [2 /*return*/, upsertThreadgate({ agent: agent, postUri: postUri }, function (prev) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (prev) { + return [2 /*return*/, __assign(__assign({}, prev), { allow: threadgateAllowUISettingToAllowRecordValue(allow) })]; + } + else { + return [2 /*return*/, createThreadgateRecord({ + post: postUri, + allow: threadgateAllowUISettingToAllowRecordValue(allow), + })]; + } + return [2 /*return*/]; + }); + }); })]; + }); + }); }, + onSuccess: function (_1, _a) { + return __awaiter(this, arguments, void 0, function (_, _b) { + var data; + var _this = this; + var postUri = _b.postUri, allow = _b.allow; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, retry(5, // 5 tries + function (// 5 tries + _e) { return true; }, function () { return __awaiter(_this, void 0, void 0, function () { + var post, threadgate, fetchedSettings, isReady; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getPost({ uri: postUri })]; + case 1: + post = _a.sent(); + threadgate = post.threadgate; + if (!threadgate) { + throw new Error("useSetThreadgateAllowMutation: could not fetch threadgate, appview may not be ready yet"); + } + fetchedSettings = threadgateViewToAllowUISetting(threadgate); + isReady = JSON.stringify(fetchedSettings) === JSON.stringify(allow); + if (!isReady) { + throw new Error("useSetThreadgateAllowMutation: appview isn't ready yet"); // try again + } + return [2 /*return*/, threadgate]; + } + }); + }); }, 1e3).catch(function () { })]; + case 1: + data = _c.sent(); + if (data) + updatePostThreadThreadgate(data); + queryClient.invalidateQueries({ + queryKey: [threadgateRecordQueryKeyRoot], + }); + queryClient.invalidateQueries({ + queryKey: [threadgateViewQueryKeyRoot], + }); + return [2 /*return*/]; + } + }); + }); + }, + }); +} +export function useToggleReplyVisibilityMutation() { + var _this = this; + var agent = useAgent(); + var queryClient = useQueryClient(); + var hiddenReplies = useThreadgateHiddenReplyUrisAPI(); + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var _this = this; + var postUri = _b.postUri, replyUri = _b.replyUri, action = _b.action; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (action === 'hide') { + hiddenReplies.addHiddenReplyUri(replyUri); + } + else if (action === 'show') { + hiddenReplies.removeHiddenReplyUri(replyUri); + } + return [4 /*yield*/, upsertThreadgate({ agent: agent, postUri: postUri }, function (prev) { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + if (prev) { + if (action === 'hide') { + return [2 /*return*/, mergeThreadgateRecords(prev, { + hiddenReplies: [replyUri], + })]; + } + else if (action === 'show') { + return [2 /*return*/, __assign(__assign({}, prev), { hiddenReplies: ((_a = prev.hiddenReplies) === null || _a === void 0 ? void 0 : _a.filter(function (uri) { return uri !== replyUri; })) || [] })]; + } + } + else { + if (action === 'hide') { + return [2 /*return*/, createThreadgateRecord({ + post: postUri, + hiddenReplies: [replyUri], + })]; + } + } + return [2 /*return*/]; + }); + }); })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { + queryClient.invalidateQueries({ + queryKey: [threadgateRecordQueryKeyRoot], + }); + }, + onError: function (_, _a) { + var replyUri = _a.replyUri, action = _a.action; + if (action === 'hide') { + hiddenReplies.removeHiddenReplyUri(replyUri); + } + else if (action === 'show') { + hiddenReplies.addHiddenReplyUri(replyUri); + } + }, + }); +} +var MaxHiddenRepliesError = /** @class */ (function (_super) { + __extends(MaxHiddenRepliesError, _super); + function MaxHiddenRepliesError(message) { + var _this = _super.call(this, message || 'Maximum number of hidden replies reached') || this; + _this.name = 'MaxHiddenRepliesError'; + return _this; + } + return MaxHiddenRepliesError; +}(Error)); +export { MaxHiddenRepliesError }; +var InvalidInteractionSettingsError = /** @class */ (function (_super) { + __extends(InvalidInteractionSettingsError, _super); + function InvalidInteractionSettingsError(message) { + var _this = _super.call(this, message || 'Invalid interaction settings') || this; + _this.name = 'InvalidInteractionSettingsError'; + return _this; + } + return InvalidInteractionSettingsError; +}(Error)); +export { InvalidInteractionSettingsError }; +export function validateThreadgateRecordOrThrow(record) { + var _a, _b; + var result = AppBskyFeedThreadgate.validateRecord(record); + if (result.success) { + if (((_b = (_a = result.value.hiddenReplies) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > MAX_HIDDEN_REPLIES) { + throw new MaxHiddenRepliesError(); + } + } + else { + throw new InvalidInteractionSettingsError(); + } +} diff --git a/src/state/queries/threadgate/types.js b/src/state/queries/threadgate/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/state/queries/threadgate/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/state/queries/threadgate/util.js b/src/state/queries/threadgate/util.js new file mode 100644 index 0000000000..4a0effa49c --- /dev/null +++ b/src/state/queries/threadgate/util.js @@ -0,0 +1,128 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { AppBskyFeedThreadgate } from '@atproto/api'; +import * as bsky from '#/types/bsky'; +export function threadgateViewToAllowUISetting(threadgateView) { + // Validate the record for clarity, since backwards compat code is a little confusing + var threadgate = threadgateView && + bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord) + ? threadgateView.record + : undefined; + return threadgateRecordToAllowUISetting(threadgate); +} +/** + * Converts a full {@link AppBskyFeedThreadgate.Record} to a list of + * {@link ThreadgateAllowUISetting}, for use by app UI. + */ +export function threadgateRecordToAllowUISetting(threadgate) { + /* + * If `threadgate` doesn't exist (default), or if `threadgate.allow === undefined`, it means + * anyone can reply. + * + * If `threadgate.allow === []` it means no one can reply, and we translate to UI code + * here. This was a historical choice, and we have no lexicon representation + * for 'replies disabled' other than an empty array. + */ + if (!threadgate || threadgate.allow === undefined) { + return [{ type: 'everybody' }]; + } + if (threadgate.allow.length === 0) { + return [{ type: 'nobody' }]; + } + var settings = threadgate.allow + .map(function (allow) { + var setting; + if (AppBskyFeedThreadgate.isMentionRule(allow)) { + setting = { type: 'mention' }; + } + else if (AppBskyFeedThreadgate.isFollowingRule(allow)) { + setting = { type: 'following' }; + } + else if (AppBskyFeedThreadgate.isListRule(allow)) { + setting = { type: 'list', list: allow.list }; + } + else if (AppBskyFeedThreadgate.isFollowerRule(allow)) { + setting = { type: 'followers' }; + } + return setting; + }) + .filter(function (n) { return !!n; }); + return settings; +} +/** + * Converts an array of {@link ThreadgateAllowUISetting} to the `allow` prop on + * {@link AppBskyFeedThreadgate.Record}. + * + * If the `allow` property on the record is undefined, we infer that to mean + * that everyone can reply. If it's an empty array, we infer that to mean that + * no one can reply. + */ +export function threadgateAllowUISettingToAllowRecordValue(threadgate) { + if (threadgate.find(function (v) { return v.type === 'everybody'; })) { + return undefined; + } + var allow = []; + if (!threadgate.find(function (v) { return v.type === 'nobody'; })) { + for (var _i = 0, threadgate_1 = threadgate; _i < threadgate_1.length; _i++) { + var rule = threadgate_1[_i]; + if (rule.type === 'mention') { + allow.push({ $type: 'app.bsky.feed.threadgate#mentionRule' }); + } + else if (rule.type === 'following') { + allow.push({ $type: 'app.bsky.feed.threadgate#followingRule' }); + } + else if (rule.type === 'followers') { + allow.push({ $type: 'app.bsky.feed.threadgate#followerRule' }); + } + else if (rule.type === 'list') { + allow.push({ + $type: 'app.bsky.feed.threadgate#listRule', + list: rule.list, + }); + } + } + } + return allow; +} +/** + * Merges two {@link AppBskyFeedThreadgate.Record} objects, combining their + * `allow` and `hiddenReplies` arrays and de-deduplicating them. + * + * Note: `allow` can be undefined here, be sure you don't accidentally set it + * to an empty array. See other comments in this file. + */ +export function mergeThreadgateRecords(prev, next) { + // can be undefined if everyone can reply! + var allow = prev.allow || next.allow + ? __spreadArray(__spreadArray([], (prev.allow || []), true), (next.allow || []), true).filter(function (v, i, a) { return a.findIndex(function (t) { return t.$type === v.$type; }) === i; }) + : undefined; + var hiddenReplies = Array.from(new Set(__spreadArray(__spreadArray([], (prev.hiddenReplies || []), true), (next.hiddenReplies || []), true))); + return createThreadgateRecord({ + post: prev.post, + allow: allow, // can be undefined! + hiddenReplies: hiddenReplies, + }); +} +/** + * Create a new {@link AppBskyFeedThreadgate.Record} object with the given + * properties. + */ +export function createThreadgateRecord(threadgate) { + if (!threadgate.post) { + throw new Error('Cannot create a threadgate record without a post URI'); + } + return { + $type: 'app.bsky.feed.threadgate', + post: threadgate.post, + createdAt: new Date().toISOString(), + allow: threadgate.allow, // can be undefined! + hiddenReplies: threadgate.hiddenReplies || [], + }; +} diff --git a/src/state/queries/trending/useGetSuggestedFeedsQuery.js b/src/state/queries/trending/useGetSuggestedFeedsQuery.js new file mode 100644 index 0000000000..f93801569f --- /dev/null +++ b/src/state/queries/trending/useGetSuggestedFeedsQuery.js @@ -0,0 +1,89 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var DEFAULT_LIMIT = 15; +export var createGetSuggestedFeedsQueryKey = function () { return ['suggested-feeds']; }; +export function useGetSuggestedFeedsQuery(_a) { + var _this = this; + var enabled = _a.enabled; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + var savedFeeds = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds; + return useQuery({ + enabled: !!preferences && enabled !== false, + staleTime: STALE.MINUTES.THREE, + queryKey: createGetSuggestedFeedsQueryKey(), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var contentLangs, data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + contentLangs = getContentLanguages().join(','); + return [4 /*yield*/, agent.app.bsky.unspecced.getSuggestedFeeds({ + limit: DEFAULT_LIMIT, + }, { + headers: __assign(__assign({}, createBskyTopicsHeader(aggregateUserInterests(preferences))), { 'Accept-Language': contentLangs }), + })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, { + feeds: data.feeds.filter(function (feed) { + var isSaved = !!(savedFeeds === null || savedFeeds === void 0 ? void 0 : savedFeeds.find(function (s) { return s.value === feed.uri; })); + return !isSaved; + }), + }]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.js b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.js new file mode 100644 index 0000000000..d2e087b432 --- /dev/null +++ b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.js @@ -0,0 +1,142 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { createBskyTopicsHeader } from '#/lib/api/feed/utils'; +import { logger } from '#/logger'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var getSuggestedOnboardingUsersQueryKeyRoot = 'unspecced-suggested-onboarding-users'; +export var createGetSuggestedOnboardingUsersQueryKey = function (props) { return [ + getSuggestedOnboardingUsersQueryKeyRoot, + props.category, + props.limit, + props.overrideInterests.join(','), +]; }; +export function useGetSuggestedOnboardingUsersQuery(props) { + var _this = this; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + return useQuery({ + enabled: !!preferences && props.enabled !== false, + staleTime: STALE.MINUTES.THREE, + queryKey: createGetSuggestedOnboardingUsersQueryKey(props), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var contentLangs, overrideInterests, data, fallbackData; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + contentLangs = getContentLanguages().join(','); + overrideInterests = props.overrideInterests.join(','); + return [4 /*yield*/, agent.app.bsky.unspecced.getSuggestedOnboardingUsers({ + category: (_a = props.category) !== null && _a !== void 0 ? _a : undefined, + limit: props.limit || 10, + }, { + headers: __assign(__assign({}, createBskyTopicsHeader(overrideInterests)), { 'Accept-Language': contentLangs }), + }) + // FALLBACK: if no results for 'all', try again with no interests specified + ]; + case 1: + data = (_c.sent()).data; + if (!(!props.category && data.actors.length === 0)) return [3 /*break*/, 3]; + logger.error("Did not get any suggested onboarding users, falling back - interests: ".concat(overrideInterests)); + return [4 /*yield*/, agent.app.bsky.unspecced.getSuggestedOnboardingUsers({ + category: (_b = props.category) !== null && _b !== void 0 ? _b : undefined, + limit: props.limit || 10, + }, { + headers: { + 'Accept-Language': contentLangs, + }, + })]; + case 2: + fallbackData = (_c.sent()).data; + return [2 /*return*/, fallbackData]; + case 3: return [2 /*return*/, data]; + } + }); + }); }, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var responses, _i, responses_1, _a, _key, response, _b, _c, actor; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + responses = queryClient.getQueriesData({ + queryKey: [getSuggestedOnboardingUsersQueryKeyRoot], + }); + _i = 0, responses_1 = responses; + _d.label = 1; + case 1: + if (!(_i < responses_1.length)) return [3 /*break*/, 6]; + _a = responses_1[_i], _key = _a[0], response = _a[1]; + if (!response) { + return [3 /*break*/, 5]; + } + _b = 0, _c = response.actors; + _d.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 5]; + actor = _c[_b]; + if (!(actor.did === did)) return [3 /*break*/, 4]; + return [4 /*yield*/, actor]; + case 3: + _d.sent(); + _d.label = 4; + case 4: + _b++; + return [3 /*break*/, 2]; + case 5: + _i++; + return [3 /*break*/, 1]; + case 6: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.js b/src/state/queries/trending/useGetSuggestedUsersQuery.js new file mode 100644 index 0000000000..733b076a65 --- /dev/null +++ b/src/state/queries/trending/useGetSuggestedUsersQuery.js @@ -0,0 +1,141 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils'; +import { logger } from '#/logger'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'; +export var createGetSuggestedUsersQueryKey = function (props) { return [ + getSuggestedUsersQueryKeyRoot, + props.category, + props.limit, +]; }; +export function useGetSuggestedUsersQuery(props) { + var _this = this; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + return useQuery({ + enabled: !!preferences && props.enabled !== false, + staleTime: STALE.MINUTES.THREE, + queryKey: createGetSuggestedUsersQueryKey(props), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var contentLangs, userInterests, data, fallbackData; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + contentLangs = getContentLanguages().join(','); + userInterests = aggregateUserInterests(preferences); + return [4 /*yield*/, agent.app.bsky.unspecced.getSuggestedUsers({ + category: (_a = props.category) !== null && _a !== void 0 ? _a : undefined, + limit: props.limit || 10, + }, { + headers: __assign(__assign({}, createBskyTopicsHeader(userInterests)), { 'Accept-Language': contentLangs }), + }) + // FALLBACK: if no results for 'all', try again with no interests specified + ]; + case 1: + data = (_c.sent()).data; + if (!(!props.category && data.actors.length === 0)) return [3 /*break*/, 3]; + logger.error("Did not get any suggested users, falling back - interests: ".concat(userInterests)); + return [4 /*yield*/, agent.app.bsky.unspecced.getSuggestedUsers({ + category: (_b = props.category) !== null && _b !== void 0 ? _b : undefined, + limit: props.limit || 10, + }, { + headers: { + 'Accept-Language': contentLangs, + }, + })]; + case 2: + fallbackData = (_c.sent()).data; + return [2 /*return*/, fallbackData]; + case 3: return [2 /*return*/, data]; + } + }); + }); }, + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var responses, _i, responses_1, _a, _key, response, _b, _c, actor; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + responses = queryClient.getQueriesData({ + queryKey: [getSuggestedUsersQueryKeyRoot], + }); + _i = 0, responses_1 = responses; + _d.label = 1; + case 1: + if (!(_i < responses_1.length)) return [3 /*break*/, 6]; + _a = responses_1[_i], _key = _a[0], response = _a[1]; + if (!response) { + return [3 /*break*/, 5]; + } + _b = 0, _c = response.actors; + _d.label = 2; + case 2: + if (!(_b < _c.length)) return [3 /*break*/, 5]; + actor = _c[_b]; + if (!(actor.did === did)) return [3 /*break*/, 4]; + return [4 /*yield*/, actor]; + case 3: + _d.sent(); + _d.label = 4; + case 4: + _b++; + return [3 /*break*/, 2]; + case 5: + _i++; + return [3 /*break*/, 1]; + case 6: return [2 /*return*/]; + } + }); +} diff --git a/src/state/queries/trending/useGetTrendsQuery.js b/src/state/queries/trending/useGetTrendsQuery.js new file mode 100644 index 0000000000..6d5bb4e0dd --- /dev/null +++ b/src/state/queries/trending/useGetTrendsQuery.js @@ -0,0 +1,99 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import React from 'react'; +import { hasMutedWord } from '@atproto/api'; +import { useQuery } from '@tanstack/react-query'; +import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var DEFAULT_LIMIT = 5; +export var createGetTrendsQueryKey = function () { return ['trends']; }; +export function useGetTrendsQuery() { + var _this = this; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + var mutedWords = React.useMemo(function () { + var _a; + return ((_a = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs) === null || _a === void 0 ? void 0 : _a.mutedWords) || []; + }, [preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs]); + return useQuery({ + enabled: !!preferences, + staleTime: STALE.MINUTES.THREE, + queryKey: createGetTrendsQueryKey(), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var contentLangs, data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + contentLangs = getContentLanguages().join(','); + return [4 /*yield*/, agent.app.bsky.unspecced.getTrends({ + limit: DEFAULT_LIMIT, + }, { + headers: __assign(__assign({}, createBskyTopicsHeader(aggregateUserInterests(preferences))), { 'Accept-Language': contentLangs }), + })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + select: React.useCallback(function (data) { + var _a; + return { + trends: ((_a = data.trends) !== null && _a !== void 0 ? _a : []).filter(function (t) { + return !hasMutedWord({ + mutedWords: mutedWords, + text: t.topic + ' ' + t.displayName + ' ' + t.category, + }); + }), + }; + }, [mutedWords]), + }); +} diff --git a/src/state/queries/trending/useTrendingTopics.js b/src/state/queries/trending/useTrendingTopics.js new file mode 100644 index 0000000000..348496c952 --- /dev/null +++ b/src/state/queries/trending/useTrendingTopics.js @@ -0,0 +1,101 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback, useMemo } from 'react'; +import { hasMutedWord } from '@atproto/api'; +import { useQuery } from '@tanstack/react-query'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var DEFAULT_LIMIT = 14; +function dedup(topics) { + var seen = new Set(); + return topics.filter(function (t) { + if (seen.has(t.link)) + return false; + seen.add(t.link); + return true; + }); +} +export var trendingTopicsQueryKey = ['trending-topics']; +export function useTrendingTopics() { + var _a; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + var mutedWords = useMemo(function () { var _a, _b; return (_b = (_a = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs) === null || _a === void 0 ? void 0 : _a.mutedWords) !== null && _b !== void 0 ? _b : []; }, [(_a = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs) === null || _a === void 0 ? void 0 : _a.mutedWords]); + return useQuery({ + refetchOnWindowFocus: true, + staleTime: STALE.MINUTES.THREE, + queryKey: trendingTopicsQueryKey, + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var data; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getTrendingTopics({ + limit: DEFAULT_LIMIT, + })]; + case 1: + data = (_c.sent()).data; + return [2 /*return*/, { + topics: (_a = data.topics) !== null && _a !== void 0 ? _a : [], + suggested: (_b = data.suggested) !== null && _b !== void 0 ? _b : [], + }]; + } + }); + }); + }, + select: useCallback(function (data) { + return { + topics: dedup(data.topics.filter(function (t) { + var _a, _b; + return !hasMutedWord({ + mutedWords: mutedWords, + text: "".concat(t.topic, " ").concat((_a = t.displayName) !== null && _a !== void 0 ? _a : '', " ").concat((_b = t.description) !== null && _b !== void 0 ? _b : ''), + }); + })), + suggested: dedup(data.suggested.filter(function (t) { + var _a, _b; + return !hasMutedWord({ + mutedWords: mutedWords, + text: "".concat(t.topic, " ").concat((_a = t.displayName) !== null && _a !== void 0 ? _a : '', " ").concat((_b = t.description) !== null && _b !== void 0 ? _b : ''), + }); + })), + }; + }, [mutedWords]), + }); +} diff --git a/src/state/queries/unstable-profile-cache.js b/src/state/queries/unstable-profile-cache.js new file mode 100644 index 0000000000..da72a762ff --- /dev/null +++ b/src/state/queries/unstable-profile-cache.js @@ -0,0 +1,32 @@ +import { useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +var unstableProfileViewCacheQueryKeyRoot = 'unstableProfileViewCache'; +export var unstableProfileViewCacheQueryKey = function (didOrHandle) { return [ + unstableProfileViewCacheQueryKeyRoot, + didOrHandle, +]; }; +/** + * Used as a rough cache of profile views to make loading snappier. This method + * accepts and stores any profile view type by both handle and DID. + * + * Access the cache via {@link useUnstableProfileViewCache}. + */ +export function unstableCacheProfileView(queryClient, profile) { + queryClient.setQueryData(unstableProfileViewCacheQueryKey(profile.handle), profile); + queryClient.setQueryData(unstableProfileViewCacheQueryKey(profile.did), profile); +} +/** + * Hook to access the unstable profile view cache. This cache can return ANY + * profile view type, so if the object shape is important, you need to use the + * identity validators shipped in the atproto SDK e.g. + * `AppBskyActorDefs.isValidProfileViewBasic` to confirm before using. + * + * To cache a profile, use {@link unstableCacheProfileView}. + */ +export function useUnstableProfileViewCache() { + var qc = useQueryClient(); + var getUnstableProfile = useCallback(function (didOrHandle) { + return qc.getQueryData(unstableProfileViewCacheQueryKey(didOrHandle)); + }, [qc]); + return { getUnstableProfile: getUnstableProfile }; +} diff --git a/src/state/queries/useCurrentAccountProfile.js b/src/state/queries/useCurrentAccountProfile.js new file mode 100644 index 0000000000..f700412762 --- /dev/null +++ b/src/state/queries/useCurrentAccountProfile.js @@ -0,0 +1,8 @@ +import { useMaybeProfileShadow } from '#/state/cache/profile-shadow'; +import { useProfileQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +export function useCurrentAccountProfile() { + var currentAccount = useSession().currentAccount; + var profile = useProfileQuery({ did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did }).data; + return useMaybeProfileShadow(profile); +} diff --git a/src/state/queries/useOnboardingSuggestedStarterPacksQuery.js b/src/state/queries/useOnboardingSuggestedStarterPacksQuery.js new file mode 100644 index 0000000000..c11a429d64 --- /dev/null +++ b/src/state/queries/useOnboardingSuggestedStarterPacksQuery.js @@ -0,0 +1,81 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var createOnboardingSuggestedStarterPacksQueryKey = function (interests) { return ['onboarding-suggested-starter-packs', interests === null || interests === void 0 ? void 0 : interests.join(',')]; }; +export function useOnboardingSuggestedStarterPacksQuery(_a) { + var _this = this; + var enabled = _a.enabled, overrideInterests = _a.overrideInterests; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + var contentLangs = getContentLanguages().join(','); + return useQuery({ + enabled: !!preferences && enabled !== false, + staleTime: STALE.MINUTES.THREE, + queryKey: createOnboardingSuggestedStarterPacksQueryKey(overrideInterests), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getOnboardingSuggestedStarterPacks({ limit: 6 }, { + headers: __assign(__assign({}, createBskyTopicsHeader(overrideInterests + ? overrideInterests.join(',') + : aggregateUserInterests(preferences))), { 'Accept-Language': contentLangs }), + })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/usePostThread/const.js b/src/state/queries/usePostThread/const.js new file mode 100644 index 0000000000..dd93f94c76 --- /dev/null +++ b/src/state/queries/usePostThread/const.js @@ -0,0 +1,20 @@ +/** + * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export var LINEAR_VIEW_BELOW = 10; +/** + * See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export var LINEAR_VIEW_BF = 1; +/** + * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export var TREE_VIEW_BELOW = 4; +/** + * See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export var TREE_VIEW_BF = undefined; +/** + * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + */ +export var TREE_VIEW_BELOW_DESKTOP = 6; diff --git a/src/state/queries/usePostThread/context.js b/src/state/queries/usePostThread/context.js new file mode 100644 index 0000000000..21775e3932 --- /dev/null +++ b/src/state/queries/usePostThread/context.js @@ -0,0 +1,14 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext } from 'react'; +var PostThreadContext = createContext(undefined); +/** + * Use the current {@link PostThreadContext}, if one is available. If not, + * returns `undefined`. + */ +export function usePostThreadContext() { + return useContext(PostThreadContext); +} +export function PostThreadContextProvider(_a) { + var children = _a.children, context = _a.context; + return (_jsx(PostThreadContext.Provider, { value: context, children: children })); +} diff --git a/src/state/queries/usePostThread/index.js b/src/state/queries/usePostThread/index.js new file mode 100644 index 0000000000..84dd913713 --- /dev/null +++ b/src/state/queries/usePostThread/index.js @@ -0,0 +1,335 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback, useMemo, useState } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useThreadPreferences } from '#/state/queries/preferences/useThreadPreferences'; +import { LINEAR_VIEW_BELOW, LINEAR_VIEW_BF, TREE_VIEW_BELOW, TREE_VIEW_BELOW_DESKTOP, TREE_VIEW_BF, } from '#/state/queries/usePostThread/const'; +import { createCacheMutator, getThreadPlaceholder, } from '#/state/queries/usePostThread/queryCache'; +import { buildThread, sortAndAnnotateThreadItems, } from '#/state/queries/usePostThread/traversal'; +import { createPostThreadOtherQueryKey, createPostThreadQueryKey, } from '#/state/queries/usePostThread/types'; +import { getThreadgateRecord } from '#/state/queries/usePostThread/utils'; +import * as views from '#/state/queries/usePostThread/views'; +import { useAgent, useSession } from '#/state/session'; +import { useMergeThreadgateHiddenReplies } from '#/state/threadgate-hidden-replies'; +import { useBreakpoints } from '#/alf'; +import { IS_WEB } from '#/env'; +export * from '#/state/queries/usePostThread/context'; +export { useUpdatePostThreadThreadgateQueryCache } from '#/state/queries/usePostThread/queryCache'; +export * from '#/state/queries/usePostThread/types'; +export function usePostThread(_a) { + var _b, _c, _d; + var anchor = _a.anchor; + var qc = useQueryClient(); + var agent = useAgent(); + var hasSession = useSession().hasSession; + var gtPhone = useBreakpoints().gtPhone; + var moderationOpts = useModerationOpts(); + var mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies(); + var _e = useThreadPreferences(), isThreadPreferencesLoaded = _e.isLoaded, sort = _e.sort, baseSetSort = _e.setSort, view = _e.view, baseSetView = _e.setView; + var below = useMemo(function () { + return view === 'linear' + ? LINEAR_VIEW_BELOW + : IS_WEB && gtPhone + ? TREE_VIEW_BELOW_DESKTOP + : TREE_VIEW_BELOW; + }, [view, gtPhone]); + var postThreadQueryKey = createPostThreadQueryKey({ + anchor: anchor, + sort: sort, + view: view, + }); + var postThreadOtherQueryKey = createPostThreadOtherQueryKey({ + anchor: anchor, + }); + var query = useQuery({ + enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts, + queryKey: postThreadQueryKey, + queryFn: function (ctx) { + return __awaiter(this, void 0, void 0, function () { + var data, result, record; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getPostThreadV2({ + anchor: anchor, + branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF, + below: below, + sort: sort, + }) + /* + * Initialize `ctx.meta` to track if we know we have additional replies + * we could fetch once we hit the end. + */ + ]; + case 1: + data = (_a.sent()).data; + /* + * Initialize `ctx.meta` to track if we know we have additional replies + * we could fetch once we hit the end. + */ + ctx.meta = ctx.meta || { + hasOtherReplies: false, + }; + /* + * If we know we have additional replies, we'll set this to true. + */ + if (data.hasOtherReplies) { + ctx.meta.hasOtherReplies = true; + } + result = { + thread: data.thread || [], + threadgate: data.threadgate, + hasOtherReplies: !!ctx.meta.hasOtherReplies, + }; + record = getThreadgateRecord(result.threadgate); + if (result.threadgate && record) { + result.threadgate.record = record; + } + return [2 /*return*/, result]; + } + }); + }); + }, + placeholderData: function () { + if (!anchor) + return; + var placeholder = getThreadPlaceholder(qc, anchor); + /* + * Always return something here, even empty data, so that + * `isPlaceholderData` is always true, which we'll use to insert + * skeletons. + */ + var thread = placeholder ? [placeholder] : []; + return { thread: thread, threadgate: undefined, hasOtherReplies: false }; + }, + select: function (data) { + var record = getThreadgateRecord(data.threadgate); + if (data.threadgate && record) { + data.threadgate.record = record; + } + return data; + }, + }); + var thread = useMemo(function () { var _a; return ((_a = query.data) === null || _a === void 0 ? void 0 : _a.thread) || []; }, [(_b = query.data) === null || _b === void 0 ? void 0 : _b.thread]); + var threadgate = useMemo(function () { var _a; return (_a = query.data) === null || _a === void 0 ? void 0 : _a.threadgate; }, [(_c = query.data) === null || _c === void 0 ? void 0 : _c.threadgate]); + var hasOtherThreadItems = useMemo(function () { var _a; return !!((_a = query.data) === null || _a === void 0 ? void 0 : _a.hasOtherReplies); }, [(_d = query.data) === null || _d === void 0 ? void 0 : _d.hasOtherReplies]); + var _f = useState(false), otherItemsVisible = _f[0], setOtherItemsVisible = _f[1]; + /** + * Creates a mutator for the post thread cache. This is used to insert + * replies into the thread cache after posting. + */ + var mutator = useMemo(function () { + return createCacheMutator({ + params: { view: view, below: below }, + postThreadQueryKey: postThreadQueryKey, + postThreadOtherQueryKey: postThreadOtherQueryKey, + queryClient: qc, + }); + }, [qc, view, below, postThreadQueryKey, postThreadOtherQueryKey]); + /** + * If we have additional items available from the server and the user has + * chosen to view them, start loading data + */ + var additionalQueryEnabled = hasOtherThreadItems && otherItemsVisible; + var additionalItemsQuery = useQuery({ + enabled: additionalQueryEnabled, + queryKey: postThreadOtherQueryKey, + queryFn: function () { + return __awaiter(this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getPostThreadOtherV2({ + anchor: anchor, + })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); + }, + }); + var serverOtherThreadItems = useMemo(function () { + var _a; + if (!additionalQueryEnabled) + return []; + if (additionalItemsQuery.isLoading) { + return Array.from({ length: 2 }).map(function (_, i) { + return views.skeleton({ + key: "other-reply-".concat(i), + item: 'reply', + }); + }); + } + else if (additionalItemsQuery.isError) { + /* + * We could insert an special error component in here, but since these + * are optional additional replies, it's not critical that they're shown + * atm. + */ + return []; + } + else if ((_a = additionalItemsQuery.data) === null || _a === void 0 ? void 0 : _a.thread) { + var threadItems_1 = sortAndAnnotateThreadItems(additionalItemsQuery.data.thread, { + view: view, + skipModerationHandling: true, + threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate === null || threadgate === void 0 ? void 0 : threadgate.record), + moderationOpts: moderationOpts, + }).threadItems; + return threadItems_1; + } + else { + return []; + } + }, [ + view, + additionalQueryEnabled, + additionalItemsQuery, + mergeThreadgateHiddenReplies, + moderationOpts, + threadgate === null || threadgate === void 0 ? void 0 : threadgate.record, + ]); + /** + * Sets the sort order for the thread and resets the additional thread items + */ + var setSort = useCallback(function (nextSort) { + setOtherItemsVisible(false); + baseSetSort(nextSort); + }, [baseSetSort, setOtherItemsVisible]); + /** + * Sets the view variant for the thread and resets the additional thread items + */ + var setView = useCallback(function (nextView) { + setOtherItemsVisible(false); + baseSetView(nextView); + }, [baseSetView, setOtherItemsVisible]); + /* + * This is the main thread response, sorted into separate buckets based on + * moderation, and annotated with all UI state needed for rendering. + */ + var _g = useMemo(function () { + return sortAndAnnotateThreadItems(thread, { + view: view, + threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate === null || threadgate === void 0 ? void 0 : threadgate.record), + moderationOpts: moderationOpts, + }); + }, [ + thread, + threadgate === null || threadgate === void 0 ? void 0 : threadgate.record, + mergeThreadgateHiddenReplies, + moderationOpts, + view, + ]), threadItems = _g.threadItems, otherThreadItems = _g.otherThreadItems; + /* + * Take all three sets of thread items and combine them into a single thread, + * along with any other thread items required for rendering e.g. "Show more + * replies" or the reply composer. + */ + var items = useMemo(function () { + return buildThread({ + threadItems: threadItems, + otherThreadItems: otherThreadItems, + serverOtherThreadItems: serverOtherThreadItems, + isLoading: query.isPlaceholderData, + hasSession: hasSession, + hasOtherThreadItems: hasOtherThreadItems, + otherItemsVisible: otherItemsVisible, + showOtherItems: function () { return setOtherItemsVisible(true); }, + }); + }, [ + threadItems, + otherThreadItems, + serverOtherThreadItems, + query.isPlaceholderData, + hasSession, + hasOtherThreadItems, + otherItemsVisible, + setOtherItemsVisible, + ]); + return useMemo(function () { + var context = { + postThreadQueryKey: postThreadQueryKey, + postThreadOtherQueryKey: postThreadOtherQueryKey, + }; + return { + context: context, + state: { + /* + * Copy in any query state that is useful + */ + isFetching: query.isFetching, + isPlaceholderData: query.isPlaceholderData, + error: query.error, + /* + * Other state + */ + sort: sort, + view: view, + otherItemsVisible: otherItemsVisible, + }, + data: { + items: items, + threadgate: threadgate, + }, + actions: { + /* + * Copy in any query actions that are useful + */ + insertReplies: mutator.insertReplies, + refetch: query.refetch, + /* + * Other actions + */ + setSort: setSort, + setView: setView, + }, + }; + }, [ + query, + mutator.insertReplies, + otherItemsVisible, + sort, + view, + setSort, + setView, + threadgate, + items, + postThreadQueryKey, + postThreadOtherQueryKey, + ]); +} diff --git a/src/state/queries/usePostThread/queryCache.js b/src/state/queries/usePostThread/queryCache.js new file mode 100644 index 0000000000..0663b0ab68 --- /dev/null +++ b/src/state/queries/usePostThread/queryCache.js @@ -0,0 +1,403 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { useCallback } from 'react'; +import { AppBskyUnspeccedDefs, AtUri, } from '@atproto/api'; +import { useQueryClient } from '@tanstack/react-query'; +import { dangerousGetPostShadow, updatePostShadow, } from '#/state/cache/post-shadow'; +import { findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData } from '#/state/queries/explore-feed-previews'; +import { findAllPostsInQueryData as findAllPostsInNotifsQueryData } from '#/state/queries/notifications/feed'; +import { findAllPostsInQueryData as findAllPostsInFeedQueryData } from '#/state/queries/post-feed'; +import { findAllPostsInQueryData as findAllPostsInQuoteQueryData } from '#/state/queries/post-quotes'; +import { findAllPostsInQueryData as findAllPostsInSearchQueryData } from '#/state/queries/search-posts'; +import { usePostThreadContext } from '#/state/queries/usePostThread'; +import { getBranch } from '#/state/queries/usePostThread/traversal'; +import { postThreadQueryKeyRoot, } from '#/state/queries/usePostThread/types'; +import { getRootPostAtUri } from '#/state/queries/usePostThread/utils'; +import { postViewToThreadPlaceholder } from '#/state/queries/usePostThread/views'; +import { didOrHandleUriMatches, getEmbeddedPost } from '#/state/queries/util'; +import { embedViewRecordToPostView } from '#/state/queries/util'; +export function createCacheMutator(_a) { + var queryClient = _a.queryClient, postThreadQueryKey = _a.postThreadQueryKey, postThreadOtherQueryKey = _a.postThreadOtherQueryKey, params = _a.params; + return { + insertReplies: function (parentUri, replies) { + /* + * Main thread query mutator. + */ + queryClient.setQueryData(postThreadQueryKey, function (data) { + if (!data) + return; + return __assign(__assign({}, data), { thread: mutator(__spreadArray([], data.thread, true)) }); + }); + /* + * Additional replies query mutator. + */ + queryClient.setQueryData(postThreadOtherQueryKey, function (data) { + if (!data) + return; + return __assign(__assign({}, data), { thread: mutator(__spreadArray([], data.thread, true)) }); + }); + function mutator(thread) { + var _a, _b; + var _loop_1 = function (i) { + var parent_1 = thread[i]; + if (!AppBskyUnspeccedDefs.isThreadItemPost(parent_1.value)) + return "continue"; + if (parent_1.uri !== parentUri) + return "continue"; + /* + * Update parent data + */ + var shadow = dangerousGetPostShadow(parent_1.value.post); + var prevOptimisticCount = shadow === null || shadow === void 0 ? void 0 : shadow.optimisticReplyCount; + var prevReplyCount = parent_1.value.post.replyCount; + // prefer optimistic count, if we already have some + var currentReplyCount = ((_a = prevOptimisticCount !== null && prevOptimisticCount !== void 0 ? prevOptimisticCount : prevReplyCount) !== null && _a !== void 0 ? _a : 0) + 1; + /* + * We must update the value in the query cache in order for thread + * traversal to properly compute required metadata. + */ + parent_1.value.post.replyCount = currentReplyCount; + /** + * Additionally, we need to update the post shadow to keep track of + * these new values, since mutating the post object above does not + * cause a re-render. + */ + updatePostShadow(queryClient, parent_1.value.post.uri, { + optimisticReplyCount: currentReplyCount, + }); + var opDid = (_b = getRootPostAtUri(parent_1.value.post)) === null || _b === void 0 ? void 0 : _b.host; + var nextPreexistingItem = thread.at(i + 1); + var isEndOfReplyChain = !nextPreexistingItem || nextPreexistingItem.depth <= parent_1.depth; + var isParentRoot = parent_1.depth === 0; + var isParentBelowRoot = parent_1.depth > 0; + var optimisticReply = replies.at(0); + var opIsReplier = AppBskyUnspeccedDefs.isThreadItemPost(optimisticReply === null || optimisticReply === void 0 ? void 0 : optimisticReply.value) + ? opDid === optimisticReply.value.post.author.did + : false; + /* + * Always insert replies if the following conditions are met. Max + * depth checks are handled below. + */ + var canAlwaysInsertReplies = isParentRoot || + (params.view === 'tree' && isParentBelowRoot) || + (params.view === 'linear' && isEndOfReplyChain); + /* + * Maybe insert replies if we're in linear view, the replier is the + * OP, and certain conditions are met + */ + var shouldReplaceWithOPReplies = params.view === 'linear' && opIsReplier && isParentBelowRoot; + if (canAlwaysInsertReplies || shouldReplaceWithOPReplies) { + var branch = getBranch(thread, i, parent_1.depth); + /* + * OP insertions replace other replies _in linear view_. + */ + var itemsToRemove = shouldReplaceWithOPReplies ? branch.length : 0; + var itemsToInsert = replies + .map(function (r, ri) { + r.depth = parent_1.depth + 1 + ri; + return r; + }) + .filter(function (r) { + // Filter out replies that are too deep for our UI + return r.depth <= params.below; + }); + thread.splice.apply(thread, __spreadArray([i + 1, itemsToRemove], itemsToInsert, false)); + } + }; + for (var i = 0; i < thread.length; i++) { + _loop_1(i); + } + return thread; + } + }, + /** + * Unused atm, post shadow does the trick, but it would be nice to clean up + * the whole sub-tree on deletes. + */ + deletePost: function (post) { + queryClient.setQueryData(postThreadQueryKey, function (queryData) { + if (!queryData) + return; + var thread = __spreadArray([], queryData.thread, true); + for (var i = 0; i < thread.length; i++) { + var existingPost = thread[i]; + if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) + continue; + if (existingPost.uri === post.uri) { + var branch = getBranch(thread, i, existingPost.depth); + thread.splice(branch.start, branch.length); + break; + } + } + return __assign(__assign({}, queryData), { thread: thread }); + }); + }, + }; +} +export function getThreadPlaceholder(queryClient, uri) { + var partial; + for (var _i = 0, _a = getThreadPlaceholderCandidates(queryClient, uri); _i < _a.length; _i++) { + var item = _a[_i]; + /* + * Currently, the backend doesn't send full post info in some cases (for + * example, for quoted posts). We use missing `likeCount` as a way to + * detect that. In the future, we should fix this on the backend, which + * will let us always stop on the first result. + * + * TODO can we send in feeds and quotes? + */ + var hasAllInfo = item.value.post.likeCount != null; + if (hasAllInfo) { + return item; + } + else { + // Keep searching, we might still find a full post in the cache. + partial = item; + } + } + return partial; +} +export function getThreadPlaceholderCandidates(queryClient, uri) { + var _i, _a, post, _b, _c, post, _d, _e, post, _f, _g, post, _h, _j, post, _k, _l, post; + return __generator(this, function (_m) { + switch (_m.label) { + case 0: + _i = 0, _a = findAllPostsInQueryData(queryClient, uri); + _m.label = 1; + case 1: + if (!(_i < _a.length)) return [3 /*break*/, 4]; + post = _a[_i]; + return [4 /*yield*/, postViewToThreadPlaceholder(post)]; + case 2: + _m.sent(); + _m.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: + _b = 0, _c = findAllPostsInNotifsQueryData(queryClient, uri); + _m.label = 5; + case 5: + if (!(_b < _c.length)) return [3 /*break*/, 8]; + post = _c[_b]; + return [4 /*yield*/, postViewToThreadPlaceholder(post)]; + case 6: + _m.sent(); + _m.label = 7; + case 7: + _b++; + return [3 /*break*/, 5]; + case 8: + _d = 0, _e = findAllPostsInFeedQueryData(queryClient, uri); + _m.label = 9; + case 9: + if (!(_d < _e.length)) return [3 /*break*/, 12]; + post = _e[_d]; + return [4 /*yield*/, postViewToThreadPlaceholder(post)]; + case 10: + _m.sent(); + _m.label = 11; + case 11: + _d++; + return [3 /*break*/, 9]; + case 12: + _f = 0, _g = findAllPostsInQuoteQueryData(queryClient, uri); + _m.label = 13; + case 13: + if (!(_f < _g.length)) return [3 /*break*/, 16]; + post = _g[_f]; + return [4 /*yield*/, postViewToThreadPlaceholder(post)]; + case 14: + _m.sent(); + _m.label = 15; + case 15: + _f++; + return [3 /*break*/, 13]; + case 16: + _h = 0, _j = findAllPostsInSearchQueryData(queryClient, uri); + _m.label = 17; + case 17: + if (!(_h < _j.length)) return [3 /*break*/, 20]; + post = _j[_h]; + return [4 /*yield*/, postViewToThreadPlaceholder(post)]; + case 18: + _m.sent(); + _m.label = 19; + case 19: + _h++; + return [3 /*break*/, 17]; + case 20: + _k = 0, _l = findAllPostsInExploreFeedPreviewsQueryData(queryClient, uri); + _m.label = 21; + case 21: + if (!(_k < _l.length)) return [3 /*break*/, 24]; + post = _l[_k]; + return [4 /*yield*/, postViewToThreadPlaceholder(post)]; + case 22: + _m.sent(); + _m.label = 23; + case 23: + _k++; + return [3 /*break*/, 21]; + case 24: return [2 /*return*/]; + } + }); +} +export function findAllPostsInQueryData(queryClient, uri) { + var atUri, queryDatas, _i, queryDatas_1, _a, _queryKey, queryData, thread, _b, thread_1, item, qp; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + atUri = new AtUri(uri); + queryDatas = queryClient.getQueriesData({ + queryKey: [postThreadQueryKeyRoot], + }); + _i = 0, queryDatas_1 = queryDatas; + _c.label = 1; + case 1: + if (!(_i < queryDatas_1.length)) return [3 /*break*/, 8]; + _a = queryDatas_1[_i], _queryKey = _a[0], queryData = _a[1]; + if (!queryData) + return [3 /*break*/, 7]; + thread = queryData.thread; + _b = 0, thread_1 = thread; + _c.label = 2; + case 2: + if (!(_b < thread_1.length)) return [3 /*break*/, 7]; + item = thread_1[_b]; + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) return [3 /*break*/, 6]; + if (!didOrHandleUriMatches(atUri, item.value.post)) return [3 /*break*/, 4]; + return [4 /*yield*/, item.value.post]; + case 3: + _c.sent(); + _c.label = 4; + case 4: + qp = getEmbeddedPost(item.value.post.embed); + if (!(qp && didOrHandleUriMatches(atUri, qp))) return [3 /*break*/, 6]; + return [4 /*yield*/, embedViewRecordToPostView(qp)]; + case 5: + _c.sent(); + _c.label = 6; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} +export function findAllProfilesInQueryData(queryClient, did) { + var queryDatas, _i, queryDatas_2, _a, _queryKey, queryData, thread, _b, thread_2, item, qp; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + queryDatas = queryClient.getQueriesData({ + queryKey: [postThreadQueryKeyRoot], + }); + _i = 0, queryDatas_2 = queryDatas; + _c.label = 1; + case 1: + if (!(_i < queryDatas_2.length)) return [3 /*break*/, 8]; + _a = queryDatas_2[_i], _queryKey = _a[0], queryData = _a[1]; + if (!queryData) + return [3 /*break*/, 7]; + thread = queryData.thread; + _b = 0, thread_2 = thread; + _c.label = 2; + case 2: + if (!(_b < thread_2.length)) return [3 /*break*/, 7]; + item = thread_2[_b]; + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) return [3 /*break*/, 6]; + if (!(item.value.post.author.did === did)) return [3 /*break*/, 4]; + return [4 /*yield*/, item.value.post.author]; + case 3: + _c.sent(); + _c.label = 4; + case 4: + qp = getEmbeddedPost(item.value.post.embed); + if (!(qp && qp.author.did === did)) return [3 /*break*/, 6]; + return [4 /*yield*/, qp.author]; + case 5: + _c.sent(); + _c.label = 6; + case 6: + _b++; + return [3 /*break*/, 2]; + case 7: + _i++; + return [3 /*break*/, 1]; + case 8: return [2 /*return*/]; + } + }); +} +export function useUpdatePostThreadThreadgateQueryCache() { + var qc = useQueryClient(); + var context = usePostThreadContext(); + return useCallback(function (threadgate) { + if (!context) + return; + function mutator(thread) { + for (var i = 0; i < thread.length; i++) { + var item = thread[i]; + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) + continue; + if (item.depth === 0) { + thread.splice(i, 1, __assign(__assign({}, item), { value: __assign(__assign({}, item.value), { post: __assign(__assign({}, item.value.post), { threadgate: threadgate }) }) })); + } + } + return thread; + } + qc.setQueryData(context.postThreadQueryKey, function (data) { + if (!data) + return; + return __assign(__assign({}, data), { thread: mutator(__spreadArray([], data.thread, true)) }); + }); + }, [qc, context]); +} diff --git a/src/state/queries/usePostThread/traversal.js b/src/state/queries/usePostThread/traversal.js new file mode 100644 index 0000000000..f479685c24 --- /dev/null +++ b/src/state/queries/usePostThread/traversal.js @@ -0,0 +1,469 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { AppBskyUnspeccedDefs } from '@atproto/api'; +import { getPostRecord, getThreadPostNoUnauthenticatedUI, getThreadPostUI, getTraversalMetadata, storeTraversalMetadata, } from '#/state/queries/usePostThread/utils'; +import * as views from '#/state/queries/usePostThread/views'; +export function sortAndAnnotateThreadItems(thread, _a) { + var _b, _c, _d, _e, _f; + var threadgateHiddenReplies = _a.threadgateHiddenReplies, moderationOpts = _a.moderationOpts, view = _a.view, skipModerationHandling = _a.skipModerationHandling; + var threadItems = []; + var otherThreadItems = []; + var metadatas = new Map(); + traversal: for (var i = 0; i < thread.length; i++) { + var item = thread[i]; + var parentMetadata = void 0; + var metadata = void 0; + if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + parentMetadata = metadatas.get(((_c = (_b = getPostRecord(item.value.post).reply) === null || _b === void 0 ? void 0 : _b.parent) === null || _c === void 0 ? void 0 : _c.uri) || ''); + metadata = getTraversalMetadata({ + item: item, + parentMetadata: parentMetadata, + prevItem: thread.at(i - 1), + nextItem: thread.at(i + 1), + }); + storeTraversalMetadata(metadatas, metadata); + } + if (item.depth < 0) { + /* + * Parents are ignored until we find the anchor post, then we walk + * _up_ from there. + */ + } + else if (item.depth === 0) { + if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) { + threadItems.push(views.threadPostNoUnauthenticated(item)); + } + else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) { + threadItems.push(views.threadPostNotFound(item)); + } + else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) { + threadItems.push(views.threadPostBlocked(item)); + } + else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + var post = views.threadPost({ + uri: item.uri, + depth: item.depth, + value: item.value, + moderationOpts: moderationOpts, + threadgateHiddenReplies: threadgateHiddenReplies, + }); + threadItems.push(post); + parentTraversal: for (var pi = i - 1; pi >= 0; pi--) { + var parent_1 = thread[pi]; + if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent_1.value)) { + var post_1 = views.threadPostNoUnauthenticated(parent_1); + post_1.ui = getThreadPostNoUnauthenticatedUI({ + depth: parent_1.depth, + // ignore for now + // prevItemDepth: thread[pi - 1]?.depth, + nextItemDepth: (_d = thread[pi + 1]) === null || _d === void 0 ? void 0 : _d.depth, + }); + threadItems.unshift(post_1); + // for now, break parent traversal at first no-unauthed + break parentTraversal; + } + else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent_1.value)) { + threadItems.unshift(views.threadPostNotFound(parent_1)); + break parentTraversal; + } + else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent_1.value)) { + threadItems.unshift(views.threadPostBlocked(parent_1)); + break parentTraversal; + } + else if (AppBskyUnspeccedDefs.isThreadItemPost(parent_1.value)) { + threadItems.unshift(views.threadPost({ + uri: parent_1.uri, + depth: parent_1.depth, + value: parent_1.value, + moderationOpts: moderationOpts, + threadgateHiddenReplies: threadgateHiddenReplies, + })); + } + } + } + } + else if (item.depth > 0) { + /* + * The API does not send down any unavailable replies, so this will + * always be false (for now). If we ever wanted to tombstone them here, + * we could. + */ + var shouldBreak = AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value) || + AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) || + AppBskyUnspeccedDefs.isThreadItemBlocked(item.value); + if (shouldBreak) { + var branch = getBranch(thread, i, item.depth); + // could insert tombstone + i = branch.end; + continue traversal; + } + else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (parentMetadata) { + /* + * Set this value before incrementing the `repliesSeenCounter` later + * on, since `repliesSeenCounter` is 1-indexed and `replyIndex` is + * 0-indexed. + */ + metadata.replyIndex = parentMetadata.repliesSeenCounter; + } + var post = views.threadPost({ + uri: item.uri, + depth: item.depth, + value: item.value, + moderationOpts: moderationOpts, + threadgateHiddenReplies: threadgateHiddenReplies, + }); + if (!post.isBlurred || skipModerationHandling) { + /* + * Not moderated, need to insert it + */ + threadItems.push(post); + /* + * Update seen reply count of parent + */ + if (parentMetadata) { + parentMetadata.repliesSeenCounter += 1; + } + } + else { + /* + * Moderated in some way, we're going to walk children + */ + var parent_2 = post; + var parentIsTopLevelReply = parent_2.depth === 1; + // get sub tree + var branch = getBranch(thread, i, item.depth); + if (parentIsTopLevelReply) { + // push branch anchor into sorted array + otherThreadItems.push(parent_2); + // skip branch anchor in branch traversal + var startIndex = branch.start + 1; + for (var ci = startIndex; ci <= branch.end; ci++) { + var child = thread[ci]; + if (AppBskyUnspeccedDefs.isThreadItemPost(child.value)) { + var childParentMetadata = metadatas.get(((_f = (_e = getPostRecord(child.value.post).reply) === null || _e === void 0 ? void 0 : _e.parent) === null || _f === void 0 ? void 0 : _f.uri) || ''); + var childMetadata = getTraversalMetadata({ + item: child, + prevItem: thread[ci - 1], + nextItem: thread[ci + 1], + parentMetadata: childParentMetadata, + }); + storeTraversalMetadata(metadatas, childMetadata); + if (childParentMetadata) { + /* + * Set this value before incrementing the + * `repliesSeenCounter` later on, since `repliesSeenCounter` + * is 1-indexed and `replyIndex` is 0-indexed. + */ + childMetadata.replyIndex = + childParentMetadata.repliesSeenCounter; + } + var childPost = views.threadPost({ + uri: child.uri, + depth: child.depth, + value: child.value, + moderationOpts: moderationOpts, + threadgateHiddenReplies: threadgateHiddenReplies, + }); + /* + * If a child is moderated in any way, drop it an its sub-branch + * entirely. To reveal these, the user must navigate to the + * parent post directly. + */ + if (childPost.isBlurred) { + ci = getBranch(thread, ci, child.depth).end; + } + else { + otherThreadItems.push(childPost); + if (childParentMetadata) { + childParentMetadata.repliesSeenCounter += 1; + } + } + } + else { + /* + * Drop the rest of the branch if we hit anything unexpected + */ + break; + } + } + } + /* + * Skip to next branch + */ + i = branch.end; + continue traversal; + } + } + } + } + /* + * Both `threadItems` and `otherThreadItems` now need to be traversed again to fully compute + * UI state based on collected metadata. These arrays will be muted in situ. + */ + for (var _i = 0, _g = [threadItems, otherThreadItems]; _i < _g.length; _i++) { + var subset = _g[_i]; + for (var i = 0; i < subset.length; i++) { + var item = subset[i]; + var prevItem = subset.at(i - 1); + var nextItem = subset.at(i + 1); + if (item.type === 'threadPost') { + var metadata = metadatas.get(item.uri); + if (metadata) { + if (metadata.parentMetadata) { + /* + * Track what's before/after now that we've applied moderation + */ + if ((prevItem === null || prevItem === void 0 ? void 0 : prevItem.type) === 'threadPost') + metadata.prevItemDepth = prevItem === null || prevItem === void 0 ? void 0 : prevItem.depth; + if ((nextItem === null || nextItem === void 0 ? void 0 : nextItem.type) === 'threadPost') + metadata.nextItemDepth = nextItem === null || nextItem === void 0 ? void 0 : nextItem.depth; + /** + * Item is also the last "sibling" if its index matches the total + * number of replies we're actually able to render to the page. + */ + var isLastSiblingDueToMissingReplies = metadata.replyIndex === + metadata.parentMetadata.repliesSeenCounter - 1; + /* + * Item can also be the last "sibling" if we know we don't have a + * next item, OR if that next item's depth is less than this item's + * depth (meaning it's a sibling of the parent, not a child of this + * item). + */ + var isImplicitlyLastSibling = metadata.nextItemDepth === undefined || + metadata.nextItemDepth < metadata.depth; + /* + * Ok now we can set the last sibling state. + */ + metadata.isLastSibling = + isImplicitlyLastSibling || isLastSiblingDueToMissingReplies; + /* + * Item is the last "child" in a branch if there is no next item, + * or if the next item's depth is less than this item's depth (a + * sibling of the parent) or equal to this item's depth (a sibling + * of this item) + */ + metadata.isLastChild = + metadata.nextItemDepth === undefined || + metadata.nextItemDepth <= metadata.depth; + /* + * If this is the last sibling, it's implicitly part of the last + * branch of this sub-tree. + */ + if (metadata.isLastSibling) { + metadata.isPartOfLastBranchFromDepth = metadata.depth; + /** + * If the parent is part of the last branch of the sub-tree, so + * is the child. However, if the child is also a last sibling, + * then we need to start tracking `isPartOfLastBranchFromDepth` + * from this point onwards, always updating it to the depth of + * the last sibling as we go down. + */ + if (!metadata.isLastSibling && + metadata.parentMetadata.isPartOfLastBranchFromDepth) { + metadata.isPartOfLastBranchFromDepth = + metadata.parentMetadata.isPartOfLastBranchFromDepth; + } + } + /* + * If this is the last sibling, and the parent has unhydrated replies, + * at some point down the line we will need to show a "read more". + */ + if (metadata.parentMetadata.repliesUnhydrated > 0 && + metadata.isLastSibling) { + metadata.upcomingParentReadMore = metadata.parentMetadata; + } + /* + * Copy in the parent's upcoming read more, if it exists. Once we + * reach the bottom, we'll insert a "read more" + */ + if (metadata.parentMetadata.upcomingParentReadMore) { + metadata.upcomingParentReadMore = + metadata.parentMetadata.upcomingParentReadMore; + } + /* + * Copy in the parent's skipped indents + */ + metadata.skippedIndentIndices = new Set(__spreadArray([], metadata.parentMetadata.skippedIndentIndices, true)); + /** + * If this is the last sibling, and the parent has no unhydrated + * replies, then we know we can skip an indent line. + */ + if (metadata.parentMetadata.repliesUnhydrated <= 0 && + metadata.isLastSibling) { + /** + * Depth is 2 more than the 0-index of the indent calculation + * bc of how we render these. So instead of handling that in the + * component, we just adjust that back to 0-index here. + */ + metadata.skippedIndentIndices.add(item.depth - 2); + } + } + /* + * If this post has unhydrated replies, and it is the last child, then + * it itself needs a "read more" + */ + if (metadata.repliesUnhydrated > 0 && metadata.isLastChild) { + metadata.precedesChildReadMore = true; + subset.splice(i + 1, 0, views.readMore(metadata)); + i++; // skip next iteration + } + /* + * Tree-view only. + * + * If there's an upcoming parent read more, this branch is part of a + * branch of the sub-tree that is deeper than the + * `upcomingParentReadMore`, and the item following the current item + * is either undefined or less-or-equal-to the depth of the + * `upcomingParentReadMore`, then we know it's time to drop in the + * parent read more. + */ + if (view === 'tree' && + metadata.upcomingParentReadMore && + metadata.isPartOfLastBranchFromDepth && + metadata.isPartOfLastBranchFromDepth >= + metadata.upcomingParentReadMore.depth && + (metadata.nextItemDepth === undefined || + metadata.nextItemDepth <= metadata.upcomingParentReadMore.depth)) { + subset.splice(i + 1, 0, views.readMore(metadata.upcomingParentReadMore)); + i++; + } + /** + * Only occurs for the first item in the thread, which may have + * additional parents not included in this request. + */ + if (item.value.moreParents) { + metadata.followsReadMoreUp = true; + subset.splice(i, 0, views.readMoreUp(metadata)); + i++; + } + /* + * Calculate the final UI state for the thread item. + */ + item.ui = getThreadPostUI(metadata); + } + } + } + } + return { + threadItems: threadItems, + otherThreadItems: otherThreadItems, + }; +} +export function buildThread(_a) { + var _b, _c, _d; + var threadItems = _a.threadItems, otherThreadItems = _a.otherThreadItems, serverOtherThreadItems = _a.serverOtherThreadItems, isLoading = _a.isLoading, hasSession = _a.hasSession, otherItemsVisible = _a.otherItemsVisible, hasOtherThreadItems = _a.hasOtherThreadItems, showOtherItems = _a.showOtherItems; + /** + * `threadItems` is memoized here, so don't mutate it directly. + */ + var items = __spreadArray([], threadItems, true); + if (isLoading) { + var anchorPost = items.at(0); + var hasAnchorFromCache = anchorPost && anchorPost.type === 'threadPost'; + var skeletonReplies = hasAnchorFromCache + ? ((_b = anchorPost.value.post.replyCount) !== null && _b !== void 0 ? _b : 4) + : 4; + if (!items.length) { + items.push(views.skeleton({ + key: 'anchor-skeleton', + item: 'anchor', + })); + } + if (hasSession) { + // we might have this from cache + var replyDisabled = hasAnchorFromCache && + ((_c = anchorPost.value.post.viewer) === null || _c === void 0 ? void 0 : _c.replyDisabled) === true; + if (hasAnchorFromCache) { + if (!replyDisabled) { + items.push({ + type: 'replyComposer', + key: 'replyComposer', + }); + } + } + else { + items.push(views.skeleton({ + key: 'replyComposer', + item: 'replyComposer', + })); + } + } + for (var i = 0; i < skeletonReplies; i++) { + items.push(views.skeleton({ + key: "anchor-skeleton-reply-".concat(i), + item: 'reply', + })); + } + } + else { + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.type === 'threadPost' && + item.depth === 0 && + !((_d = item.value.post.viewer) === null || _d === void 0 ? void 0 : _d.replyDisabled) && + hasSession) { + items.splice(i + 1, 0, { + type: 'replyComposer', + key: 'replyComposer', + }); + break; + } + } + if (otherThreadItems.length || hasOtherThreadItems) { + if (otherItemsVisible) { + items.push.apply(items, otherThreadItems); + items.push.apply(items, serverOtherThreadItems); + } + else { + items.push({ + type: 'showOtherReplies', + key: 'showOtherReplies', + onPress: showOtherItems, + }); + } + } + } + return items; +} +/** + * Get the start and end index of a "branch" of the thread. A "branch" is a + * parent and it's children (not siblings). Returned indices are inclusive of + * the parent and its last child. + * + * items[] (index, depth) + * └─┬ anchor ──────── (0, 0) + * ├─── branch ───── (1, 1) + * ├──┬ branch ───── (2, 1) (start) + * │ ├──┬ leaf ──── (3, 2) + * │ │ └── leaf ── (4, 3) + * │ └─── leaf ──── (5, 2) (end) + * ├─── branch ───── (6, 1) + * └─── branch ───── (7, 1) + * + * const { start: 2, end: 5, length: 3 } = getBranch(items, 2, 1) + */ +export function getBranch(thread, branchStartIndex, branchStartDepth) { + var end = branchStartIndex; + for (var ci = branchStartIndex + 1; ci < thread.length; ci++) { + var next = thread[ci]; + if (next.depth > branchStartDepth) { + end = ci; + } + else { + end = ci - 1; + break; + } + } + return { + start: branchStartIndex, + end: end, + length: end - branchStartIndex, + }; +} diff --git a/src/state/queries/usePostThread/types.js b/src/state/queries/usePostThread/types.js new file mode 100644 index 0000000000..0c10395231 --- /dev/null +++ b/src/state/queries/usePostThread/types.js @@ -0,0 +1,5 @@ +export var postThreadQueryKeyRoot = 'post-thread-v2'; +export var createPostThreadQueryKey = function (props) { + return [postThreadQueryKeyRoot, props]; +}; +export var createPostThreadOtherQueryKey = function (props) { return [postThreadQueryKeyRoot, 'other', props]; }; diff --git a/src/state/queries/usePostThread/utils.js b/src/state/queries/usePostThread/utils.js new file mode 100644 index 0000000000..3349300ac8 --- /dev/null +++ b/src/state/queries/usePostThread/utils.js @@ -0,0 +1,114 @@ +import { AppBskyFeedPost, AppBskyFeedThreadgate, AppBskyUnspeccedDefs, AtUri, } from '@atproto/api'; +import { isDevMode } from '#/storage/hooks/dev-mode'; +import * as bsky from '#/types/bsky'; +export function getThreadgateRecord(view) { + return bsky.dangerousIsType(view === null || view === void 0 ? void 0 : view.record, AppBskyFeedThreadgate.isRecord) + ? view === null || view === void 0 ? void 0 : view.record + : undefined; +} +export function getRootPostAtUri(post) { + var _a, _b; + if (bsky.dangerousIsType(post.record, AppBskyFeedPost.isRecord)) { + /** + * If the record has no `reply` field, it is a root post. + */ + if (!post.record.reply) { + return new AtUri(post.uri); + } + if ((_b = (_a = post.record.reply) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.uri) { + return new AtUri(post.record.reply.root.uri); + } + } +} +export function getPostRecord(post) { + return post.record; +} +export function getTraversalMetadata(_a) { + var item = _a.item, prevItem = _a.prevItem, nextItem = _a.nextItem, parentMetadata = _a.parentMetadata; + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + throw new Error("Expected thread item to be a post"); + } + var repliesCount = item.value.post.replyCount || 0; + var repliesUnhydrated = item.value.moreReplies || 0; + var metadata = { + depth: item.depth, + /* + * Unknown until after traversal + */ + isLastChild: false, + /* + * Unknown until after traversal + */ + isLastSibling: false, + /* + * If it's a top level reply, bc we render each top-level branch as a + * separate tree, it's implicitly part of the last branch. For subsequent + * replies, we'll override this after traversal. + */ + isPartOfLastBranchFromDepth: item.depth === 1 ? 1 : undefined, + nextItemDepth: nextItem === null || nextItem === void 0 ? void 0 : nextItem.depth, + parentMetadata: parentMetadata, + prevItemDepth: prevItem === null || prevItem === void 0 ? void 0 : prevItem.depth, + /* + * Unknown until after traversal + */ + precedesChildReadMore: false, + /* + * Unknown until after traversal + */ + followsReadMoreUp: false, + postData: { + uri: item.uri, + authorHandle: item.value.post.author.handle, + }, + repliesCount: repliesCount, + repliesUnhydrated: repliesUnhydrated, + repliesSeenCounter: 0, + replyIndex: 0, + skippedIndentIndices: new Set(), + }; + if (isDevMode()) { + // @ts-ignore dev only for debugging + metadata.postData.text = getPostRecord(item.value.post).text; + } + return metadata; +} +export function storeTraversalMetadata(metadatas, metadata) { + metadatas.set(metadata.postData.uri, metadata); + if (isDevMode()) { + // @ts-ignore dev only for debugging + metadatas.set(metadata.postData.text, metadata); + // @ts-ignore + window.__thread = metadatas; + } +} +export function getThreadPostUI(_a) { + var depth = _a.depth, repliesCount = _a.repliesCount, prevItemDepth = _a.prevItemDepth, isLastChild = _a.isLastChild, skippedIndentIndices = _a.skippedIndentIndices, repliesSeenCounter = _a.repliesSeenCounter, repliesUnhydrated = _a.repliesUnhydrated, precedesChildReadMore = _a.precedesChildReadMore, followsReadMoreUp = _a.followsReadMoreUp; + var isReplyAndHasReplies = depth > 0 && + repliesCount > 0 && + (repliesCount - repliesUnhydrated === repliesSeenCounter || + repliesSeenCounter > 0); + return { + isAnchor: depth === 0, + showParentReplyLine: followsReadMoreUp || + (!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth), + showChildReplyLine: depth < 0 || isReplyAndHasReplies, + indent: depth, + /* + * If there are no slices below this one, or the next slice has a depth <= + * than the depth of this post, it's the last child of the reply tree. It + * is not necessarily the last leaf in the parent branch, since it could + * have another sibling. + */ + isLastChild: isLastChild, + skippedIndentIndices: skippedIndentIndices, + precedesChildReadMore: precedesChildReadMore !== null && precedesChildReadMore !== void 0 ? precedesChildReadMore : false, + }; +} +export function getThreadPostNoUnauthenticatedUI(_a) { + var depth = _a.depth, prevItemDepth = _a.prevItemDepth; + return { + showChildReplyLine: depth < 0, + showParentReplyLine: Boolean(prevItemDepth && prevItemDepth < depth), + }; +} diff --git a/src/state/queries/usePostThread/views.js b/src/state/queries/usePostThread/views.js new file mode 100644 index 0000000000..17eecf42d3 --- /dev/null +++ b/src/state/queries/usePostThread/views.js @@ -0,0 +1,125 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { AtUri, moderatePost, } from '@atproto/api'; +import { makeProfileLink } from '#/lib/routes/links'; +export function threadPostNoUnauthenticated(_a) { + var uri = _a.uri, depth = _a.depth, value = _a.value; + return { + type: 'threadPostNoUnauthenticated', + key: uri, + uri: uri, + depth: depth, + value: value, + // @ts-ignore populated by the traversal + ui: {}, + }; +} +export function threadPostNotFound(_a) { + var uri = _a.uri, depth = _a.depth, value = _a.value; + return { + type: 'threadPostNotFound', + key: uri, + uri: uri, + depth: depth, + value: value, + }; +} +export function threadPostBlocked(_a) { + var uri = _a.uri, depth = _a.depth, value = _a.value; + return { + type: 'threadPostBlocked', + key: uri, + uri: uri, + depth: depth, + value: value, + }; +} +export function threadPost(_a) { + var _b; + var uri = _a.uri, depth = _a.depth, value = _a.value, moderationOpts = _a.moderationOpts, threadgateHiddenReplies = _a.threadgateHiddenReplies; + var moderation = moderatePost(value.post, moderationOpts); + var modui = moderation.ui('contentList'); + var blurred = modui.blur || modui.filter; + var muted = ((_b = (modui.blurs[0] || modui.filters[0])) === null || _b === void 0 ? void 0 : _b.type) === 'muted'; + var hiddenByThreadgate = threadgateHiddenReplies.has(uri); + var isOwnPost = value.post.author.did === moderationOpts.userDid; + var isBlurred = (hiddenByThreadgate || blurred || muted) && !isOwnPost; + return { + type: 'threadPost', + key: uri, + uri: uri, + depth: depth, + value: __assign(__assign({}, value), { + /* + * Do not spread anything here, load bearing for post shadow strict + * equality reference checks. + */ + post: value.post }), + isBlurred: isBlurred, + moderation: moderation, + // @ts-ignore populated by the traversal + ui: {}, + }; +} +export function readMore(_a) { + var depth = _a.depth, repliesUnhydrated = _a.repliesUnhydrated, skippedIndentIndices = _a.skippedIndentIndices, postData = _a.postData; + var urip = new AtUri(postData.uri); + var href = makeProfileLink({ + did: urip.host, + handle: postData.authorHandle, + }, 'post', urip.rkey); + return { + type: 'readMore', + key: "readMore:".concat(postData.uri), + href: href, + moreReplies: repliesUnhydrated, + depth: depth, + skippedIndentIndices: skippedIndentIndices, + }; +} +export function readMoreUp(_a) { + var postData = _a.postData; + var urip = new AtUri(postData.uri); + var href = makeProfileLink({ + did: urip.host, + handle: postData.authorHandle, + }, 'post', urip.rkey); + return { + type: 'readMoreUp', + key: "readMoreUp:".concat(postData.uri), + href: href, + }; +} +export function skeleton(_a) { + var key = _a.key, item = _a.item; + return { + type: 'skeleton', + key: key, + item: item, + }; +} +export function postViewToThreadPlaceholder(post) { + return { + $type: 'app.bsky.unspecced.getPostThreadV2#threadItem', + uri: post.uri, + depth: 0, // reset to 0 for highlighted post + value: { + $type: 'app.bsky.unspecced.defs#threadItemPost', + post: post, + opThread: false, + moreParents: false, + moreReplies: 0, + hiddenByThreadgate: false, + mutedByViewer: false, + }, + }; +} diff --git a/src/state/queries/useSuggestedStarterPacksQuery.js b/src/state/queries/useSuggestedStarterPacksQuery.js new file mode 100644 index 0000000000..c7801970c4 --- /dev/null +++ b/src/state/queries/useSuggestedStarterPacksQuery.js @@ -0,0 +1,84 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useQuery } from '@tanstack/react-query'; +import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils'; +import { getContentLanguages } from '#/state/preferences/languages'; +import { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var createSuggestedStarterPacksQueryKey = function (interests) { return [ + 'suggested-starter-packs', + interests === null || interests === void 0 ? void 0 : interests.join(','), +]; }; +export function useSuggestedStarterPacksQuery(_a) { + var _this = this; + var enabled = _a.enabled, overrideInterests = _a.overrideInterests; + var agent = useAgent(); + var preferences = usePreferencesQuery().data; + var contentLangs = getContentLanguages().join(','); + return useQuery({ + enabled: !!preferences && enabled !== false, + staleTime: STALE.MINUTES.THREE, + queryKey: createSuggestedStarterPacksQueryKey(overrideInterests), + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.unspecced.getSuggestedStarterPacks(undefined, { + headers: __assign(__assign({}, createBskyTopicsHeader(overrideInterests + ? overrideInterests.join(',') + : aggregateUserInterests(preferences))), { 'Accept-Language': contentLangs }), + })]; + case 1: + data = (_a.sent()).data; + return [2 /*return*/, data]; + } + }); + }); }, + }); +} diff --git a/src/state/queries/util.js b/src/state/queries/util.js new file mode 100644 index 0000000000..1b55ba4488 --- /dev/null +++ b/src/state/queries/util.js @@ -0,0 +1,96 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedPost, } from '@atproto/api'; +import * as bsky from '#/types/bsky'; +export function truncateAndInvalidate(queryClient, queryKey) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + queryClient.setQueriesData({ queryKey: queryKey }, function (data) { + if (data) { + return { + pageParams: data.pageParams.slice(0, 1), + pages: data.pages.slice(0, 1), + }; + } + return data; + }); + return [2 /*return*/, queryClient.invalidateQueries({ queryKey: queryKey })]; + }); + }); +} +// Given an AtUri, this function will check if the AtUri matches a +// hit regardless of whether the AtUri uses a DID or handle as a host. +// +// AtUri should be the URI that is being searched for, while currentUri +// is the URI that is being checked. currentAuthor is the author +// of the currentUri that is being checked. +export function didOrHandleUriMatches(atUri, record) { + if (atUri.host.startsWith('did:')) { + return atUri.href === record.uri; + } + return atUri.host === record.author.handle && record.uri.endsWith(atUri.rkey); +} +export function getEmbeddedPost(v) { + if (bsky.dangerousIsType(v, AppBskyEmbedRecord.isView)) { + if (AppBskyEmbedRecord.isViewRecord(v.record) && + AppBskyFeedPost.isRecord(v.record.value)) { + return v.record; + } + } + if (bsky.dangerousIsType(v, AppBskyEmbedRecordWithMedia.isView)) { + if (AppBskyEmbedRecord.isViewRecord(v.record.record) && + AppBskyFeedPost.isRecord(v.record.record.value)) { + return v.record.record; + } + } +} +export function embedViewRecordToPostView(v) { + var _a; + return { + uri: v.uri, + cid: v.cid, + author: v.author, + record: v.value, + indexedAt: v.indexedAt, + labels: v.labels, + embed: (_a = v.embeds) === null || _a === void 0 ? void 0 : _a[0], + likeCount: v.likeCount, + quoteCount: v.quoteCount, + replyCount: v.replyCount, + repostCount: v.repostCount, + }; +} diff --git a/src/state/queries/verification/useUpdateProfileVerificationCache.js b/src/state/queries/verification/useUpdateProfileVerificationCache.js new file mode 100644 index 0000000000..9b78f8cb1f --- /dev/null +++ b/src/state/queries/verification/useUpdateProfileVerificationCache.js @@ -0,0 +1,78 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { logger } from '#/logger'; +import { updateProfileShadow } from '#/state/cache/profile-shadow'; +import { useAgent } from '#/state/session'; +/** + * Fetches a fresh verification state from the app view and updates our profile + * cache. This state is computed using a variety of factors on the server, so + * we need to get this data from the server. + */ +export function useUpdateProfileVerificationCache() { + var _this = this; + var qc = useQueryClient(); + var agent = useAgent(); + return useCallback(function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var updated, e_1; + var _c; + var profile = _b.profile; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + _d.trys.push([0, 2, , 3]); + return [4 /*yield*/, agent.getProfile({ + actor: (_c = profile.did) !== null && _c !== void 0 ? _c : '', + })]; + case 1: + updated = (_d.sent()).data; + updateProfileShadow(qc, profile.did, { + verification: updated.verification, + }); + return [3 /*break*/, 3]; + case 2: + e_1 = _d.sent(); + logger.error("useUpdateProfileVerificationCache failed", { + safeMessage: e_1, + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }, [agent, qc]); +} diff --git a/src/state/queries/verification/useVerificationCreateMutation.js b/src/state/queries/verification/useVerificationCreateMutation.js new file mode 100644 index 0000000000..9e97630d25 --- /dev/null +++ b/src/state/queries/verification/useVerificationCreateMutation.js @@ -0,0 +1,100 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { useMutation } from '@tanstack/react-query'; +import { until } from '#/lib/async/until'; +import { useUpdateProfileVerificationCache } from '#/state/queries/verification/useUpdateProfileVerificationCache'; +import { useAgent, useSession } from '#/state/session'; +import { useAnalytics } from '#/analytics'; +export function useVerificationCreateMutation() { + var ax = useAnalytics(); + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + var updateProfileVerificationCache = useUpdateProfileVerificationCache(); + return useMutation({ + mutationFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var uri; + var profile = _b.profile; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) { + throw new Error('User not logged in'); + } + return [4 /*yield*/, agent.app.bsky.graph.verification.create({ repo: currentAccount.did }, { + subject: profile.did, + createdAt: new Date().toISOString(), + handle: profile.handle, + displayName: profile.displayName || '', + })]; + case 1: + uri = (_c.sent()).uri; + return [4 /*yield*/, until(5, 1e3, function (_a) { + var profile = _a.data; + if (profile.verification && + profile.verification.verifications.find(function (v) { return v.uri === uri; })) { + return true; + } + return false; + }, function () { + var _a; + return agent.getProfile({ actor: (_a = profile.did) !== null && _a !== void 0 ? _a : '' }); + })]; + case 2: + _c.sent(); + return [2 /*return*/]; + } + }); + }); + }, + onSuccess: function (_1, _a) { + return __awaiter(this, arguments, void 0, function (_, _b) { + var profile = _b.profile; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + ax.metric('verification:create', {}); + return [4 /*yield*/, updateProfileVerificationCache({ profile: profile })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); + }, + }); +} diff --git a/src/state/queries/verification/useVerificationsRemoveMutation.js b/src/state/queries/verification/useVerificationsRemoveMutation.js new file mode 100644 index 0000000000..6ba73edb23 --- /dev/null +++ b/src/state/queries/verification/useVerificationsRemoveMutation.js @@ -0,0 +1,102 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { AtUri, } from '@atproto/api'; +import { useMutation } from '@tanstack/react-query'; +import { until } from '#/lib/async/until'; +import { useUpdateProfileVerificationCache } from '#/state/queries/verification/useUpdateProfileVerificationCache'; +import { useAgent, useSession } from '#/state/session'; +import { useAnalytics } from '#/analytics'; +export function useVerificationsRemoveMutation() { + var ax = useAnalytics(); + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + var updateProfileVerificationCache = useUpdateProfileVerificationCache(); + return useMutation({ + mutationFn: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var uris; + var profile = _b.profile, verifications = _b.verifications; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!currentAccount) { + throw new Error('User not logged in'); + } + uris = verifications.map(function (v) { return v.uri; }); + return [4 /*yield*/, Promise.all(uris.map(function (uri) { + return agent.app.bsky.graph.verification.delete({ + repo: currentAccount.did, + rkey: new AtUri(uri).rkey, + }); + }))]; + case 1: + _c.sent(); + return [4 /*yield*/, until(5, 1e3, function (_a) { + var _b; + var profile = _a.data; + if (!((_b = profile.verification) === null || _b === void 0 ? void 0 : _b.verifications.some(function (v) { return uris.includes(v.uri); }))) { + return true; + } + return false; + }, function () { + var _a; + return agent.getProfile({ actor: (_a = profile.did) !== null && _a !== void 0 ? _a : '' }); + })]; + case 2: + _c.sent(); + return [2 /*return*/]; + } + }); + }); + }, + onSuccess: function (_1, _a) { + return __awaiter(this, arguments, void 0, function (_, _b) { + var profile = _b.profile; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + ax.metric('verification:revoke', {}); + return [4 /*yield*/, updateProfileVerificationCache({ profile: profile })]; + case 1: + _c.sent(); + return [2 /*return*/]; + } + }); + }); + }, + }); +} diff --git a/src/state/service-config.js b/src/state/service-config.js new file mode 100644 index 0000000000..a5af70e895 --- /dev/null +++ b/src/state/service-config.js @@ -0,0 +1,53 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { useLanguagePrefs } from '#/state/preferences/languages'; +import { useServiceConfigQuery } from '#/state/queries/service-config'; +import { device } from '#/storage'; +var TrendingContext = createContext({ + enabled: false, +}); +TrendingContext.displayName = 'TrendingContext'; +var CheckEmailConfirmedContext = createContext(null); +export function Provider(_a) { + var _b; + var children = _a.children; + var langPrefs = useLanguagePrefs(); + var _c = useServiceConfigQuery(), config = _c.data, isInitialLoad = _c.isLoading; + var trending = useMemo(function () { + if (__DEV__) { + return { enabled: true }; + } + /* + * Only English during beta period + */ + if (!!langPrefs.contentLanguages.length && + !langPrefs.contentLanguages.includes('en')) { + return { enabled: false }; + } + /* + * While loading, use cached value + */ + var cachedEnabled = device.get(['trendingBetaEnabled']); + if (isInitialLoad) { + return { enabled: Boolean(cachedEnabled) }; + } + var enabled = Boolean(config === null || config === void 0 ? void 0 : config.topicsEnabled); + // update cache + device.set(['trendingBetaEnabled'], enabled); + return { enabled: enabled }; + }, [isInitialLoad, config, langPrefs.contentLanguages]); + // probably true, so default to true when loading + // if the call fails, the query will set it to false for us + var checkEmailConfirmed = (_b = config === null || config === void 0 ? void 0 : config.checkEmailConfirmed) !== null && _b !== void 0 ? _b : true; + return (_jsx(TrendingContext.Provider, { value: trending, children: _jsx(CheckEmailConfirmedContext.Provider, { value: checkEmailConfirmed, children: children }) })); +} +export function useTrendingConfig() { + return useContext(TrendingContext); +} +export function useCheckEmailConfirmed() { + var ctx = useContext(CheckEmailConfirmedContext); + if (ctx === null) { + throw new Error('useCheckEmailConfirmed must be used within a ServiceConfigManager'); + } + return ctx; +} diff --git a/src/state/session/__tests__/session-test.js b/src/state/session/__tests__/session-test.js new file mode 100644 index 0000000000..747edd3e45 --- /dev/null +++ b/src/state/session/__tests__/session-test.js @@ -0,0 +1,894 @@ +import { BskyAgent } from '@atproto/api'; +import { describe, expect, it, jest } from '@jest/globals'; +import { agentToSessionAccountOrThrow } from '../agent'; +import { getInitialState, reducer } from '../reducer'; +jest.mock('jwt-decode', function () { return ({ + jwtDecode: function (_token) { + return {}; + }, +}); }); +jest.mock('../../birthdate'); +jest.mock('../../../ageAssurance/data'); +jest.mock('#/lib/notifications/notifications', function () { return ({ + unregisterPushToken: function (_agents) { + return Promise.resolve(); + }, +}); }); +describe('session', function () { + it('can log in and out', function () { + var state = getInitialState([]); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": false,\n }\n "); + var agent = new BskyAgent({ service: 'https://alice.com' }); + agent.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent, + newAccount: agentToSessionAccountOrThrow(agent), + }, + ]); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].did).toBe('alice-did'); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-1\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-1\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + state = run(state, [ + { + type: 'logged-out-every-account', + }, + ]); + // Should keep the account but clear out the tokens. + expect(state.currentAgentState.did).toBe(undefined); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].did).toBe('alice-did'); + expect(state.accounts[0].accessJwt).toBe(undefined); + expect(state.accounts[0].refreshJwt).toBe(undefined); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": true,\n }\n "); + }); + it('switches to the latest account, stores all of them', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + // Switch to Alice. + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].did).toBe('alice-did'); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(state.currentAgentState.agent).toBe(agent1); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-1\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-1\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + var agent2 = new BskyAgent({ service: 'https://bob.com' }); + agent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }; + state = run(state, [ + { + // Switch to Bob. + type: 'switched-to-account', + newAgent: agent2, + newAccount: agentToSessionAccountOrThrow(agent2), + }, + ]); + expect(state.accounts.length).toBe(2); + // Bob should float upwards. + expect(state.accounts[0].did).toBe('bob-did'); + expect(state.accounts[1].did).toBe('alice-did'); + expect(state.currentAgentState.did).toBe('bob-did'); + expect(state.currentAgentState.agent).toBe(agent2); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"bob-access-jwt-1\",\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"bob-refresh-jwt-1\",\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"alice-access-jwt-1\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-1\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://bob.com/\",\n },\n \"did\": \"bob-did\",\n },\n \"needsPersist\": true,\n }\n "); + var agent3 = new BskyAgent({ service: 'https://alice.com' }); + agent3.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }; + state = run(state, [ + { + // Switch back to Alice. + type: 'switched-to-account', + newAgent: agent3, + newAccount: agentToSessionAccountOrThrow(agent3), + }, + ]); + expect(state.accounts.length).toBe(2); + // Alice should float upwards. + expect(state.accounts[0].did).toBe('alice-did'); + expect(state.accounts[0].handle).toBe('alice-updated.test'); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(state.currentAgentState.agent).toBe(agent3); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-2\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-2\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"bob-access-jwt-1\",\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"bob-refresh-jwt-1\",\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + var agent4 = new BskyAgent({ service: 'https://jay.com' }); + agent4.sessionManager.session = { + active: true, + did: 'jay-did', + handle: 'jay.test', + accessJwt: 'jay-access-jwt-1', + refreshJwt: 'jay-refresh-jwt-1', + }; + state = run(state, [ + { + // Switch to Jay. + type: 'switched-to-account', + newAgent: agent4, + newAccount: agentToSessionAccountOrThrow(agent4), + }, + ]); + expect(state.accounts.length).toBe(3); + expect(state.accounts[0].did).toBe('jay-did'); + expect(state.currentAgentState.did).toBe('jay-did'); + expect(state.currentAgentState.agent).toBe(agent4); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"jay-access-jwt-1\",\n \"active\": true,\n \"did\": \"jay-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"jay.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"jay-refresh-jwt-1\",\n \"service\": \"https://jay.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"alice-access-jwt-2\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-2\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"bob-access-jwt-1\",\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"bob-refresh-jwt-1\",\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://jay.com/\",\n },\n \"did\": \"jay-did\",\n },\n \"needsPersist\": true,\n }\n "); + state = run(state, [ + { + // Log everyone out. + type: 'logged-out-every-account', + }, + ]); + expect(state.accounts.length).toBe(3); + expect(state.currentAgentState.did).toBe(undefined); + // All tokens should be gone. + expect(state.accounts[0].accessJwt).toBe(undefined); + expect(state.accounts[0].refreshJwt).toBe(undefined); + expect(state.accounts[1].accessJwt).toBe(undefined); + expect(state.accounts[1].refreshJwt).toBe(undefined); + expect(state.accounts[2].accessJwt).toBe(undefined); + expect(state.accounts[2].refreshJwt).toBe(undefined); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"jay-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"jay.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://jay.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": true,\n }\n "); + }); + it('can log back in after logging out', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1'); + expect(state.currentAgentState.did).toBe('alice-did'); + state = run(state, [ + { + type: 'logged-out-every-account', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe(undefined); + expect(state.accounts[0].refreshJwt).toBe(undefined); + expect(state.currentAgentState.did).toBe(undefined); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": true,\n }\n "); + var agent2 = new BskyAgent({ service: 'https://alice.com' }); + agent2.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent2, + newAccount: agentToSessionAccountOrThrow(agent2), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2'); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-2\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-2\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + }); + it('can remove active account', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1'); + expect(state.currentAgentState.did).toBe('alice-did'); + state = run(state, [ + { + type: 'removed-account', + accountDid: 'alice-did', + }, + ]); + expect(state.accounts.length).toBe(0); + expect(state.currentAgentState.did).toBe(undefined); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": true,\n }\n "); + }); + it('can remove inactive account', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + var agent2 = new BskyAgent({ service: 'https://bob.com' }); + agent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + { + type: 'switched-to-account', + newAgent: agent2, + newAccount: agentToSessionAccountOrThrow(agent2), + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.currentAgentState.did).toBe('bob-did'); + state = run(state, [ + { + type: 'removed-account', + accountDid: 'alice-did', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.currentAgentState.did).toBe('bob-did'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"bob-access-jwt-1\",\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"bob-refresh-jwt-1\",\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://bob.com/\",\n },\n \"did\": \"bob-did\",\n },\n \"needsPersist\": true,\n }\n "); + state = run(state, [ + { + type: 'removed-account', + accountDid: 'bob-did', + }, + ]); + expect(state.accounts.length).toBe(0); + expect(state.currentAgentState.did).toBe(undefined); + }); + it('can log out of the current account', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1'); + expect(state.currentAgentState.did).toBe('alice-did'); + var agent2 = new BskyAgent({ service: 'https://bob.com' }); + agent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent2, + newAccount: agentToSessionAccountOrThrow(agent2), + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-1'); + expect(state.accounts[0].refreshJwt).toBe('bob-refresh-jwt-1'); + expect(state.currentAgentState.did).toBe('bob-did'); + state = run(state, [ + { + type: 'logged-out-current-account', + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.accounts[0].accessJwt).toBe(undefined); + expect(state.accounts[0].refreshJwt).toBe(undefined); + expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-1'); + expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-1'); + expect(state.currentAgentState.did).toBe(undefined); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"alice-access-jwt-1\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-1\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": true,\n }\n "); + }); + it('updates stored account with refreshed tokens', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.currentAgentState.did).toBe('alice-did'); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + email: 'alice@foo.bar', + emailAuthFactor: false, + emailConfirmed: false, + }; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].email).toBe('alice@foo.bar'); + expect(state.accounts[0].handle).toBe('alice-updated.test'); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2'); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-2\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": \"alice@foo.bar\",\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-2\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-3', + refreshJwt: 'alice-refresh-jwt-3', + email: 'alice@foo.baz', + emailAuthFactor: true, + emailConfirmed: true, + }; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].email).toBe('alice@foo.baz'); + expect(state.accounts[0].handle).toBe('alice-updated.test'); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-3'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-3'); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-3\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": \"alice@foo.baz\",\n \"emailAuthFactor\": true,\n \"emailConfirmed\": true,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-3\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-4', + refreshJwt: 'alice-refresh-jwt-4', + email: 'alice@foo.baz', + emailAuthFactor: false, + emailConfirmed: false, + }; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].email).toBe('alice@foo.baz'); + expect(state.accounts[0].handle).toBe('alice-updated.test'); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-4'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-4'); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-4\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": \"alice@foo.baz\",\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-4\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + }); + it('bails out of update on identical objects', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.currentAgentState.did).toBe('alice-did'); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2'); + var lastState = state; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(lastState === state).toBe(true); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-3', + refreshJwt: 'alice-refresh-jwt-3', + }; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-3'); + }); + it('accepts updates from a stale agent', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + var agent2 = new BskyAgent({ service: 'https://bob.com' }); + agent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }; + state = run(state, [ + { + // Switch to Alice. + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + { + // Switch to Bob. + type: 'switched-to-account', + newAgent: agent2, + newAccount: agentToSessionAccountOrThrow(agent2), + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.currentAgentState.did).toBe('bob-did'); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + email: 'alice@foo.bar', + emailAuthFactor: false, + emailConfirmed: false, + }; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.accounts[1].did).toBe('alice-did'); + // Should update Alice's tokens because otherwise they'll be stale. + expect(state.accounts[1].handle).toBe('alice-updated.test'); + expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-2'); + expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-2'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"bob-access-jwt-1\",\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"bob-refresh-jwt-1\",\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"alice-access-jwt-2\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": \"alice@foo.bar\",\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-2\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://bob.com/\",\n },\n \"did\": \"bob-did\",\n },\n \"needsPersist\": true,\n }\n "); + agent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob-updated.test', + accessJwt: 'bob-access-jwt-2', + refreshJwt: 'bob-refresh-jwt-2', + }; + state = run(state, [ + { + // Update Bob. + type: 'received-agent-event', + accountDid: 'bob-did', + agent: agent2, + refreshedAccount: agentToSessionAccountOrThrow(agent2), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.accounts[0].did).toBe('bob-did'); + // Should update Bob's tokens because otherwise they'll be stale. + expect(state.accounts[0].handle).toBe('bob-updated.test'); + expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-2'); + expect(state.accounts[0].refreshJwt).toBe('bob-refresh-jwt-2'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"bob-access-jwt-2\",\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"bob-refresh-jwt-2\",\n \"service\": \"https://bob.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"alice-access-jwt-2\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": \"alice@foo.bar\",\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice-updated.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-2\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://bob.com/\",\n },\n \"did\": \"bob-did\",\n },\n \"needsPersist\": true,\n }\n "); + // Ignore other events for inactive agent. + var lastState = state; + agent1.sessionManager.session = undefined; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: undefined, + sessionEvent: 'network-error', + }, + ]); + expect(lastState === state).toBe(true); + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: undefined, + sessionEvent: 'expired', + }, + ]); + expect(lastState === state).toBe(true); + }); + it('ignores updates from a removed agent', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + var agent2 = new BskyAgent({ service: 'https://bob.com' }); + agent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + { + type: 'switched-to-account', + newAgent: agent2, + newAccount: agentToSessionAccountOrThrow(agent2), + }, + { + type: 'removed-account', + accountDid: 'alice-did', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.currentAgentState.did).toBe('bob-did'); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: agentToSessionAccountOrThrow(agent1), + sessionEvent: 'update', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].did).toBe('bob-did'); + expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-1'); + expect(state.currentAgentState.did).toBe('bob-did'); + }); + it('ignores network errors', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + // Switch to Alice. + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.currentAgentState.did).toBe('alice-did'); + agent1.sessionManager.session = undefined; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: undefined, + sessionEvent: 'network-error', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1'); + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1'); + expect(state.currentAgentState.did).toBe('alice-did'); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"alice-access-jwt-1\",\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"alice-refresh-jwt-1\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://alice.com/\",\n },\n \"did\": \"alice-did\",\n },\n \"needsPersist\": true,\n }\n "); + }); + it('resets tokens on expired event', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1'); + expect(state.currentAgentState.did).toBe('alice-did'); + agent1.sessionManager.session = undefined; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: undefined, + sessionEvent: 'expired', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe(undefined); + expect(state.accounts[0].refreshJwt).toBe(undefined); + expect(state.currentAgentState.did).toBe(undefined); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": true,\n }\n "); + }); + it('resets tokens on created-failed event', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1'); + expect(state.currentAgentState.did).toBe('alice-did'); + agent1.sessionManager.session = undefined; + state = run(state, [ + { + type: 'received-agent-event', + accountDid: 'alice-did', + agent: agent1, + refreshedAccount: undefined, + sessionEvent: 'create-failed', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].accessJwt).toBe(undefined); + expect(state.accounts[0].refreshJwt).toBe(undefined); + expect(state.currentAgentState.did).toBe(undefined); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": undefined,\n \"active\": true,\n \"did\": \"alice-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"alice.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": undefined,\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": true,\n }\n "); + }); + it('replaces local accounts with synced accounts', function () { + var state = getInitialState([]); + var agent1 = new BskyAgent({ service: 'https://alice.com' }); + agent1.sessionManager.session = { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }; + var agent2 = new BskyAgent({ service: 'https://bob.com' }); + agent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }; + state = run(state, [ + { + type: 'switched-to-account', + newAgent: agent1, + newAccount: agentToSessionAccountOrThrow(agent1), + }, + { + type: 'switched-to-account', + newAgent: agent2, + newAccount: agentToSessionAccountOrThrow(agent2), + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.currentAgentState.did).toBe('bob-did'); + var anotherTabAgent1 = new BskyAgent({ service: 'https://jay.com' }); + anotherTabAgent1.sessionManager.session = { + active: true, + did: 'jay-did', + handle: 'jay.test', + accessJwt: 'jay-access-jwt-1', + refreshJwt: 'jay-refresh-jwt-1', + }; + var anotherTabAgent2 = new BskyAgent({ service: 'https://alice.com' }); + anotherTabAgent2.sessionManager.session = { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-2', + refreshJwt: 'bob-refresh-jwt-2', + }; + state = run(state, [ + { + type: 'synced-accounts', + syncedAccounts: [ + agentToSessionAccountOrThrow(anotherTabAgent1), + agentToSessionAccountOrThrow(anotherTabAgent2), + ], + syncedCurrentDid: 'bob-did', + }, + ]); + expect(state.accounts.length).toBe(2); + expect(state.accounts[0].did).toBe('jay-did'); + expect(state.accounts[1].did).toBe('bob-did'); + expect(state.accounts[1].accessJwt).toBe('bob-access-jwt-2'); + // Keep Bob logged in. + // (We patch up agent.session outside the reducer for this to work.) + expect(state.currentAgentState.did).toBe('bob-did'); + expect(state.needsPersist).toBe(false); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"jay-access-jwt-1\",\n \"active\": true,\n \"did\": \"jay-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"jay.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"jay-refresh-jwt-1\",\n \"service\": \"https://jay.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n {\n \"accessJwt\": \"bob-access-jwt-2\",\n \"active\": true,\n \"did\": \"bob-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"bob.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"bob-refresh-jwt-2\",\n \"service\": \"https://alice.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://bob.com/\",\n },\n \"did\": \"bob-did\",\n },\n \"needsPersist\": false,\n }\n "); + var anotherTabAgent3 = new BskyAgent({ service: 'https://clarence.com' }); + anotherTabAgent3.sessionManager.session = { + active: true, + did: 'clarence-did', + handle: 'clarence.test', + accessJwt: 'clarence-access-jwt-2', + refreshJwt: 'clarence-refresh-jwt-2', + }; + state = run(state, [ + { + type: 'synced-accounts', + syncedAccounts: [agentToSessionAccountOrThrow(anotherTabAgent3)], + syncedCurrentDid: 'clarence-did', + }, + ]); + expect(state.accounts.length).toBe(1); + expect(state.accounts[0].did).toBe('clarence-did'); + // Log out because we have no matching user. + // (In practice, we'll resume this session outside the reducer.) + expect(state.currentAgentState.did).toBe(undefined); + expect(state.needsPersist).toBe(false); + expect(printState(state)).toMatchInlineSnapshot("\n {\n \"accounts\": [\n {\n \"accessJwt\": \"clarence-access-jwt-2\",\n \"active\": true,\n \"did\": \"clarence-did\",\n \"email\": undefined,\n \"emailAuthFactor\": false,\n \"emailConfirmed\": false,\n \"handle\": \"clarence.test\",\n \"isSelfHosted\": true,\n \"pdsUrl\": undefined,\n \"refreshJwt\": \"clarence-refresh-jwt-2\",\n \"service\": \"https://clarence.com/\",\n \"signupQueued\": false,\n \"status\": undefined,\n },\n ],\n \"currentAgentState\": {\n \"agent\": {\n \"service\": \"https://public.api.bsky.app/\",\n },\n \"did\": undefined,\n },\n \"needsPersist\": false,\n }\n "); + }); +}); +function run(initialState, actions) { + var state = initialState; + for (var _i = 0, actions_1 = actions; _i < actions_1.length; _i++) { + var action = actions_1[_i]; + state = reducer(state, action); + } + return state; +} +function printState(state) { + return { + accounts: state.accounts, + currentAgentState: { + agent: { service: state.currentAgentState.agent.service }, + did: state.currentAgentState.did, + }, + needsPersist: state.needsPersist, + }; +} diff --git a/src/state/session/additional-moderation-authorities.js b/src/state/session/additional-moderation-authorities.js new file mode 100644 index 0000000000..db540e3dad --- /dev/null +++ b/src/state/session/additional-moderation-authorities.js @@ -0,0 +1,90 @@ +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { BskyAgent } from '@atproto/api'; +import { logger } from '#/logger'; +import { device } from '#/storage'; +export var BR_LABELER = 'did:plc:ekitcvx7uwnauoqy5oest3hm'; // Brazil +export var DE_LABELER = 'did:plc:r55ow3tocux5kafs5dq445fy'; // Germany +export var RU_LABELER = 'did:plc:crm2agcxvvlj6hilnjdc4hox'; // Russia +export var GB_LABELER = 'did:plc:gvkp7euswjjrctjmqwhhfzif'; // United Kingdom +export var AU_LABELER = 'did:plc:dsynw7isrf2eqlhcjx3ffnmt'; // Australia +export var TR_LABELER = 'did:plc:cquoj7aozvmkud2gifeinkda'; // Turkey +export var JP_LABELER = 'did:plc:vhgppeyjwgrr37vm4v6ggd5a'; // Japan +export var ES_LABELER = 'did:plc:zlbbuj5nov4ixhvgl3bj47em'; // Spain +export var PK_LABELER = 'did:plc:zrp6a3tvprrsgawsbswbxu7m'; // Pakistan +export var IN_LABELER = 'did:plc:srr4rdvgzkbx6t7fxqtt6j5t'; // India +/** + * For all EU countries + */ +export var EU_LABELER = 'did:plc:z57lz5dhgz2dkjogoysm3vut'; +var MODERATION_AUTHORITIES = { + BR: [BR_LABELER], // Brazil + RU: [RU_LABELER], // Russia + GB: [GB_LABELER], // United Kingdom + AU: [AU_LABELER], // Australia + TR: [TR_LABELER], // Turkey + JP: [JP_LABELER], // Japan + PK: [PK_LABELER], // Pakistan + IN: [IN_LABELER], // India + // EU countries + AT: [EU_LABELER], // Austria + BE: [EU_LABELER], // Belgium + BG: [EU_LABELER], // Bulgaria + HR: [EU_LABELER], // Croatia + CY: [EU_LABELER], // Cyprus + CZ: [EU_LABELER], // Czech Republic + DK: [EU_LABELER], // Denmark + EE: [EU_LABELER], // Estonia + FI: [EU_LABELER], // Finland + FR: [EU_LABELER], // France + DE: [EU_LABELER, DE_LABELER], // Germany + GR: [EU_LABELER], // Greece + HU: [EU_LABELER], // Hungary + IE: [EU_LABELER], // Ireland + IT: [EU_LABELER], // Italy + LV: [EU_LABELER], // Latvia + LT: [EU_LABELER], // Lithuania + LU: [EU_LABELER], // Luxembourg + MT: [EU_LABELER], // Malta + NL: [EU_LABELER], // Netherlands + PL: [EU_LABELER], // Poland + PT: [EU_LABELER], // Portugal + RO: [EU_LABELER], // Romania + SK: [EU_LABELER], // Slovakia + SI: [EU_LABELER], // Slovenia + ES: [EU_LABELER, ES_LABELER], // Spain + SE: [EU_LABELER], // Sweden +}; +var MODERATION_AUTHORITIES_DIDS = Array.from(new Set(Object.values(MODERATION_AUTHORITIES).flat())); +export function isNonConfigurableModerationAuthority(did) { + return MODERATION_AUTHORITIES_DIDS.includes(did); +} +export function configureAdditionalModerationAuthorities() { + var _a; + var geolocation = device.get(['mergedGeolocation']); + // default to all + var additionalLabelers = MODERATION_AUTHORITIES_DIDS; + if (geolocation === null || geolocation === void 0 ? void 0 : geolocation.countryCode) { + // overwrite with only those necessary + additionalLabelers = (_a = MODERATION_AUTHORITIES[geolocation.countryCode]) !== null && _a !== void 0 ? _a : []; + } + else { + logger.info("no geolocation, cannot apply mod authorities"); + } + if (__DEV__) { + additionalLabelers = []; + } + var appLabelers = Array.from(new Set(__spreadArray(__spreadArray([], BskyAgent.appLabelers, true), additionalLabelers, true))); + logger.info("applying mod authorities", { + additionalLabelers: additionalLabelers, + appLabelers: appLabelers, + }); + BskyAgent.configure({ appLabelers: appLabelers }); +} diff --git a/src/state/session/agent-config.js b/src/state/session/agent-config.js new file mode 100644 index 0000000000..b13be46605 --- /dev/null +++ b/src/state/session/agent-config.js @@ -0,0 +1,63 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import AsyncStorage from '@react-native-async-storage/async-storage'; +var PREFIX = 'agent-labelers'; +export function saveLabelers(did, value) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, AsyncStorage.setItem("".concat(PREFIX, ":").concat(did), JSON.stringify(value))]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function readLabelers(did) { + return __awaiter(this, void 0, void 0, function () { + var rawData; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, AsyncStorage.getItem("".concat(PREFIX, ":").concat(did))]; + case 1: + rawData = _a.sent(); + return [2 /*return*/, rawData ? JSON.parse(rawData) : undefined]; + } + }); + }); +} diff --git a/src/state/session/agent.js b/src/state/session/agent.js new file mode 100644 index 0000000000..54f51c79e9 --- /dev/null +++ b/src/state/session/agent.js @@ -0,0 +1,432 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { Agent as BaseAgent, BskyAgent, } from '@atproto/api'; +import { TID } from '@atproto/common-web'; +import { networkRetry } from '#/lib/async/retry'; +import { BLUESKY_PROXY_HEADER, BSKY_SERVICE, DISCOVER_SAVED_FEED, IS_PROD_SERVICE, PUBLIC_BSKY_SERVICE, TIMELINE_SAVED_FEED, } from '#/lib/constants'; +import { getAge } from '#/lib/strings/time'; +import { logger } from '#/logger'; +import { snoozeBirthdateUpdateAllowedForDid } from '#/state/birthdate'; +import { snoozeEmailConfirmationPrompt } from '#/state/shell/reminders'; +import { prefetchAgeAssuranceData, setBirthdateForDid, setCreatedAtForDid, } from '#/ageAssurance/data'; +import { features } from '#/analytics'; +import { emitNetworkConfirmed, emitNetworkLost } from '../events'; +import { addSessionErrorLog } from './logging'; +import { configureModerationForAccount, configureModerationForGuest, } from './moderation'; +import { isSessionExpired, isSignupQueued } from './util'; +export function createPublicAgent() { + configureModerationForGuest(); // Side effect but only relevant for tests + var agent = new BskyAppAgent({ service: PUBLIC_BSKY_SERVICE }); + agent.configureProxy(BLUESKY_PROXY_HEADER.get()); + return agent; +} +export function createAgentAndResume(storedAccount, onSessionChange) { + return __awaiter(this, void 0, void 0, function () { + var agent, gates, moderation, prevSession, aa; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + agent = new BskyAppAgent({ service: storedAccount.service }); + if (storedAccount.pdsUrl) { + agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl); + } + gates = features.refresh({ + strategy: 'prefer-low-latency', + }); + moderation = configureModerationForAccount(agent, storedAccount); + prevSession = sessionAccountToSession(storedAccount); + if (!isSessionExpired(storedAccount)) return [3 /*break*/, 2]; + return [4 /*yield*/, networkRetry(1, function () { return agent.resumeSession(prevSession); })]; + case 1: + _a.sent(); + return [3 /*break*/, 3]; + case 2: + agent.sessionManager.session = prevSession; + if (!storedAccount.signupQueued) { + networkRetry(3, function () { return agent.resumeSession(prevSession); }).catch(function (e) { + logger.error("networkRetry failed to resume session", { + status: (e === null || e === void 0 ? void 0 : e.status) || 'unknown', + // this field name is ignored by Sentry scrubbers + safeMessage: (e === null || e === void 0 ? void 0 : e.message) || 'unknown', + }); + throw e; + }); + } + _a.label = 3; + case 3: + aa = prefetchAgeAssuranceData({ agent: agent }); + agent.configureProxy(BLUESKY_PROXY_HEADER.get()); + return [2 /*return*/, agent.prepare({ + resolvers: [gates, moderation, aa], + onSessionChange: onSessionChange, + })]; + } + }); + }); +} +export function createAgentAndLogin(_a, onSessionChange_1) { + return __awaiter(this, arguments, void 0, function (_b, onSessionChange) { + var agent, account, gates, moderation, aa; + var service = _b.service, identifier = _b.identifier, password = _b.password, authFactorToken = _b.authFactorToken; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + agent = new BskyAppAgent({ service: service }); + return [4 /*yield*/, agent.login({ + identifier: identifier, + password: password, + authFactorToken: authFactorToken, + allowTakendown: true, + })]; + case 1: + _c.sent(); + account = agentToSessionAccountOrThrow(agent); + gates = features.refresh({ strategy: 'prefer-fresh-gates' }); + moderation = configureModerationForAccount(agent, account); + aa = prefetchAgeAssuranceData({ agent: agent }); + agent.configureProxy(BLUESKY_PROXY_HEADER.get()); + return [2 /*return*/, agent.prepare({ + resolvers: [gates, moderation, aa], + onSessionChange: onSessionChange, + })]; + } + }); + }); +} +export function createAgentAndCreateAccount(_a, onSessionChange_1) { + return __awaiter(this, arguments, void 0, function (_b, onSessionChange) { + var agent, account, gates, moderation, createdAt, birthdate, aa; + var service = _b.service, email = _b.email, password = _b.password, handle = _b.handle, birthDate = _b.birthDate, inviteCode = _b.inviteCode, verificationPhone = _b.verificationPhone, verificationCode = _b.verificationCode; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + agent = new BskyAppAgent({ service: service }); + return [4 /*yield*/, agent.createAccount({ + email: email, + password: password, + handle: handle, + inviteCode: inviteCode, + verificationPhone: verificationPhone, + verificationCode: verificationCode, + })]; + case 1: + _c.sent(); + account = agentToSessionAccountOrThrow(agent); + gates = features.refresh({ strategy: 'prefer-fresh-gates' }); + moderation = configureModerationForAccount(agent, account); + createdAt = new Date().toISOString(); + birthdate = birthDate.toISOString(); + /* + * Since we have a race with account creation, profile creation, and AA + * state, set these values locally to ensure sync reads. Values are written + * to the server in the next step, so on subsequent reloads, the server will + * be the source of truth. + */ + setCreatedAtForDid({ did: account.did, createdAt: createdAt }); + setBirthdateForDid({ did: account.did, birthdate: birthdate }); + snoozeBirthdateUpdateAllowedForDid(account.did); + aa = prefetchAgeAssuranceData({ agent: agent }); + // Not awaited so that we can still get into onboarding. + // This is OK because we won't let you toggle adult stuff until you set the date. + if (IS_PROD_SERVICE(service)) { + Promise.allSettled([ + networkRetry(3, function () { + return agent.setPersonalDetails({ + birthDate: birthdate, + }); + }).catch(function (e) { + logger.info("createAgentAndCreateAccount: failed to set birthDate"); + throw e; + }), + networkRetry(3, function () { + return agent.upsertProfile(function (prev) { + var next = prev || {}; + next.displayName = handle; + next.createdAt = createdAt; + return next; + }); + }).catch(function (e) { + logger.info("createAgentAndCreateAccount: failed to set initial profile"); + throw e; + }), + networkRetry(1, function () { + return agent.overwriteSavedFeeds([ + __assign(__assign({}, DISCOVER_SAVED_FEED), { id: TID.nextStr() }), + __assign(__assign({}, TIMELINE_SAVED_FEED), { id: TID.nextStr() }), + ]); + }).catch(function (e) { + logger.info("createAgentAndCreateAccount: failed to set initial feeds"); + throw e; + }), + getAge(birthDate) < 18 && + networkRetry(3, function () { + return agent.com.atproto.repo.putRecord({ + repo: account.did, + collection: 'chat.bsky.actor.declaration', + rkey: 'self', + record: { + $type: 'chat.bsky.actor.declaration', + allowIncoming: 'none', + }, + }); + }).catch(function (e) { + logger.info("createAgentAndCreateAccount: failed to set chat declaration"); + throw e; + }), + ].filter(Boolean)).then(function (promises) { + var rejected = promises.filter(function (p) { return p.status === 'rejected'; }); + if (rejected.length > 0) { + logger.error("session: createAgentAndCreateAccount failed to save personal details and feeds"); + } + }); + } + else { + Promise.allSettled([ + networkRetry(3, function () { + return agent.setPersonalDetails({ + birthDate: birthDate.toISOString(), + }); + }).catch(function (e) { + logger.info("createAgentAndCreateAccount: failed to set birthDate"); + throw e; + }), + networkRetry(3, function () { + return agent.upsertProfile(function (prev) { + var next = prev || {}; + next.createdAt = (prev === null || prev === void 0 ? void 0 : prev.createdAt) || new Date().toISOString(); + return next; + }); + }).catch(function (e) { + logger.info("createAgentAndCreateAccount: failed to set initial profile"); + throw e; + }), + ].filter(Boolean)).then(function (promises) { + var rejected = promises.filter(function (p) { return p.status === 'rejected'; }); + if (rejected.length > 0) { + logger.error("session: createAgentAndCreateAccount failed to save personal details and feeds"); + } + }); + } + try { + // snooze first prompt after signup, defer to next prompt + snoozeEmailConfirmationPrompt(); + } + catch (e) { + logger.error(e, { message: "session: failed snoozeEmailConfirmationPrompt" }); + } + agent.configureProxy(BLUESKY_PROXY_HEADER.get()); + return [2 /*return*/, agent.prepare({ + resolvers: [gates, moderation, aa], + onSessionChange: onSessionChange, + })]; + } + }); + }); +} +export function agentToSessionAccountOrThrow(agent) { + var account = agentToSessionAccount(agent); + if (!account) { + throw Error('Expected an active session'); + } + return account; +} +export function agentToSessionAccount(agent) { + var _a; + if (!agent.session) { + return undefined; + } + return { + service: agent.serviceUrl.toString(), + did: agent.session.did, + handle: agent.session.handle, + email: agent.session.email, + emailConfirmed: agent.session.emailConfirmed || false, + emailAuthFactor: agent.session.emailAuthFactor || false, + refreshJwt: agent.session.refreshJwt, + accessJwt: agent.session.accessJwt, + signupQueued: isSignupQueued(agent.session.accessJwt), + active: agent.session.active, + status: agent.session.status, + pdsUrl: (_a = agent.pdsUrl) === null || _a === void 0 ? void 0 : _a.toString(), + isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE), + }; +} +export function sessionAccountToSession(account) { + var _a, _b, _c; + return { + // Sorted in the same property order as when returned by BskyAgent (alphabetical). + accessJwt: (_a = account.accessJwt) !== null && _a !== void 0 ? _a : '', + did: account.did, + email: account.email, + emailAuthFactor: account.emailAuthFactor, + emailConfirmed: account.emailConfirmed, + handle: account.handle, + refreshJwt: (_b = account.refreshJwt) !== null && _b !== void 0 ? _b : '', + /** + * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188 + */ + active: (_c = account.active) !== null && _c !== void 0 ? _c : true, + status: account.status, + }; +} +var Agent = /** @class */ (function (_super) { + __extends(Agent, _super); + function Agent(proxyHeader, options) { + var _this = _super.call(this, options) || this; + if (proxyHeader) { + _this.configureProxy(proxyHeader); + } + return _this; + } + return Agent; +}(BaseAgent)); +export { Agent }; +// Not exported. Use factories above to create it. +// WARN: In the factories above, we _manually set a proxy header_ for the agent after we do whatever it is we are supposed to do. +// Ideally, we wouldn't be doing this. However, since there is so much logic that requires making calls to the PDS right now, it +// feels safer to just let those run as-is and set the header afterward. +var realFetch = globalThis.fetch; +var BskyAppAgent = /** @class */ (function (_super) { + __extends(BskyAppAgent, _super); + function BskyAppAgent(_a) { + var service = _a.service; + var _this = _super.call(this, { + service: service, + fetch: function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return __awaiter(this, void 0, void 0, function () { + var success, result, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + success = false; + _a.label = 1; + case 1: + _a.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, realFetch.apply(void 0, args)]; + case 2: + result = _a.sent(); + success = true; + return [2 /*return*/, result]; + case 3: + e_1 = _a.sent(); + success = false; + throw e_1; + case 4: + if (success) { + emitNetworkConfirmed(); + } + else { + emitNetworkLost(); + } + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); + }, + persistSession: function (event) { + if (_this.persistSessionHandler) { + _this.persistSessionHandler(event); + } + }, + }) || this; + _this.persistSessionHandler = undefined; + return _this; + } + BskyAppAgent.prototype.prepare = function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var account; + var _this = this; + var resolvers = _b.resolvers, onSessionChange = _b.onSessionChange; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + // There's nothing else left to do, so block on them here. + return [4 /*yield*/, Promise.all(resolvers) + // Now the agent is ready. + ]; + case 1: + // There's nothing else left to do, so block on them here. + _c.sent(); + account = agentToSessionAccountOrThrow(this); + this.persistSessionHandler = function (event) { + onSessionChange(_this, account.did, event); + if (event !== 'create' && event !== 'update') { + addSessionErrorLog(account.did, event); + } + }; + return [2 /*return*/, { account: account, agent: this }]; + } + }); + }); + }; + BskyAppAgent.prototype.dispose = function () { + this.sessionManager.session = undefined; + this.persistSessionHandler = undefined; + }; + return BskyAppAgent; +}(BskyAgent)); diff --git a/src/state/session/index.js b/src/state/session/index.js new file mode 100644 index 0000000000..b6d405d17b --- /dev/null +++ b/src/state/session/index.js @@ -0,0 +1,415 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react'; +import * as persisted from '#/state/persisted'; +import { useCloseAllActiveElements } from '#/state/util'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { AnalyticsContext, useAnalyticsBase, utils } from '#/analytics'; +import { IS_WEB } from '#/env'; +import { emitSessionDropped } from '../events'; +import { agentToSessionAccount, createAgentAndCreateAccount, createAgentAndLogin, createAgentAndResume, sessionAccountToSession, } from './agent'; +import { getInitialState, reducer } from './reducer'; +export { isSignupQueued } from './util'; +import { addSessionDebugLog } from './logging'; +import { clearPersistedQueryStorage } from '#/lib/persisted-query-storage'; +import { useOnboardingDispatch } from '#/state/shell/onboarding'; +import { clearAgeAssuranceData, clearAgeAssuranceDataForDid, } from '#/ageAssurance/data'; +var StateContext = createContext({ + accounts: [], + currentAccount: undefined, + hasSession: false, +}); +StateContext.displayName = 'SessionStateContext'; +var AgentContext = createContext(null); +AgentContext.displayName = 'SessionAgentContext'; +var ApiContext = createContext({ + createAccount: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); }, + login: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); }, + logoutCurrentAccount: function () { }, + logoutEveryAccount: function () { }, + resumeSession: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); }, + removeAccount: function () { }, + partialRefreshSession: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); }, +}); +ApiContext.displayName = 'SessionApiContext'; +var SessionStore = /** @class */ (function () { + function SessionStore() { + var _this = this; + this.listeners = new Set(); + this.getState = function () { + return _this.state; + }; + this.subscribe = function (listener) { + _this.listeners.add(listener); + return function () { + _this.listeners.delete(listener); + }; + }; + this.dispatch = function (action) { + var nextState = reducer(_this.state, action); + _this.state = nextState; + // Persist synchronously without waiting for the React render cycle. + if (nextState.needsPersist) { + nextState.needsPersist = false; + var persistedData = { + accounts: nextState.accounts, + currentAccount: nextState.accounts.find(function (a) { return a.did === nextState.currentAgentState.did; }), + }; + addSessionDebugLog({ type: 'persisted:broadcast', data: persistedData }); + persisted.write('session', persistedData); + } + _this.listeners.forEach(function (listener) { return listener(); }); + }; + // Careful: By the time this runs, `persisted` needs to already be filled. + var initialState = getInitialState(persisted.get('session').accounts); + addSessionDebugLog({ type: 'reducer:init', state: initialState }); + this.state = initialState; + } + return SessionStore; +}()); +export function Provider(_a) { + var _this = this; + var children = _a.children; + var ax = useAnalyticsBase(); + var cancelPendingTask = useOneTaskAtATime(); + var store = useState(function () { return new SessionStore(); })[0]; + var state = useSyncExternalStore(store.subscribe, store.getState); + var onboardingDispatch = useOnboardingDispatch(); + var onAgentSessionChange = useCallback(function (agent, accountDid, sessionEvent) { + var refreshedAccount = agentToSessionAccount(agent); // Mutable, so snapshot it right away. + if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { + emitSessionDropped(); + } + store.dispatch({ + type: 'received-agent-event', + agent: agent, + refreshedAccount: refreshedAccount, + accountDid: accountDid, + sessionEvent: sessionEvent, + }); + }, [store]); + var createAccount = useCallback(function (params, metrics) { return __awaiter(_this, void 0, void 0, function () { + var signal, _a, agent, account; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + addSessionDebugLog({ type: 'method:start', method: 'createAccount' }); + signal = cancelPendingTask(); + ax.metric('account:create:begin', {}); + return [4 /*yield*/, createAgentAndCreateAccount(params, onAgentSessionChange)]; + case 1: + _a = _b.sent(), agent = _a.agent, account = _a.account; + if (signal.aborted) { + return [2 /*return*/]; + } + store.dispatch({ + type: 'switched-to-account', + newAgent: agent, + newAccount: account, + }); + ax.metric('account:create:success', metrics, { + session: utils.accountToSessionMetadata(account), + }); + addSessionDebugLog({ type: 'method:end', method: 'createAccount', account: account }); + return [2 /*return*/]; + } + }); + }); }, [ax, store, onAgentSessionChange, cancelPendingTask]); + var login = useCallback(function (params, logContext) { return __awaiter(_this, void 0, void 0, function () { + var signal, _a, agent, account; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + addSessionDebugLog({ type: 'method:start', method: 'login' }); + signal = cancelPendingTask(); + return [4 /*yield*/, createAgentAndLogin(params, onAgentSessionChange)]; + case 1: + _a = _b.sent(), agent = _a.agent, account = _a.account; + if (signal.aborted) { + return [2 /*return*/]; + } + store.dispatch({ + type: 'switched-to-account', + newAgent: agent, + newAccount: account, + }); + ax.metric('account:loggedIn', { logContext: logContext, withPassword: true }, { session: utils.accountToSessionMetadata(account) }); + addSessionDebugLog({ type: 'method:end', method: 'login', account: account }); + return [2 /*return*/]; + } + }); + }); }, [ax, store, onAgentSessionChange, cancelPendingTask]); + var logoutCurrentAccount = useCallback(function (logContext) { + addSessionDebugLog({ type: 'method:start', method: 'logout' }); + cancelPendingTask(); + var prevState = store.getState(); + store.dispatch({ + type: 'logged-out-current-account', + }); + ax.metric('account:loggedOut', { logContext: logContext, scope: 'current' }, { + session: utils.accountToSessionMetadata(prevState.accounts.find(function (a) { return a.did === prevState.currentAgentState.did; })), + }); + addSessionDebugLog({ type: 'method:end', method: 'logout' }); + if (prevState.currentAgentState.did) { + clearAgeAssuranceDataForDid({ did: prevState.currentAgentState.did }); + void clearPersistedQueryStorage(prevState.currentAgentState.did); + } + // reset onboarding flow on logout + onboardingDispatch({ type: 'skip' }); + }, [ax, store, cancelPendingTask, onboardingDispatch]); + var logoutEveryAccount = useCallback(function (logContext) { + addSessionDebugLog({ type: 'method:start', method: 'logout' }); + cancelPendingTask(); + var prevState = store.getState(); + store.dispatch({ + type: 'logged-out-every-account', + }); + ax.metric('account:loggedOut', { logContext: logContext, scope: 'every' }, { + session: utils.accountToSessionMetadata(prevState.accounts.find(function (a) { return a.did === prevState.currentAgentState.did; })), + }); + addSessionDebugLog({ type: 'method:end', method: 'logout' }); + clearAgeAssuranceData(); + for (var _i = 0, _a = prevState.accounts; _i < _a.length; _i++) { + var account = _a[_i]; + void clearPersistedQueryStorage(account.did); + } + // reset onboarding flow on logout + onboardingDispatch({ type: 'skip' }); + }, [store, cancelPendingTask, onboardingDispatch, ax]); + var resumeSession = useCallback(function (storedAccount_1) { + var args_1 = []; + for (var _i = 1; _i < arguments.length; _i++) { + args_1[_i - 1] = arguments[_i]; + } + return __awaiter(_this, __spreadArray([storedAccount_1], args_1, true), void 0, function (storedAccount, isSwitchingAccounts) { + var signal, _a, agent, account; + if (isSwitchingAccounts === void 0) { isSwitchingAccounts = false; } + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + addSessionDebugLog({ + type: 'method:start', + method: 'resumeSession', + account: storedAccount, + }); + signal = cancelPendingTask(); + return [4 /*yield*/, createAgentAndResume(storedAccount, onAgentSessionChange)]; + case 1: + _a = _b.sent(), agent = _a.agent, account = _a.account; + if (signal.aborted) { + return [2 /*return*/]; + } + store.dispatch({ + type: 'switched-to-account', + newAgent: agent, + newAccount: account, + }); + addSessionDebugLog({ type: 'method:end', method: 'resumeSession', account: account }); + if (isSwitchingAccounts) { + // reset onboarding flow on switch account + onboardingDispatch({ type: 'skip' }); + } + return [2 /*return*/]; + } + }); + }); + }, [store, onAgentSessionChange, cancelPendingTask, onboardingDispatch]); + var partialRefreshSession = useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + var agent, signal, data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + agent = state.currentAgentState.agent; + signal = cancelPendingTask(); + return [4 /*yield*/, agent.com.atproto.server.getSession()]; + case 1: + data = (_a.sent()).data; + if (signal.aborted) + return [2 /*return*/]; + store.dispatch({ + type: 'partial-refresh-session', + accountDid: agent.session.did, + patch: { + emailConfirmed: data.emailConfirmed, + emailAuthFactor: data.emailAuthFactor, + }, + }); + return [2 /*return*/]; + } + }); + }); }, [store, state, cancelPendingTask]); + var removeAccount = useCallback(function (account) { + addSessionDebugLog({ + type: 'method:start', + method: 'removeAccount', + account: account, + }); + cancelPendingTask(); + store.dispatch({ + type: 'removed-account', + accountDid: account.did, + }); + addSessionDebugLog({ type: 'method:end', method: 'removeAccount', account: account }); + clearAgeAssuranceDataForDid({ did: account.did }); + }, [store, cancelPendingTask]); + useEffect(function () { + return persisted.onUpdate('session', function (nextSession) { + var _a; + var synced = nextSession; + addSessionDebugLog({ type: 'persisted:receive', data: synced }); + store.dispatch({ + type: 'synced-accounts', + syncedAccounts: synced.accounts, + syncedCurrentDid: (_a = synced.currentAccount) === null || _a === void 0 ? void 0 : _a.did, + }); + var syncedAccount = synced.accounts.find(function (a) { var _a; return a.did === ((_a = synced.currentAccount) === null || _a === void 0 ? void 0 : _a.did); }); + if (syncedAccount && syncedAccount.refreshJwt) { + if (syncedAccount.did !== state.currentAgentState.did) { + resumeSession(syncedAccount); + } + else { + var agent_1 = state.currentAgentState.agent; + var prevSession = agent_1.session; + agent_1.sessionManager.session = sessionAccountToSession(syncedAccount); + addSessionDebugLog({ + type: 'agent:patch', + agent: agent_1, + prevSession: prevSession, + nextSession: agent_1.session, + }); + } + } + }); + }, [store, state, resumeSession]); + var stateContext = useMemo(function () { return ({ + accounts: state.accounts, + currentAccount: state.accounts.find(function (a) { return a.did === state.currentAgentState.did; }), + hasSession: !!state.currentAgentState.did, + }); }, [state]); + var api = useMemo(function () { return ({ + createAccount: createAccount, + login: login, + logoutCurrentAccount: logoutCurrentAccount, + logoutEveryAccount: logoutEveryAccount, + resumeSession: resumeSession, + removeAccount: removeAccount, + partialRefreshSession: partialRefreshSession, + }); }, [ + createAccount, + login, + logoutCurrentAccount, + logoutEveryAccount, + resumeSession, + removeAccount, + partialRefreshSession, + ]); + // @ts-expect-error window type is not declared, debug only + if (__DEV__ && IS_WEB) + window.agent = state.currentAgentState.agent; + var agent = state.currentAgentState.agent; + var currentAgentRef = useRef(agent); + useEffect(function () { + if (currentAgentRef.current !== agent) { + // Read the previous value and immediately advance the pointer. + var prevAgent = currentAgentRef.current; + currentAgentRef.current = agent; + addSessionDebugLog({ type: 'agent:switch', prevAgent: prevAgent, nextAgent: agent }); + // We never reuse agents so let's fully neutralize the previous one. + // This ensures it won't try to consume any refresh tokens. + prevAgent.dispose(); + } + }, [agent]); + return (_jsx(AgentContext.Provider, { value: agent, children: _jsx(StateContext.Provider, { value: stateContext, children: _jsx(ApiContext.Provider, { value: api, children: _jsx(AnalyticsContext, { metadata: utils.useMeta({ + session: utils.accountToSessionMetadata(stateContext.currentAccount), + }), children: children }) }) }) })); +} +function useOneTaskAtATime() { + var abortController = useRef(null); + var cancelPendingTask = useCallback(function () { + if (abortController.current) { + abortController.current.abort(); + } + abortController.current = new AbortController(); + return abortController.current.signal; + }, []); + return cancelPendingTask; +} +export function useSession() { + return useContext(StateContext); +} +export function useSessionApi() { + return useContext(ApiContext); +} +export function useRequireAuth() { + var hasSession = useSession().hasSession; + var closeAll = useCloseAllActiveElements(); + var signinDialogControl = useGlobalDialogsControlContext().signinDialogControl; + return useCallback(function (fn) { + if (hasSession) { + fn(); + } + else { + closeAll(); + signinDialogControl.open(); + } + }, [hasSession, signinDialogControl, closeAll]); +} +export function useAgent() { + var agent = useContext(AgentContext); + if (!agent) { + throw Error('useAgent() must be below .'); + } + return agent; +} diff --git a/src/state/session/logging.js b/src/state/session/logging.js new file mode 100644 index 0000000000..c445f5c69d --- /dev/null +++ b/src/state/session/logging.js @@ -0,0 +1,13 @@ +export function wrapSessionReducerForLogging(reducer) { + return function loggingWrapper(prevState, action) { + var nextState = reducer(prevState, action); + addSessionDebugLog({ type: 'reducer:call', prevState: prevState, action: action, nextState: nextState }); + return nextState; + }; +} +/** + * Stubs, previously used to log session errors to Statsig. We may revive this + * using Sentry or Bitdrift in the future. + */ +export function addSessionErrorLog(_did, _event) { } +export function addSessionDebugLog(_log) { } diff --git a/src/state/session/moderation.js b/src/state/session/moderation.js new file mode 100644 index 0000000000..296b4d9ff9 --- /dev/null +++ b/src/state/session/moderation.js @@ -0,0 +1,99 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { BSKY_LABELER_DID, BskyAgent } from '@atproto/api'; +import { IS_TEST_USER } from '#/lib/constants'; +import { configureAdditionalModerationAuthorities } from './additional-moderation-authorities'; +import { readLabelers } from './agent-config'; +export function configureModerationForGuest() { + // This global mutation is *only* OK because this code is only relevant for testing. + // Don't add any other global behavior here! + switchToBskyAppLabeler(); + configureAdditionalModerationAuthorities(); +} +export function configureModerationForAccount(agent, account) { + return __awaiter(this, void 0, void 0, function () { + var labelerDids; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // This global mutation is *only* OK because this code is only relevant for testing. + // Don't add any other global behavior here! + switchToBskyAppLabeler(); + if (!IS_TEST_USER(account.handle)) return [3 /*break*/, 2]; + return [4 /*yield*/, trySwitchToTestAppLabeler(agent)]; + case 1: + _a.sent(); + _a.label = 2; + case 2: return [4 /*yield*/, readLabelers(account.did).catch(function (_) { })]; + case 3: + labelerDids = _a.sent(); + if (labelerDids) { + agent.configureLabelersHeader(labelerDids.filter(function (did) { return did !== BSKY_LABELER_DID; })); + } + else { + // If there are no headers in the storage, we'll not send them on the initial requests. + // If we wanted to fix this, we could block on the preferences query here. + } + configureAdditionalModerationAuthorities(); + return [2 /*return*/]; + } + }); + }); +} +function switchToBskyAppLabeler() { + BskyAgent.configure({ appLabelers: [BSKY_LABELER_DID] }); +} +function trySwitchToTestAppLabeler(agent) { + return __awaiter(this, void 0, void 0, function () { + var did; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, agent + .resolveHandle({ handle: 'mod-authority.test' }) + .catch(function (_) { return undefined; })]; + case 1: + did = (_a = (_b.sent())) === null || _a === void 0 ? void 0 : _a.data.did; + if (did) { + console.warn('USING TEST ENV MODERATION'); + BskyAgent.configure({ appLabelers: [did] }); + } + return [2 /*return*/]; + } + }); + }); +} diff --git a/src/state/session/reducer.js b/src/state/session/reducer.js new file mode 100644 index 0000000000..0058b792ce --- /dev/null +++ b/src/state/session/reducer.js @@ -0,0 +1,198 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { unregisterPushToken } from '#/lib/notifications/notifications'; +import { logger } from '#/lib/notifications/util'; +import { createPublicAgent } from './agent'; +import { wrapSessionReducerForLogging } from './logging'; +import { createTemporaryAgentsAndResume } from './util'; +function createPublicAgentState() { + return { + agent: createPublicAgent(), + did: undefined, + }; +} +export function getInitialState(persistedAccounts) { + return { + accounts: persistedAccounts, + currentAgentState: createPublicAgentState(), + needsPersist: false, + }; +} +var reducer = function (state, action) { + var _a, _b; + switch (action.type) { + case 'received-agent-event': { + var agent = action.agent, accountDid_1 = action.accountDid, refreshedAccount_1 = action.refreshedAccount, sessionEvent = action.sessionEvent; + if (refreshedAccount_1 === undefined && + agent !== state.currentAgentState.agent) { + // If the session got cleared out (e.g. due to expiry or network error) but + // this account isn't the active one, don't clear it out at this time. + // This way, if the problem is transient, it'll work on next resume. + return state; + } + if (sessionEvent === 'network-error') { + // Assume it's transient. + return state; + } + var existingAccount = state.accounts.find(function (a) { return a.did === accountDid_1; }); + if (!existingAccount || + JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount_1)) { + // Fast path without a state update. + return state; + } + return { + accounts: state.accounts.map(function (a) { + if (a.did === accountDid_1) { + if (refreshedAccount_1) { + return refreshedAccount_1; + } + else { + return __assign(__assign({}, a), { + // If we didn't receive a refreshed account, clear out the tokens. + accessJwt: undefined, refreshJwt: undefined }); + } + } + else { + return a; + } + }), + currentAgentState: refreshedAccount_1 + ? state.currentAgentState + : createPublicAgentState(), // Log out if expired. + needsPersist: true, + }; + } + case 'switched-to-account': { + var newAccount_1 = action.newAccount, newAgent = action.newAgent; + return { + accounts: __spreadArray([ + newAccount_1 + ], state.accounts.filter(function (a) { return a.did !== newAccount_1.did; }), true), + currentAgentState: { + did: newAccount_1.did, + agent: newAgent, + }, + needsPersist: true, + }; + } + case 'removed-account': { + var accountDid_2 = action.accountDid; + // side effect + var account = state.accounts.find(function (a) { return a.did === accountDid_2; }); + if (account) { + createTemporaryAgentsAndResume([account]) + .then(function (agents) { return unregisterPushToken(agents); }) + .then(function () { + return logger.debug('Push token unregistered', { did: accountDid_2 }); + }) + .catch(function (err) { + logger.error('Failed to unregister push token', { + did: accountDid_2, + error: err, + }); + }); + } + return { + accounts: state.accounts.filter(function (a) { return a.did !== accountDid_2; }), + currentAgentState: state.currentAgentState.did === accountDid_2 + ? createPublicAgentState() // Log out if removing the current one. + : state.currentAgentState, + needsPersist: true, + }; + } + case 'logged-out-current-account': { + var currentAgentState = state.currentAgentState; + var accountDid_3 = currentAgentState.did; + // side effect + var account = state.accounts.find(function (a) { return a.did === accountDid_3; }); + if (account && accountDid_3) { + createTemporaryAgentsAndResume([account]) + .then(function (agents) { return unregisterPushToken(agents); }) + .then(function () { + return logger.debug('Push token unregistered', { did: accountDid_3 }); + }) + .catch(function (err) { + logger.error('Failed to unregister push token', { + did: accountDid_3, + error: err, + }); + }); + } + return { + accounts: state.accounts.map(function (a) { + return a.did === accountDid_3 + ? __assign(__assign({}, a), { refreshJwt: undefined, accessJwt: undefined }) : a; + }), + currentAgentState: createPublicAgentState(), + needsPersist: true, + }; + } + case 'logged-out-every-account': { + createTemporaryAgentsAndResume(state.accounts) + .then(function (agents) { return unregisterPushToken(agents); }) + .then(function () { return logger.debug('Push token unregistered'); }) + .catch(function (err) { + logger.error('Failed to unregister push token', { + error: err, + }); + }); + return { + accounts: state.accounts.map(function (a) { return (__assign(__assign({}, a), { + // Clear tokens for *every* account (this is a hard logout). + refreshJwt: undefined, accessJwt: undefined })); }), + currentAgentState: createPublicAgentState(), + needsPersist: true, + }; + } + case 'synced-accounts': { + var syncedAccounts = action.syncedAccounts, syncedCurrentDid = action.syncedCurrentDid; + return { + accounts: syncedAccounts, + currentAgentState: syncedCurrentDid === state.currentAgentState.did + ? state.currentAgentState + : createPublicAgentState(), // Log out if different user. + needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. + }; + } + case 'partial-refresh-session': { + var accountDid_4 = action.accountDid, patch_1 = action.patch; + var agent = state.currentAgentState.agent; + /* + * Only mutating values that are safe. Be very careful with this. + */ + if (agent.session) { + agent.session.emailConfirmed = + (_a = patch_1.emailConfirmed) !== null && _a !== void 0 ? _a : agent.session.emailConfirmed; + agent.session.emailAuthFactor = + (_b = patch_1.emailAuthFactor) !== null && _b !== void 0 ? _b : agent.session.emailAuthFactor; + } + return __assign(__assign({}, state), { currentAgentState: __assign(__assign({}, state.currentAgentState), { agent: agent }), accounts: state.accounts.map(function (a) { + var _a, _b; + if (a.did === accountDid_4) { + return __assign(__assign({}, a), { emailConfirmed: (_a = patch_1.emailConfirmed) !== null && _a !== void 0 ? _a : a.emailConfirmed, emailAuthFactor: (_b = patch_1.emailAuthFactor) !== null && _b !== void 0 ? _b : a.emailAuthFactor }); + } + return a; + }), needsPersist: true }); + } + } +}; +reducer = wrapSessionReducerForLogging(reducer); +export { reducer }; diff --git a/src/state/session/types.js b/src/state/session/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/state/session/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/state/session/util.js b/src/state/session/util.js new file mode 100644 index 0000000000..6244ae4dd2 --- /dev/null +++ b/src/state/session/util.js @@ -0,0 +1,101 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import AtpAgent from '@atproto/api'; +import { jwtDecode } from 'jwt-decode'; +import { isJwtExpired } from '#/lib/jwt'; +import { hasProp } from '#/lib/type-guards'; +import * as persisted from '#/state/persisted'; +import { sessionAccountToSession } from './agent'; +export function readLastActiveAccount() { + var _a = persisted.get('session'), currentAccount = _a.currentAccount, accounts = _a.accounts; + return accounts.find(function (a) { return a.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); +} +export function isSignupQueued(accessJwt) { + if (accessJwt) { + var sessData = jwtDecode(accessJwt); + return (hasProp(sessData, 'scope') && + sessData.scope === 'com.atproto.signupQueued'); + } + return false; +} +export function isSessionExpired(account) { + if (account.accessJwt) { + return isJwtExpired(account.accessJwt); + } + else { + return true; + } +} +/** + * Creates and attempted to resumeSession for every stored session. + * Intended to be used to send push token revokations just before logout. + */ +export function createTemporaryAgentsAndResume(accounts) { + return __awaiter(this, void 0, void 0, function () { + var agents; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Promise.allSettled(accounts.map(function (account) { return __awaiter(_this, void 0, void 0, function () { + var agent, session, res; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + agent = new AtpAgent({ service: account.service }); + if (account.pdsUrl) { + agent.sessionManager.pdsUrl = new URL(account.pdsUrl); + } + session = sessionAccountToSession(account); + return [4 /*yield*/, agent.resumeSession(session)]; + case 1: + res = _a.sent(); + if (!res.success) + throw new Error('Failed to resume session'); + agent.assertAuthenticated(); // confirm auth success + return [2 /*return*/, agent]; + } + }); + }); }))]; + case 1: + agents = _a.sent(); + return [2 /*return*/, agents + .filter(function (x) { return x.status === 'fulfilled'; }) + .map(function (promise) { return promise.value; })]; + } + }); + }); +} diff --git a/src/state/shell/color-mode.js b/src/state/shell/color-mode.js new file mode 100644 index 0000000000..167d086f04 --- /dev/null +++ b/src/state/shell/color-mode.js @@ -0,0 +1,52 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import * as persisted from '#/state/persisted'; +var stateContext = React.createContext({ + colorMode: 'system', + darkTheme: 'dark', +}); +stateContext.displayName = 'ColorModeStateContext'; +var setContext = React.createContext({}); +setContext.displayName = 'ColorModeSetContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(function () { + return persisted.get('colorMode'); + }), colorMode = _b[0], setColorMode = _b[1]; + var _c = React.useState(function () { + return persisted.get('darkTheme'); + }), darkTheme = _c[0], setDarkTheme = _c[1]; + var stateContextValue = React.useMemo(function () { return ({ + colorMode: colorMode, + darkTheme: darkTheme, + }); }, [colorMode, darkTheme]); + var setContextValue = React.useMemo(function () { return ({ + setColorMode: function (_colorMode) { + setColorMode(_colorMode); + persisted.write('colorMode', _colorMode); + }, + setDarkTheme: function (_darkTheme) { + setDarkTheme(_darkTheme); + persisted.write('darkTheme', _darkTheme); + }, + }); }, []); + React.useEffect(function () { + var unsub1 = persisted.onUpdate('darkTheme', function (nextDarkTheme) { + setDarkTheme(nextDarkTheme); + }); + var unsub2 = persisted.onUpdate('colorMode', function (nextColorMode) { + setColorMode(nextColorMode); + }); + return function () { + unsub1(); + unsub2(); + }; + }, []); + return (_jsx(stateContext.Provider, { value: stateContextValue, children: _jsx(setContext.Provider, { value: setContextValue, children: children }) })); +} +export function useThemePrefs() { + return React.useContext(stateContext); +} +export function useSetThemePrefs() { + return React.useContext(setContext); +} diff --git a/src/state/shell/composer/index.js b/src/state/shell/composer/index.js new file mode 100644 index 0000000000..7bba8b61dc --- /dev/null +++ b/src/state/shell/composer/index.js @@ -0,0 +1,101 @@ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback'; +import { postUriToRelativePath, toBskyAppUrl } from '#/lib/strings/url-helpers'; +import { purgeTemporaryImageFiles } from '#/state/gallery'; +import { precacheResolveLinkQuery, RQKEY_GIF_ROOT, RQKEY_LINK_ROOT, } from '#/state/queries/resolve-link'; +import * as Toast from '#/view/com/util/Toast'; +var stateContext = React.createContext(undefined); +stateContext.displayName = 'ComposerStateContext'; +var controlsContext = React.createContext({ + openComposer: function (_opts) { }, + closeComposer: function () { + return false; + }, +}); +controlsContext.displayName = 'ComposerControlsContext'; +export function Provider(_a) { + var children = _a.children; + var _ = useLingui()._; + var _b = React.useState(), state = _b[0], setState = _b[1]; + var queryClient = useQueryClient(); + var openComposer = useNonReactiveCallback(function (opts) { + var _a, _b, _c, _d, _e; + if (opts.quote) { + var path = postUriToRelativePath(opts.quote.uri); + if (path) { + var appUrl = toBskyAppUrl(path); + precacheResolveLinkQuery(queryClient, appUrl, { + type: 'record', + kind: 'post', + record: { + cid: opts.quote.cid, + uri: opts.quote.uri, + }, + view: opts.quote, + }); + } + } + var author = ((_a = opts.replyTo) === null || _a === void 0 ? void 0 : _a.author) || ((_b = opts.quote) === null || _b === void 0 ? void 0 : _b.author); + var isBlocked = Boolean(author && + (((_c = author.viewer) === null || _c === void 0 ? void 0 : _c.blocking) || + ((_d = author.viewer) === null || _d === void 0 ? void 0 : _d.blockedBy) || + ((_e = author.viewer) === null || _e === void 0 ? void 0 : _e.blockingByList))); + if (isBlocked) { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Cannot interact with a blocked user"], ["Cannot interact with a blocked user"])))), 'exclamation-circle'); + } + else { + setState(function (prevOpts) { + if (prevOpts) { + // Never replace an already open composer. + return prevOpts; + } + return opts; + }); + } + }); + var closeComposer = useNonReactiveCallback(function () { + var wasOpen = !!state; + if (wasOpen) { + setState(undefined); + purgeTemporaryImageFiles(); + // Purging deletes cached thumbnails on disk, so remove the query + // caches that may hold references to those now-deleted file paths. + // Without this, restoring a draft would serve stale ResolvedLink + // data pointing at missing files, causing "Failed to load blob". + queryClient.removeQueries({ queryKey: [RQKEY_LINK_ROOT] }); + queryClient.removeQueries({ queryKey: [RQKEY_GIF_ROOT] }); + } + return wasOpen; + }); + var api = React.useMemo(function () { return ({ + openComposer: openComposer, + closeComposer: closeComposer, + }); }, [openComposer, closeComposer]); + return (_jsx(stateContext.Provider, { value: state, children: _jsx(controlsContext.Provider, { value: api, children: children }) })); +} +export function useComposerState() { + return React.useContext(stateContext); +} +export function useComposerControls() { + var closeComposer = React.useContext(controlsContext).closeComposer; + return React.useMemo(function () { return ({ closeComposer: closeComposer }); }, [closeComposer]); +} +/** + * DO NOT USE DIRECTLY. The deprecation notice as a warning only, it's not + * actually deprecated. + * + * @deprecated use `#/lib/hooks/useOpenComposer` instead + */ +export function useOpenComposer() { + var openComposer = React.useContext(controlsContext).openComposer; + return React.useMemo(function () { return ({ openComposer: openComposer }); }, [openComposer]); +} +var templateObject_1; diff --git a/src/state/shell/composer/useComposerKeyboardShortcut.js b/src/state/shell/composer/useComposerKeyboardShortcut.js new file mode 100644 index 0000000000..b35ecb68bf --- /dev/null +++ b/src/state/shell/composer/useComposerKeyboardShortcut.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { useDialogStateContext } from '#/state/dialogs'; +import { useLightbox } from '#/state/lightbox'; +import { useModals } from '#/state/modals'; +import { useSession } from '#/state/session'; +import { useIsDrawerOpen } from '#/state/shell/drawer-open'; +/** + * Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2} + */ +function shouldIgnore(event) { + var target = event.target || event.srcElement; + if (!target) + return false; + var tagName = target.tagName; + if (!tagName) + return false; + var isInput = tagName === 'INPUT' && + ![ + 'checkbox', + 'radio', + 'range', + 'button', + 'file', + 'reset', + 'submit', + 'color', + ].includes(target.type); + // ignore: isContentEditable === 'true', and