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..62810b505f --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.js @@ -0,0 +1,195 @@ +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, 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 } from './BottomSheetPortal'; +import { Context as PortalContext } from './BottomSheetPortal'; +var screenHeight = Dimensions.get('screen').height; +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 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 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, + }, + 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..a4b1123933 --- /dev/null +++ b/src/App.native.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; +}; +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 { 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/macro'; +import { useLingui } from '@lingui/react'; +import * as Sentry from '@sentry/react-native'; +import { KeyboardControllerProvider } from '#/lib/hooks/useEnableKeyboardController'; +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 { 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(); +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(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..4f8ec6d5eb --- /dev/null +++ b/src/App.web.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 __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 React, { useEffect, useState } from 'react'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { msg } from '@lingui/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 { 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(); +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: 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'); + }); + }, [_]); + // wait for session to resume + if (!isReady || !hasCheckedReferrer) + return _jsx(Splash, { isReady: true }); + return (_jsx(Alf, { theme: theme, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(ContextMenuProvider, { children: _jsx(VideoVolumeProvider, { children: _jsx(ActiveVideoProvider, { 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(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]; + React.useEffect(function () { + Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(function () { + return setReady(true); + }); + }, []); + if (!isReady) { + return _jsx(Splash, { isReady: true }); + } + /* + * 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(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..bbb4e7ed5c --- /dev/null +++ b/src/Navigation.js @@ -0,0 +1,636 @@ +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, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useRef } from 'react'; +import { Linking } from 'react-native'; +import * as Notifications from 'expo-notifications'; +import { i18n } from '@lingui/core'; +import { msg } from '@lingui/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_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(); + return (_jsxs(HomeTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Home", children: [_jsx(HomeTab.Screen, { name: "Home", getComponent: function () { return HomeScreen; } }), _jsx(HomeTab.Screen, { name: "Start", getComponent: function () { return HomeScreen; } }), 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; + } + }, +}; +/** + * Used to ensure we don't handle the same notification twice + */ +var lastHandledNotificationDateDedupe; +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(); + /** + * 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() { + return __awaiter(this, void 0, void 0, function () { + var response, payload, path, _a, screen_1, params; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!IS_NATIVE) + return [2 /*return*/]; + return [4 /*yield*/, Linking.getInitialURL()]; + case 1: + // deep links take precedence - on android, + // getLastNotificationResponseAsync returns a "notification" + // that is actually a deep link. avoid handling it twice -sfn + if (_b.sent()) { + return [2 /*return*/]; + } + return [4 /*yield*/, Notifications.getLastNotificationResponseAsync()]; + case 2: + response = _b.sent(); + if (response) { + notyLogger.debug("handlePushNotificationEntry: response", { response: response }); + if (response.notification.date === lastHandledNotificationDateDedupe) + return [2 /*return*/]; + lastHandledNotificationDateDedupe = response.notification.date; + payload = getNotificationPayload(response.notification); + if (payload) { + ax.metric('notifications:openApp', { + reason: payload.reason, + causedBoot: true, + }); + if (payload.reason === 'chat-message') { + handleChatMessage(payload); + } + else { + path = notificationToURL(payload); + if (path === '/notifications') { + resetToTab('NotificationsTab'); + notyLogger.debug("handlePushNotificationEntry: default navigate"); + } + else if (path) { + _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, + }); + } + } + } + } + return [2 /*return*/]; + } + }); + }); + } + 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, + }); + } + } + }); + 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..764313985d --- /dev/null +++ b/src/Splash.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 { 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.png'; +// @ts-ignore +import darkSplashImagePointer from '../assets/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 _this = this; + 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 () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + // 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), + }); + }); + return [2 /*return*/]; + }); + }); }); + }); + }) + .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..8d04ca9f49 --- /dev/null +++ b/src/Splash.web.js @@ -0,0 +1,14 @@ +import { jsx as _jsx } 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 { View } from 'react-native'; +import Svg, { Path } from 'react-native-svg'; +import { atoms as a } from '#/alf'; +var size = 100; +var ratio = 57 / 64; +export function Splash() { + return (_jsx(View, { style: [a.fixed, a.inset_0, a.align_center, a.justify_center], 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..7f52cb4603 --- /dev/null +++ b/src/ageAssurance/components/NoAccessScreen.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..3b7d827453 --- /dev/null +++ b/src/ageAssurance/components/RedirectOverlay.js @@ -0,0 +1,239 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..e6737de270 --- /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 AsyncStorage from '@react-native-async-storage/async-storage'; +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 { 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: AsyncStorage, + 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..307c818578 --- /dev/null +++ b/src/alf/atoms.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); +}; +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'; +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 ease-out 0.1s, fadeIn ease-out 0.1s', + }), + /** + * {@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..2a21852318 --- /dev/null +++ b/src/analytics/features/types.js @@ -0,0 +1,11 @@ +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 || (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..5bb898d4f2 --- /dev/null +++ b/src/analytics/identifiers/session.js @@ -0,0 +1,34 @@ +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; +} +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..66e0e372ad --- /dev/null +++ b/src/analytics/identifiers/session.web.js @@ -0,0 +1,37 @@ +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; +} +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..016bbd11c5 --- /dev/null +++ b/src/analytics/index.js @@ -0,0 +1,140 @@ +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, useEffect, 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 { useGeolocation } from '#/geolocation'; +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(['mergedGeolocation']) || { + 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 = useGeolocation(); + 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-effect: we need to synchronously set this during the + * same render cycle. It does not trigger a re-render, it just + * sets properties on the singleton GrowthBook instance. + */ + setAttributes(parentContext.metadata); + useEffect(function () { + feats.setTrackingCallback(function (experiment, result) { + parentContext.metric('experiment:viewed', { + experimentId: experiment.key, + variationId: result.key, + }); + }); + }, [parentContext.metric]); + 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..b196b643db --- /dev/null +++ b/src/analytics/metrics/client.js @@ -0,0 +1,155 @@ +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 = { + 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..bc4a4fcbb1 --- /dev/null +++ b/src/components/AccountList.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 React, { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useActorStatus } from '#/lib/actor-status'; +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'; +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..16c1729aba --- /dev/null +++ b/src/components/Admonition.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 __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'; +export var colors = { + warning: '#FFC404', +}; +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: colors.warning, + 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: colors.warning, + 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..b3b7d1fe52 --- /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/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..109c597a1f --- /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/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..ae77806996 --- /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/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..47ed47faeb --- /dev/null +++ b/src/components/ContextMenu/context.js @@ -0,0 +1,28 @@ +import React from 'react'; +export var Context = React.createContext(null); +Context.displayName = 'ContextMenuContext'; +export var MenuContext = React.createContext(null); +MenuContext.displayName = 'ContextMenuMenuContext'; +export var ItemContext = React.createContext(null); +ItemContext.displayName = 'ContextMenuItemContext'; +export function useContextMenuContext() { + var context = React.useContext(Context); + if (!context) { + throw new Error('useContextMenuContext must be used within a Context.Provider'); + } + return context; +} +export function useContextMenuMenuContext() { + var context = React.useContext(MenuContext); + if (!context) { + throw new Error('useContextMenuMenuContext must be used within a Context.Provider'); + } + return context; +} +export function useContextMenuItemContext() { + var context = React.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..f55ac6eeaf --- /dev/null +++ b/src/components/ContextMenu/index.js @@ -0,0 +1,663 @@ +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 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/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: IS_IOS ? 1.25 : 0.75, + damping: 50, + stiffness: 1100, + 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 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, + animationSV: animationSV, + translationSV: translationSV, + mode: mode, + open: function (evt, mode) { + setMeasurement(evt); + setMode(mode); + animationSV.set(withSpring(1, SPRING_IN)); + }, + close: function () { + animationSV.set(withSpring(0, SPRING_OUT, function (finished) { + if (finished) { + hoverablesSV.set({}); + translationSV.set(0); + 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, + 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 topInset = useSafeAreaInsets().top; + 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(); + Keyboard.dismiss(); + return [4 /*yield*/, Promise.all([ + new Promise(function (resolve) { + var _a; + (_a = ref.current) === null || _a === void 0 ? void 0 : _a.measureInWindow(function (x, y, width, height) { + return resolve({ + x: x, + y: y + + platform({ + default: 0, + android: topInset, // not included in measurement + }), + width: width, + height: height, + }); + }); + }), + 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]; + setImage(capture); + setPendingMeasurement({ measurement: measurement, mode: mode }); + return [2 /*return*/]; + } + }); + }); }); + var doubleTapGesture = useMemo(function () { + return Gesture.Tap() + .numberOfTaps(2) + .hitSlop(HITSLOP_10) + .onEnd(function () { return 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, 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, onDisplay = _a.onDisplay, label = _a.label; + var _ = useLingui()._; + var animatedStyles = useAnimatedStyle(function () { return ({ + transform: [{ translateY: translation.get() * animation.get() }], + }); }); + 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 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..7f9bc20c83 --- /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 () { }, + IS_NATIVEDialog: 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..0e814c46ea --- /dev/null +++ b/src/components/Dialog/index.js @@ -0,0 +1,281 @@ +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, TextInput, View, } from 'react-native'; +import { KeyboardAwareScrollView, useKeyboardHandler, 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/macro'; +import { useLingui } from '@lingui/react'; +import { useEnableKeyboardController } from '#/lib/hooks/useEnableKeyboardController'; +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 { 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 } 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, + IS_NATIVEDialog: true, + nativeSnapPoint: snapPoint, + disableDrag: disableDrag, + setDisableDrag: setDisableDrag, + isWithinDialog: true, + }); }, [close, snapPoint, disableDrag, setDisableDrag]); + return (_jsx(BottomSheet, __assign({ ref: ref, cornerRadius: 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 }) }) }))); +} +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, + { + 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(); + useEnableKeyboardController(IS_IOS); + var _c = React.useState(0), keyboardHeight = _c[0], setKeyboardHeight = _c[1]; + useKeyboardHandler({ + onEnd: function (e) { + 'worklet'; + runOnJS(setKeyboardHeight)(e.height); + }, + }, []); + var paddingBottom = 0; + if (IS_IOS) { + paddingBottom += keyboardHeight / 4; + if (nativeSnapPoint === BottomSheetSnapPoint.Full) { + paddingBottom += insets.bottom + tokens.space.md; + } + paddingBottom = Math.max(paddingBottom, tokens.space._2xl); + } + else { + paddingBottom += keyboardHeight; + if (nativeSnapPoint === BottomSheetSnapPoint.Full) { + paddingBottom += insets.top; + } + paddingBottom += + Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl; + } + 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(KeyboardAwareScrollView, __assign({ contentContainerStyle: [ + a.pt_2xl, + a.px_xl, + { paddingBottom: paddingBottom }, + contentContainerStyle, + ], ref: ref, showsVerticalScrollIndicator: IS_ANDROID ? false : undefined }, props, { bounces: nativeSnapPoint === BottomSheetSnapPoint.Full, bottomOffset: 30, 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 footer = _a.footer, style = _a.style, props = __rest(_a, ["footer", "style"]); + var insets = useSafeAreaInsets(); + var _b = useDialogContext(), nativeSnapPoint = _b.nativeSnapPoint, disableDrag = _b.disableDrag, setDisableDrag = _b.setDisableDrag; + useEnableKeyboardController(IS_IOS); + 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", bounces: nativeSnapPoint === BottomSheetSnapPoint.Full, ListFooterComponent: _jsx(View, { style: { height: insets.bottom + 100 } }), ref: ref, showsVerticalScrollIndicator: IS_ANDROID ? false : undefined }, props, { style: [a.h_full, style] })), footer] })); +}); +export function FlatListFooter(_a) { + var children = _a.children; + 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, { 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, + 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; +} +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..7880035660 --- /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, TouchableWithoutFeedback, View, } from 'react-native'; +import { msg } from '@lingui/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, + IS_NATIVEDialog: 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(TouchableWithoutFeedback, { 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; + var t = useTheme(); + return (_jsx(View, { 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; +} +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..661c1d3dee --- /dev/null +++ b/src/components/Dialog/shared.js @@ -0,0 +1,29 @@ +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'; +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: 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: 6 }], children: renderLeft() })), children, renderRight && (_jsx(View, { style: [a.absolute, { right: 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..e9ef0dab81 --- /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 } 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) 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/Error.js b/src/components/Error.js new file mode 100644 index 0000000000..e51eb95198 --- /dev/null +++ b/src/components/Error.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 { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..d9159597a7 --- /dev/null +++ b/src/components/FeedCard.js @@ -0,0 +1,266 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..74e9850338 --- /dev/null +++ b/src/components/FeedInterstitials.js @@ -0,0 +1,583 @@ +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 React, { useCallback, useEffect, useRef } from 'react'; +import { ScrollView, View } from 'react-native'; +import Animated, { LinearTransition } from 'react-native-reanimated'; +import { AtUri } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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, 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 = React.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 = React.useState(new Set()), dismissedDids = _d[0], setDismissedDids = _d[1]; + var _e = React.useState(new Set()), dismissingDids = _e[0], setDismissingDids = _e[1]; + var onDismiss = React.useCallback(function (dismissedDid) { + // Start the fade animation + setDismissingDids(function (prev) { return new Set(prev).add(dismissedDid); }); + // After animation completes, actually remove from list + setTimeout(function () { + setDismissedDids(function (prev) { return new Set(prev).add(dismissedDid); }); + setDismissingDids(function (prev) { + var next = new Set(prev); + next.delete(dismissedDid); + return next; + }); + }, DISMISS_ANIMATION_DURATION); + }, []); + // Combine profiles from the actor-specific query with fallback suggestions + var allProfiles = React.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; })) !== 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(profile); + } + } + for (var _c = 0, fallbackProfiles_1 = fallbackProfiles; _c < fallbackProfiles_1.length; _c++) { + var profile = fallbackProfiles_1[_c]; + if (!seen.has(profile.did) && profile.did !== did) { + seen.add(profile.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]); + var filteredProfiles = React.useMemo(function () { + return allProfiles.filter(function (p) { return !dismissedDids.has(p.did); }); + }, [allProfiles, dismissedDids]); + // Fetch more when running low + React.useEffect(function () { + if (moderationOpts && + filteredProfiles.length < maxLength && + hasNextPage && + !isFetchingNextPage) { + fetchNextPage(); + } + }, [ + filteredProfiles.length, + maxLength, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + moderationOpts, + ]); + return (_jsx(ProfileGrid, { isSuggestionsLoading: isSuggestionsLoading, profiles: filteredProfiles, totalProfileCount: allProfiles.length, recId: data === null || data === void 0 ? void 0 : data.recId, error: error, viewContext: "profile", onDismiss: onDismiss, dismissingDids: dismissingDids })); +} +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 = React.useState(new Set()), dismissedDids = _c[0], setDismissedDids = _c[1]; + var _d = React.useState(new Set()), dismissingDids = _d[0], setDismissingDids = _d[1]; + var onDismiss = React.useCallback(function (did) { + // Start the fade animation + setDismissingDids(function (prev) { return new Set(prev).add(did); }); + // After animation completes, actually remove from list + setTimeout(function () { + setDismissedDids(function (prev) { return new Set(prev).add(did); }); + setDismissingDids(function (prev) { + var next = new Set(prev); + next.delete(did); + return next; + }); + }, DISMISS_ANIMATION_DURATION); + }, []); + // Combine profiles from experimental query with paginated suggestions + var allProfiles = React.useMemo(function () { + var _a; + var fallbackProfiles = (_a = moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages.flatMap(function (page) { return page.actors; })) !== 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(profile); + } + } + for (var _b = 0, fallbackProfiles_2 = fallbackProfiles; _b < fallbackProfiles_2.length; _b++) { + var profile = fallbackProfiles_2[_b]; + if (!seen.has(profile.did)) { + seen.add(profile.did); + combined.push(profile); + } + } + return combined; + }, [experimentalProfiles, moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages]); + var filteredProfiles = React.useMemo(function () { + return allProfiles.filter(function (p) { return !dismissedDids.has(p.did); }); + }, [allProfiles, dismissedDids]); + // Fetch more when running low + React.useEffect(function () { + if (moderationOpts && + filteredProfiles.length < maxLength && + hasNextPage && + !isFetchingNextPage) { + 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, dismissingDids: dismissingDids })); +} +export function ProfileGrid(_a) { + var isSuggestionsLoading = _a.isSuggestionsLoading, error = _a.error, profiles = _a.profiles, totalProfileCount = _a.totalProfileCount, recId = _a.recId, _b = _a.viewContext, viewContext = _b === void 0 ? 'feed' : _b, onDismiss = _a.onDismiss, dismissingDids = _a.dismissingDids, _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.did)) { + seenProfilesRef.current.add(profile.did); + ax.metric('suggestedUser:seen', { + logContext: logContext, + recId: recId, + position: index, + suggestedDid: profile.did, + category: null, + }); + } + }); + }, [ax, isLoading, error, profiles, maxLength, logContext, recId]); + // 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: LinearTransition.duration(DISMISS_ANIMATION_DURATION), style: [ + a.flex_1, + gtMobile && + web([ + a.flex_0, + a.flex_grow, + { width: "calc(30% - ".concat(a.gap_md.gap / 2, "px)") }, + ]), + { + opacity: (dismissingDids === null || dismissingDids === void 0 ? void 0 : dismissingDids.has(profile.did)) ? 0 : 1, + transitionProperty: 'opacity', + transitionDuration: "".concat(DISMISS_ANIMATION_DURATION, "ms"), + }, + ], children: _jsx(ProfileCard.Link, { profile: profile, onPress: function () { + ax.metric('suggestedUser:press', { + logContext: isFeedContext + ? 'InterstitialDiscover' + : 'InterstitialProfile', + recId: recId, + position: index, + suggestedDid: profile.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.did); + ax.metric('suggestedUser:dismiss', { + logContext: isFeedContext + ? 'InterstitialDiscover' + : 'InterstitialProfile', + position: index, + suggestedDid: profile.did, + recId: 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, 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, moderationOpts: moderationOpts }), _jsx(ProfileCard.Description, { profile: profile, numberOfLines: 2, style: [ + t.atoms.text_contrast_medium, + a.text_center, + a.text_xs, + ] })] })] }), _jsx(ProfileCard.FollowButton, { profile: profile, moderationOpts: moderationOpts, logContext: "FeedInterstitial", withIcon: false, style: [a.rounded_sm], onFollow: function () { + ax.metric('suggestedUser:follow', { + logContext: isFeedContext + ? 'InterstitialDiscover' + : 'InterstitialProfile', + location: 'Card', + recId: recId, + position: index, + suggestedDid: profile.did, + category: null, + }); + } })] }) })); + } }) }, profile.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: isFeedContext ? (_jsx(Trans, { children: "Suggested for you" })) : (_jsx(Trans, { children: "Similar accounts" })) }), !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 }), 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 = React.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..2c14fc8b38 --- /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/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..a9939abd1f --- /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/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..4a113e808d --- /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/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..c2ae5f63f3 --- /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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..1b19642708 --- /dev/null +++ b/src/components/LabelingServiceCard/index.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 { View } from 'react-native'; +import { msg, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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.handle, + }, + }, 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..4070bf9339 --- /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/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..8c6c660cde --- /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/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..4ae3aa4f9c --- /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/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..436569f2b5 --- /dev/null +++ b/src/components/ListCard.js @@ -0,0 +1,84 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..5e1adad550 --- /dev/null +++ b/src/components/Lists.js @@ -0,0 +1,84 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..3d51d61957 --- /dev/null +++ b/src/components/MediaPreview.js @@ -0,0 +1,84 @@ +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/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: _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(); + 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; + if (!thumbnail) { + return (_jsx(View, { style: [ + { backgroundColor: 'black' }, + a.flex_1, + a.aspect_square, + { maxWidth: 100 }, + a.justify_center, + a.align_center, + ], children: _jsx(PlayButtonIcon, { size: 24 }) })); + } + 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', + right: 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..b27a2c2aa3 --- /dev/null +++ b/src/components/Menu/index.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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_md, + 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_md, + 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..dbe5a6a535 --- /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/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..6129621c7b --- /dev/null +++ b/src/components/NewskieDialog.js @@ -0,0 +1,93 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..0136c52987 --- /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/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..77fd5099bc --- /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/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..241182c267 --- /dev/null +++ b/src/components/PolicyUpdateOverlay/updates/202508/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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..e3e6450b23 --- /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/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..2d6f4938cb --- /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/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..782cd921ab --- /dev/null +++ b/src/components/Post/Embed/ExternalEmbed/Gif.js @@ -0,0 +1,116 @@ +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 { Pressable, StyleSheet, TouchableOpacity, View, } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_20 } from '#/lib/constants'; +import { clamp } from '#/lib/numbers'; +import { useAutoplayDisabled } from '#/state/preferences'; +import { useLargeAltBadgeEnabled } from '#/state/preferences/large-alt-badge'; +import { atoms as a, useTheme } from '#/alf'; +import { Fill } from '#/components/Fill'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import { Text } from '#/components/Typography'; +import { PlayButtonIcon } from '#/components/video/PlayButtonIcon'; +import { IS_WEB } from '#/env'; +import { GifView } from '../../../../../modules/expo-bluesky-gif-view'; +function PlaybackControls(_a) { + var onPress = _a.onPress, isPlaying = _a.isPlaying, isLoaded = _a.isLoaded; + var _ = useLingui()._; + var t = useTheme(); + return (_jsx(Pressable, { accessibilityRole: "button", accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Plays or pauses the GIF"], ["Plays or pauses the GIF"])))), accessibilityLabel: isPlaying ? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Pause"], ["Pause"])))) : _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Play"], ["Play"])))), style: [ + a.absolute, + a.align_center, + a.justify_center, + !isLoaded && a.border, + t.atoms.border_contrast_medium, + a.inset_0, + a.w_full, + a.h_full, + { + zIndex: 2, + backgroundColor: !isLoaded + ? t.atoms.bg_contrast_25.backgroundColor + : undefined, + }, + ], onPress: onPress, children: !isLoaded ? (_jsx(View, { children: _jsx(View, { style: [a.align_center, a.justify_center], children: _jsx(Loader, { size: "xl" }) }) })) : !isPlaying ? (_jsx(PlayButtonIcon, {})) : undefined })); +} +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 = React.useRef(null); + var _c = React.useState({ + isPlaying: !autoplayDisabled, + isLoaded: false, + }), playerState = _c[0], setPlayerState = _c[1]; + var onPlayerStateChange = React.useCallback(function (e) { + setPlayerState(e.nativeEvent); + }, []); + var onPress = React.useCallback(function () { + var _a; + (_a = playerRef.current) === null || _a === void 0 ? void 0 : _a.toggleAsync(); + }, []); + var aspectRatio = 1; + if (params.dimensions) { + aspectRatio = clamp(params.dimensions.width / params.dimensions.height, 0.75, 4); + } + return (_jsx(View, { style: [ + a.rounded_md, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + { 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(PlaybackControls, { onPress: onPress, isPlaying: playerState.isPlaying, isLoaded: playerState.isLoaded }), _jsx(GifView, { source: params.playerUri, placeholderSource: thumb, style: [a.flex_1], autoplay: !autoplayDisabled, onPlayerStateChange: onPlayerStateChange, ref: playerRef, accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __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, + }, + ] })), !hideAlt && isPreferredAltText && _jsx(AltText, { text: altText })] }) })); +} +function AltText(_a) { + var text = _a.text; + var control = Prompt.usePromptControl(); + var largeAltBadge = useLargeAltBadgeEnabled(); + var _ = useLingui()._; + return (_jsxs(_Fragment, { children: [_jsx(TouchableOpacity, { testID: "altTextButton", accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Show alt text"], ["Show alt text"])))), accessibilityHint: "", hitSlop: HITSLOP_20, onPress: control.open, style: styles.altContainer, children: _jsx(Text, { style: [styles.alt, largeAltBadge && a.text_xs], accessible: false, children: _jsx(Trans, { children: "ALT" }) }) }), _jsxs(Prompt.Outer, { control: control, 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_6 || (templateObject_6 = __makeTemplateObject(["Close"], ["Close"])))), color: "secondary" }) })] })] })); +} +var styles = StyleSheet.create({ + altContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: IS_WEB ? 8 : 6, + paddingVertical: IS_WEB ? 6 : 3, + position: 'absolute', + // Related to margin/gap hack. This keeps the alt label in the same position + // on all platforms + right: IS_WEB ? 8 : 5, + bottom: IS_WEB ? 8 : 5, + zIndex: 2, + }, + alt: { + color: 'white', + fontSize: IS_WEB ? 10 : 7, + fontWeight: '600', + }, +}); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/Post/Embed/ExternalEmbed/index.js b/src/components/Post/Embed/ExternalEmbed/index.js new file mode 100644 index 0000000000..7192bb52ed --- /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/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..c8a280f9fb --- /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; + 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, + } })) : (_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/VideoEmbedInner/TimeIndicator.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.js new file mode 100644 index 0000000000..f0f89193ca --- /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/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..41fa8997ba --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.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 } 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/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 { 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); + } + return (_jsxs(View, { style: [a.flex_1, a.relative], children: [_jsx(BlueskyVideoView, { url: embed.playlist, autoplay: !autoplayDisabled && !isWithinMessage, beginMuted: autoplayDisabled ? false : muted, style: [a.rounded_sm], onActiveChange: function (e) { + setIsActive(e.nativeEvent.isActive); + }, onLoadingChange: function (e) { + setIsLoading(e.nativeEvent.isLoading); + }, onMutedChange: function (e) { + 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: "" }), _jsx(VideoControls, { 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 VideoControls(_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..b898a10fe6 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.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 __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/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 hlsRef = useHLS({ + playlist: embed.playlist, + setHasSubtitleTrack: setHasSubtitleTrack, + setError: setError, + videoRef: videoRef, + setHlsLoading: setHlsLoading, + }); + 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: !focused, "aria-labelledby": embed.alt ? figId : undefined, onTimeUpdate: function (e) { + lastKnownTime.current = e.currentTarget.currentTime; + } }), embed.alt && (_jsx("figcaption", { id: figId, style: { + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: 0, + }, 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 })] }) })); +} +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); }); }); + } + }); + var flushOnLoop = useNonReactiveCallback(function () { + if (!Hls) + return; + if (!hlsRef.current) + return; + var hls = hlsRef.current; + // the above callback 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([]); + } + }); + 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); + // manually loop, so if we've flushed the first buffer it doesn't get confused + var abortController = new AbortController(); + var signal = abortController.signal; + var videoNode = videoRef.current; + videoNode.addEventListener('ended', function () { + flushOnLoop(); + videoNode.currentTime = 0; + videoNode.play(); + }, { signal: signal }); + 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(); + abortController.abort(); + }; + }, [ + playlist, + setError, + setHasSubtitleTrack, + videoRef, + handleFragChange, + flushOnLoop, + Hls, + ]); + return hlsRef; +} +var templateObject_1; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.js new file mode 100644 index 0000000000..e7ec0a6612 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.js @@ -0,0 +1,3 @@ +export function VideoEmbedInnerWeb() { + throw new Error('VideoEmbedInnerWeb may not be used on native.'); +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.js new file mode 100644 index 0000000000..9851e56c93 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateIcon } from '#/components/icons/ArrowRotate'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { Text as TypoText } from '#/components/Typography'; +export function Container(_a) { + var children = _a.children; + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_1, + t.atoms.bg_contrast_25, + a.justify_center, + a.align_center, + a.px_lg, + a.rounded_md, + a.overflow_hidden, + a.gap_lg, + ], children: [children, _jsx(MediaInsetBorder, {})] })); +} +export function Text(_a) { + var children = _a.children; + var t = useTheme(); + return (_jsx(TypoText, { style: [ + a.text_center, + t.atoms.text_contrast_high, + a.text_md, + a.leading_snug, + { maxWidth: 300 }, + ], children: children })); +} +export function RetryButton(_a) { + var onPress = _a.onPress; + var _ = useLingui()._; + return (_jsxs(Button, { onPress: onPress, size: "small", color: "secondary_inverted", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Retry"], ["Retry"])))), children: [_jsx(ButtonIcon, { icon: ArrowRotateIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) })] })); +} +var templateObject_1; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.js new file mode 100644 index 0000000000..5afd6ec1c9 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.js @@ -0,0 +1,9 @@ +var latestBandwidthEstimate; +export function get() { + return latestBandwidthEstimate; +} +export function set(estimate) { + if (!isNaN(estimate)) { + latestBandwidthEstimate = estimate; + } +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.js new file mode 100644 index 0000000000..0234f4157a --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.js @@ -0,0 +1,12 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { PressableWithHover } from '#/view/com/util/PressableWithHover'; +import { atoms as a, useTheme, web } from '#/alf'; +export function ControlButton(_a) { + var active = _a.active, activeLabel = _a.activeLabel, inactiveLabel = _a.inactiveLabel, ActiveIcon = _a.activeIcon, InactiveIcon = _a.inactiveIcon, onPress = _a.onPress; + var t = useTheme(); + return (_jsx(PressableWithHover, { accessibilityRole: "button", accessibilityLabel: active ? activeLabel : inactiveLabel, accessibilityHint: "", onPress: onPress, style: [ + a.p_xs, + a.rounded_full, + web({ transition: 'background-color 0.1s' }), + ], hoverStyle: { backgroundColor: 'rgba(255, 255, 255, 0.2)' }, children: active ? (_jsx(ActiveIcon, { fill: t.palette.white, width: 20, "aria-hidden": true })) : (_jsx(InactiveIcon, { fill: t.palette.white, width: 20, "aria-hidden": true })) })); +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.js new file mode 100644 index 0000000000..67eeb720ac --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.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; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { clamp } from '#/lib/numbers'; +import { atoms as a, useTheme, web } from '#/alf'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { IS_WEB_FIREFOX, IS_WEB_TOUCH_DEVICE } from '#/env'; +import { formatTime } from './utils'; +export function Scrubber(_a) { + var duration = _a.duration, currentTime = _a.currentTime, onSeek = _a.onSeek, onSeekEnd = _a.onSeekEnd, onSeekStart = _a.onSeekStart, seekLeft = _a.seekLeft, seekRight = _a.seekRight, togglePlayPause = _a.togglePlayPause, drawFocus = _a.drawFocus; + var _ = useLingui()._; + var t = useTheme(); + var _b = useState(false), scrubberActive = _b[0], setScrubberActive = _b[1]; + var _c = useInteractionState(), hovered = _c.state, onStartHover = _c.onIn, onEndHover = _c.onOut; + var _d = useInteractionState(), focused = _d.state, onFocus = _d.onIn, onBlur = _d.onOut; + var _e = useState(0), seekPosition = _e[0], setSeekPosition = _e[1]; + var isSeekingRef = useRef(false); + var barRef = useRef(null); + var circleRef = useRef(null); + var seek = useCallback(function (evt) { + if (!barRef.current) + return; + var _a = barRef.current.getBoundingClientRect(), left = _a.left, width = _a.width; + var x = evt.clientX; + var percent = clamp((x - left) / width, 0, 1) * duration; + onSeek(percent); + setSeekPosition(percent); + }, [duration, onSeek]); + var onPointerDown = useCallback(function (evt) { + var target = evt.target; + if (target instanceof Element) { + evt.preventDefault(); + target.setPointerCapture(evt.pointerId); + isSeekingRef.current = true; + seek(evt); + setScrubberActive(true); + onSeekStart(); + } + }, [seek, onSeekStart]); + var onPointerMove = useCallback(function (evt) { + if (isSeekingRef.current) { + evt.preventDefault(); + seek(evt); + } + }, [seek]); + var onPointerUp = useCallback(function (evt) { + var target = evt.target; + if (isSeekingRef.current && target instanceof Element) { + evt.preventDefault(); + target.releasePointerCapture(evt.pointerId); + isSeekingRef.current = false; + onSeekEnd(); + setScrubberActive(false); + } + }, [onSeekEnd]); + useEffect(function () { + // HACK: there's divergent browser behaviour about what to do when + // a pointerUp event is fired outside the element that captured the + // pointer. Firefox clicks on the element the mouse is over, so we have + // to make everything unclickable while seeking -sfn + if (IS_WEB_FIREFOX && scrubberActive) { + document.body.classList.add('force-no-clicks'); + return function () { + document.body.classList.remove('force-no-clicks'); + }; + } + }, [scrubberActive, onSeekEnd]); + useEffect(function () { + if (!circleRef.current) + return; + if (focused) { + var abortController_1 = new AbortController(); + var signal = abortController_1.signal; + circleRef.current.addEventListener('keydown', function (evt) { + // space: play/pause + // arrow left: seek backward + // arrow right: seek forward + if (evt.key === ' ') { + evt.preventDefault(); + drawFocus(); + togglePlayPause(); + } + else if (evt.key === 'ArrowLeft') { + evt.preventDefault(); + drawFocus(); + seekLeft(); + } + else if (evt.key === 'ArrowRight') { + evt.preventDefault(); + drawFocus(); + seekRight(); + } + }, { signal: signal }); + return function () { return abortController_1.abort(); }; + } + }, [focused, seekLeft, seekRight, togglePlayPause, drawFocus]); + var progress = scrubberActive ? seekPosition : currentTime; + var progressPercent = (progress / duration) * 100; + if (duration < 3) + return null; + return (_jsx(View, { testID: "scrubber", style: [ + { height: IS_WEB_TOUCH_DEVICE ? 32 : 18, width: '100%' }, + a.flex_shrink_0, + a.px_xs, + ], onPointerEnter: onStartHover, onPointerLeave: onEndHover, children: _jsxs("div", { ref: barRef, style: { + flex: 1, + display: 'flex', + alignItems: 'center', + position: 'relative', + cursor: scrubberActive ? 'grabbing' : 'grab', + padding: '4px 0', + }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, children: [_jsx(View, { style: [ + a.w_full, + a.rounded_full, + a.overflow_hidden, + { backgroundColor: 'rgba(255, 255, 255, 0.4)' }, + { height: hovered || scrubberActive ? 6 : 3 }, + web({ transition: 'height 0.1s ease' }), + ], children: duration > 0 && (_jsx(View, { style: [ + a.h_full, + { backgroundColor: t.palette.white }, + { width: "".concat(progressPercent, "%") }, + ] })) }), _jsx("div", { ref: circleRef, "aria-label": _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause"], ["Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause"])))), role: "slider", "aria-valuemax": duration, "aria-valuemin": 0, "aria-valuenow": currentTime, "aria-valuetext": _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["", " of ", ""], ["", " of ", ""])), formatTime(currentTime), formatTime(duration))), tabIndex: 0, onFocus: onFocus, onBlur: onBlur, style: { + position: 'absolute', + height: 16, + width: 16, + left: "calc(".concat(progressPercent, "% - 8px)"), + borderRadius: 8, + pointerEvents: 'none', + }, children: _jsx(View, { style: [ + a.w_full, + a.h_full, + a.rounded_full, + { backgroundColor: t.palette.white }, + { + transform: [ + { + scale: hovered || scrubberActive || focused + ? scrubberActive + ? 1 + : 0.6 + : 0, + }, + ], + }, + ] }) })] }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.js new file mode 100644 index 0000000000..91cc7e1b34 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.js @@ -0,0 +1,249 @@ +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, useRef, useState } from 'react'; +import { Pressable, View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { clamp } from '#/lib/numbers'; +import { useAutoplayDisabled, useSetSubtitlesEnabled, useSubtitlesEnabled, } from '#/state/preferences'; +import { atoms as a, useTheme, web } from '#/alf'; +import { useIsWithinMessage } from '#/components/dms/MessageContext'; +import { useFullscreen } from '#/components/hooks/useFullscreen'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { ArrowsDiagonalIn_Stroke2_Corner0_Rounded as ArrowsInIcon, ArrowsDiagonalOut_Stroke2_Corner0_Rounded as ArrowsOutIcon, } from '#/components/icons/ArrowsDiagonal'; +import { CC_Filled_Corner0_Rounded as CCActiveIcon, CC_Stroke2_Corner0_Rounded as CCInactiveIcon, } from '#/components/icons/CC'; +import { Pause_Filled_Corner0_Rounded as PauseIcon } from '#/components/icons/Pause'; +import { Play_Filled_Corner0_Rounded as PlayIcon } from '#/components/icons/Play'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_WEB_MOBILE_IOS, IS_WEB_TOUCH_DEVICE } from '#/env'; +import { TimeIndicator } from '../TimeIndicator'; +import { ControlButton } from './ControlButton'; +import { Scrubber } from './Scrubber'; +import { formatTime, useVideoElement } from './utils'; +import { VolumeControl } from './VolumeControl'; +export function Controls(_a) { + var videoRef = _a.videoRef, hlsRef = _a.hlsRef, active = _a.active, setActive = _a.setActive, focused = _a.focused, setFocused = _a.setFocused, onScreen = _a.onScreen, fullscreenRef = _a.fullscreenRef, hlsLoading = _a.hlsLoading, hasSubtitleTrack = _a.hasSubtitleTrack; + var _b = useVideoElement(videoRef), play = _b.play, pause = _b.pause, playing = _b.playing, muted = _b.muted, changeMuted = _b.changeMuted, togglePlayPause = _b.togglePlayPause, currentTime = _b.currentTime, duration = _b.duration, buffering = _b.buffering, error = _b.error, canPlay = _b.canPlay; + var t = useTheme(); + var _ = useLingui()._; + var subtitlesEnabled = useSubtitlesEnabled(); + var setSubtitlesEnabled = useSetSubtitlesEnabled(); + var _c = useInteractionState(), hovered = _c.state, onHover = _c.onIn, onEndHover = _c.onOut; + var _d = useFullscreen(fullscreenRef), isFullscreen = _d[0], toggleFullscreen = _d[1]; + var _e = useInteractionState(), hasFocus = _e.state, onFocus = _e.onIn, onBlur = _e.onOut; + var _f = useState(false), interactingViaKeypress = _f[0], setInteractingViaKeypress = _f[1]; + var showSpinner = hlsLoading || buffering; + var _g = useInteractionState(), volumeHovered = _g.state, onVolumeHover = _g.onIn, onVolumeEndHover = _g.onOut; + var onKeyDown = useCallback(function () { + setInteractingViaKeypress(true); + }, []); + useEffect(function () { + if (interactingViaKeypress) { + document.addEventListener('click', function () { return setInteractingViaKeypress(false); }); + return function () { + document.removeEventListener('click', function () { + return setInteractingViaKeypress(false); + }); + }; + } + }, [interactingViaKeypress]); + useEffect(function () { + if (isFullscreen) { + document.documentElement.style.scrollbarGutter = 'unset'; + return function () { + document.documentElement.style.removeProperty('scrollbar-gutter'); + }; + } + }, [isFullscreen]); + // pause + unfocus when another video is active + useEffect(function () { + if (!active) { + pause(); + setFocused(false); + } + }, [active, pause, setFocused]); + // autoplay/pause based on visibility + var isWithinMessage = useIsWithinMessage(); + var autoplayDisabled = useAutoplayDisabled() || isWithinMessage; + useEffect(function () { + if (active) { + if (onScreen) { + if (!autoplayDisabled) + play(); + } + else { + pause(); + } + } + }, [onScreen, pause, active, play, autoplayDisabled]); + // use minimal quality when not focused + useEffect(function () { + if (!hlsRef.current) + return; + if (focused) { + // allow 30s of buffering + hlsRef.current.config.maxMaxBufferLength = 30; + } + else { + // back to what we initially set + hlsRef.current.config.maxMaxBufferLength = 10; + } + }, [hlsRef, focused]); + useEffect(function () { + if (!hlsRef.current) + return; + if (hasSubtitleTrack && subtitlesEnabled && canPlay) { + hlsRef.current.subtitleTrack = 0; + } + else { + hlsRef.current.subtitleTrack = -1; + } + }, [hasSubtitleTrack, subtitlesEnabled, hlsRef, canPlay]); + // clicking on any button should focus the player, if it's not already focused + var drawFocus = useCallback(function () { + if (!active) { + setActive(); + } + setFocused(true); + }, [active, setActive, setFocused]); + var onPressEmptySpace = useCallback(function () { + if (!focused) { + drawFocus(); + if (autoplayDisabled) + play(); + } + else { + togglePlayPause(); + } + }, [togglePlayPause, drawFocus, focused, autoplayDisabled, play]); + var onPressPlayPause = useCallback(function () { + drawFocus(); + togglePlayPause(); + }, [drawFocus, togglePlayPause]); + var onPressSubtitles = useCallback(function () { + drawFocus(); + setSubtitlesEnabled(!subtitlesEnabled); + }, [drawFocus, setSubtitlesEnabled, subtitlesEnabled]); + var onPressFullscreen = useCallback(function () { + drawFocus(); + toggleFullscreen(); + }, [drawFocus, toggleFullscreen]); + var onSeek = useCallback(function (time) { + if (!videoRef.current) + return; + if (videoRef.current.fastSeek) { + videoRef.current.fastSeek(time); + } + else { + videoRef.current.currentTime = time; + } + }, [videoRef]); + var playStateBeforeSeekRef = useRef(false); + var onSeekStart = useCallback(function () { + drawFocus(); + playStateBeforeSeekRef.current = playing; + pause(); + }, [playing, pause, drawFocus]); + var onSeekEnd = useCallback(function () { + if (playStateBeforeSeekRef.current) { + play(); + } + }, [play]); + var seekLeft = useCallback(function () { + if (!videoRef.current) + return; + var currentTime = videoRef.current.currentTime; + var duration = videoRef.current.duration || 0; + onSeek(clamp(currentTime - 5, 0, duration)); + }, [onSeek, videoRef]); + var seekRight = useCallback(function () { + if (!videoRef.current) + return; + var currentTime = videoRef.current.currentTime; + var duration = videoRef.current.duration || 0; + onSeek(clamp(currentTime + 5, 0, duration)); + }, [onSeek, videoRef]); + var _h = useState(true), showCursor = _h[0], setShowCursor = _h[1]; + var cursorTimeoutRef = useRef(undefined); + var onPointerMoveEmptySpace = useCallback(function () { + setShowCursor(true); + if (cursorTimeoutRef.current) { + clearTimeout(cursorTimeoutRef.current); + } + cursorTimeoutRef.current = setTimeout(function () { + setShowCursor(false); + onEndHover(); + }, 2000); + }, [onEndHover]); + var onPointerLeaveEmptySpace = useCallback(function () { + setShowCursor(false); + if (cursorTimeoutRef.current) { + clearTimeout(cursorTimeoutRef.current); + } + }, []); + // these are used to trigger the hover state. on mobile, the hover state + // should stick around for a bit after they tap, and if the controls aren't + // present this initial tab should *only* show the controls and not activate anything + var onPointerDown = useCallback(function (evt) { + if (evt.pointerType !== 'mouse' && !hovered) { + evt.preventDefault(); + } + clearTimeout(timeoutRef.current); + }, [hovered]); + var timeoutRef = useRef(undefined); + var onHoverWithTimeout = useCallback(function () { + onHover(); + clearTimeout(timeoutRef.current); + }, [onHover]); + var onEndHoverWithTimeout = useCallback(function (evt) { + // if touch, end after 3s + // if mouse, end immediately + if (evt.pointerType !== 'mouse') { + setTimeout(onEndHover, 3000); + } + else { + onEndHover(); + } + }, [onEndHover]); + var showControls = ((focused || autoplayDisabled) && !playing) || + (interactingViaKeypress ? hasFocus : hovered); + return (_jsxs("div", { style: { + position: 'absolute', + inset: 0, + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', + }, onClick: function (evt) { + evt.stopPropagation(); + setInteractingViaKeypress(false); + }, onPointerEnter: onHoverWithTimeout, onPointerMove: onHoverWithTimeout, onPointerLeave: onEndHoverWithTimeout, onPointerDown: onPointerDown, onFocus: onFocus, onBlur: onBlur, onKeyDown: onKeyDown, children: [_jsx(Pressable, { accessibilityRole: "button", onPointerEnter: onPointerMoveEmptySpace, onPointerMove: onPointerMoveEmptySpace, onPointerLeave: onPointerLeaveEmptySpace, accessibilityLabel: _(!focused + ? msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Unmute video"], ["Unmute video"]))) : playing + ? msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Pause video"], ["Pause video"]))) : msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Play video"], ["Play video"])))), accessibilityHint: "", style: [ + a.flex_1, + web({ cursor: showCursor || !playing ? 'pointer' : 'none' }), + ], onPress: onPressEmptySpace }), !showControls && !focused && duration > 0 && (_jsx(TimeIndicator, { time: Math.floor(duration - currentTime) })), _jsxs(View, { style: [ + a.flex_shrink_0, + a.w_full, + a.px_xs, + web({ + background: 'linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.7))', + }), + { opacity: showControls ? 1 : 0 }, + { transition: 'opacity 0.2s ease-in-out' }, + ], children: [(!volumeHovered || IS_WEB_TOUCH_DEVICE) && (_jsx(Scrubber, { duration: duration, currentTime: currentTime, onSeek: onSeek, onSeekStart: onSeekStart, onSeekEnd: onSeekEnd, seekLeft: seekLeft, seekRight: seekRight, togglePlayPause: togglePlayPause, drawFocus: drawFocus })), _jsxs(View, { style: [ + a.flex_1, + a.px_xs, + a.pb_sm, + a.gap_sm, + a.flex_row, + a.align_center, + ], children: [_jsx(ControlButton, { active: playing, activeLabel: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Pause"], ["Pause"])))), inactiveLabel: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Play"], ["Play"])))), activeIcon: PauseIcon, inactiveIcon: PlayIcon, onPress: onPressPlayPause }), _jsx(View, { style: a.flex_1 }), Math.round(duration) > 0 && (_jsxs(Text, { style: [ + a.px_xs, + { color: t.palette.white, fontVariant: ['tabular-nums'] }, + ], children: [formatTime(currentTime), " / ", formatTime(duration)] })), hasSubtitleTrack && (_jsx(ControlButton, { active: subtitlesEnabled, activeLabel: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Disable subtitles"], ["Disable subtitles"])))), inactiveLabel: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Enable subtitles"], ["Enable subtitles"])))), activeIcon: CCActiveIcon, inactiveIcon: CCInactiveIcon, onPress: onPressSubtitles })), _jsx(VolumeControl, { muted: muted, changeMuted: changeMuted, hovered: volumeHovered, onHover: onVolumeHover, onEndHover: onVolumeEndHover, drawFocus: drawFocus }), !IS_WEB_MOBILE_IOS && (_jsx(ControlButton, { active: isFullscreen, activeLabel: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Exit fullscreen"], ["Exit fullscreen"])))), inactiveLabel: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Enter fullscreen"], ["Enter fullscreen"])))), activeIcon: ArrowsInIcon, inactiveIcon: ArrowsOutIcon, onPress: onPressFullscreen }))] })] }), (showSpinner || error) && (_jsxs(View, { pointerEvents: "none", style: [a.absolute, a.inset_0, a.justify_center, a.align_center], children: [showSpinner && _jsx(Loader, { fill: t.palette.white, size: "lg" }), error && (_jsx(Text, { style: { color: t.palette.white }, children: _jsx(Trans, { children: "An error occurred" }) }))] }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.js new file mode 100644 index 0000000000..ad11bcdbee --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.js @@ -0,0 +1,3 @@ +export function Controls() { + throw new Error('VideoWebControls may not be used on native.'); +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.js new file mode 100644 index 0000000000..5cec50a928 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.js @@ -0,0 +1,61 @@ +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 Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a } from '#/alf'; +import { Mute_Stroke2_Corner0_Rounded as MuteIcon } from '#/components/icons/Mute'; +import { SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon } from '#/components/icons/Speaker'; +import { useVideoVolumeState } from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'; +import { IS_WEB_SAFARI, IS_WEB_TOUCH_DEVICE } from '#/env'; +import { ControlButton } from './ControlButton'; +export function VolumeControl(_a) { + var muted = _a.muted, changeMuted = _a.changeMuted, hovered = _a.hovered, onHover = _a.onHover, onEndHover = _a.onEndHover, drawFocus = _a.drawFocus; + var _ = useLingui()._; + var _b = useVideoVolumeState(), volume = _b[0], setVolume = _b[1]; + var onVolumeChange = useCallback(function (evt) { + drawFocus(); + var vol = sliderVolumeToVideoVolume(Number(evt.target.value)); + setVolume(vol); + changeMuted(vol === 0); + }, [setVolume, drawFocus, changeMuted]); + var sliderVolume = muted ? 0 : videoVolumeToSliderVolume(volume); + var isZeroVolume = volume === 0; + var onPressMute = useCallback(function () { + drawFocus(); + if (isZeroVolume) { + setVolume(1); + changeMuted(false); + } + else { + changeMuted(function (prevMuted) { return !prevMuted; }); + } + }, [drawFocus, setVolume, isZeroVolume, changeMuted]); + return (_jsxs(View, { onPointerEnter: onHover, onPointerLeave: onEndHover, style: [a.relative], children: [hovered && !IS_WEB_TOUCH_DEVICE && (_jsx(Animated.View, { entering: FadeIn.duration(100), exiting: FadeOut.duration(100), style: [a.absolute, a.w_full, { height: 100, bottom: '100%' }], children: _jsx(View, { style: [ + a.flex_1, + a.mb_xs, + a.px_2xs, + a.py_xs, + { backgroundColor: 'rgba(0, 0, 0, 0.6)' }, + a.rounded_xs, + a.align_center, + ], children: _jsx("input", { type: "range", min: 0, max: 100, value: sliderVolume, "aria-label": _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Volume"], ["Volume"])))), style: + // Ridiculous safari hack for old version of safari. Fixed in sonoma beta -h + IS_WEB_SAFARI + ? { height: 92, minHeight: '100%' } + : { height: '100%' }, onChange: onVolumeChange, + // @ts-expect-error for old versions of firefox, and then re-using it for targeting the CSS -sfn + orient: "vertical" }) }) })), _jsx(ControlButton, { active: muted || volume === 0, activeLabel: _(msg({ message: "Unmute", context: 'video' })), inactiveLabel: _(msg({ message: "Mute", context: 'video' })), activeIcon: MuteIcon, inactiveIcon: UnmuteIcon, onPress: onPressMute })] })); +} +function sliderVolumeToVideoVolume(value) { + return Math.pow(value / 100, 4); +} +function videoVolumeToSliderVolume(value) { + return Math.round(Math.pow(value, 1 / 4) * 100); +} +var templateObject_1; diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.js b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.js new file mode 100644 index 0000000000..de24841c59 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.js @@ -0,0 +1,274 @@ +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'; +import { logger } from '#/logger'; +import { useVideoVolumeState } from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'; +import { IS_WEB_SAFARI } from '#/env'; +export function useVideoElement(ref) { + var _this = this; + var _a = useState(false), playing = _a[0], setPlaying = _a[1]; + var _b = useState(true), muted = _b[0], setMuted = _b[1]; + var _c = useState(0), currentTime = _c[0], setCurrentTime = _c[1]; + var _d = useVideoVolumeState(), volume = _d[0], setVolume = _d[1]; + var _e = useState(0), duration = _e[0], setDuration = _e[1]; + var _f = useState(false), buffering = _f[0], setBuffering = _f[1]; + var _g = useState(false), error = _g[0], setError = _g[1]; + var _h = useState(false), canPlay = _h[0], setCanPlay = _h[1]; + var playWhenReadyRef = useRef(false); + useEffect(function () { + if (!ref.current) + return; + ref.current.volume = volume; + }, [ref, volume]); + useEffect(function () { + if (!ref.current) + return; + var bufferingTimeout; + function round(num) { + return Math.round(num * 100) / 100; + } + // Initial values + setCurrentTime(round(ref.current.currentTime) || 0); + setDuration(round(ref.current.duration) || 0); + setMuted(ref.current.muted); + setPlaying(!ref.current.paused); + setVolume(ref.current.volume); + var handleTimeUpdate = function () { + if (!ref.current) + return; + setCurrentTime(round(ref.current.currentTime) || 0); + // HACK: Safari randomly fires `stalled` events when changing between segments + // let's just clear the buffering state if the video is still progressing -sfn + if (IS_WEB_SAFARI) { + if (bufferingTimeout) + clearTimeout(bufferingTimeout); + setBuffering(false); + } + }; + var handleDurationChange = function () { + if (!ref.current) + return; + setDuration(round(ref.current.duration) || 0); + }; + var handlePlay = function () { + setPlaying(true); + }; + var handlePause = function () { + setPlaying(false); + }; + var handleVolumeChange = function () { + if (!ref.current) + return; + setMuted(ref.current.muted); + }; + var handleError = function () { + setError(true); + }; + var handleCanPlay = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (bufferingTimeout) + clearTimeout(bufferingTimeout); + setBuffering(false); + setCanPlay(true); + if (!ref.current) + return [2 /*return*/]; + if (!playWhenReadyRef.current) return [3 /*break*/, 5]; + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, ref.current.play()]; + case 2: + _c.sent(); + return [3 /*break*/, 4]; + case 3: + e_1 = _c.sent(); + if (!((_a = e_1.message) === null || _a === void 0 ? void 0 : _a.includes("The request is not allowed by the user agent")) && + !((_b = e_1.message) === null || _b === void 0 ? void 0 : _b.includes("The play() request was interrupted by a call to pause()"))) { + throw e_1; + } + return [3 /*break*/, 4]; + case 4: + playWhenReadyRef.current = false; + _c.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); }; + var handleCanPlayThrough = function () { + if (bufferingTimeout) + clearTimeout(bufferingTimeout); + setBuffering(false); + }; + var handleWaiting = function () { + if (bufferingTimeout) + clearTimeout(bufferingTimeout); + bufferingTimeout = setTimeout(function () { + setBuffering(true); + }, 500); // Delay to avoid frequent buffering state changes + }; + var handlePlaying = function () { + if (bufferingTimeout) + clearTimeout(bufferingTimeout); + setBuffering(false); + setError(false); + }; + var handleStalled = function () { + if (bufferingTimeout) + clearTimeout(bufferingTimeout); + bufferingTimeout = setTimeout(function () { + setBuffering(true); + }, 500); // Delay to avoid frequent buffering state changes + }; + var handleEnded = function () { + setPlaying(false); + setBuffering(false); + setError(false); + }; + var abortController = new AbortController(); + ref.current.addEventListener('timeupdate', handleTimeUpdate, { + signal: abortController.signal, + }); + ref.current.addEventListener('durationchange', handleDurationChange, { + signal: abortController.signal, + }); + ref.current.addEventListener('play', handlePlay, { + signal: abortController.signal, + }); + ref.current.addEventListener('pause', handlePause, { + signal: abortController.signal, + }); + ref.current.addEventListener('volumechange', handleVolumeChange, { + signal: abortController.signal, + }); + ref.current.addEventListener('error', handleError, { + signal: abortController.signal, + }); + ref.current.addEventListener('canplay', handleCanPlay, { + signal: abortController.signal, + }); + ref.current.addEventListener('canplaythrough', handleCanPlayThrough, { + signal: abortController.signal, + }); + ref.current.addEventListener('waiting', handleWaiting, { + signal: abortController.signal, + }); + ref.current.addEventListener('playing', handlePlaying, { + signal: abortController.signal, + }); + ref.current.addEventListener('stalled', handleStalled, { + signal: abortController.signal, + }); + ref.current.addEventListener('ended', handleEnded, { + signal: abortController.signal, + }); + return function () { + abortController.abort(); + clearTimeout(bufferingTimeout); + }; + }, [ref, setVolume]); + var play = useCallback(function () { + if (!ref.current) + return; + if (ref.current.ended) { + ref.current.currentTime = 0; + } + if (ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) { + playWhenReadyRef.current = true; + } + else { + var promise = ref.current.play(); + if (promise !== undefined) { + promise.catch(function (err) { + var _a; + if ( + // ignore this common error. it's fine + !((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes("The play() request was interrupted by a call to pause()"))) { + logger.error('Error playing video:', { message: err }); + } + }); + } + } + }, [ref]); + var pause = useCallback(function () { + if (!ref.current) + return; + ref.current.pause(); + playWhenReadyRef.current = false; + }, [ref]); + var togglePlayPause = useCallback(function () { + if (!ref.current) + return; + if (ref.current.paused) { + play(); + } + else { + pause(); + } + }, [ref, play, pause]); + var changeMuted = useCallback(function (newMuted) { + if (!ref.current) + return; + var value = typeof newMuted === 'function' ? newMuted(ref.current.muted) : newMuted; + ref.current.muted = value; + }, [ref]); + return { + play: play, + pause: pause, + togglePlayPause: togglePlayPause, + duration: duration, + currentTime: currentTime, + playing: playing, + muted: muted, + changeMuted: changeMuted, + buffering: buffering, + error: error, + canPlay: canPlay, + }; +} +export function formatTime(time) { + if (isNaN(time)) { + return '--'; + } + time = Math.round(time); + var minutes = Math.floor(time / 60); + var seconds = String(time % 60).padStart(2, '0'); + return "".concat(minutes, ":").concat(seconds); +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoVolumeContext.js b/src/components/Post/Embed/VideoEmbed/VideoVolumeContext.js new file mode 100644 index 0000000000..6c5c962c63 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoVolumeContext.js @@ -0,0 +1,30 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +var Context = React.createContext(null); +Context.displayName = 'VideoVolumeContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(true), muted = _b[0], setMuted = _b[1]; + var _c = React.useState(1), volume = _c[0], setVolume = _c[1]; + var value = React.useMemo(function () { return ({ + muted: muted, + setMuted: setMuted, + volume: volume, + setVolume: setVolume, + }); }, [muted, setMuted, volume, setVolume]); + return _jsx(Context.Provider, { value: value, children: children }); +} +export function useVideoVolumeState() { + var context = React.useContext(Context); + if (!context) { + throw new Error('useVideoVolumeState must be used within a VideoVolumeProvider'); + } + return [context.volume, context.setVolume]; +} +export function useVideoMuteState() { + var context = React.useContext(Context); + if (!context) { + throw new Error('useVideoMuteState must be used within a VideoVolumeProvider'); + } + return [context.muted, context.setMuted]; +} diff --git a/src/components/Post/Embed/VideoEmbed/index.js b/src/components/Post/Embed/VideoEmbed/index.js new file mode 100644 index 0000000000..208cfff1ba --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/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; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useRef, useState } from 'react'; +import { ActivityIndicator, View } from 'react-native'; +import { ImageBackground } from 'expo-image'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { ErrorBoundary } from '#/view/com/util/ErrorBoundary'; +import { atoms as a } from '#/alf'; +import { Button } from '#/components/Button'; +import { useThrottledValue } from '#/components/hooks/useThrottledValue'; +import { ConstrainedImage } from '#/components/images/AutoSizedImage'; +import { PlayButtonIcon } from '#/components/video/PlayButtonIcon'; +import { VideoEmbedInnerNative } from './VideoEmbedInner/VideoEmbedInnerNative'; +import * as VideoFallback from './VideoEmbedInner/VideoFallback'; +export function VideoEmbed(_a) { + var embed = _a.embed; + var _b = useState(0), key = _b[0], setKey = _b[1]; + var renderError = useCallback(function (error) { return (_jsx(VideoError, { error: error, retry: function () { return setKey(key + 1); } })); }, [key]); + var aspectRatio; + var dims = embed.aspectRatio; + if (dims) { + aspectRatio = dims.width / dims.height; + if (Number.isNaN(aspectRatio)) { + aspectRatio = undefined; + } + } + var constrained; + if (aspectRatio !== undefined) { + var ratio = 1 / 2; // max of 1:2 ratio in feeds + constrained = Math.max(aspectRatio, ratio); + } + var contents = (_jsx(ErrorBoundary, { renderError: renderError, children: _jsx(InnerWrapper, { embed: embed }) }, key)); + return (_jsx(View, { style: [a.pt_xs], children: _jsx(ConstrainedImage, { aspectRatio: constrained || 1, + // slightly smaller max height than images + // images use 16 / 9, for reference + minMobileAspectRatio: 14 / 9, children: contents }) })); +} +function InnerWrapper(_a) { + var embed = _a.embed; + var _ = useLingui()._; + var ref = useRef(null); + var _b = useState('pending'), status = _b[0], setStatus = _b[1]; + var _c = useState(false), isLoading = _c[0], setIsLoading = _c[1]; + var _d = useState(false), isActive = _d[0], setIsActive = _d[1]; + var showSpinner = useThrottledValue(isActive && isLoading, 100); + var showOverlay = !isActive || + isLoading || + (status === 'paused' && !isActive) || + status === 'pending'; + if (!isActive && status !== 'pending') { + setStatus('pending'); + } + return (_jsxs(_Fragment, { children: [_jsx(VideoEmbedInnerNative, { embed: embed, setStatus: setStatus, setIsLoading: setIsLoading, setIsActive: setIsActive, ref: ref }), _jsx(ImageBackground, { source: { uri: embed.thumbnail }, accessibilityIgnoresInvertColors: true, style: [ + a.absolute, + a.inset_0, + { + backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here, + // the play button won't show up on the first render on android 🥴😮‍💨 + display: showOverlay ? 'flex' : 'none', + }, + ], cachePolicy: "memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android + , children: showOverlay && (_jsx(Button, { style: [a.flex_1, a.align_center, a.justify_center], onPress: function () { + var _a; + (_a = ref.current) === null || _a === void 0 ? void 0 : _a.togglePlayback(); + }, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Play video"], ["Play video"])))), children: showSpinner ? (_jsx(View, { style: [ + a.rounded_full, + a.p_xs, + a.align_center, + a.justify_center, + ], children: _jsx(ActivityIndicator, { size: "large", color: "white" }) })) : (_jsx(PlayButtonIcon, {})) })) })] })); +} +function VideoError(_a) { + var retry = _a.retry; + return (_jsxs(VideoFallback.Container, { children: [_jsx(VideoFallback.Text, { children: _jsx(Trans, { children: "An error occurred while loading the video. Please try again later." }) }), _jsx(VideoFallback.RetryButton, { onPress: retry })] })); +} +var templateObject_1; diff --git a/src/components/Post/Embed/VideoEmbed/index.web.js b/src/components/Post/Embed/VideoEmbed/index.web.js new file mode 100644 index 0000000000..cda722ceda --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/index.web.js @@ -0,0 +1,156 @@ +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 { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { ErrorBoundary } from '#/view/com/util/ErrorBoundary'; +import { atoms as a, useTheme } from '#/alf'; +import { useIsWithinMessage } from '#/components/dms/MessageContext'; +import { useFullscreen } from '#/components/hooks/useFullscreen'; +import { ConstrainedImage } from '#/components/images/AutoSizedImage'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import { HLSUnsupportedError, VideoEmbedInnerWeb, VideoNotFoundError, } from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb'; +import { IS_WEB_FIREFOX } from '#/env'; +import { useActiveVideoWeb } from './ActiveVideoWebContext'; +import * as VideoFallback from './VideoEmbedInner/VideoFallback'; +export function VideoEmbed(_a) { + var embed = _a.embed; + var t = useTheme(); + var ref = useRef(null); + var _b = useActiveVideoWeb(), active = _b.active, setActive = _b.setActive, sendPosition = _b.sendPosition, currentActiveView = _b.currentActiveView; + var _c = useState(false), onScreen = _c[0], setOnScreen = _c[1]; + var isFullscreen = useFullscreen()[0]; + var lastKnownTime = useRef(undefined); + useEffect(function () { + if (!ref.current) + return; + if (isFullscreen && !IS_WEB_FIREFOX) + return; + var observer = new IntersectionObserver(function (entries) { + var entry = entries[0]; + if (!entry) + return; + setOnScreen(entry.isIntersecting); + sendPosition(entry.boundingClientRect.y + entry.boundingClientRect.height / 2); + }, { threshold: 0.5 }); + observer.observe(ref.current); + return function () { return observer.disconnect(); }; + }, [sendPosition, isFullscreen]); + var _d = useState(0), key = _d[0], setKey = _d[1]; + var renderError = useCallback(function (error) { return (_jsx(VideoError, { error: error, retry: function () { return setKey(key + 1); } })); }, [key]); + var aspectRatio; + var dims = embed.aspectRatio; + if (dims) { + aspectRatio = dims.width / dims.height; + if (Number.isNaN(aspectRatio)) { + aspectRatio = undefined; + } + } + var constrained; + if (aspectRatio !== undefined) { + var ratio = 1 / 2; // max of 1:2 ratio in feeds + constrained = Math.max(aspectRatio, ratio); + } + var contents = (_jsx("div", { ref: ref, style: { + display: 'flex', + flex: 1, + cursor: 'default', + backgroundColor: t.palette.black, + backgroundImage: "url(".concat(embed.thumbnail, ")"), + backgroundSize: 'contain', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + }, onClick: function (evt) { return evt.stopPropagation(); }, children: _jsx(ErrorBoundary, { renderError: renderError, children: _jsx(OnlyNearScreen, { children: _jsx(VideoEmbedInnerWeb, { embed: embed, active: active, setActive: setActive, onScreen: onScreen, lastKnownTime: lastKnownTime }) }) }, key) })); + return (_jsx(View, { style: [a.pt_xs], children: _jsx(ViewportObserver, { sendPosition: sendPosition, isAnyViewActive: currentActiveView !== null, children: _jsxs(ConstrainedImage, { fullBleed: true, aspectRatio: constrained || 1, + // slightly smaller max height than images + // images use 16 / 9, for reference + minMobileAspectRatio: 14 / 9, children: [contents, _jsx(MediaInsetBorder, {})] }) }) })); +} +var NearScreenContext = createContext(false); +NearScreenContext.displayName = 'VideoNearScreenContext'; +/** + * Renders a 100vh tall div and watches it with an IntersectionObserver to + * send the position of the div when it's near the screen. + * + * IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container. + */ +function ViewportObserver(_a) { + var children = _a.children, sendPosition = _a.sendPosition, isAnyViewActive = _a.isAnyViewActive; + var ref = useRef(null); + var _b = useState(false), nearScreen = _b[0], setNearScreen = _b[1]; + var isFullscreen = useFullscreen()[0]; + var isWithinMessage = useIsWithinMessage(); + // Send position when scrolling. This is done with an IntersectionObserver + // observing a div of 100vh height + useEffect(function () { + if (!ref.current) + return; + if (isFullscreen && !IS_WEB_FIREFOX) + return; + var observer = new IntersectionObserver(function (entries) { + var entry = entries[0]; + if (!entry) + return; + var position = entry.boundingClientRect.y + entry.boundingClientRect.height / 2; + sendPosition(position); + setNearScreen(entry.isIntersecting); + }, { threshold: Array.from({ length: 101 }, function (_, i) { return i / 100; }) }); + observer.observe(ref.current); + return function () { return observer.disconnect(); }; + }, [sendPosition, isFullscreen]); + // In case scrolling hasn't started yet, send up the position + useEffect(function () { + if (ref.current && !isAnyViewActive) { + var rect = ref.current.getBoundingClientRect(); + var position = rect.y + rect.height / 2; + sendPosition(position); + } + }, [isAnyViewActive, sendPosition]); + return (_jsxs(View, { style: [a.flex_1, a.flex_row], children: [_jsx(NearScreenContext.Provider, { value: nearScreen, children: children }), _jsx("div", { ref: ref, style: __assign(__assign({}, (isWithinMessage + ? { top: 0, height: '100%' } + : { top: 'calc(50% - 50vh)', height: '100vh' })), { position: 'absolute', left: '50%', width: 1, pointerEvents: 'none' }) })] })); +} +/** + * Awkward data flow here, but we need to hide the video when it's not near the screen. + * But also, ViewportObserver _must_ not be within a `overflow: hidden` container. + * So we put it at the top level of the component tree here, then hide the children of + * the auto-resizing container. + */ +export var OnlyNearScreen = function (_a) { + var children = _a.children; + var nearScreen = useContext(NearScreenContext); + return nearScreen ? children : null; +}; +function VideoError(_a) { + var error = _a.error, retry = _a.retry; + var _ = useLingui()._; + var showRetryButton = true; + var text = null; + if (error instanceof VideoNotFoundError) { + text = _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Video not found."], ["Video not found."])))); + } + else if (error instanceof HLSUnsupportedError) { + showRetryButton = false; + text = _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Your browser does not support the video format. Please try a different browser."], ["Your browser does not support the video format. Please try a different browser."])))); + } + else { + text = _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["An error occurred while loading the video. Please try again."], ["An error occurred while loading the video. Please try again."])))); + } + return (_jsxs(VideoFallback.Container, { children: [_jsx(VideoFallback.Text, { children: text }), showRetryButton && _jsx(VideoFallback.RetryButton, { onPress: retry })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/Post/Embed/index.js b/src/components/Post/Embed/index.js new file mode 100644 index 0000000000..4d32ce9d64 --- /dev/null +++ b/src/components/Post/Embed/index.js @@ -0,0 +1,182 @@ +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, useMemo } from 'react'; +import { View } from 'react-native'; +import { AppBskyFeedPost, AtUri, moderatePost, RichText as RichTextAPI, } from '@atproto/api'; +import { Trans } from '@lingui/macro'; +import { useQueryClient } from '@tanstack/react-query'; +import { makeProfileLink } from '#/lib/routes/links'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { unstableCacheProfileView } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { Link } from '#/view/com/util/Link'; +import { PostMeta } from '#/view/com/util/PostMeta'; +import { atoms as a, useTheme } from '#/alf'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { ContentHider } from '#/components/moderation/ContentHider'; +import { PostAlerts } from '#/components/moderation/PostAlerts'; +import { RichText } from '#/components/RichText'; +import { Embed as StarterPackCard } from '#/components/StarterPack/StarterPackCard'; +import { SubtleHover } from '#/components/SubtleHover'; +import * as bsky from '#/types/bsky'; +import { parseEmbed, } from '#/types/bsky/post'; +import { ExternalEmbed } from './ExternalEmbed'; +import { ModeratedFeedEmbed } from './FeedEmbed'; +import { ImageEmbed } from './ImageEmbed'; +import { ModeratedListEmbed } from './ListEmbed'; +import { PostPlaceholder as PostPlaceholderText } from './PostPlaceholder'; +import { PostEmbedViewContext, QuoteEmbedViewContext, } from './types'; +import { VideoEmbed } from './VideoEmbed'; +export { PostEmbedViewContext, QuoteEmbedViewContext } from './types'; +export function Embed(_a) { + var rawEmbed = _a.embed, rest = __rest(_a, ["embed"]); + var embed = parseEmbed(rawEmbed); + switch (embed.type) { + case 'images': + case 'link': + case 'video': { + return _jsx(MediaEmbed, __assign({ embed: embed }, rest)); + } + case 'feed': + case 'list': + case 'starter_pack': + case 'labeler': + case 'post': + case 'post_not_found': + case 'post_blocked': + case 'post_detached': { + return _jsx(RecordEmbed, __assign({ embed: embed }, rest)); + } + case 'post_with_media': { + return (_jsxs(View, { style: rest.style, children: [_jsx(MediaEmbed, __assign({ embed: embed.media }, rest)), _jsx(RecordEmbed, __assign({ embed: embed.view }, rest))] })); + } + default: { + return null; + } + } +} +function MediaEmbed(_a) { + var _b, _c, _d; + var embed = _a.embed, rest = __rest(_a, ["embed"]); + switch (embed.type) { + case 'images': { + return (_jsx(ContentHider, { modui: (_b = rest.moderation) === null || _b === void 0 ? void 0 : _b.ui('contentMedia'), activeStyle: [a.mt_sm], children: _jsx(ImageEmbed, __assign({ embed: embed }, rest)) })); + } + case 'link': { + return (_jsx(ContentHider, { modui: (_c = rest.moderation) === null || _c === void 0 ? void 0 : _c.ui('contentMedia'), activeStyle: [a.mt_sm], children: _jsx(ExternalEmbed, { link: embed.view.external, onOpen: rest.onOpen, style: [a.mt_sm, rest.style] }) })); + } + case 'video': { + return (_jsx(ContentHider, { modui: (_d = rest.moderation) === null || _d === void 0 ? void 0 : _d.ui('contentMedia'), activeStyle: [a.mt_sm], children: _jsx(VideoEmbed, { embed: embed.view }) })); + } + default: { + return null; + } + } +} +function RecordEmbed(_a) { + var embed = _a.embed, rest = __rest(_a, ["embed"]); + switch (embed.type) { + case 'feed': { + return (_jsx(View, { style: a.mt_sm, children: _jsx(ModeratedFeedEmbed, __assign({ embed: embed }, rest)) })); + } + case 'list': { + return (_jsx(View, { style: a.mt_sm, children: _jsx(ModeratedListEmbed, { embed: embed }) })); + } + case 'starter_pack': { + return (_jsx(View, { style: a.mt_sm, children: _jsx(StarterPackCard, { starterPack: embed.view }) })); + } + case 'labeler': { + // not implemented + return null; + } + case 'post': { + if (rest.isWithinQuote && !rest.allowNestedQuotes) { + return null; + } + return (_jsx(QuoteEmbed, __assign({}, rest, { embed: embed, viewContext: rest.viewContext === PostEmbedViewContext.Feed + ? QuoteEmbedViewContext.FeedEmbedRecordWithMedia + : undefined, isWithinQuote: rest.isWithinQuote, allowNestedQuotes: rest.allowNestedQuotes }))); + } + case 'post_not_found': { + return (_jsx(PostPlaceholderText, { children: _jsx(Trans, { children: "Deleted" }) })); + } + case 'post_blocked': { + return (_jsx(PostPlaceholderText, { children: _jsx(Trans, { children: "Blocked" }) })); + } + case 'post_detached': { + return _jsx(PostDetachedEmbed, { embed: embed }); + } + default: { + return null; + } + } +} +export function PostDetachedEmbed(_a) { + var embed = _a.embed; + var currentAccount = useSession().currentAccount; + var isViewerOwner = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) + ? embed.view.uri.includes(currentAccount.did) + : false; + return (_jsx(PostPlaceholderText, { children: isViewerOwner ? (_jsx(Trans, { children: "Removed by you" })) : (_jsx(Trans, { children: "Removed by author" })) })); +} +/* + * Nests parent `Embed` component and therefore must live in this file to avoid + * circular imports. + */ +export function QuoteEmbed(_a) { + var embed = _a.embed, onOpen = _a.onOpen, style = _a.style, parentIsWithinQuote = _a.isWithinQuote, parentAllowNestedQuotes = _a.allowNestedQuotes; + var moderationOpts = useModerationOpts(); + var quote = useMemo(function () { + var _a; + return (__assign(__assign({}, embed.view), { $type: 'app.bsky.feed.defs#postView', record: embed.view.value, embed: (_a = embed.view.embeds) === null || _a === void 0 ? void 0 : _a[0] })); + }, [embed]); + var moderation = useMemo(function () { + return moderationOpts ? moderatePost(quote, moderationOpts) : undefined; + }, [quote, moderationOpts]); + var t = useTheme(); + var queryClient = useQueryClient(); + var itemUrip = new AtUri(quote.uri); + var itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey); + var itemTitle = "Post by ".concat(quote.author.handle); + var richText = useMemo(function () { + if (!bsky.dangerousIsType(quote.record, AppBskyFeedPost.isRecord)) + return undefined; + var _a = quote.record, text = _a.text, facets = _a.facets; + return text.trim() + ? new RichTextAPI({ text: text, facets: facets }) + : undefined; + }, [quote.record]); + var onBeforePress = useCallback(function () { + unstableCacheProfileView(queryClient, quote.author); + onOpen === null || onOpen === void 0 ? void 0 : onOpen(); + }, [queryClient, quote.author, onOpen]); + var _b = useInteractionState(), hover = _b.state, onPointerEnter = _b.onIn, onPointerLeave = _b.onOut; + var _c = useInteractionState(), pressed = _c.state, onPressIn = _c.onIn, onPressOut = _c.onOut; + return (_jsx(View, { style: [a.mt_sm], onPointerEnter: onPointerEnter, onPointerLeave: onPointerLeave, children: _jsx(ContentHider, { modui: moderation === null || moderation === void 0 ? void 0 : moderation.ui('contentList'), style: [a.rounded_md, a.border, t.atoms.border_contrast_low, style], activeStyle: [a.p_md, a.pt_sm], childContainerStyle: [a.pt_sm], children: function (_a) { + var active = _a.active; + return (_jsxs(_Fragment, { children: [!active && (_jsx(SubtleHover, { native: true, hover: hover || pressed, style: [a.rounded_md] })), _jsxs(Link, { style: [!active && a.p_md], hoverStyle: t.atoms.border_contrast_high, href: itemHref, title: itemTitle, onBeforePress: onBeforePress, onPressIn: onPressIn, onPressOut: onPressOut, children: [_jsx(View, { pointerEvents: "none", children: _jsx(PostMeta, { author: quote.author, moderation: moderation, showAvatar: true, postHref: itemHref, timestamp: quote.indexedAt }) }), moderation ? (_jsx(PostAlerts, { modui: moderation.ui('contentView'), style: [a.py_xs] })) : null, richText ? (_jsx(RichText, { value: richText, style: a.text_md, numberOfLines: 20, disableLinks: true })) : null, quote.embed && (_jsx(Embed, { embed: quote.embed, moderation: moderation, isWithinQuote: parentIsWithinQuote !== null && parentIsWithinQuote !== void 0 ? parentIsWithinQuote : true, + // already within quote? override nested + allowNestedQuotes: parentIsWithinQuote ? false : parentAllowNestedQuotes }))] })] })); + } }) })); +} diff --git a/src/components/Post/Embed/types.js b/src/components/Post/Embed/types.js new file mode 100644 index 0000000000..9b4cfae887 --- /dev/null +++ b/src/components/Post/Embed/types.js @@ -0,0 +1,10 @@ +export var PostEmbedViewContext; +(function (PostEmbedViewContext) { + PostEmbedViewContext["ThreadHighlighted"] = "ThreadHighlighted"; + PostEmbedViewContext["Feed"] = "Feed"; + PostEmbedViewContext["FeedEmbedRecordWithMedia"] = "FeedEmbedRecordWithMedia"; +})(PostEmbedViewContext || (PostEmbedViewContext = {})); +export var QuoteEmbedViewContext; +(function (QuoteEmbedViewContext) { + QuoteEmbedViewContext["FeedEmbedRecordWithMedia"] = "FeedEmbedRecordWithMedia"; +})(QuoteEmbedViewContext || (QuoteEmbedViewContext = {})); diff --git a/src/components/Post/PostRepliedTo.js b/src/components/Post/PostRepliedTo.js new file mode 100644 index 0000000000..b6436bd5dc --- /dev/null +++ b/src/components/Post/PostRepliedTo.js @@ -0,0 +1,37 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/macro'; +import { useSession } from '#/state/session'; +import { UserInfoText } from '#/view/com/util/UserInfoText'; +import { atoms as a, useTheme } from '#/alf'; +import { ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon } from '#/components/icons/ArrowCornerDownRight'; +import { ProfileHoverCard } from '#/components/ProfileHoverCard'; +import { Text } from '#/components/Typography'; +export function PostRepliedTo(_a) { + var parentAuthor = _a.parentAuthor, isParentBlocked = _a.isParentBlocked, isParentNotFound = _a.isParentNotFound; + var t = useTheme(); + var currentAccount = useSession().currentAccount; + var textStyle = [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]; + var label; + if (isParentBlocked) { + label = _jsx(Trans, { context: "description", children: "Replied to a blocked post" }); + } + else if (isParentNotFound) { + label = _jsx(Trans, { context: "description", children: "Replied to a post" }); + } + else if (parentAuthor) { + var did = typeof parentAuthor === 'string' ? parentAuthor : parentAuthor.did; + var isMe = (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === did; + if (isMe) { + label = _jsx(Trans, { context: "description", children: "Replied to you" }); + } + else { + label = (_jsxs(Trans, { context: "description", children: ["Replied to", ' ', _jsx(ProfileHoverCard, { did: did, children: _jsx(UserInfoText, { did: did, attr: "displayName", style: textStyle }) })] })); + } + } + if (!label) { + // Should not happen. + return null; + } + return (_jsxs(View, { style: [a.flex_row, a.align_center, a.pb_xs, a.gap_xs], children: [_jsx(ArrowCornerDownRightIcon, { size: "xs", style: [t.atoms.text_contrast_medium, { top: -1 }] }), _jsx(Text, { style: [a.flex_1, textStyle], numberOfLines: 1, children: label })] })); +} diff --git a/src/components/Post/ShowMoreTextButton.js b/src/components/Post/ShowMoreTextButton.js new file mode 100644 index 0000000000..08cb956c0a --- /dev/null +++ b/src/components/Post/ShowMoreTextButton.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 } from "react/jsx-runtime"; +import { useCallback, useMemo } from 'react'; +import { LayoutAnimation } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_10 } from '#/lib/constants'; +import { atoms as a, flatten, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { Text } from '#/components/Typography'; +export function ShowMoreTextButton(_a) { + var onPressProp = _a.onPress, style = _a.style; + var t = useTheme(); + var _ = useLingui()._; + var onPress = useCallback(function () { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + onPressProp(); + }, [onPressProp]); + var textStyle = useMemo(function () { + return flatten([a.leading_snug, a.text_sm, style]); + }, [style]); + return (_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Expand post text"], ["Expand post text"])))), onPress: onPress, style: [ + a.self_start, + { + paddingBottom: textStyle.fontSize / 3, + }, + ], hitSlop: HITSLOP_10, children: function (_a) { + var pressed = _a.pressed, hovered = _a.hovered; + return (_jsx(Text, { style: [ + textStyle, + { + color: t.palette.primary_500, + opacity: pressed ? 0.6 : 1, + textDecorationLine: hovered ? 'underline' : undefined, + }, + ], children: _jsx(Trans, { children: "Show More" }) })); + } })); +} +var templateObject_1; diff --git a/src/components/PostControls/BookmarkButton.js b/src/components/PostControls/BookmarkButton.js new file mode 100644 index 0000000000..578906feb3 --- /dev/null +++ b/src/components/PostControls/BookmarkButton.js @@ -0,0 +1,181 @@ +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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { memo } from 'react'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { useFeedFeedbackContext } from '#/state/feed-feedback'; +import { useBookmarkMutation } from '#/state/queries/bookmarks/useBookmarkMutation'; +import { useRequireAuth } from '#/state/session'; +import { useTheme } from '#/alf'; +import { Bookmark, BookmarkFilled } from '#/components/icons/Bookmark'; +import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from '#/components/icons/Trash'; +import * as toast from '#/components/Toast'; +import { useAnalytics } from '#/analytics'; +import { PostControlButton, PostControlButtonIcon } from './PostControlButton'; +export var BookmarkButton = memo(function BookmarkButton(_a) { + var _this = this; + var post = _a.post, big = _a.big, logContext = _a.logContext, hitSlop = _a.hitSlop; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var bookmark = useBookmarkMutation().mutateAsync; + var cleanError = useCleanError(); + var requireAuth = useRequireAuth(); + var feedDescriptor = useFeedFeedbackContext().feedDescriptor; + var viewer = post.viewer; + var isBookmarked = !!(viewer === null || viewer === void 0 ? void 0 : viewer.bookmarked); + var undoLabel = _(msg({ + message: "Undo", + context: "Button label to undo saving/removing a post from saved posts.", + })); + var save = 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 e_1, _b, raw, clean; + var _c = _a === void 0 ? {} : _a, disableUndo = _c.disableUndo; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + _d.trys.push([0, 2, , 3]); + return [4 /*yield*/, bookmark({ + action: 'create', + post: post, + })]; + case 1: + _d.sent(); + ax.metric('post:bookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + }); + toast.show(_jsxs(toast.Outer, { children: [_jsx(toast.Icon, {}), _jsx(toast.Text, { children: _jsx(Trans, { children: "Post saved" }) }), !disableUndo && (_jsx(toast.Action, { label: undoLabel, onPress: function () { return remove({ disableUndo: true }); }, children: undoLabel }))] }), { + type: 'success', + }); + return [3 /*break*/, 3]; + case 2: + e_1 = _d.sent(); + _b = cleanError(e_1), raw = _b.raw, clean = _b.clean; + toast.show(clean || raw || e_1, { + type: 'error', + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); + }; + var remove = 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 e_2, _b, raw, clean; + var _c = _a === void 0 ? {} : _a, disableUndo = _c.disableUndo; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + _d.trys.push([0, 2, , 3]); + return [4 /*yield*/, bookmark({ + action: 'delete', + uri: post.uri, + })]; + case 1: + _d.sent(); + ax.metric('post:unbookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + }); + toast.show(_jsxs(toast.Outer, { children: [_jsx(toast.Icon, { icon: TrashIcon }), _jsx(toast.Text, { children: _jsx(Trans, { children: "Removed from saved posts" }) }), !disableUndo && (_jsx(toast.Action, { label: undoLabel, onPress: function () { return save({ disableUndo: true }); }, children: undoLabel }))] })); + return [3 /*break*/, 3]; + case 2: + e_2 = _d.sent(); + _b = cleanError(e_2), raw = _b.raw, clean = _b.clean; + toast.show(clean || raw || e_2, { + type: 'error', + }); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); + }; + var onHandlePress = function () { + return requireAuth(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!isBookmarked) return [3 /*break*/, 2]; + return [4 /*yield*/, remove()]; + case 1: + _a.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, save()]; + case 3: + _a.sent(); + _a.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); }); + }; + return (_jsx(PostControlButton, { testID: "postBookmarkBtn", big: big, label: isBookmarked + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Remove from saved posts"], ["Remove from saved posts"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Add to saved posts"], ["Add to saved posts"])))), onPress: onHandlePress, hitSlop: hitSlop, children: _jsx(PostControlButtonIcon, { fill: isBookmarked ? t.palette.primary_500 : undefined, icon: isBookmarked ? BookmarkFilled : Bookmark }) })); +}); +var templateObject_1, templateObject_2; diff --git a/src/components/PostControls/DiscoverDebug.js b/src/components/PostControls/DiscoverDebug.js new file mode 100644 index 0000000000..5201982906 --- /dev/null +++ b/src/components/PostControls/DiscoverDebug.js @@ -0,0 +1,34 @@ +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 * as Clipboard from 'expo-clipboard'; +import { t } from '@lingui/macro'; +import { DISCOVER_DEBUG_DIDS } from '#/lib/constants'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_INTERNAL } from '#/env'; +export function DiscoverDebug(_a) { + var feedContext = _a.feedContext; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var isDiscoverDebugUser = IS_INTERNAL || + DISCOVER_DEBUG_DIDS[(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) || ''] || + ax.features.enabled(ax.features.DebugFeedContext); + var theme = useTheme(); + return (isDiscoverDebugUser && + feedContext && (_jsx(Pressable, { accessible: false, hitSlop: 10, style: [a.absolute, { zIndex: 1000, maxWidth: 65, bottom: -4 }, a.left_0], onPress: function (e) { + e.stopPropagation(); + Clipboard.setStringAsync(feedContext); + Toast.show(t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Copied to clipboard"], ["Copied to clipboard"])))); + }, children: _jsx(Text, { numberOfLines: 1, style: { + color: theme.palette.contrast_400, + fontSize: 7, + }, children: feedContext }) }))); +} +var templateObject_1; diff --git a/src/components/PostControls/PostControlButton.js b/src/components/PostControls/PostControlButton.js new file mode 100644 index 0000000000..e2a14d4de2 --- /dev/null +++ b/src/components/PostControls/PostControlButton.js @@ -0,0 +1,83 @@ +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, useContext, useMemo } from 'react'; +import { useHaptics } from '#/lib/haptics'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { Text } from '#/components/Typography'; +export var DEFAULT_HITSLOP = { top: 5, bottom: 10, left: 10, right: 10 }; +var PostControlContext = createContext({}); +PostControlContext.displayName = 'PostControlContext'; +// Base button style, which the the other ones extend +export function PostControlButton(_a) { + var ref = _a.ref, onPress = _a.onPress, onLongPress = _a.onLongPress, children = _a.children, big = _a.big, active = _a.active, activeColor = _a.activeColor, props = __rest(_a, ["ref", "onPress", "onLongPress", "children", "big", "active", "activeColor"]); + var t = useTheme(); + var playHaptic = useHaptics(); + var ctx = useMemo(function () { return ({ + big: big, + active: active, + color: { + color: activeColor && active ? activeColor : t.palette.contrast_500, + }, + }); }, [big, active, activeColor, t.palette.contrast_500]); + var style = useMemo(function () { return [ + a.flex_row, + a.align_center, + a.gap_xs, + a.bg_transparent, + { padding: 5 }, + ]; }, []); + var handlePress = useMemo(function () { + if (!onPress) + return; + return function (evt) { + playHaptic('Light'); + onPress(evt); + }; + }, [onPress, playHaptic]); + var handleLongPress = useMemo(function () { + if (!onLongPress) + return; + return function (evt) { + playHaptic('Heavy'); + onLongPress(evt); + }; + }, [onLongPress, playHaptic]); + return (_jsx(Button, __assign({ ref: ref, onPress: handlePress, onLongPress: handleLongPress, style: style, hoverStyle: t.atoms.bg_contrast_25, shape: "round", variant: "ghost", color: "secondary" }, props, { hitSlop: __assign(__assign({}, DEFAULT_HITSLOP), (props.hitSlop || {})), children: typeof children === 'function' ? (function (args) { return (_jsx(PostControlContext.Provider, { value: ctx, children: children(args) })); }) : (_jsx(PostControlContext.Provider, { value: ctx, children: children })) }))); +} +export function PostControlButtonIcon(_a) { + var Comp = _a.icon, style = _a.style, rest = __rest(_a, ["icon", "style"]); + var _b = useContext(PostControlContext), big = _b.big, color = _b.color; + return (_jsx(Comp, __assign({ style: [color, a.pointer_events_none, style] }, rest, { width: big ? 22 : 18 }))); +} +export function PostControlButtonText(_a) { + var style = _a.style, props = __rest(_a, ["style"]); + var _b = useContext(PostControlContext), big = _b.big, active = _b.active, color = _b.color; + return (_jsx(Text, __assign({ style: [ + color, + big ? a.text_md : a.text_sm, + active && a.font_semi_bold, + style, + ] }, props))); +} diff --git a/src/components/PostControls/PostMenu/PostMenuItems.js b/src/components/PostControls/PostMenu/PostMenuItems.js new file mode 100644 index 0000000000..e10288c267 --- /dev/null +++ b/src/components/PostControls/PostMenu/PostMenuItems.js @@ -0,0 +1,481 @@ +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, useMemo } from 'react'; +import { Platform, } from 'react-native'; +import * as Clipboard from 'expo-clipboard'; +import { AppBskyFeedPost, AtUri, } from '@atproto/api'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { DISCOVER_DEBUG_DIDS } from '#/lib/constants'; +import { useOpenLink } from '#/lib/hooks/useOpenLink'; +import { useTranslate } from '#/lib/hooks/useTranslate'; +import { getCurrentRoute } from '#/lib/routes/helpers'; +import { makeProfileLink } from '#/lib/routes/links'; +import { richTextToString } from '#/lib/strings/rich-text-helpers'; +import { toShareUrl } from '#/lib/strings/url-helpers'; +import { logger } from '#/logger'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useFeedFeedbackContext } from '#/state/feed-feedback'; +import { useHiddenPosts, useHiddenPostsApi, useLanguagePrefs, } from '#/state/preferences'; +import { usePinnedPostMutation } from '#/state/queries/pinned-post'; +import { usePostDeleteMutation, useThreadMuteMutationQueue, } from '#/state/queries/post'; +import { useToggleQuoteDetachmentMutation } from '#/state/queries/postgate'; +import { getMaybeDetachedQuoteEmbed } from '#/state/queries/postgate/util'; +import { useProfileBlockMutationQueue, useProfileMuteMutationQueue, } from '#/state/queries/profile'; +import { InvalidInteractionSettingsError, MAX_HIDDEN_REPLIES, MaxHiddenRepliesError, useToggleReplyVisibilityMutation, } from '#/state/queries/threadgate'; +import { useRequireAuth, useSession } from '#/state/session'; +import { useMergedThreadgateHiddenReplies } from '#/state/threadgate-hidden-replies'; +import * as Toast from '#/view/com/util/Toast'; +import { useDialogControl } from '#/components/Dialog'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { PostInteractionSettingsDialog, usePrefetchPostInteractionSettings, } from '#/components/dialogs/PostInteractionSettingsDialog'; +import { Atom_Stroke2_Corner0_Rounded as AtomIcon } from '#/components/icons/Atom'; +import { BubbleQuestion_Stroke2_Corner0_Rounded as Translate } from '#/components/icons/Bubble'; +import { Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon } from '#/components/icons/Clipboard'; +import { EmojiSad_Stroke2_Corner0_Rounded as EmojiSad, EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile, } from '#/components/icons/Emoji'; +import { Eye_Stroke2_Corner0_Rounded as Eye } from '#/components/icons/Eye'; +import { EyeSlash_Stroke2_Corner0_Rounded as EyeSlash } from '#/components/icons/EyeSlash'; +import { Filter_Stroke2_Corner0_Rounded as Filter } from '#/components/icons/Filter'; +import { Mute_Stroke2_Corner0_Rounded as Mute, Mute_Stroke2_Corner0_Rounded as MuteIcon, } from '#/components/icons/Mute'; +import { PersonX_Stroke2_Corner0_Rounded as PersonX } from '#/components/icons/Person'; +import { Pin_Stroke2_Corner0_Rounded as PinIcon } from '#/components/icons/Pin'; +import { SettingsGear2_Stroke2_Corner0_Rounded as Gear } from '#/components/icons/SettingsGear2'; +import { SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute, SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon, } from '#/components/icons/Speaker'; +import { Trash_Stroke2_Corner0_Rounded as Trash } from '#/components/icons/Trash'; +import { Warning_Stroke2_Corner0_Rounded as Warning } from '#/components/icons/Warning'; +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 { useAnalytics } from '#/analytics'; +import { IS_INTERNAL } from '#/env'; +import * as bsky from '#/types/bsky'; +var PostMenuItems = function (_a) { + var _b, _c, _d, _e, _f, _g, _h, _j; + var post = _a.post, postFeedContext = _a.postFeedContext, postReqId = _a.postReqId, record = _a.record, richText = _a.richText, threadgateRecord = _a.threadgateRecord, onShowLess = _a.onShowLess, logContext = _a.logContext; + var _k = useSession(), hasSession = _k.hasSession, currentAccount = _k.currentAccount; + var _ = useLingui()._; + var ax = useAnalytics(); + var langPrefs = useLanguagePrefs(); + var deletePostMutate = usePostDeleteMutation().mutateAsync; + var _l = usePinnedPostMutation(), pinPostMutate = _l.mutateAsync, isPinPending = _l.isPending; + var requireSignIn = useRequireAuth(); + var hiddenPosts = useHiddenPosts(); + var hidePost = useHiddenPostsApi().hidePost; + var feedFeedback = useFeedFeedbackContext(); + var openLink = useOpenLink(); + var translate = useTranslate(); + var navigation = useNavigation(); + var mutedWordsDialogControl = useGlobalDialogsControlContext().mutedWordsDialogControl; + var blockPromptControl = useDialogControl(); + var reportDialogControl = useReportDialogControl(); + var deletePromptControl = useDialogControl(); + var hidePromptControl = useDialogControl(); + var postInteractionSettingsDialogControl = useDialogControl(); + var quotePostDetachConfirmControl = useDialogControl(); + var hideReplyConfirmControl = useDialogControl(); + var toggleReplyVisibility = useToggleReplyVisibilityMutation().mutateAsync; + var postUri = post.uri; + var postCid = post.cid; + var postAuthor = useProfileShadow(post.author); + var quoteEmbed = useMemo(function () { + if (!currentAccount || !post.embed) + return; + return getMaybeDetachedQuoteEmbed({ + viewerDid: currentAccount.did, + post: post, + }); + }, [post, currentAccount]); + var rootUri = ((_c = (_b = record.reply) === null || _b === void 0 ? void 0 : _b.root) === null || _c === void 0 ? void 0 : _c.uri) || postUri; + var isReply = Boolean(record.reply); + var _m = useThreadMuteMutationQueue(post, rootUri), isThreadMuted = _m[0], muteThread = _m[1], unmuteThread = _m[2]; + var isPostHidden = hiddenPosts && hiddenPosts.includes(postUri); + var isAuthor = postAuthor.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var isRootPostAuthor = new AtUri(rootUri).host === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ + threadgateRecord: threadgateRecord, + }); + var isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri); + var isPinned = (_d = post.viewer) === null || _d === void 0 ? void 0 : _d.pinned; + var _o = useToggleQuoteDetachmentMutation(), toggleQuoteDetachment = _o.mutateAsync, isDetachPending = _o.isPending; + var queueBlock = useProfileBlockMutationQueue(postAuthor)[0]; + var _p = useProfileMuteMutationQueue(postAuthor), queueMute = _p[0], queueUnmute = _p[1]; + var prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({ + postUri: post.uri, + rootPostUri: rootUri, + }); + var href = useMemo(function () { + var urip = new AtUri(postUri); + return makeProfileLink(postAuthor, 'post', urip.rkey); + }, [postUri, postAuthor]); + var onDeletePost = function () { + deletePostMutate({ uri: postUri }).then(function () { + Toast.show(_(msg({ message: 'Post deleted', context: 'toast' }))); + var route = getCurrentRoute(navigation.getState()); + if (route.name === 'PostThread') { + var params = route.params; + if (currentAccount && + isAuthor && + (params.name === currentAccount.handle || + params.name === currentAccount.did)) { + var currentHref = makeProfileLink(postAuthor, 'post', params.rkey); + if (currentHref === href && navigation.canGoBack()) { + navigation.goBack(); + } + } + } + }, function (e) { + logger.error('Failed to delete post', { message: e }); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to delete post, please try again"], ["Failed to delete post, please try again"])))), 'xmark'); + }); + }; + var onToggleThreadMute = function () { + try { + if (isThreadMuted) { + unmuteThread(); + ax.metric('post:unmute', { + uri: postUri, + authorDid: postAuthor.did, + logContext: logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You will now receive notifications for this thread"], ["You will now receive notifications for this thread"]))))); + } + else { + muteThread(); + ax.metric('post:mute', { + uri: postUri, + authorDid: postAuthor.did, + logContext: logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }); + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You will no longer receive notifications for this thread"], ["You will no longer receive notifications for this thread"]))))); + } + } + catch (e) { + if ((e === null || e === void 0 ? void 0 : e.name) !== 'AbortError') { + logger.error('Failed to toggle thread mute', { message: e }); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Failed to toggle thread mute, please try again"], ["Failed to toggle thread mute, please try again"])))), 'xmark'); + } + } + }; + var onCopyPostText = function () { + var str = richTextToString(richText, true); + Clipboard.setStringAsync(str); + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Copied to clipboard"], ["Copied to clipboard"])))), 'clipboard-check'); + }; + var onPressTranslate = function () { + var _a; + translate(record.text, langPrefs.primaryLanguage); + 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, + }); + } + }; + var onHidePost = function () { + hidePost({ uri: postUri }); + ax.metric('thread:click:hideReplyForMe', {}); + }; + var hideInPWI = !!((_e = postAuthor.labels) === null || _e === void 0 ? void 0 : _e.find(function (label) { return label.val === '!no-unauthenticated'; })); + var onPressShowMore = function () { + feedFeedback.sendInteraction({ + event: 'app.bsky.feed.defs#requestMore', + item: postUri, + feedContext: postFeedContext, + reqId: postReqId, + }); + ax.metric('post:showMore', { + uri: postUri, + authorDid: postAuthor.did, + logContext: logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }); + Toast.show(_(msg({ message: 'Feedback sent to feed operator', context: 'toast' }))); + }; + var onPressShowLess = function () { + feedFeedback.sendInteraction({ + event: 'app.bsky.feed.defs#requestLess', + item: postUri, + feedContext: postFeedContext, + reqId: postReqId, + }); + ax.metric('post:showLess', { + uri: postUri, + authorDid: postAuthor.did, + logContext: logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }); + if (onShowLess) { + onShowLess({ + item: postUri, + feedContext: postFeedContext, + }); + } + else { + Toast.show(_(msg({ message: 'Feedback sent to feed operator', context: 'toast' }))); + } + }; + var onToggleQuotePostAttachment = function () { return __awaiter(void 0, void 0, void 0, function () { + var action, isDetach, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!quoteEmbed) + return [2 /*return*/]; + action = quoteEmbed.isDetached ? 'reattach' : 'detach'; + isDetach = action === 'detach'; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, toggleQuoteDetachment({ + post: post, + quoteUri: quoteEmbed.uri, + action: quoteEmbed.isDetached ? 'reattach' : 'detach', + })]; + case 2: + _a.sent(); + Toast.show(isDetach + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Quote post was successfully detached"], ["Quote post was successfully detached"])))) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Quote post was re-attached"], ["Quote post was re-attached"]))))); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + Toast.show(_(msg({ message: 'Updating quote attachment failed', context: 'toast' }))); + logger.error("Failed to ".concat(action, " quote"), { safeMessage: e_1.message }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var canHidePostForMe = !isAuthor && !isPostHidden; + var canHideReplyForEveryone = !isAuthor && isRootPostAuthor && !isPostHidden && isReply; + var canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer; + var onToggleReplyVisibility = function () { return __awaiter(void 0, void 0, void 0, function () { + var action, isHide, e_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // TODO no threadgate? + if (!canHideReplyForEveryone) + return [2 /*return*/]; + action = isReplyHiddenByThreadgate ? 'show' : 'hide'; + isHide = action === 'hide'; + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, toggleReplyVisibility({ + postUri: rootUri, + replyUri: postUri, + action: action, + }) + // Log metric only when hiding (not when showing) + ]; + case 2: + _a.sent(); + // Log metric only when hiding (not when showing) + if (isHide) { + ax.metric('thread:click:hideReplyForEveryone', {}); + } + Toast.show(isHide + ? _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Reply was successfully hidden"], ["Reply was successfully hidden"])))) + : _(msg({ message: 'Reply visibility updated', context: 'toast' }))); + return [3 /*break*/, 4]; + case 3: + e_2 = _a.sent(); + if (e_2 instanceof MaxHiddenRepliesError) { + Toast.show(_(msg({ + message: "You can hide a maximum of ".concat(MAX_HIDDEN_REPLIES, " replies."), + context: 'toast', + }))); + } + else if (e_2 instanceof InvalidInteractionSettingsError) { + Toast.show(_(msg({ message: 'Invalid interaction settings.', context: 'toast' }))); + } + else { + Toast.show(_(msg({ + message: 'Updating reply visibility failed', + context: 'toast', + }))); + logger.error("Failed to ".concat(action, " reply"), { safeMessage: e_2.message }); + } + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var onPressPin = function () { + ax.metric(isPinned ? 'post:unpin' : 'post:pin', {}); + pinPostMutate({ + postUri: postUri, + postCid: postCid, + action: isPinned ? 'unpin' : 'pin', + }); + }; + var onBlockAuthor = function () { return __awaiter(void 0, 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*/, queueBlock()]; + case 1: + _a.sent(); + Toast.show(_(msg({ message: 'Account blocked', context: 'toast' }))); + 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 block account', { message: e_3 }); + Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_3.toString())), 'xmark'); + } + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + var onMuteAuthor = function () { return __awaiter(void 0, void 0, void 0, function () { + var e_4, e_5; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!((_a = postAuthor.viewer) === null || _a === void 0 ? void 0 : _a.muted)) return [3 /*break*/, 5]; + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, queueUnmute()]; + case 2: + _b.sent(); + Toast.show(_(msg({ message: 'Account unmuted', context: 'toast' }))); + return [3 /*break*/, 4]; + case 3: + e_4 = _b.sent(); + if ((e_4 === null || e_4 === void 0 ? void 0 : e_4.name) !== 'AbortError') { + logger.error('Failed to unmute account', { message: e_4 }); + Toast.show(_(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_4.toString())), 'xmark'); + } + return [3 /*break*/, 4]; + case 4: return [3 /*break*/, 8]; + case 5: + _b.trys.push([5, 7, , 8]); + return [4 /*yield*/, queueMute()]; + case 6: + _b.sent(); + Toast.show(_(msg({ message: 'Account muted', context: 'toast' }))); + return [3 /*break*/, 8]; + case 7: + e_5 = _b.sent(); + if ((e_5 === null || e_5 === void 0 ? void 0 : e_5.name) !== 'AbortError') { + logger.error('Failed to mute account', { message: e_5 }); + Toast.show(_(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["There was an issue! ", ""], ["There was an issue! ", ""])), e_5.toString())), 'xmark'); + } + return [3 /*break*/, 8]; + case 8: return [2 /*return*/]; + } + }); + }); }; + var onReportMisclassification = function () { + var url = "https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=".concat(toShareUrl(href)); + openLink(url); + }; + var onSignIn = function () { return requireSignIn(function () { }); }; + var isDiscoverDebugUser = IS_INTERNAL || + DISCOVER_DEBUG_DIDS[(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) || ''] || + ax.features.enabled(ax.features.DebugFeedContext); + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Outer, { children: [isAuthor && (_jsxs(_Fragment, { children: [_jsx(Menu.Group, { children: _jsxs(Menu.Item, { testID: "pinPostBtn", label: isPinned + ? _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Unpin from profile"], ["Unpin from profile"])))) + : _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Pin to your profile"], ["Pin to your profile"])))), disabled: isPinPending, onPress: onPressPin, children: [_jsx(Menu.ItemText, { children: isPinned + ? _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Unpin from profile"], ["Unpin from profile"])))) + : _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Pin to your profile"], ["Pin to your profile"])))) }), _jsx(Menu.ItemIcon, { icon: isPinPending ? Loader : PinIcon, position: "right" })] }) }), _jsx(Menu.Divider, {})] })), _jsx(Menu.Group, { children: !hideInPWI || hasSession ? (_jsxs(_Fragment, { children: [_jsxs(Menu.Item, { testID: "postDropdownTranslateBtn", label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Translate"], ["Translate"])))), onPress: onPressTranslate, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Translate"], ["Translate"])))) }), _jsx(Menu.ItemIcon, { icon: Translate, position: "right" })] }), _jsxs(Menu.Item, { testID: "postDropdownCopyTextBtn", label: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Copy post text"], ["Copy post text"])))), onPress: onCopyPostText, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Copy post text"], ["Copy post text"])))) }), _jsx(Menu.ItemIcon, { icon: ClipboardIcon, position: "right" })] })] })) : (_jsxs(Menu.Item, { testID: "postDropdownSignInBtn", label: _(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Sign in to view post"], ["Sign in to view post"])))), onPress: onSignIn, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Sign in to view post"], ["Sign in to view post"])))) }), _jsx(Menu.ItemIcon, { icon: Eye, position: "right" })] })) }), hasSession && feedFeedback.enabled && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { testID: "postDropdownShowMoreBtn", label: _(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Show more like this"], ["Show more like this"])))), onPress: onPressShowMore, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Show more like this"], ["Show more like this"])))) }), _jsx(Menu.ItemIcon, { icon: EmojiSmile, position: "right" })] }), _jsxs(Menu.Item, { testID: "postDropdownShowLessBtn", label: _(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Show less like this"], ["Show less like this"])))), onPress: onPressShowLess, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Show less like this"], ["Show less like this"])))) }), _jsx(Menu.ItemIcon, { icon: EmojiSad, position: "right" })] })] })] })), isDiscoverDebugUser && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsxs(Menu.Item, { testID: "postDropdownReportMisclassificationBtn", label: _(msg(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Assign topic for algo"], ["Assign topic for algo"])))), onPress: onReportMisclassification, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_27 || (templateObject_27 = __makeTemplateObject(["Assign topic for algo"], ["Assign topic for algo"])))) }), _jsx(Menu.ItemIcon, { icon: AtomIcon, position: "right" })] })] })), hasSession && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { testID: "postDropdownMuteThreadBtn", label: isThreadMuted ? _(msg(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Unmute thread"], ["Unmute thread"])))) : _(msg(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Mute thread"], ["Mute thread"])))), onPress: onToggleThreadMute, children: [_jsx(Menu.ItemText, { children: isThreadMuted ? _(msg(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Unmute thread"], ["Unmute thread"])))) : _(msg(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Mute thread"], ["Mute thread"])))) }), _jsx(Menu.ItemIcon, { icon: isThreadMuted ? Unmute : Mute, position: "right" })] }), _jsxs(Menu.Item, { testID: "postDropdownMuteWordsBtn", label: _(msg(templateObject_32 || (templateObject_32 = __makeTemplateObject(["Mute words & tags"], ["Mute words & tags"])))), onPress: function () { return mutedWordsDialogControl.open(); }, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_33 || (templateObject_33 = __makeTemplateObject(["Mute words & tags"], ["Mute words & tags"])))) }), _jsx(Menu.ItemIcon, { icon: Filter, position: "right" })] })] })] })), hasSession && + (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsxs(Menu.Group, { children: [canHidePostForMe && (_jsxs(Menu.Item, { testID: "postDropdownHideBtn", label: isReply + ? _(msg(templateObject_34 || (templateObject_34 = __makeTemplateObject(["Hide reply for me"], ["Hide reply for me"])))) + : _(msg(templateObject_35 || (templateObject_35 = __makeTemplateObject(["Hide post for me"], ["Hide post for me"])))), onPress: function () { return hidePromptControl.open(); }, children: [_jsx(Menu.ItemText, { children: isReply + ? _(msg(templateObject_36 || (templateObject_36 = __makeTemplateObject(["Hide reply for me"], ["Hide reply for me"])))) + : _(msg(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Hide post for me"], ["Hide post for me"])))) }), _jsx(Menu.ItemIcon, { icon: EyeSlash, position: "right" })] })), canHideReplyForEveryone && (_jsxs(Menu.Item, { testID: "postDropdownHideBtn", label: isReplyHiddenByThreadgate + ? _(msg(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Show reply for everyone"], ["Show reply for everyone"])))) + : _(msg(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Hide reply for everyone"], ["Hide reply for everyone"])))), onPress: isReplyHiddenByThreadgate + ? onToggleReplyVisibility + : function () { return hideReplyConfirmControl.open(); }, children: [_jsx(Menu.ItemText, { children: isReplyHiddenByThreadgate + ? _(msg(templateObject_40 || (templateObject_40 = __makeTemplateObject(["Show reply for everyone"], ["Show reply for everyone"])))) + : _(msg(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Hide reply for everyone"], ["Hide reply for everyone"])))) }), _jsx(Menu.ItemIcon, { icon: isReplyHiddenByThreadgate ? Eye : EyeSlash, position: "right" })] })), canDetachQuote && (_jsxs(Menu.Item, { disabled: isDetachPending, testID: "postDropdownHideBtn", label: quoteEmbed.isDetached + ? _(msg(templateObject_42 || (templateObject_42 = __makeTemplateObject(["Re-attach quote"], ["Re-attach quote"])))) + : _(msg(templateObject_43 || (templateObject_43 = __makeTemplateObject(["Detach quote"], ["Detach quote"])))), onPress: quoteEmbed.isDetached + ? onToggleQuotePostAttachment + : function () { return quotePostDetachConfirmControl.open(); }, children: [_jsx(Menu.ItemText, { children: quoteEmbed.isDetached + ? _(msg(templateObject_44 || (templateObject_44 = __makeTemplateObject(["Re-attach quote"], ["Re-attach quote"])))) + : _(msg(templateObject_45 || (templateObject_45 = __makeTemplateObject(["Detach quote"], ["Detach quote"])))) }), _jsx(Menu.ItemIcon, { icon: isDetachPending + ? Loader + : quoteEmbed.isDetached + ? Eye + : EyeSlash, position: "right" })] }))] })] })), hasSession && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsxs(Menu.Group, { children: [!isAuthor && (_jsxs(_Fragment, { children: [_jsxs(Menu.Item, { testID: "postDropdownMuteBtn", label: ((_f = postAuthor.viewer) === null || _f === void 0 ? void 0 : _f.muted) + ? _(msg(templateObject_46 || (templateObject_46 = __makeTemplateObject(["Unmute account"], ["Unmute account"])))) + : _(msg(templateObject_47 || (templateObject_47 = __makeTemplateObject(["Mute account"], ["Mute account"])))), onPress: onMuteAuthor, children: [_jsx(Menu.ItemText, { children: ((_g = postAuthor.viewer) === null || _g === void 0 ? void 0 : _g.muted) + ? _(msg(templateObject_48 || (templateObject_48 = __makeTemplateObject(["Unmute account"], ["Unmute account"])))) + : _(msg(templateObject_49 || (templateObject_49 = __makeTemplateObject(["Mute account"], ["Mute account"])))) }), _jsx(Menu.ItemIcon, { icon: ((_h = postAuthor.viewer) === null || _h === void 0 ? void 0 : _h.muted) ? UnmuteIcon : MuteIcon, position: "right" })] }), !((_j = postAuthor.viewer) === null || _j === void 0 ? void 0 : _j.blocking) && (_jsxs(Menu.Item, { testID: "postDropdownBlockBtn", label: _(msg(templateObject_50 || (templateObject_50 = __makeTemplateObject(["Block account"], ["Block account"])))), onPress: function () { return blockPromptControl.open(); }, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_51 || (templateObject_51 = __makeTemplateObject(["Block account"], ["Block account"])))) }), _jsx(Menu.ItemIcon, { icon: PersonX, position: "right" })] })), _jsxs(Menu.Item, { testID: "postDropdownReportBtn", label: _(msg(templateObject_52 || (templateObject_52 = __makeTemplateObject(["Report post"], ["Report post"])))), onPress: function () { return reportDialogControl.open(); }, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_53 || (templateObject_53 = __makeTemplateObject(["Report post"], ["Report post"])))) }), _jsx(Menu.ItemIcon, { icon: Warning, position: "right" })] })] })), isAuthor && (_jsxs(_Fragment, { children: [_jsxs(Menu.Item, __assign({ testID: "postDropdownEditPostInteractions", label: _(msg(templateObject_54 || (templateObject_54 = __makeTemplateObject(["Edit interaction settings"], ["Edit interaction settings"])))), onPress: function () { return postInteractionSettingsDialogControl.open(); } }, (isAuthor + ? Platform.select({ + web: { + onHoverIn: prefetchPostInteractionSettings, + }, + native: { + onPressIn: prefetchPostInteractionSettings, + }, + }) + : {}), { children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_55 || (templateObject_55 = __makeTemplateObject(["Edit interaction settings"], ["Edit interaction settings"])))) }), _jsx(Menu.ItemIcon, { icon: Gear, position: "right" })] })), _jsxs(Menu.Item, { testID: "postDropdownDeleteBtn", label: _(msg(templateObject_56 || (templateObject_56 = __makeTemplateObject(["Delete post"], ["Delete post"])))), onPress: function () { return deletePromptControl.open(); }, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_57 || (templateObject_57 = __makeTemplateObject(["Delete post"], ["Delete post"])))) }), _jsx(Menu.ItemIcon, { icon: Trash, position: "right" })] })] }))] })] }))] }), _jsx(Prompt.Basic, { control: deletePromptControl, title: _(msg(templateObject_58 || (templateObject_58 = __makeTemplateObject(["Delete this post?"], ["Delete this post?"])))), description: _(msg(templateObject_59 || (templateObject_59 = __makeTemplateObject(["If you remove this post, you won't be able to recover it."], ["If you remove this post, you won't be able to recover it."])))), onConfirm: onDeletePost, confirmButtonCta: _(msg(templateObject_60 || (templateObject_60 = __makeTemplateObject(["Delete"], ["Delete"])))), confirmButtonColor: "negative" }), _jsx(Prompt.Basic, { control: hidePromptControl, title: isReply ? _(msg(templateObject_61 || (templateObject_61 = __makeTemplateObject(["Hide this reply?"], ["Hide this reply?"])))) : _(msg(templateObject_62 || (templateObject_62 = __makeTemplateObject(["Hide this post?"], ["Hide this post?"])))), description: _(msg(templateObject_63 || (templateObject_63 = __makeTemplateObject(["This post will be hidden from feeds and threads. This cannot be undone."], ["This post will be hidden from feeds and threads. This cannot be undone."])))), onConfirm: onHidePost, confirmButtonCta: _(msg(templateObject_64 || (templateObject_64 = __makeTemplateObject(["Hide"], ["Hide"])))) }), _jsx(ReportDialog, { control: reportDialogControl, subject: __assign(__assign({}, post), { $type: 'app.bsky.feed.defs#postView' }) }), _jsx(PostInteractionSettingsDialog, { control: postInteractionSettingsDialogControl, postUri: post.uri, rootPostUri: rootUri, initialThreadgateView: post.threadgate }), _jsx(Prompt.Basic, { control: quotePostDetachConfirmControl, title: _(msg(templateObject_65 || (templateObject_65 = __makeTemplateObject(["Detach quote post?"], ["Detach quote post?"])))), description: _(msg(templateObject_66 || (templateObject_66 = __makeTemplateObject(["This will remove your post from this quote post for all users, and replace it with a placeholder."], ["This will remove your post from this quote post for all users, and replace it with a placeholder."])))), onConfirm: onToggleQuotePostAttachment, confirmButtonCta: _(msg(templateObject_67 || (templateObject_67 = __makeTemplateObject(["Yes, detach"], ["Yes, detach"])))) }), _jsx(Prompt.Basic, { control: hideReplyConfirmControl, title: _(msg(templateObject_68 || (templateObject_68 = __makeTemplateObject(["Hide this reply?"], ["Hide this reply?"])))), description: _(msg(templateObject_69 || (templateObject_69 = __makeTemplateObject(["This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others."], ["This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others."])))), onConfirm: onToggleReplyVisibility, confirmButtonCta: _(msg(templateObject_70 || (templateObject_70 = __makeTemplateObject(["Yes, hide"], ["Yes, hide"])))) }), _jsx(Prompt.Basic, { control: blockPromptControl, title: _(msg(templateObject_71 || (templateObject_71 = __makeTemplateObject(["Block Account?"], ["Block Account?"])))), description: _(msg(templateObject_72 || (templateObject_72 = __makeTemplateObject(["Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."], ["Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."])))), onConfirm: onBlockAuthor, confirmButtonCta: _(msg(templateObject_73 || (templateObject_73 = __makeTemplateObject(["Block"], ["Block"])))), confirmButtonColor: "negative" })] })); +}; +PostMenuItems = memo(PostMenuItems); +export { PostMenuItems }; +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; diff --git a/src/components/PostControls/PostMenu/index.js b/src/components/PostControls/PostMenu/index.js new file mode 100644 index 0000000000..e362c3fa72 --- /dev/null +++ b/src/components/PostControls/PostMenu/index.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; +}; +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 { memo, useMemo, useState } from 'react'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { EventStopper } from '#/view/com/util/EventStopper'; +import { DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal } from '#/components/icons/DotGrid'; +import { useMenuControl } from '#/components/Menu'; +import * as Menu from '#/components/Menu'; +import { PostControlButton, PostControlButtonIcon } from '../PostControlButton'; +import { PostMenuItems } from './PostMenuItems'; +var PostMenuButton = function (_a) { + var testID = _a.testID, post = _a.post, postFeedContext = _a.postFeedContext, postReqId = _a.postReqId, big = _a.big, record = _a.record, richText = _a.richText, timestamp = _a.timestamp, threadgateRecord = _a.threadgateRecord, onShowLess = _a.onShowLess, hitSlop = _a.hitSlop, logContext = _a.logContext; + var _ = useLingui()._; + var menuControl = useMenuControl(); + var _b = useState(false), hasBeenOpen = _b[0], setHasBeenOpen = _b[1]; + var lazyMenuControl = useMemo(function () { return (__assign(__assign({}, menuControl), { open: function () { + setHasBeenOpen(true); + // HACK. We need the state update to be flushed by the time + // menuControl.open() fires but RN doesn't expose flushSync. + setTimeout(menuControl.open); + } })); }, [menuControl, setHasBeenOpen]); + return (_jsx(EventStopper, { onKeyDown: false, children: _jsxs(Menu.Root, { control: lazyMenuControl, children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Open post options menu"], ["Open post options menu"])))), children: function (_a) { + var props = _a.props; + return (_jsx(PostControlButton, __assign({ testID: "postDropdownBtn", big: big, label: props.accessibilityLabel }, props, { hitSlop: hitSlop, children: _jsx(PostControlButtonIcon, { icon: DotsHorizontal }) }))); + } }), hasBeenOpen && ( + // Lazily initialized. Once mounted, they stay mounted. + _jsx(PostMenuItems, { testID: testID, post: post, postFeedContext: postFeedContext, postReqId: postReqId, record: record, richText: richText, timestamp: timestamp, threadgateRecord: threadgateRecord, onShowLess: onShowLess, logContext: logContext }))] }) })); +}; +PostMenuButton = memo(PostMenuButton); +export { PostMenuButton }; +var templateObject_1; diff --git a/src/components/PostControls/RepostButton.js b/src/components/PostControls/RepostButton.js new file mode 100644 index 0000000000..bb1a1e28f3 --- /dev/null +++ b/src/components/PostControls/RepostButton.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 { memo, useCallback } from 'react'; +import { View } from 'react-native'; +import { msg, plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useHaptics } from '#/lib/haptics'; +import { useRequireAuth } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon } from '#/components/icons/Quote'; +import { Repost_Stroke2_Corner3_Rounded as RepostIcon } from '#/components/icons/Repost'; +import { useFormatPostStatCount } from '#/components/PostControls/util'; +import { Text } from '#/components/Typography'; +import { PostControlButton, PostControlButtonIcon, PostControlButtonText, } from './PostControlButton'; +var RepostButton = function (_a) { + var isReposted = _a.isReposted, repostCount = _a.repostCount, onRepost = _a.onRepost, onQuote = _a.onQuote, big = _a.big, embeddingDisabled = _a.embeddingDisabled; + var t = useTheme(); + var _ = useLingui()._; + var requireAuth = useRequireAuth(); + var dialogControl = Dialog.useDialogControl(); + var formatPostStatCount = useFormatPostStatCount(); + var onPress = function () { return requireAuth(function () { return dialogControl.open(); }); }; + var onLongPress = function () { + return requireAuth(function () { + if (embeddingDisabled) { + dialogControl.open(); + } + else { + onQuote(); + } + }); + }; + return (_jsxs(_Fragment, { children: [_jsxs(PostControlButton, { testID: "repostBtn", active: isReposted, activeColor: t.palette.positive_500, big: big, onPress: onPress, onLongPress: onLongPress, label: isReposted + ? _(msg({ + message: "Undo repost (".concat(plural(repostCount || 0, { + one: '# repost', + other: '# reposts', + }), ")"), + comment: 'Accessibility label for the repost button when the post has been reposted, verb followed by number of reposts and noun', + })) + : _(msg({ + message: "Repost (".concat(plural(repostCount || 0, { + one: '# repost', + other: '# reposts', + }), ")"), + comment: 'Accessibility label for the repost button when the post has not been reposted, verb form followed by number of reposts and noun form', + })), children: [_jsx(PostControlButtonIcon, { icon: RepostIcon }), typeof repostCount !== 'undefined' && repostCount > 0 && (_jsx(PostControlButtonText, { testID: "repostCount", children: formatPostStatCount(repostCount) }))] }), _jsxs(Dialog.Outer, { control: dialogControl, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(RepostButtonDialogInner, { isReposted: isReposted, onRepost: onRepost, onQuote: onQuote, embeddingDisabled: embeddingDisabled })] })] })); +}; +RepostButton = memo(RepostButton); +export { RepostButton }; +var RepostButtonDialogInner = function (_a) { + var isReposted = _a.isReposted, onRepost = _a.onRepost, onQuote = _a.onQuote, embeddingDisabled = _a.embeddingDisabled; + var t = useTheme(); + var _ = useLingui()._; + var playHaptic = useHaptics(); + var control = Dialog.useDialogContext(); + var onPressRepost = useCallback(function () { + if (!isReposted) + playHaptic(); + control.close(function () { + onRepost(); + }); + }, [control, isReposted, onRepost, playHaptic]); + var onPressQuote = useCallback(function () { + playHaptic(); + control.close(function () { + onQuote(); + }); + }, [control, onQuote, playHaptic]); + var onPressClose = useCallback(function () { return control.close(); }, [control]); + return (_jsx(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Repost or quote post"], ["Repost or quote post"])))), children: _jsxs(View, { style: a.gap_xl, children: [_jsxs(View, { style: a.gap_xs, children: [_jsxs(Button, { style: [a.justify_start, a.px_md, a.gap_sm], label: isReposted + ? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Remove repost"], ["Remove repost"])))) + : _(msg({ message: "Repost", context: 'action' })), onPress: onPressRepost, size: "large", variant: "ghost", color: "primary", children: [_jsx(RepostIcon, { size: "lg", fill: t.palette.primary_500 }), _jsx(Text, { style: [a.font_semi_bold, a.text_xl], children: isReposted ? (_jsx(Trans, { children: "Remove repost" })) : (_jsx(Trans, { context: "action", children: "Repost" })) })] }), _jsxs(Button, { disabled: embeddingDisabled, testID: "quoteBtn", style: [a.justify_start, a.px_md, a.gap_sm], label: embeddingDisabled + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Quote posts disabled"], ["Quote posts disabled"])))) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Quote post"], ["Quote post"])))), onPress: onPressQuote, size: "large", variant: "ghost", color: "primary", children: [_jsx(QuoteIcon, { size: "lg", fill: embeddingDisabled + ? t.atoms.text_contrast_low.color + : t.palette.primary_500 }), _jsx(Text, { style: [ + a.font_semi_bold, + a.text_xl, + embeddingDisabled && t.atoms.text_contrast_low, + ], children: embeddingDisabled ? (_jsx(Trans, { children: "Quote posts disabled" })) : (_jsx(Trans, { children: "Quote post" })) })] })] }), _jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Cancel quote post"], ["Cancel quote post"])))), onPress: onPressClose, size: "large", color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] }) })); +}; +RepostButtonDialogInner = memo(RepostButtonDialogInner); +export { RepostButtonDialogInner }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/PostControls/RepostButton.web.js b/src/components/PostControls/RepostButton.web.js new file mode 100644 index 0000000000..e6ef8493e4 --- /dev/null +++ b/src/components/PostControls/RepostButton.web.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; +}; +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 { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useRequireAuth } from '#/state/session'; +import { useSession } from '#/state/session'; +import { EventStopper } from '#/view/com/util/EventStopper'; +import { useTheme } from '#/alf'; +import { CloseQuote_Stroke2_Corner1_Rounded as Quote } from '#/components/icons/Quote'; +import { Repost_Stroke2_Corner2_Rounded as Repost } from '#/components/icons/Repost'; +import * as Menu from '#/components/Menu'; +import { PostControlButton, PostControlButtonIcon, PostControlButtonText, } from './PostControlButton'; +import { useFormatPostStatCount } from './util'; +export var RepostButton = function (_a) { + var isReposted = _a.isReposted, repostCount = _a.repostCount, onRepost = _a.onRepost, onQuote = _a.onQuote, big = _a.big, embeddingDisabled = _a.embeddingDisabled; + var t = useTheme(); + var _ = useLingui()._; + var hasSession = useSession().hasSession; + var requireAuth = useRequireAuth(); + var formatPostStatCount = useFormatPostStatCount(); + return hasSession ? (_jsx(EventStopper, { onKeyDown: false, children: _jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Repost or quote post"], ["Repost or quote post"])))), children: function (_a) { + var props = _a.props; + return (_jsxs(PostControlButton, __assign({ testID: "repostBtn", active: isReposted, activeColor: t.palette.positive_500, label: props.accessibilityLabel, big: big }, props, { children: [_jsx(PostControlButtonIcon, { icon: Repost }), typeof repostCount !== 'undefined' && repostCount > 0 && (_jsx(PostControlButtonText, { testID: "repostCount", children: formatPostStatCount(repostCount) }))] }))); + } }), _jsxs(Menu.Outer, { style: { minWidth: 170 }, children: [_jsxs(Menu.Item, { label: isReposted + ? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Undo repost"], ["Undo repost"])))) + : _(msg({ message: "Repost", context: "action" })), testID: "repostDropdownRepostBtn", onPress: onRepost, children: [_jsx(Menu.ItemText, { children: isReposted + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Undo repost"], ["Undo repost"])))) + : _(msg({ message: "Repost", context: "action" })) }), _jsx(Menu.ItemIcon, { icon: Repost, position: "right" })] }), _jsxs(Menu.Item, { disabled: embeddingDisabled, label: embeddingDisabled + ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Quote posts disabled"], ["Quote posts disabled"])))) + : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Quote post"], ["Quote post"])))), testID: "repostDropdownQuoteBtn", onPress: onQuote, children: [_jsx(Menu.ItemText, { children: embeddingDisabled + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Quote posts disabled"], ["Quote posts disabled"])))) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Quote post"], ["Quote post"])))) }), _jsx(Menu.ItemIcon, { icon: Quote, position: "right" })] })] })] }) })) : (_jsxs(PostControlButton, { onPress: function () { return requireAuth(function () { }); }, active: isReposted, activeColor: t.palette.positive_500, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Repost or quote post"], ["Repost or quote post"])))), big: big, children: [_jsx(PostControlButtonIcon, { icon: Repost }), typeof repostCount !== 'undefined' && repostCount > 0 && (_jsx(PostControlButtonText, { testID: "repostCount", children: formatPostStatCount(repostCount) }))] })); +}; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/components/PostControls/ShareMenu/RecentChats.js b/src/components/PostControls/ShareMenu/RecentChats.js new file mode 100644 index 0000000000..6ce96ac3bf --- /dev/null +++ b/src/components/PostControls/ShareMenu/RecentChats.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; +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { ScrollView, View } from 'react-native'; +import { moderateProfile } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { isBlockedOrBlocking, isMuted } from '#/lib/moderation/blocked-and-muted'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useListConvosQuery } from '#/state/queries/messages/list-conversations'; +import { useSession } from '#/state/session'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, tokens, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { useDialogContext } from '#/components/Dialog'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +import { useAnalytics } from '#/analytics'; +export function RecentChats(_a) { + var _b, _c; + var postUri = _a.postUri; + var ax = useAnalytics(); + var control = useDialogContext(); + var currentAccount = useSession().currentAccount; + var data = useListConvosQuery({ status: 'accepted' }).data; + var convos = (_c = (_b = data === null || data === void 0 ? void 0 : data.pages[0]) === null || _b === void 0 ? void 0 : _b.convos) === null || _c === void 0 ? void 0 : _c.slice(0, 10); + var moderationOpts = useModerationOpts(); + var navigation = useNavigation(); + var onSelectChat = function (convoId) { + control.close(function () { + ax.metric('share:press:recentDm', {}); + navigation.navigate('MessagesConversation', { + conversation: convoId, + embed: postUri, + }); + }); + }; + if (!moderationOpts) + return null; + return (_jsxs(View, { style: [a.relative, a.flex_1, { marginHorizontal: tokens.space.md * -1 }], children: [_jsx(ScrollView, { horizontal: true, style: [a.flex_1, a.pt_2xs, { minHeight: 98 }], contentContainerStyle: [a.gap_sm, a.px_md], showsHorizontalScrollIndicator: false, nestedScrollEnabled: true, children: convos && convos.length > 0 ? (convos.map(function (convo) { + var otherMember = convo.members.find(function (member) { return member.did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); + if (!otherMember || + otherMember.handle === 'missing.invalid' || + convo.muted) + return null; + return (_jsx(RecentChatItem, { profile: otherMember, onPress: function () { return onSelectChat(convo.id); }, moderationOpts: moderationOpts }, convo.id)); + })) : (_jsxs(_Fragment, { children: [_jsx(ConvoSkeleton, {}), _jsx(ConvoSkeleton, {}), _jsx(ConvoSkeleton, {}), _jsx(ConvoSkeleton, {}), _jsx(ConvoSkeleton, {})] })) }), convos && convos.length === 0 && _jsx(NoConvos, {})] })); +} +var WIDTH = 80; +function RecentChatItem(_a) { + var _b; + var profileUnshadowed = _a.profile, onPress = _a.onPress, moderationOpts = _a.moderationOpts; + var _ = useLingui()._; + var t = useTheme(); + var profile = useProfileShadow(profileUnshadowed); + var moderation = moderateProfile(profile, moderationOpts); + var name = sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')); + var verification = useSimpleVerificationState({ profile: profile }); + if (isBlockedOrBlocking(profile) || isMuted(profile)) { + return null; + } + return (_jsxs(Button, { onPress: onPress, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Send post to ", ""], ["Send post to ", ""])), name)), style: [ + a.flex_col, + { width: WIDTH }, + a.gap_sm, + a.justify_start, + a.align_center, + ], children: [_jsx(UserAvatar, { avatar: profile.avatar, size: WIDTH - 8, type: ((_b = profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', 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, t.atoms.text_contrast_medium], numberOfLines: 1, children: name }), verification.showBadge && (_jsx(View, { style: [a.pl_2xs], children: _jsx(VerificationCheck, { width: 10, verifier: verification.role === 'verifier' }) }))] })] })); +} +function ConvoSkeleton() { + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_col, + { width: WIDTH, height: WIDTH + 15 }, + a.gap_xs, + a.justify_start, + a.align_center, + ], children: [_jsx(View, { style: [ + t.atoms.bg_contrast_50, + { width: WIDTH - 8, height: WIDTH - 8 }, + a.rounded_full, + ] }), _jsx(View, { style: [ + t.atoms.bg_contrast_50, + { width: WIDTH - 8, height: 10 }, + a.rounded_xs, + ] })] })); +} +function NoConvos() { + var t = useTheme(); + return (_jsxs(View, { style: [ + a.absolute, + a.inset_0, + a.justify_center, + a.align_center, + a.px_2xl, + ], children: [_jsx(View, { style: [a.absolute, a.inset_0, t.atoms.bg_contrast_25, { opacity: 0.5 }] }), _jsx(Text, { style: [ + a.text_sm, + t.atoms.text_contrast_high, + a.text_center, + a.font_semi_bold, + ], children: _jsx(Trans, { children: "Start a conversation, and it will appear here." }) })] })); +} +var templateObject_1; diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.js b/src/components/PostControls/ShareMenu/ShareMenuItems.js new file mode 100644 index 0000000000..ef0ddc013f --- /dev/null +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.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; +}; +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 } from 'react'; +import * as ExpoClipboard from 'expo-clipboard'; +import { AtUri } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { makeProfileLink } from '#/lib/routes/links'; +import { shareText, shareUrl } from '#/lib/sharing'; +import { toShareUrl } from '#/lib/strings/url-helpers'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +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 { useDialogControl } from '#/components/Dialog'; +import { SendViaChatDialog } from '#/components/dms/dialogs/ShareViaChatDialog'; +import { ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon } from '#/components/icons/ArrowOutOfBox'; +import { ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon } from '#/components/icons/ChainLink'; +import { Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon } from '#/components/icons/Clipboard'; +import { PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon } from '#/components/icons/PaperPlane'; +import * as Menu from '#/components/Menu'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS } from '#/env'; +import { useDevMode } from '#/storage/hooks/dev-mode'; +import { RecentChats } from './RecentChats'; +var ShareMenuItems = function (_a) { + var post = _a.post, onShareProp = _a.onShare; + var ax = useAnalytics(); + var hasSession = useSession().hasSession; + var _ = useLingui()._; + var navigation = useNavigation(); + var sendViaChatControl = useDialogControl(); + var devModeEnabled = useDevMode()[0]; + var aa = useAgeAssurance(); + var postUri = post.uri; + var postAuthor = useProfileShadow(post.author); + var href = useMemo(function () { + var urip = new AtUri(postUri); + return makeProfileLink(postAuthor, 'post', urip.rkey); + }, [postUri, postAuthor]); + var hideInPWI = useMemo(function () { + var _a; + return !!((_a = postAuthor.labels) === null || _a === void 0 ? void 0 : _a.find(function (label) { return label.val === '!no-unauthenticated'; })); + }, [postAuthor]); + var onSharePost = function () { + ax.metric('share:press:nativeShare', {}); + var url = toShareUrl(href); + shareUrl(url); + onShareProp(); + }; + var onCopyLink = function () { return __awaiter(void 0, void 0, void 0, function () { + var url; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + ax.metric('share:press:copyLink', {}); + url = toShareUrl(href); + if (!IS_IOS) return [3 /*break*/, 2]; + // iOS only + return [4 /*yield*/, ExpoClipboard.setUrlAsync(url)]; + case 1: + // iOS only + _a.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, ExpoClipboard.setStringAsync(url)]; + case 3: + _a.sent(); + _a.label = 4; + case 4: + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Copied to clipboard"], ["Copied to clipboard"])))), 'clipboard-check'); + onShareProp(); + return [2 /*return*/]; + } + }); + }); }; + var onSelectChatToShareTo = function (conversation) { + navigation.navigate('MessagesConversation', { + conversation: conversation, + embed: postUri, + }); + }; + var onShareATURI = function () { + shareText(postUri); + }; + var onShareAuthorDID = function () { + shareText(postAuthor.did); + }; + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Outer, { children: [hasSession && aa.state.access === aa.Access.Full && (_jsxs(Menu.Group, { children: [_jsx(Menu.ContainerItem, { children: _jsx(RecentChats, { postUri: postUri }) }), _jsxs(Menu.Item, { testID: "postDropdownSendViaDMBtn", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Send via direct message"], ["Send via direct message"])))), onPress: function () { + ax.metric('share:press:openDmSearch', {}); + sendViaChatControl.open(); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Send via direct message" }) }), _jsx(Menu.ItemIcon, { icon: PaperPlaneIcon, position: "right" })] })] })), _jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { testID: "postDropdownShareBtn", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Share via..."], ["Share via..."])))), onPress: onSharePost, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Share via..." }) }), _jsx(Menu.ItemIcon, { icon: ArrowOutOfBoxIcon, position: "right" })] }), _jsxs(Menu.Item, { testID: "postDropdownShareBtn", label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Copy link to post"], ["Copy link to post"])))), onPress: onCopyLink, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Copy link to post" }) }), _jsx(Menu.ItemIcon, { icon: ChainLinkIcon, position: "right" })] })] }), hideInPWI && (_jsx(Menu.Group, { children: _jsx(Menu.ContainerItem, { children: _jsx(Admonition, { type: "warning", style: [a.flex_1, a.border_0, a.p_0, a.bg_transparent], children: _jsx(Trans, { children: "This post is only visible to logged-in users." }) }) }) })), devModeEnabled && (_jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { testID: "postAtUriShareBtn", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Share post at:// URI"], ["Share post at:// URI"])))), onPress: onShareATURI, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Share post at:// URI" }) }), _jsx(Menu.ItemIcon, { icon: ClipboardIcon, position: "right" })] }), _jsxs(Menu.Item, { testID: "postAuthorDIDShareBtn", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Share author DID"], ["Share author DID"])))), onPress: onShareAuthorDID, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Share author DID" }) }), _jsx(Menu.ItemIcon, { icon: ClipboardIcon, position: "right" })] })] }))] }), _jsx(SendViaChatDialog, { control: sendViaChatControl, onSelectChat: onSelectChatToShareTo })] })); +}; +ShareMenuItems = memo(ShareMenuItems); +export { ShareMenuItems }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.types.js b/src/components/PostControls/ShareMenu/ShareMenuItems.types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.web.js b/src/components/PostControls/ShareMenu/ShareMenuItems.web.js new file mode 100644 index 0000000000..6a101d1308 --- /dev/null +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.web.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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo, useMemo } from 'react'; +import { AtUri } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { makeProfileLink } from '#/lib/routes/links'; +import { shareText, shareUrl } from '#/lib/sharing'; +import { toShareUrl } from '#/lib/strings/url-helpers'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useSession } from '#/state/session'; +import { useBreakpoints } from '#/alf'; +import { useDialogControl } from '#/components/Dialog'; +import { EmbedDialog } from '#/components/dialogs/Embed'; +import { SendViaChatDialog } from '#/components/dms/dialogs/ShareViaChatDialog'; +import { ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon } from '#/components/icons/ChainLink'; +import { Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon } from '#/components/icons/Clipboard'; +import { CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon } from '#/components/icons/CodeBrackets'; +import { PaperPlane_Stroke2_Corner0_Rounded as Send } from '#/components/icons/PaperPlane'; +import * as Menu from '#/components/Menu'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import { useDevMode } from '#/storage/hooks/dev-mode'; +var ShareMenuItems = function (_a) { + var post = _a.post, record = _a.record, timestamp = _a.timestamp, onShareProp = _a.onShare; + var ax = useAnalytics(); + var hasSession = useSession().hasSession; + var gtMobile = useBreakpoints().gtMobile; + var _ = useLingui()._; + var navigation = useNavigation(); + var embedPostControl = useDialogControl(); + var sendViaChatControl = useDialogControl(); + var devModeEnabled = useDevMode()[0]; + var aa = useAgeAssurance(); + var postUri = post.uri; + var postCid = post.cid; + var postAuthor = useProfileShadow(post.author); + var href = useMemo(function () { + var urip = new AtUri(postUri); + return makeProfileLink(postAuthor, 'post', urip.rkey); + }, [postUri, postAuthor]); + var hideInPWI = useMemo(function () { + var _a; + return !!((_a = postAuthor.labels) === null || _a === void 0 ? void 0 : _a.find(function (label) { return label.val === '!no-unauthenticated'; })); + }, [postAuthor]); + var onCopyLink = function () { + ax.metric('share:press:copyLink', {}); + var url = toShareUrl(href); + shareUrl(url); + onShareProp(); + }; + var onSelectChatToShareTo = function (conversation) { + ax.metric('share:press:dmSelected', {}); + navigation.navigate('MessagesConversation', { + conversation: conversation, + embed: postUri, + }); + }; + var canEmbed = IS_WEB && gtMobile && !hideInPWI; + var onShareATURI = function () { + shareText(postUri); + }; + var onShareAuthorDID = function () { + shareText(postAuthor.did); + }; + var copyLinkItem = (_jsxs(Menu.Item, { testID: "postDropdownShareBtn", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Copy link to post"], ["Copy link to post"])))), onPress: onCopyLink, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Copy link to post" }) }), _jsx(Menu.ItemIcon, { icon: ChainLinkIcon, position: "right" })] })); + return (_jsxs(_Fragment, { children: [_jsxs(Menu.Outer, { children: [!hideInPWI && copyLinkItem, hasSession && aa.state.access === aa.Access.Full && (_jsxs(Menu.Item, { testID: "postDropdownSendViaDMBtn", label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Send via direct message"], ["Send via direct message"])))), onPress: function () { + ax.metric('share:press:openDmSearch', {}); + sendViaChatControl.open(); + }, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Send via direct message" }) }), _jsx(Menu.ItemIcon, { icon: Send, position: "right" })] })), canEmbed && (_jsxs(Menu.Item, { testID: "postDropdownEmbedBtn", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Embed post"], ["Embed post"])))), onPress: function () { + ax.metric('share:press:embed', {}); + embedPostControl.open(); + }, children: [_jsx(Menu.ItemText, { children: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Embed post"], ["Embed post"])))) }), _jsx(Menu.ItemIcon, { icon: CodeBracketsIcon, position: "right" })] })), hideInPWI && (_jsxs(_Fragment, { children: [hasSession && _jsx(Menu.Divider, {}), copyLinkItem, _jsx(Menu.LabelText, { style: { maxWidth: 220 }, children: _jsx(Trans, { children: "Note: This post is only visible to logged-in users." }) })] })), devModeEnabled && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsxs(Menu.Item, { testID: "postAtUriShareBtn", label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Copy post at:// URI"], ["Copy post at:// URI"])))), onPress: onShareATURI, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Copy post at:// URI" }) }), _jsx(Menu.ItemIcon, { icon: ClipboardIcon, position: "right" })] }), _jsxs(Menu.Item, { testID: "postAuthorDIDShareBtn", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Copy author DID"], ["Copy author DID"])))), onPress: onShareAuthorDID, children: [_jsx(Menu.ItemText, { children: _jsx(Trans, { children: "Copy author DID" }) }), _jsx(Menu.ItemIcon, { icon: ClipboardIcon, position: "right" })] })] }))] }), canEmbed && (_jsx(EmbedDialog, { control: embedPostControl, postCid: postCid, postUri: postUri, record: record, postAuthor: postAuthor, timestamp: timestamp })), _jsx(SendViaChatDialog, { control: sendViaChatControl, onSelectChat: onSelectChatToShareTo })] })); +}; +ShareMenuItems = memo(ShareMenuItems); +export { ShareMenuItems }; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/PostControls/ShareMenu/index.js b/src/components/PostControls/ShareMenu/index.js new file mode 100644 index 0000000000..351c4eebc0 --- /dev/null +++ b/src/components/PostControls/ShareMenu/index.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; +}; +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 { memo, useMemo, useState } from 'react'; +import { AtUri, } from '@atproto/api'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { makeProfileLink } from '#/lib/routes/links'; +import { shareUrl } from '#/lib/sharing'; +import { toShareUrl } from '#/lib/strings/url-helpers'; +import { useFeedFeedbackContext } from '#/state/feed-feedback'; +import { EventStopper } from '#/view/com/util/EventStopper'; +import { native } from '#/alf'; +import { ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon } from '#/components/icons/ArrowShareRight'; +import { useMenuControl } from '#/components/Menu'; +import * as Menu from '#/components/Menu'; +import { useAnalytics } from '#/analytics'; +import { PostControlButton, PostControlButtonIcon } from '../PostControlButton'; +import { ShareMenuItems } from './ShareMenuItems'; +var ShareMenuButton = function (_a) { + var testID = _a.testID, post = _a.post, big = _a.big, record = _a.record, richText = _a.richText, timestamp = _a.timestamp, threadgateRecord = _a.threadgateRecord, onShare = _a.onShare, hitSlop = _a.hitSlop, logContext = _a.logContext; + var ax = useAnalytics(); + var _ = useLingui()._; + var feedDescriptor = useFeedFeedbackContext().feedDescriptor; + var menuControl = useMenuControl(); + var _b = useState(false), hasBeenOpen = _b[0], setHasBeenOpen = _b[1]; + var lazyMenuControl = useMemo(function () { return (__assign(__assign({}, menuControl), { open: function () { + setHasBeenOpen(true); + // HACK. We need the state update to be flushed by the time + // menuControl.open() fires but RN doesn't expose flushSync. + setTimeout(menuControl.open); + ax.metric('post:share', { + uri: post.uri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + postContext: big ? 'thread' : 'feed', + }); + } })); }, [ + ax, + menuControl, + setHasBeenOpen, + big, + logContext, + feedDescriptor, + post.uri, + post.author.did, + ]); + var onNativeLongPress = function () { + ax.metric('share:press:nativeShare', {}); + var urip = new AtUri(post.uri); + var href = makeProfileLink(post.author, 'post', urip.rkey); + var url = toShareUrl(href); + shareUrl(url); + onShare(); + }; + return (_jsx(EventStopper, { onKeyDown: false, children: _jsxs(Menu.Root, { control: lazyMenuControl, children: [_jsx(Menu.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Open share menu"], ["Open share menu"])))), children: function (_a) { + var props = _a.props; + return (_jsx(PostControlButton, __assign({ testID: "postShareBtn", big: big, label: props.accessibilityLabel }, props, { onLongPress: native(onNativeLongPress), hitSlop: hitSlop, children: _jsx(PostControlButtonIcon, { icon: ArrowShareRightIcon }) }))); + } }), hasBeenOpen && ( + // Lazily initialized. Once mounted, they stay mounted. + _jsx(ShareMenuItems, { testID: testID, post: post, record: record, richText: richText, timestamp: timestamp, threadgateRecord: threadgateRecord, onShare: onShare }))] }) })); +}; +ShareMenuButton = memo(ShareMenuButton); +export { ShareMenuButton }; +var templateObject_1; diff --git a/src/components/PostControls/index.js b/src/components/PostControls/index.js new file mode 100644 index 0000000000..a247d38bed --- /dev/null +++ b/src/components/PostControls/index.js @@ -0,0 +1,283 @@ +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 { memo, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { msg, plural } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { CountWheel } from '#/lib/custom-animations/CountWheel'; +import { AnimatedLikeIcon } from '#/lib/custom-animations/LikeIcon'; +import { useHaptics } from '#/lib/haptics'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { useFeedFeedbackContext } from '#/state/feed-feedback'; +import { usePostLikeMutationQueue, usePostRepostMutationQueue, } from '#/state/queries/post'; +import { useRequireAuth } from '#/state/session'; +import { ProgressGuideAction, useProgressGuideControls, } from '#/state/shell/progress-guide'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Reply as Bubble } from '#/components/icons/Reply'; +import { useFormatPostStatCount } from '#/components/PostControls/util'; +import * as Skele from '#/components/Skeleton'; +import { useAnalytics } from '#/analytics'; +import { BookmarkButton } from './BookmarkButton'; +import { PostControlButton, PostControlButtonIcon, PostControlButtonText, } from './PostControlButton'; +import { PostMenuButton } from './PostMenu'; +import { RepostButton } from './RepostButton'; +import { ShareMenuButton } from './ShareMenu'; +var PostControls = function (_a) { + var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; + var big = _a.big, post = _a.post, record = _a.record, richText = _a.richText, feedContext = _a.feedContext, reqId = _a.reqId, style = _a.style, onPressReply = _a.onPressReply, onPostReply = _a.onPostReply, logContext = _a.logContext, threadgateRecord = _a.threadgateRecord, onShowLess = _a.onShowLess, viaRepost = _a.viaRepost, variant = _a.variant; + var ax = useAnalytics(); + var _ = useLingui()._; + var openComposer = useOpenComposer().openComposer; + var feedDescriptor = useFeedFeedbackContext().feedDescriptor; + var _p = usePostLikeMutationQueue(post, viaRepost, feedDescriptor, logContext), queueLike = _p[0], queueUnlike = _p[1]; + var _q = usePostRepostMutationQueue(post, viaRepost, feedDescriptor, logContext), queueRepost = _q[0], queueUnrepost = _q[1]; + var requireAuth = useRequireAuth(); + var sendInteraction = useFeedFeedbackContext().sendInteraction; + var captureAction = useProgressGuideControls().captureAction; + var playHaptic = useHaptics(); + var isBlocked = Boolean(((_b = post.author.viewer) === null || _b === void 0 ? void 0 : _b.blocking) || + ((_c = post.author.viewer) === null || _c === void 0 ? void 0 : _c.blockedBy) || + ((_d = post.author.viewer) === null || _d === void 0 ? void 0 : _d.blockingByList)); + var replyDisabled = (_e = post.viewer) === null || _e === void 0 ? void 0 : _e.replyDisabled; + var gtPhone = useBreakpoints().gtPhone; + var formatPostStatCount = useFormatPostStatCount(); + var _r = useState(false), hasLikeIconBeenToggled = _r[0], setHasLikeIconBeenToggled = _r[1]; + var onPressToggleLike = function () { return __awaiter(void 0, void 0, void 0, function () { + var e_1; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (isBlocked) { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Cannot interact with a blocked user"], ["Cannot interact with a blocked user"])))), 'exclamation-circle'); + return [2 /*return*/]; + } + _b.label = 1; + case 1: + _b.trys.push([1, 6, , 7]); + setHasLikeIconBeenToggled(true); + if (!!((_a = post.viewer) === null || _a === void 0 ? void 0 : _a.like)) return [3 /*break*/, 3]; + playHaptic('Light'); + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionLike', + feedContext: feedContext, + reqId: reqId, + }); + captureAction(ProgressGuideAction.Like); + return [4 /*yield*/, queueLike()]; + case 2: + _b.sent(); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, queueUnlike()]; + case 4: + _b.sent(); + _b.label = 5; + case 5: return [3 /*break*/, 7]; + case 6: + e_1 = _b.sent(); + if ((e_1 === null || e_1 === void 0 ? void 0 : e_1.name) !== 'AbortError') { + throw e_1; + } + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); }; + var onRepost = function () { return __awaiter(void 0, void 0, void 0, function () { + var e_2; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (isBlocked) { + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Cannot interact with a blocked user"], ["Cannot interact with a blocked user"])))), 'exclamation-circle'); + return [2 /*return*/]; + } + _b.label = 1; + case 1: + _b.trys.push([1, 6, , 7]); + if (!!((_a = post.viewer) === null || _a === void 0 ? void 0 : _a.repost)) return [3 /*break*/, 3]; + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionRepost', + feedContext: feedContext, + reqId: reqId, + }); + return [4 /*yield*/, queueRepost()]; + case 2: + _b.sent(); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, queueUnrepost()]; + case 4: + _b.sent(); + _b.label = 5; + case 5: return [3 /*break*/, 7]; + case 6: + e_2 = _b.sent(); + if ((e_2 === null || e_2 === void 0 ? void 0 : e_2.name) !== 'AbortError') { + throw e_2; + } + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); }; + var onQuote = function () { + if (isBlocked) { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Cannot interact with a blocked user"], ["Cannot interact with a blocked user"])))), 'exclamation-circle'); + return; + } + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionQuote', + feedContext: feedContext, + reqId: reqId, + }); + ax.metric('post:clickQuotePost', { + uri: post.uri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + }); + openComposer({ + quote: post, + onPost: onPostReply, + }); + }; + var onShare = function () { + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionShare', + feedContext: feedContext, + reqId: reqId, + }); + }; + var secondaryControlSpacingStyles = useSecondaryControlSpacingStyles({ + variant: variant, + big: big, + gtPhone: gtPhone, + }); + return (_jsxs(View, { style: [ + a.flex_row, + a.justify_between, + a.align_center, + !big && a.pt_2xs, + a.gap_md, + style, + ], children: [_jsxs(View, { style: [a.flex_row, a.flex_1, { maxWidth: 320 }], children: [_jsx(View, { style: [ + a.flex_1, + a.align_start, + { marginLeft: big ? -2 : -6 }, + replyDisabled ? { opacity: 0.6 } : undefined, + ], children: _jsxs(PostControlButton, { testID: "replyBtn", onPress: !replyDisabled + ? function () { + return requireAuth(function () { + ax.metric('post:clickReply', { + uri: post.uri, + authorDid: post.author.did, + logContext: logContext, + feedDescriptor: feedDescriptor, + }); + onPressReply(); + }); + } + : undefined, label: _(msg({ + message: "Reply (".concat(plural(post.replyCount || 0, { + one: '# reply', + other: '# replies', + }), ")"), + comment: 'Accessibility label for the reply button, verb form followed by number of replies and noun form', + })), big: big, children: [_jsx(PostControlButtonIcon, { icon: Bubble }), typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (_jsx(PostControlButtonText, { children: formatPostStatCount(post.replyCount) }))] }) }), _jsx(View, { style: [a.flex_1, a.align_start], children: _jsx(RepostButton, { isReposted: !!((_f = post.viewer) === null || _f === void 0 ? void 0 : _f.repost), repostCount: ((_g = post.repostCount) !== null && _g !== void 0 ? _g : 0) + ((_h = post.quoteCount) !== null && _h !== void 0 ? _h : 0), onRepost: onRepost, onQuote: onQuote, big: big, embeddingDisabled: Boolean((_j = post.viewer) === null || _j === void 0 ? void 0 : _j.embeddingDisabled) }) }), _jsx(View, { style: [a.flex_1, a.align_start], children: _jsxs(PostControlButton, { testID: "likeBtn", big: big, onPress: function () { return requireAuth(function () { return onPressToggleLike(); }); }, label: ((_k = post.viewer) === null || _k === void 0 ? void 0 : _k.like) + ? _(msg({ + message: "Unlike (".concat(plural(post.likeCount || 0, { + one: '# like', + other: '# likes', + }), ")"), + comment: 'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun', + })) + : _(msg({ + message: "Like (".concat(plural(post.likeCount || 0, { + one: '# like', + other: '# likes', + }), ")"), + comment: 'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form', + })), children: [_jsx(AnimatedLikeIcon, { isLiked: Boolean((_l = post.viewer) === null || _l === void 0 ? void 0 : _l.like), big: big, hasBeenToggled: hasLikeIconBeenToggled }), _jsx(CountWheel, { likeCount: (_m = post.likeCount) !== null && _m !== void 0 ? _m : 0, big: big, isLiked: Boolean((_o = post.viewer) === null || _o === void 0 ? void 0 : _o.like), hasBeenToggled: hasLikeIconBeenToggled })] }) }), _jsx(View, {})] }), _jsxs(View, { style: [a.flex_row, a.justify_end, secondaryControlSpacingStyles], children: [_jsx(BookmarkButton, { post: post, big: big, logContext: logContext, hitSlop: { + right: secondaryControlSpacingStyles.gap / 2, + } }), _jsx(ShareMenuButton, { testID: "postShareBtn", post: post, big: big, record: record, richText: richText, timestamp: post.indexedAt, threadgateRecord: threadgateRecord, onShare: onShare, hitSlop: { + left: secondaryControlSpacingStyles.gap / 2, + right: secondaryControlSpacingStyles.gap / 2, + }, logContext: logContext }), _jsx(PostMenuButton, { testID: "postDropdownBtn", post: post, postFeedContext: feedContext, postReqId: reqId, big: big, record: record, richText: richText, timestamp: post.indexedAt, threadgateRecord: threadgateRecord, onShowLess: onShowLess, hitSlop: { + left: secondaryControlSpacingStyles.gap / 2, + }, logContext: logContext })] })] })); +}; +PostControls = memo(PostControls); +export { PostControls }; +export function PostControlsSkeleton(_a) { + var big = _a.big, style = _a.style, variant = _a.variant; + var gtPhone = useBreakpoints().gtPhone; + var rowHeight = big ? 32 : 28; + var padding = 4; + var size = rowHeight - padding * 2; + var secondaryControlSpacingStyles = useSecondaryControlSpacingStyles({ + variant: variant, + big: big, + gtPhone: gtPhone, + }); + var itemStyles = { + padding: padding, + }; + return (_jsxs(Skele.Row, { style: [a.flex_row, a.justify_between, a.align_center, a.gap_md, style], children: [_jsxs(View, { style: [a.flex_row, a.flex_1, { maxWidth: 320 }], children: [_jsx(View, { style: [itemStyles, a.flex_1, a.align_start, { marginLeft: -padding }], children: _jsx(Skele.Pill, { blend: true, size: size }) }), _jsx(View, { style: [itemStyles, a.flex_1, a.align_start], children: _jsx(Skele.Pill, { blend: true, size: size }) }), _jsx(View, { style: [itemStyles, a.flex_1, a.align_start], children: _jsx(Skele.Pill, { blend: true, size: size }) })] }), _jsxs(View, { style: [a.flex_row, a.justify_end, secondaryControlSpacingStyles], children: [_jsx(View, { style: itemStyles, children: _jsx(Skele.Circle, { blend: true, size: size }) }), _jsx(View, { style: itemStyles, children: _jsx(Skele.Circle, { blend: true, size: size }) }), _jsx(View, { style: itemStyles, children: _jsx(Skele.Circle, { blend: true, size: size }) })] })] })); +} +function useSecondaryControlSpacingStyles(_a) { + var variant = _a.variant, big = _a.big, gtPhone = _a.gtPhone; + return useMemo(function () { + var gap = 0; // default, we want `gap` to be defined on the resulting object + if (variant !== 'compact') + gap = a.gap_xs.gap; + if (big || gtPhone) + gap = a.gap_sm.gap; + return { gap: gap }; + }, [variant, big, gtPhone]); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/PostControls/util.js b/src/components/PostControls/util.js new file mode 100644 index 0000000000..30a5571299 --- /dev/null +++ b/src/components/PostControls/util.js @@ -0,0 +1,19 @@ +import { useCallback } from 'react'; +import { useLingui } from '@lingui/react'; +/** + * This matches `formatCount` from `view/com/util/numeric/format.ts`, but has + * additional truncation logic for large numbers. `roundingMode` should always + * match the original impl, regardless of if we add more formatting here. + */ +export function useFormatPostStatCount() { + var i18n = useLingui().i18n; + return useCallback(function (postStatCount) { + var isOver10k = postStatCount >= 10000; + return i18n.number(postStatCount, { + notation: 'compact', + maximumFractionDigits: isOver10k ? 0 : 1, + // @ts-expect-error - roundingMode not in the types + roundingMode: 'trunc', + }); + }, [i18n]); +} diff --git a/src/components/ProfileCard.js b/src/components/ProfileCard.js new file mode 100644 index 0000000000..8399ae2df3 --- /dev/null +++ b/src/components/ProfileCard.js @@ -0,0 +1,358 @@ +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 { useMemo } from 'react'; +import { View, } from 'react-native'; +import { moderateProfile, RichText as RichTextApi, } from '@atproto/api'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useActorStatus } from '#/lib/actor-status'; +import { getModerationCauseKey } from '#/lib/moderation'; +import { forceLTR } from '#/lib/strings/bidi'; +import { NON_BREAKING_SPACE } from '#/lib/strings/constants'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useProfileFollowMutationQueue } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { PreviewableUserAvatar, UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, platform, useTheme, } from '#/alf'; +import { Button, ButtonIcon, ButtonText, } from '#/components/Button'; +import { Check_Stroke2_Corner0_Rounded as Check } from '#/components/icons/Check'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { Link as InternalLink } from '#/components/Link'; +import * as Pills from '#/components/Pills'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +export function Default(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, _b = _a.logContext, logContext = _b === void 0 ? 'ProfileCard' : _b, testID = _a.testID, position = _a.position, contextProfileDid = _a.contextProfileDid; + return (_jsx(Link, { testID: testID, profile: profile, children: _jsx(Card, { profile: profile, moderationOpts: moderationOpts, logContext: logContext, position: position, contextProfileDid: contextProfileDid }) })); +} +export function Card(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, _b = _a.logContext, logContext = _b === void 0 ? 'ProfileCard' : _b, position = _a.position, contextProfileDid = _a.contextProfileDid; + return (_jsxs(Outer, { children: [_jsxs(Header, { children: [_jsx(Avatar, { profile: profile, moderationOpts: moderationOpts }), _jsx(NameAndHandle, { profile: profile, moderationOpts: moderationOpts }), _jsx(FollowButton, { profile: profile, moderationOpts: moderationOpts, logContext: logContext, position: position, contextProfileDid: contextProfileDid })] }), _jsx(Labels, { profile: profile, moderationOpts: moderationOpts }), _jsx(Description, { profile: profile })] })); +} +export function Outer(_a) { + var children = _a.children; + return _jsx(View, { style: [a.w_full, a.flex_1, a.gap_xs], 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 Link(_a) { + var profile = _a.profile, children = _a.children, style = _a.style, rest = __rest(_a, ["profile", "children", "style"]); + var _ = useLingui()._; + return (_jsx(InternalLink, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["View ", "'s profile"], ["View ", "'s profile"])), profile.displayName || sanitizeHandle(profile.handle))), to: { + screen: 'Profile', + params: { name: profile.did }, + }, style: [a.flex_col, style] }, rest, { children: children }))); +} +export function Avatar(_a) { + var _b; + var profile = _a.profile, moderationOpts = _a.moderationOpts, onPress = _a.onPress, disabledPreview = _a.disabledPreview, liveOverride = _a.liveOverride, _c = _a.size, size = _c === void 0 ? 40 : _c; + var moderation = moderateProfile(profile, moderationOpts); + var live = useActorStatus(profile).isActive; + return disabledPreview ? (_jsx(UserAvatar, { size: size, avatar: profile.avatar, type: ((_b = profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', moderation: moderation.ui('avatar'), live: liveOverride !== null && liveOverride !== void 0 ? liveOverride : live })) : (_jsx(PreviewableUserAvatar, { size: size, profile: profile, moderation: moderation.ui('avatar'), onBeforePress: onPress, live: liveOverride !== null && liveOverride !== void 0 ? liveOverride : live })); +} +export function AvatarPlaceholder(_a) { + var _b = _a.size, size = _b === void 0 ? 40 : _b; + var t = useTheme(); + return (_jsx(View, { style: [ + a.rounded_full, + t.atoms.bg_contrast_25, + { + width: size, + height: size, + }, + ] })); +} +export function NameAndHandle(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, _b = _a.inline, inline = _b === void 0 ? false : _b; + if (inline) { + return (_jsx(InlineNameAndHandle, { profile: profile, moderationOpts: moderationOpts })); + } + else { + return (_jsxs(View, { style: [a.flex_1], children: [_jsx(Name, { profile: profile, moderationOpts: moderationOpts }), _jsx(Handle, { profile: profile })] })); + } +} +function InlineNameAndHandle(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts; + var t = useTheme(); + var verification = useSimpleVerificationState({ profile: profile }); + var moderation = moderateProfile(profile, moderationOpts); + var name = sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')); + var handle = sanitizeHandle(profile.handle, '@'); + return (_jsxs(View, { style: [a.flex_row, a.align_end, a.flex_shrink], children: [_jsx(Text, { emoji: true, style: [ + a.font_semi_bold, + a.leading_tight, + a.flex_shrink_0, + { maxWidth: '70%' }, + ], numberOfLines: 1, children: forceLTR(name) }), verification.showBadge && (_jsx(View, { style: [ + a.pl_2xs, + a.self_center, + { marginTop: platform({ default: 0, android: -1 }) }, + ], children: _jsx(VerificationCheck, { width: platform({ android: 13, default: 12 }), verifier: verification.role === 'verifier' }) })), _jsx(Text, { emoji: true, style: [ + a.leading_tight, + t.atoms.text_contrast_medium, + { flexShrink: 10 }, + ], numberOfLines: 1, children: NON_BREAKING_SPACE + handle })] })); +} +export function Name(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, style = _a.style, textStyle = _a.textStyle; + 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.flex_row, a.align_center, a.max_w_full, style], children: [_jsx(Text, { emoji: true, style: [ + a.text_md, + a.font_semi_bold, + a.leading_snug, + a.self_start, + a.flex_shrink, + textStyle, + ], numberOfLines: 1, children: name }), verification.showBadge && (_jsx(View, { style: [a.pl_xs], children: _jsx(VerificationCheck, { width: 14, verifier: verification.role === 'verifier' }) }))] })); +} +export function Handle(_a) { + var profile = _a.profile, textStyle = _a.textStyle; + var t = useTheme(); + var handle = sanitizeHandle(profile.handle, '@'); + return (_jsx(Text, { emoji: true, style: [a.leading_snug, t.atoms.text_contrast_medium, textStyle], numberOfLines: 1, children: handle })); +} +export function NameAndHandlePlaceholder() { + var t = useTheme(); + return (_jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsx(View, { style: [ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + width: '60%', + height: 14, + }, + ] }), _jsx(View, { style: [ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + width: '40%', + height: 10, + }, + ] })] })); +} +export function NamePlaceholder(_a) { + var style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + width: '60%', + height: 14, + }, + style, + ] })); +} +export function Description(_a) { + var profileUnshadowed = _a.profile, _b = _a.numberOfLines, numberOfLines = _b === void 0 ? 3 : _b, style = _a.style; + var profile = useProfileShadow(profileUnshadowed); + var rt = useMemo(function () { + if (!('description' in profile)) + return; + var rt = new RichTextApi({ text: profile.description || '' }); + rt.detectFacetsWithoutResolution(); + return rt; + }, [profile]); + if (!rt) + return null; + if (profile.viewer && + (profile.viewer.blockedBy || + profile.viewer.blocking || + profile.viewer.blockingByList)) + return null; + return (_jsx(View, { style: [a.pt_xs], children: _jsx(RichText, { value: rt, style: style, numberOfLines: numberOfLines, disableLinks: true }) })); +} +export function DescriptionPlaceholder(_a) { + var _b = _a.numberOfLines, numberOfLines = _b === void 0 ? 3 : _b; + var t = useTheme(); + return (_jsx(View, { style: [a.pt_2xs, { gap: 6 }], children: Array(numberOfLines) + .fill(0) + .map(function (_, i) { return (_jsx(View, { style: [ + a.rounded_xs, + a.w_full, + t.atoms.bg_contrast_25, + { height: 12, width: i + 1 === numberOfLines ? '60%' : '100%' }, + ] }, i)); }) })); +} +export function FollowButton(props) { + var _a = useSession(), currentAccount = _a.currentAccount, hasSession = _a.hasSession; + var isMe = props.profile.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + return hasSession && !isMe ? _jsx(FollowButtonInner, __assign({}, props)) : null; +} +export function FollowButtonInner(_a) { + var _this = this; + var _b; + var profileUnshadowed = _a.profile, moderationOpts = _a.moderationOpts, logContext = _a.logContext, onPressProp = _a.onPress, onFollow = _a.onFollow, colorInverted = _a.colorInverted, _c = _a.withIcon, withIcon = _c === void 0 ? true : _c, position = _a.position, contextProfileDid = _a.contextProfileDid, rest = __rest(_a, ["profile", "moderationOpts", "logContext", "onPress", "onFollow", "colorInverted", "withIcon", "position", "contextProfileDid"]); + var _ = useLingui()._; + var profile = useProfileShadow(profileUnshadowed); + var moderation = moderateProfile(profile, moderationOpts); + var _d = useProfileFollowMutationQueue(profile, logContext, position, contextProfileDid), queueFollow = _d[0], queueUnfollow = _d[1]; + var isRound = Boolean(rest.shape && rest.shape === 'round'); + var onPressFollow = 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, 3, , 4]); + return [4 /*yield*/, queueFollow()]; + case 2: + _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Following ", ""], ["Following ", ""])), sanitizeDisplayName(profile.displayName || profile.handle, moderation.ui('displayName'))))); + onPressProp === null || onPressProp === void 0 ? void 0 : onPressProp(e); + onFollow === null || onFollow === void 0 ? void 0 : onFollow(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + if ((err_1 === null || err_1 === void 0 ? void 0 : err_1.name) !== 'AbortError') { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["An issue occurred, please try again."], ["An issue occurred, please try again."])))), 'xmark'); + } + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var onPressUnfollow = function (e) { return __awaiter(_this, void 0, void 0, function () { + var err_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + e.preventDefault(); + e.stopPropagation(); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, queueUnfollow()]; + case 2: + _a.sent(); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["No longer following ", ""], ["No longer following ", ""])), sanitizeDisplayName(profile.displayName || profile.handle, moderation.ui('displayName'))))); + onPressProp === null || onPressProp === void 0 ? void 0 : onPressProp(e); + return [3 /*break*/, 4]; + case 3: + err_2 = _a.sent(); + if ((err_2 === null || err_2 === void 0 ? void 0 : err_2.name) !== 'AbortError') { + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["An issue occurred, please try again."], ["An issue occurred, please try again."])))), 'xmark'); + } + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var unfollowLabel = _(msg({ + message: 'Following', + comment: 'User is following this account, click to unfollow', + })); + var followLabel = ((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.followedBy) + ? _(msg({ + message: 'Follow back', + comment: 'User is not following this account, click to follow back', + })) + : _(msg({ + message: 'Follow', + comment: 'User is not following this account, click to follow', + })); + if (!profile.viewer) + return null; + if (profile.viewer.blockedBy || + profile.viewer.blocking || + profile.viewer.blockingByList) + return null; + return (_jsx(View, { children: profile.viewer.following ? (_jsxs(Button, __assign({ label: unfollowLabel, size: "small", variant: "solid", color: "secondary" }, rest, { onPress: onPressUnfollow, children: [withIcon && (_jsx(ButtonIcon, { icon: Check, position: isRound ? undefined : 'left' })), isRound ? null : _jsx(ButtonText, { children: unfollowLabel })] }))) : (_jsxs(Button, __assign({ label: followLabel, size: "small", variant: "solid", color: colorInverted ? 'secondary_inverted' : 'primary' }, rest, { onPress: onPressFollow, children: [withIcon && (_jsx(ButtonIcon, { icon: Plus, position: isRound ? undefined : 'left' })), isRound ? null : _jsx(ButtonText, { children: followLabel })] }))) })); +} +export function FollowButtonPlaceholder(_a) { + var style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.rounded_sm, + t.atoms.bg_contrast_25, + a.w_full, + { + height: 33, + }, + style, + ] })); +} +export function Labels(_a) { + var _b; + var profile = _a.profile, moderationOpts = _a.moderationOpts; + var moderation = moderateProfile(profile, moderationOpts); + var modui = moderation.ui('profileList'); + var followedBy = (_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.followedBy; + if (!followedBy && !modui.inform && !modui.alert) { + return null; + } + return (_jsxs(Pills.Row, { style: [a.pt_xs], children: [followedBy && _jsx(Pills.FollowsYou, {}), modui.alerts.map(function (alert) { return (_jsx(Pills.Label, { cause: alert }, getModerationCauseKey(alert))); }), modui.informs.map(function (inform) { return (_jsx(Pills.Label, { cause: inform }, getModerationCauseKey(inform))); })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/ProfileHoverCard/index.js b/src/components/ProfileHoverCard/index.js new file mode 100644 index 0000000000..3086b6a195 --- /dev/null +++ b/src/components/ProfileHoverCard/index.js @@ -0,0 +1,4 @@ +export function ProfileHoverCard(_a) { + var children = _a.children; + return children; +} diff --git a/src/components/ProfileHoverCard/index.web.js b/src/components/ProfileHoverCard/index.web.js new file mode 100644 index 0000000000..3fb8340fa0 --- /dev/null +++ b/src/components/ProfileHoverCard/index.web.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 { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useCallback } from 'react'; +import { View } from 'react-native'; +import { moderateProfile, } from '@atproto/api'; +import { flip, offset, shift, size, useFloating } from '@floating-ui/react-dom'; +import { msg, plural } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { useActorStatus } from '#/lib/actor-status'; +import { getModerationCauseKey } from '#/lib/moderation'; +import { makeProfileLink } from '#/lib/routes/links'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useProfileShadow } from '#/state/cache/profile-shadow'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { usePrefetchProfileQuery, useProfileQuery } from '#/state/queries/profile'; +import { useSession } from '#/state/session'; +import { formatCount } from '#/view/com/util/numeric/format'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { ProfileHeaderHandle } from '#/screens/Profile/Header/Handle'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useFollowMethods } from '#/components/hooks/useFollowMethods'; +import { useRichText } from '#/components/hooks/useRichText'; +import { Check_Stroke2_Corner0_Rounded as Check } from '#/components/icons/Check'; +import { PlusLarge_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { KnownFollowers, shouldShowKnownFollowers, } from '#/components/KnownFollowers'; +import { InlineLinkText, Link } from '#/components/Link'; +import { LiveStatus } from '#/components/live/LiveStatusDialog'; +import { Loader } from '#/components/Loader'; +import * as Pills from '#/components/Pills'; +import { Portal } from '#/components/Portal'; +import { RichText } from '#/components/RichText'; +import { Text } from '#/components/Typography'; +import { useSimpleVerificationState } from '#/components/verification'; +import { VerificationCheck } from '#/components/verification/VerificationCheck'; +import { IS_WEB_TOUCH_DEVICE } from '#/env'; +var floatingMiddlewares = [ + offset(4), + flip({ padding: 16 }), + shift({ padding: 16 }), + size({ + padding: 16, + apply: function (_a) { + var availableWidth = _a.availableWidth, availableHeight = _a.availableHeight, elements = _a.elements; + Object.assign(elements.floating.style, { + maxWidth: "".concat(availableWidth, "px"), + maxHeight: "".concat(availableHeight, "px"), + }); + }, + }), +]; +export function ProfileHoverCard(props) { + var prefetchProfileQuery = usePrefetchProfileQuery(); + var prefetchedProfile = React.useRef(false); + var onPointerMove = function () { + if (!prefetchedProfile.current) { + prefetchedProfile.current = true; + prefetchProfileQuery(props.did); + } + }; + if (props.disable || IS_WEB_TOUCH_DEVICE) { + return props.children; + } + else { + return (_jsx(View, { onPointerMove: onPointerMove, style: [a.flex_shrink, props.inline && a.inline, props.style], children: _jsx(ProfileHoverCardInner, __assign({}, props)) })); + } +} +var SHOW_DELAY = 500; +var SHOW_DURATION = 300; +var HIDE_DELAY = 150; +var HIDE_DURATION = 200; +export function ProfileHoverCardInner(props) { + var _this = this; + var navigation = useNavigation(); + var _a = useFloating({ + middleware: floatingMiddlewares, + }), refs = _a.refs, floatingStyles = _a.floatingStyles; + var _b = React.useReducer( + // Tip: console.log(state, action) when debugging. + function (state, action) { + // Pressing within a card should always hide it. + // No matter which stage we're in. + if (action === 'pressed') { + return hidden(); + } + // --- Hidden --- + // In the beginning, the card is not displayed. + function hidden() { + return { stage: 'hidden' }; + } + if (state.stage === 'hidden') { + // The user can kick things off by hovering a target. + if (action === 'hovered-target') { + return mightShow({ + reason: action, + }); + } + } + // --- Might Show --- + // The card is not visible yet but we're considering showing it. + function mightShow(_a) { + var _b = _a.waitMs, waitMs = _b === void 0 ? SHOW_DELAY : _b, reason = _a.reason; + return { + stage: 'might-show', + reason: reason, + effect: function () { + var id = setTimeout(function () { return dispatch('hovered-long-enough'); }, waitMs); + return function () { + clearTimeout(id); + }; + }, + }; + } + if (state.stage === 'might-show') { + // We'll make a decision at the end of a grace period timeout. + if (action === 'unhovered-target' || action === 'unhovered-card') { + return hidden(); + } + if (action === 'hovered-long-enough') { + return showing({ + reason: state.reason, + }); + } + } + // --- Showing --- + // The card is beginning to show up and then will remain visible. + function showing(_a) { + var reason = _a.reason; + return { + stage: 'showing', + reason: reason, + effect: function () { + function onScroll() { + dispatch('scrolled-while-showing'); + } + window.addEventListener('scroll', onScroll); + return function () { return window.removeEventListener('scroll', onScroll); }; + }, + }; + } + if (state.stage === 'showing') { + // If the user moves the pointer away, we'll begin to consider hiding it. + if (action === 'unhovered-target' || action === 'unhovered-card') { + return mightHide(); + } + // Scrolling away if the hover is on the target instantly hides without a delay. + // If the hover is already on the card, we won't this. + if (state.reason === 'hovered-target' && + action === 'scrolled-while-showing') { + return hiding(); + } + } + // --- Might Hide --- + // The user has moved hover away from a visible card. + function mightHide(_a) { + var _b = _a === void 0 ? {} : _a, _c = _b.waitMs, waitMs = _c === void 0 ? HIDE_DELAY : _c; + return { + stage: 'might-hide', + effect: function () { + var id = setTimeout(function () { return dispatch('unhovered-long-enough'); }, waitMs); + return function () { return clearTimeout(id); }; + }, + }; + } + if (state.stage === 'might-hide') { + // We'll make a decision based on whether it received hover again in time. + if (action === 'hovered-target' || action === 'hovered-card') { + return showing({ + reason: action, + }); + } + if (action === 'unhovered-long-enough') { + return hiding(); + } + } + // --- Hiding --- + // The user waited enough outside that we're hiding the card. + function hiding(_a) { + var _b = _a === void 0 ? {} : _a, _c = _b.animationDurationMs, animationDurationMs = _c === void 0 ? HIDE_DURATION : _c; + return { + stage: 'hiding', + effect: function () { + var id = setTimeout(function () { return dispatch('finished-animating-hide'); }, animationDurationMs); + return function () { return clearTimeout(id); }; + }, + }; + } + if (state.stage === 'hiding') { + // While hiding, we don't want to be interrupted by anything else. + // When the animation finishes, we loop back to the initial hidden state. + if (action === 'finished-animating-hide') { + return hidden(); + } + } + return state; + }, { stage: 'hidden' }), currentState = _b[0], dispatch = _b[1]; + React.useEffect(function () { + if (currentState.effect) { + var effect = currentState.effect; + return effect(); + } + }, [currentState]); + var prefetchProfileQuery = usePrefetchProfileQuery(); + var prefetchedProfile = React.useRef(false); + var prefetchIfNeeded = React.useCallback(function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (!prefetchedProfile.current) { + prefetchedProfile.current = true; + prefetchProfileQuery(props.did); + } + return [2 /*return*/]; + }); + }); }, [prefetchProfileQuery, props.did]); + var didFireHover = React.useRef(false); + var onPointerMoveTarget = React.useCallback(function () { + prefetchIfNeeded(); + // Conceptually we want something like onPointerEnter, + // but we want to ignore entering only due to scrolling. + // So instead we hover on the first onPointerMove. + if (!didFireHover.current) { + didFireHover.current = true; + dispatch('hovered-target'); + } + }, [prefetchIfNeeded]); + var onPointerLeaveTarget = React.useCallback(function () { + didFireHover.current = false; + dispatch('unhovered-target'); + }, []); + var onPointerEnterCard = React.useCallback(function () { + dispatch('hovered-card'); + }, []); + var onPointerLeaveCard = React.useCallback(function () { + dispatch('unhovered-card'); + }, []); + var onPress = React.useCallback(function () { + dispatch('pressed'); + }, []); + var isVisible = currentState.stage === 'showing' || + currentState.stage === 'might-hide' || + currentState.stage === 'hiding'; + var animationStyle = { + animation: currentState.stage === 'hiding' + ? "fadeOut ".concat(HIDE_DURATION, "ms both") + : "fadeIn ".concat(SHOW_DURATION, "ms both"), + }; + return (_jsxs(View + // @ts-ignore View is being used as div + , { + // @ts-ignore View is being used as div + ref: refs.setReference, onPointerMove: onPointerMoveTarget, onPointerLeave: onPointerLeaveTarget, + // @ts-ignore web only prop + onMouseUp: onPress, style: [a.flex_shrink, props.inline && a.inline], children: [props.children, isVisible && (_jsx(Portal, { children: _jsx("div", { ref: refs.setFloating, style: floatingStyles, onPointerEnter: onPointerEnterCard, onPointerLeave: onPointerLeaveCard, children: _jsx("div", { style: __assign({ willChange: 'transform' }, animationStyle), children: _jsx(Card, { did: props.did, hide: onPress, navigation: navigation }) }) }) }))] })); +} +var Card = function (_a) { + var did = _a.did, hide = _a.hide, navigation = _a.navigation; + var t = useTheme(); + var profile = useProfileQuery({ did: did }); + var moderationOpts = useModerationOpts(); + var data = profile.data; + var status = useActorStatus(data); + var onPressOpenProfile = useCallback(function () { + if (!status.isActive || !data) + return; + hide(); + navigation.push('Profile', { + name: data.handle, + }); + }, [hide, navigation, status, data]); + return (_jsx(View, { style: [ + !status.isActive && a.p_lg, + a.border, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg, + t.atoms.border_contrast_low, + t.atoms.shadow_lg, + { width: status.isActive ? 350 : 300 }, + a.max_w_full, + ], children: data && moderationOpts ? (status.isActive ? (_jsx(LiveStatus, { status: status, profile: data, embed: status.embed, padding: "lg", onPressOpenProfile: onPressOpenProfile })) : (_jsx(Inner, { profile: data, moderationOpts: moderationOpts, hide: hide }))) : (_jsx(View, { style: [ + a.justify_center, + a.align_center, + { minHeight: 200 }, + a.w_full, + ], children: _jsx(Loader, { size: "xl" }) })) })); +}; +Card = React.memo(Card); +function Inner(_a) { + var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; + var profile = _a.profile, moderationOpts = _a.moderationOpts, hide = _a.hide; + var t = useTheme(); + var _p = useLingui(), _ = _p._, i18n = _p.i18n; + var currentAccount = useSession().currentAccount; + var moderation = React.useMemo(function () { return moderateProfile(profile, moderationOpts); }, [profile, moderationOpts]); + var descriptionRT = useRichText((_b = profile.description) !== null && _b !== void 0 ? _b : '')[0]; + var profileShadow = useProfileShadow(profile); + var _q = useFollowMethods({ + profile: profileShadow, + logContext: 'ProfileHoverCard', + }), follow = _q.follow, unfollow = _q.unfollow; + var isBlockedUser = ((_c = profile.viewer) === null || _c === void 0 ? void 0 : _c.blocking) || + ((_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.blockedBy) || + ((_e = profile.viewer) === null || _e === void 0 ? void 0 : _e.blockingByList); + 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', + }); + var profileURL = makeProfileLink({ + did: profile.did, + handle: profile.handle, + }); + var isMe = React.useMemo(function () { return (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === profile.did; }, [currentAccount, profile]); + var isLabeler = (_f = profile.associated) === null || _f === void 0 ? void 0 : _f.labeler; + var verification = useSimpleVerificationState({ profile: profile }); + return (_jsxs(View, { children: [_jsxs(View, { style: [a.flex_row, a.justify_between, a.align_start], children: [_jsx(Link, { to: profileURL, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["View profile"], ["View profile"])))), onPress: hide, children: _jsx(UserAvatar, { size: 64, avatar: profile.avatar, type: isLabeler ? 'labeler' : 'user', moderation: moderation.ui('avatar') }) }), !isMe && + !isLabeler && + (isBlockedUser ? (_jsx(Link, { to: profileURL, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["View blocked user's profile"], ["View blocked user's profile"])))), onPress: hide, size: "small", color: "secondary", variant: "solid", style: [a.rounded_full], children: _jsx(ButtonText, { children: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["View profile"], ["View profile"])))) }) })) : (_jsxs(Button, { size: "small", color: ((_g = profileShadow.viewer) === null || _g === void 0 ? void 0 : _g.following) ? 'secondary' : 'primary', variant: "solid", label: ((_h = profileShadow.viewer) === null || _h === void 0 ? void 0 : _h.following) + ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Following"], ["Following"])))) + : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Follow"], ["Follow"])))), style: [a.rounded_full], onPress: ((_j = profileShadow.viewer) === null || _j === void 0 ? void 0 : _j.following) ? unfollow : follow, children: [_jsx(ButtonIcon, { position: "left", icon: ((_k = profileShadow.viewer) === null || _k === void 0 ? void 0 : _k.following) ? Check : Plus }), _jsx(ButtonText, { children: ((_l = profileShadow.viewer) === null || _l === void 0 ? void 0 : _l.following) + ? _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Following"], ["Following"])))) + : _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Follow"], ["Follow"])))) })] })))] }), _jsx(Link, { to: profileURL, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["View profile"], ["View profile"])))), onPress: hide, children: _jsxs(View, { style: [a.pb_sm, a.flex_1], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.pt_md, a.pb_xs], children: [_jsx(Text, { numberOfLines: 1, style: [ + a.text_lg, + a.leading_snug, + a.font_semi_bold, + a.self_start, + ], children: sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName')) }), verification.showBadge && (_jsx(View, { style: [ + a.pl_xs, + { + marginTop: -2, + }, + ], children: _jsx(VerificationCheck, { width: 16, verifier: verification.role === 'verifier' }) }))] }), _jsx(ProfileHeaderHandle, { profile: profileShadow, disableTaps: true })] }) }), isBlockedUser && (_jsx(View, { style: [a.flex_row, a.flex_wrap, a.gap_xs], children: moderation.ui('profileView').alerts.map(function (cause) { return (_jsx(Pills.Label, { size: "lg", cause: cause, disableDetailsDialog: true }, getModerationCauseKey(cause))); }) })), !isBlockedUser && (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.flex_row, a.flex_wrap, a.gap_md, a.pt_xs], children: [_jsxs(InlineLinkText, { to: makeProfileLink(profile, 'followers'), label: "".concat(followers, " ").concat(pluralizedFollowers), style: [t.atoms.text], onPress: hide, children: [_jsxs(Text, { style: [a.text_md, a.font_semi_bold], children: [followers, " "] }), _jsx(Text, { style: [t.atoms.text_contrast_medium], children: pluralizedFollowers })] }), _jsxs(InlineLinkText, { to: makeProfileLink(profile, 'follows'), label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["", " following"], ["", " following"])), following)), style: [t.atoms.text], onPress: hide, children: [_jsxs(Text, { style: [a.text_md, a.font_semi_bold], children: [following, " "] }), _jsx(Text, { style: [t.atoms.text_contrast_medium], children: pluralizedFollowings })] })] }), ((_m = profile.description) === null || _m === void 0 ? void 0 : _m.trim()) && !moderation.ui('profileView').blur ? (_jsx(View, { style: [a.pt_md], children: _jsx(RichText, { numberOfLines: 8, value: descriptionRT, onLinkPress: hide }) })) : undefined, !isMe && + shouldShowKnownFollowers((_o = profile.viewer) === null || _o === void 0 ? void 0 : _o.knownFollowers) && (_jsx(View, { style: [a.flex_row, a.align_center, a.gap_sm, a.pt_md], children: _jsx(KnownFollowers, { profile: profile, moderationOpts: moderationOpts, onLinkPress: hide }) }))] }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/src/components/ProfileHoverCard/types.js b/src/components/ProfileHoverCard/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/ProfileHoverCard/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/ProgressGuide/FollowDialog.js b/src/components/ProgressGuide/FollowDialog.js new file mode 100644 index 0000000000..97c6100ee4 --- /dev/null +++ b/src/components/ProgressGuide/FollowDialog.js @@ -0,0 +1,383 @@ +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 { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { TextInput, useWindowDimensions, View, } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { popularInterests, useInterestsDisplayNames } from '#/lib/interests'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useActorSearch } from '#/state/queries/actor-search'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useGetSuggestedUsersQuery } from '#/state/queries/trending/useGetSuggestedUsersQuery'; +import { useSession } from '#/state/session'; +import { atoms as a, native, useBreakpoints, useTheme, web, } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon } from '#/components/icons/Arrow'; +import { MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon } from '#/components/icons/MagnifyingGlass'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { boostInterests, InterestTabs } from '#/components/InterestTabs'; +import * as ProfileCard from '#/components/ProfileCard'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import { ProgressGuideTask } from './Task'; +export function FollowDialog(_a) { + var guide = _a.guide, showArrow = _a.showArrow; + var ax = useAnalytics(); + var _ = useLingui()._; + var control = Dialog.useDialogControl(); + var gtPhone = useBreakpoints().gtPhone; + var minHeight = useWindowDimensions().height; + return (_jsxs(_Fragment, { children: [_jsxs(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Find people to follow"], ["Find people to follow"])))), onPress: function () { + control.open(); + ax.metric('progressGuide:followDialog:open', {}); + }, size: gtPhone ? 'small' : 'large', color: "primary", children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Find people to follow" }) }), showArrow && _jsx(ButtonIcon, { icon: ArrowRightIcon })] }), _jsxs(Dialog.Outer, { control: control, nativeOptions: { minHeight: minHeight }, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { guide: guide })] })] })); +} +/** + * Same as {@link FollowDialog} but without a progress guide. + */ +export function FollowDialogWithoutGuide(_a) { + var control = _a.control; + var minHeight = useWindowDimensions().height; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { minHeight: minHeight }, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, {})] })); +} +// Fine to keep this top-level. +var lastSelectedInterest = ''; +var lastSearchText = ''; +function DialogInner(_a) { + var _b; + var guide = _a.guide; + 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)); + var _c = useState(function () { + return lastSelectedInterest || + (personalizedInterests && interests.includes(personalizedInterests[0]) + ? personalizedInterests[0] + : interests[0]); + }), selectedInterest = _c[0], setSelectedInterest = _c[1]; + var _d = useState(lastSearchText), searchText = _d[0], setSearchText = _d[1]; + var moderationOpts = useModerationOpts(); + var listRef = useRef(null); + var inputRef = useRef(null); + var _e = useState(0), headerHeight = _e[0], setHeaderHeight = _e[1]; + var currentAccount = useSession().currentAccount; + useEffect(function () { + lastSearchText = searchText; + lastSelectedInterest = selectedInterest; + }, [searchText, selectedInterest]); + var _f = useGetSuggestedUsersQuery({ + category: selectedInterest, + limit: 50, + }), suggestions = _f.data, isFetchingSuggestions = _f.isFetching, suggestionsError = _f.error; + var _g = useActorSearch({ + enabled: !!searchText, + query: searchText, + }), searchResults = _g.data, isFetchingSearchResults = _g.isFetching, searchResultsError = _g.error, isSearchResultsError = _g.isError; + var hasSearchText = !!searchText; + var resultsKey = searchText || selectedInterest; + var items = useMemo(function () { + var _a; + var results = hasSearchText + ? searchResults === null || searchResults === void 0 ? void 0 : searchResults.pages.flatMap(function (p) { return p.actors; }) + : suggestions === null || suggestions === void 0 ? void 0 : suggestions.actors; + var _items = []; + if (isFetchingSuggestions || isFetchingSearchResults) { + var placeholders = Array(10) + .fill(0) + .map(function (__, i) { return ({ + type: 'placeholder', + key: i + '', + }); }); + _items.push.apply(_items, placeholders); + } + else if ((hasSearchText && searchResultsError) || + (!hasSearchText && suggestionsError) || + !(results === null || results === void 0 ? void 0 : results.length)) { + _items.push({ + type: 'empty', + key: 'empty', + message: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["We're having network issues, try again"], ["We're having network issues, try again"])))), + }); + } + else { + var seen = new Set(); + for (var _i = 0, results_1 = results; _i < results_1.length; _i++) { + var profile = results_1[_i]; + if (seen.has(profile.did)) + continue; + if (profile.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) + continue; + if ((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following) + continue; + seen.add(profile.did); + _items.push({ + type: 'profile', + // Don't share identity across tabs or typing attempts + key: resultsKey + ':' + profile.did, + profile: profile, + }); + } + } + return _items; + }, [ + _, + suggestions, + suggestionsError, + isFetchingSuggestions, + searchResults, + searchResultsError, + isFetchingSearchResults, + currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + hasSearchText, + resultsKey, + ]); + if (searchText && + !isFetchingSearchResults && + !items.length && + !isSearchResultsError) { + items.push({ type: 'empty', key: 'empty', message: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["No results"], ["No results"])))) }); + } + var renderItems = useCallback(function (_a) { + var item = _a.item, index = _a.index; + switch (item.type) { + case 'profile': { + return (_jsx(FollowProfileCard, { profile: item.profile, moderationOpts: moderationOpts, noBorder: index === 0 })); + } + case 'placeholder': { + return _jsx(ProfileCardSkeleton, {}, item.key); + } + case 'empty': { + return _jsx(Empty, { message: item.message }, item.key); + } + default: + return null; + } + }, [moderationOpts]); + // Track seen profiles + var seenProfilesRef = useRef(new Set()); + var itemsRef = useRef(items); + itemsRef.current = items; + var selectedInterestRef = useRef(selectedInterest); + selectedInterestRef.current = selectedInterest; + var onViewableItemsChanged = useRef(function (_a) { + var viewableItems = _a.viewableItems; + var _loop_1 = function (viewableItem) { + var item = viewableItem.item; + if (item.type === 'profile') { + if (!seenProfilesRef.current.has(item.profile.did)) { + seenProfilesRef.current.add(item.profile.did); + var position = itemsRef.current.findIndex(function (i) { return i.type === 'profile' && i.profile.did === item.profile.did; }); + ax.metric('suggestedUser:seen', { + logContext: 'ProgressGuide', + recId: undefined, + position: position !== -1 ? position : 0, + suggestedDid: item.profile.did, + category: selectedInterestRef.current, + }); + } + } + }; + for (var _i = 0, viewableItems_1 = viewableItems; _i < viewableItems_1.length; _i++) { + var viewableItem = viewableItems_1[_i]; + _loop_1(viewableItem); + } + }).current; + var viewabilityConfig = useRef({ + itemVisiblePercentThreshold: 50, + }).current; + var onSelectTab = useCallback(function (interest) { + var _a, _b; + setSelectedInterest(interest); + (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.clear(); + setSearchText(''); + (_b = listRef.current) === null || _b === void 0 ? void 0 : _b.scrollToOffset({ + offset: 0, + animated: false, + }); + }, [setSelectedInterest, setSearchText]); + var listHeader = (_jsx(Header, { guide: guide, inputRef: inputRef, listRef: listRef, searchText: searchText, onSelectTab: onSelectTab, setHeaderHeight: setHeaderHeight, setSearchText: setSearchText, interests: interests, selectedInterest: selectedInterest, interestsDisplayNames: interestsDisplayNames })); + return (_jsx(Dialog.InnerFlatList, { ref: listRef, data: items, renderItem: renderItems, ListHeaderComponent: listHeader, stickyHeaderIndices: [0], keyExtractor: function (item) { return item.key; }, style: [ + a.px_0, + web([a.py_0, { height: '100vh', maxHeight: 600 }]), + native({ height: '100%' }), + ], webInnerContentContainerStyle: a.py_0, webInnerStyle: [a.py_0, { maxWidth: 500, minWidth: 200 }], keyboardDismissMode: "on-drag", scrollIndicatorInsets: { top: headerHeight }, initialNumToRender: 8, maxToRenderPerBatch: 8, onViewableItemsChanged: onViewableItemsChanged, viewabilityConfig: viewabilityConfig })); +} +var Header = function (_a) { + var guide = _a.guide, inputRef = _a.inputRef, listRef = _a.listRef, searchText = _a.searchText, onSelectTab = _a.onSelectTab, setHeaderHeight = _a.setHeaderHeight, setSearchText = _a.setSearchText, interests = _a.interests, selectedInterest = _a.selectedInterest, interestsDisplayNames = _a.interestsDisplayNames; + var t = useTheme(); + var control = Dialog.useDialogContext(); + return (_jsxs(View, { onLayout: function (evt) { return setHeaderHeight(evt.nativeEvent.layout.height); }, style: [ + a.relative, + web(a.pt_lg), + native(a.pt_4xl), + a.pb_xs, + a.border_b, + t.atoms.border_contrast_low, + t.atoms.bg, + ], children: [_jsx(HeaderTop, { guide: guide }), _jsxs(View, { style: [web(a.pt_xs), a.pb_xs], children: [_jsx(SearchInput, { inputRef: inputRef, defaultValue: 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 }), _jsx(InterestTabs, { onSelectTab: onSelectTab, interests: interests, selectedInterest: selectedInterest, disabled: !!searchText, interestsDisplayNames: interestsDisplayNames, TabComponent: Tab })] })] })); +}; +Header = memo(Header); +function HeaderTop(_a) { + var guide = _a.guide; + var _ = useLingui()._; + var t = useTheme(); + var control = Dialog.useDialogContext(); + return (_jsxs(View, { style: [ + a.px_lg, + a.relative, + a.flex_row, + a.justify_between, + a.align_center, + ], children: [_jsx(Text, { style: [ + a.z_10, + a.text_lg, + a.font_bold, + a.leading_tight, + t.atoms.text_contrast_high, + ], children: _jsx(Trans, { children: "Find people to follow" }) }), guide && (_jsx(View, { style: IS_WEB && { paddingRight: 36 }, children: _jsx(ProgressGuideTask, { current: guide.numFollows + 1, total: 10 + 1, title: "".concat(guide.numFollows, " / 10"), tabularNumsTitle: true }) })), IS_WEB ? (_jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Close"], ["Close"])))), size: "small", shape: "round", variant: IS_WEB ? 'ghost' : 'solid', color: "secondary", style: [ + a.absolute, + a.z_20, + web({ right: 8 }), + native({ right: 0 }), + native({ height: 32, width: 32, borderRadius: 16 }), + ], onPress: function () { return control.close(); }, children: _jsx(ButtonIcon, { icon: X, size: "md" }) })) : null] })); +} +var Tab = function (_a) { + var onSelectTab = _a.onSelectTab, interest = _a.interest, active = _a.active, index = _a.index, interestsDisplayName = _a.interestsDisplayName, onLayout = _a.onLayout; + var t = useTheme(); + var _ = useLingui()._; + var label = active + ? _(msg({ + message: "Search for \"".concat(interestsDisplayName, "\" (active)"), + comment: 'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is currently selected.', + })) + : _(msg({ + message: "Search for \"".concat(interestsDisplayName, "\""), + comment: 'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested 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); }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(View, { style: [ + a.rounded_full, + a.px_lg, + a.py_sm, + a.border, + active || hovered || pressed + ? [ + t.atoms.bg_contrast_25, + { borderColor: t.atoms.bg_contrast_25.backgroundColor }, + ] + : [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)); +}; +Tab = memo(Tab); +var FollowProfileCard = function (_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, noBorder = _a.noBorder; + return (_jsx(FollowProfileCardInner, { profile: profile, moderationOpts: moderationOpts, noBorder: noBorder })); +}; +FollowProfileCard = memo(FollowProfileCard); +function FollowProfileCardInner(_a) { + var profile = _a.profile, moderationOpts = _a.moderationOpts, onFollow = _a.onFollow, noBorder = _a.noBorder; + var control = Dialog.useDialogContext(); + var t = useTheme(); + return (_jsx(ProfileCard.Link, { profile: profile, style: [a.flex_1], onPress: function () { return control.close(); }, children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed; + return (_jsx(CardOuter, { style: [ + a.flex_1, + noBorder && a.border_t_0, + (hovered || pressed) && t.atoms.bg_contrast_25, + ], children: _jsxs(ProfileCard.Outer, { children: [_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { disabledPreview: !IS_WEB, profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts }), _jsx(ProfileCard.FollowButton, { profile: profile, moderationOpts: moderationOpts, logContext: "PostOnboardingFindFollows", shape: "round", onPress: onFollow, colorInverted: true })] }), _jsx(ProfileCard.Description, { profile: profile, numberOfLines: 2 })] }) })); + } })); +} +function CardOuter(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.w_full, + a.py_md, + a.px_lg, + a.border_t, + t.atoms.border_contrast_low, + style, + ], children: children })); +} +function SearchInput(_a) { + var onChangeText = _a.onChangeText, onEscape = _a.onEscape, inputRef = _a.inputRef, defaultValue = _a.defaultValue; + 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, a.px_lg, a.py_xs], children: [_jsx(SearchIcon, { size: "md", fill: interacted ? t.palette.primary_500 : t.palette.contrast_300 }), _jsx(TextInput, { ref: inputRef, placeholder: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Search by name or interest"], ["Search by name or interest"])))), defaultValue: defaultValue, 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", accessibilityLabel: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Search profiles"], ["Search profiles"])))), accessibilityHint: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Searches for profiles"], ["Searches for profiles"])))) })] }))); +} +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" })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/ProgressGuide/List.js b/src/components/ProgressGuide/List.js new file mode 100644 index 0000000000..ed4b983ae3 --- /dev/null +++ b/src/components/ProgressGuide/List.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, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useProfileFollowsQuery } from '#/state/queries/profile-follows'; +import { useSession } from '#/state/session'; +import { useProgressGuide, useProgressGuideControls, } from '#/state/shell/progress-guide'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon } from '#/components/Button'; +import { Person_Stroke2_Corner2_Rounded as PersonIcon } from '#/components/icons/Person'; +import { TimesLarge_Stroke2_Corner0_Rounded as Times } from '#/components/icons/Times'; +import { Text } from '#/components/Typography'; +import { FollowDialog } from './FollowDialog'; +import { ProgressGuideTask } from './Task'; +var TOTAL_AVATARS = 10; +export function ProgressGuideList(_a) { + var _b, _c, _d, _e, _f, _g; + var style = _a.style; + var t = useTheme(); + var _ = useLingui()._; + var gtPhone = useBreakpoints().gtPhone; + var rightNavVisible = useLayoutBreakpoints().rightNavVisible; + var currentAccount = useSession().currentAccount; + var followProgressGuide = useProgressGuide('follow-10'); + var followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7'); + var guide = followProgressGuide || followAndLikeProgressGuide; + var endProgressGuide = useProgressGuideControls().endProgressGuide; + var follows = useProfileFollowsQuery(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, { + limit: TOTAL_AVATARS, + }).data; + var actualFollowsCount = (_e = (_d = (_c = (_b = follows === null || follows === void 0 ? void 0 : follows.pages) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.follows) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0; + // Hide if user already follows 10+ people + if ((guide === null || guide === void 0 ? void 0 : guide.guide) === 'follow-10' && actualFollowsCount >= TOTAL_AVATARS) { + return null; + } + // Inline layout when left nav visible but no right sidebar (800-1100px) + var inlineLayout = gtPhone && !rightNavVisible; + if (guide) { + return (_jsxs(View, { style: [ + a.flex_col, + a.gap_md, + a.rounded_md, + t.atoms.bg_contrast_25, + a.p_lg, + style, + ], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.justify_between], children: [_jsx(Text, { style: [t.atoms.text, a.font_semi_bold, a.text_md], children: _jsx(Trans, { children: "Follow 10 people to get started" }) }), _jsx(Button, { variant: "ghost", size: "tiny", color: "secondary", shape: "round", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Dismiss getting started guide"], ["Dismiss getting started guide"])))), onPress: endProgressGuide, style: [a.bg_transparent, { marginTop: -6, marginRight: -6 }], children: _jsx(ButtonIcon, { icon: Times, size: "xs" }) })] }), guide.guide === 'follow-10' && (_jsxs(View, { style: [ + inlineLayout + ? [ + a.flex_row, + a.flex_wrap, + a.align_center, + a.justify_between, + a.gap_sm, + ] + : a.flex_col, + !inlineLayout && a.gap_md, + ], children: [_jsx(StackedAvatars, { follows: (_g = (_f = follows === null || follows === void 0 ? void 0 : follows.pages) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.follows }), _jsx(FollowDialog, { guide: guide, showArrow: inlineLayout })] })), guide.guide === 'like-10-and-follow-7' && (_jsxs(_Fragment, { children: [_jsx(ProgressGuideTask, { current: guide.numLikes + 1, total: 10 + 1, title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Like 10 posts"], ["Like 10 posts"])))), subtitle: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Teach our algorithm what you like"], ["Teach our algorithm what you like"])))) }), _jsx(ProgressGuideTask, { current: guide.numFollows + 1, total: 7 + 1, title: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Follow 7 accounts"], ["Follow 7 accounts"])))), subtitle: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Bluesky is better with friends!"], ["Bluesky is better with friends!"])))) })] }))] })); + } + return null; +} +function StackedAvatars(_a) { + var _b; + var follows = _a.follows; + var t = useTheme(); + var centerColumnOffset = useLayoutBreakpoints().centerColumnOffset; + // Smaller avatars for narrower viewport + var avatarSize = centerColumnOffset ? 30 : 37; + var overlap = centerColumnOffset ? 9 : 11; + var iconSize = centerColumnOffset ? 14 : 18; + // Use actual follows count, not the guide's event counter + var followedAvatars = (_b = follows === null || follows === void 0 ? void 0 : follows.slice(0, TOTAL_AVATARS)) !== null && _b !== void 0 ? _b : []; + var remainingSlots = TOTAL_AVATARS - followedAvatars.length; + // Total width calculation: first avatar + (remaining * visible portion) + var totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap); + return (_jsxs(View, { style: [a.flex_row, a.self_start, { width: totalWidth }], children: [followedAvatars.map(function (follow, i) { return (_jsx(View, { style: [ + a.rounded_full, + { + marginLeft: i === 0 ? 0 : -overlap, + zIndex: TOTAL_AVATARS - i, + borderWidth: 2, + borderColor: t.atoms.bg_contrast_25.backgroundColor, + }, + ], children: _jsx(UserAvatar, { type: "user", size: avatarSize - 4, avatar: follow.avatar }) }, follow.did)); }), Array(remainingSlots) + .fill(0) + .map(function (_, i) { return (_jsx(View, { style: [ + a.align_center, + a.justify_center, + a.rounded_full, + t.atoms.bg_contrast_100, + { + width: avatarSize, + height: avatarSize, + marginLeft: followedAvatars.length === 0 && i === 0 ? 0 : -overlap, + zIndex: TOTAL_AVATARS - followedAvatars.length - i, + borderWidth: 2, + borderColor: t.atoms.bg_contrast_25.backgroundColor, + }, + ], children: _jsx(PersonIcon, { width: iconSize, height: iconSize, fill: t.atoms.text_contrast_low.color }) }, "placeholder-".concat(i))); })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/ProgressGuide/Task.js b/src/components/ProgressGuide/Task.js new file mode 100644 index 0000000000..682c0b6d36 --- /dev/null +++ b/src/components/ProgressGuide/Task.js @@ -0,0 +1,16 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import * as Progress from 'react-native-progress'; +import { atoms as a, useTheme } from '#/alf'; +import { AnimatedCheck } from '../anim/AnimatedCheck'; +import { Text } from '../Typography'; +export function ProgressGuideTask(_a) { + var current = _a.current, total = _a.total, title = _a.title, subtitle = _a.subtitle, tabularNumsTitle = _a.tabularNumsTitle; + var t = useTheme(); + return (_jsxs(View, { style: [a.flex_row, a.gap_sm, !subtitle && a.align_center], children: [current === total ? (_jsx(AnimatedCheck, { playOnMount: true, fill: t.palette.primary_500, width: 20 })) : (_jsx(Progress.Circle, { progress: current / total, color: t.palette.primary_400, size: 20, thickness: 3, borderWidth: 0, unfilledColor: t.palette.contrast_100 })), _jsxs(View, { style: [a.flex_col, a.gap_xs, subtitle && { marginTop: -2 }], children: [_jsx(Text, { style: [ + a.text_sm, + a.font_semi_bold, + a.leading_tight, + tabularNumsTitle && { fontVariant: ['tabular-nums'] }, + ], children: title }), subtitle && (_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium, a.leading_tight], children: subtitle }))] })] })); +} diff --git a/src/components/ProgressGuide/Toast.js b/src/components/ProgressGuide/Toast.js new file mode 100644 index 0000000000..5cf6c53579 --- /dev/null +++ b/src/components/ProgressGuide/Toast.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; +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import React, { useImperativeHandle } from 'react'; +import { Pressable, useWindowDimensions, View } from 'react-native'; +import Animated, { Easing, runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +import { Portal } from '#/components/Portal'; +import { IS_WEB } from '#/env'; +import { AnimatedCheck } from '../anim/AnimatedCheck'; +import { Text } from '../Typography'; +export var ProgressGuideToast = React.forwardRef(function ProgressGuideToast(_a, ref) { + var title = _a.title, subtitle = _a.subtitle, visibleDuration = _a.visibleDuration; + var t = useTheme(); + var _ = useLingui()._; + var insets = useSafeAreaInsets(); + var _b = React.useState(false), isOpen = _b[0], setIsOpen = _b[1]; + var translateY = useSharedValue(0); + var opacity = useSharedValue(0); + var animatedCheckRef = React.useRef(null); + var timeoutRef = React.useRef(undefined); + var winDim = useWindowDimensions(); + /** + * Methods + */ + var close = React.useCallback(function () { + // clear the timeout, in case this was called imperatively + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = undefined; + } + // animate the opacity then set isOpen to false when done + var setIsntOpen = function () { return setIsOpen(false); }; + opacity.set(function () { + return withTiming(0, { + duration: 400, + easing: Easing.out(Easing.cubic), + }, function () { return runOnJS(setIsntOpen)(); }); + }); + }, [setIsOpen, opacity]); + var open = React.useCallback(function () { + // set isOpen=true to render + setIsOpen(true); + // animate the vertical translation, the opacity, and the checkmark + var playCheckmark = function () { var _a; return (_a = animatedCheckRef.current) === null || _a === void 0 ? void 0 : _a.play(); }; + opacity.set(0); + opacity.set(function () { + return withTiming(1, { + duration: 100, + easing: Easing.out(Easing.cubic), + }, function () { return runOnJS(playCheckmark)(); }); + }); + translateY.set(0); + translateY.set(function () { + return withTiming(insets.top + 10, { + duration: 500, + easing: Easing.out(Easing.cubic), + }); + }); + // start the countdown timer to autoclose + timeoutRef.current = setTimeout(close, visibleDuration || 5e3); + }, [setIsOpen, translateY, opacity, insets, close, visibleDuration]); + useImperativeHandle(ref, function () { return ({ + open: open, + close: close, + }); }, [open, close]); + var containerStyle = React.useMemo(function () { + var left = 10; + var right = 10; + if (IS_WEB && winDim.width > 400) { + left = right = (winDim.width - 380) / 2; + } + return { + position: IS_WEB ? 'fixed' : 'absolute', + top: 0, + left: left, + right: right, + }; + }, [winDim.width]); + var animatedStyle = useAnimatedStyle(function () { return ({ + transform: [{ translateY: translateY.get() }], + opacity: opacity.get(), + }); }); + return (isOpen && (_jsx(Portal, { children: _jsx(Animated.View, { style: [ + // @ts-ignore position: fixed is web only + containerStyle, + animatedStyle, + ], children: _jsxs(Pressable, { style: [ + t.atoms.bg, + a.flex_row, + a.align_center, + a.gap_md, + a.border, + t.atoms.border_contrast_high, + a.rounded_md, + a.px_lg, + a.py_md, + a.shadow_sm, + { + shadowRadius: 8, + shadowOpacity: 0.1, + shadowOffset: { width: 0, height: 2 }, + elevation: 8, + }, + ], onPress: close, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Tap to dismiss"], ["Tap to dismiss"])))), accessibilityHint: "", children: [_jsx(AnimatedCheck, { fill: t.palette.primary_500, ref: animatedCheckRef }), _jsxs(View, { children: [_jsx(Text, { style: [a.text_md, a.font_semi_bold], children: title }), subtitle && (_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium], children: subtitle }))] })] }) }) }))); +}); +var templateObject_1; diff --git a/src/components/Prompt.js b/src/components/Prompt.js new file mode 100644 index 0000000000..80c37a9de8 --- /dev/null +++ b/src/components/Prompt.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 __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 { View } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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'; +export { useDialogControl as usePromptControl, } from '#/components/Dialog'; +var Context = React.createContext({ + titleId: '', + descriptionId: '', +}); +Context.displayName = 'PromptContext'; +export function Outer(_a) { + var children = _a.children, control = _a.control, testID = _a.testID, nativeOptions = _a.nativeOptions; + var titleId = React.useId(); + var descriptionId = React.useId(); + var context = React.useMemo(function () { return ({ titleId: titleId, descriptionId: descriptionId }); }, [titleId, descriptionId]); + return (_jsxs(Dialog.Outer, { control: control, testID: testID, webOptions: { alignCenter: true }, nativeOptions: __assign({ preventExpansion: true }, nativeOptions), children: [_jsx(Dialog.Handle, {}), _jsx(Context.Provider, { value: context, children: _jsx(Dialog.ScrollableInner, { accessibilityLabelledBy: titleId, accessibilityDescribedBy: descriptionId, style: web({ maxWidth: 400 }), children: children }) })] })); +} +export function TitleText(_a) { + var children = _a.children, style = _a.style; + var titleId = React.useContext(Context).titleId; + return (_jsx(Text, { nativeID: titleId, style: [ + a.flex_1, + a.text_2xl, + a.font_semi_bold, + a.pb_sm, + a.leading_snug, + style, + ], children: children })); +} +export function DescriptionText(_a) { + var children = _a.children, selectable = _a.selectable; + var t = useTheme(); + var descriptionId = React.useContext(Context).descriptionId; + return (_jsx(Text, { nativeID: descriptionId, selectable: selectable, style: [a.text_md, a.leading_snug, t.atoms.text_contrast_high, a.pb_lg], children: children })); +} +export function Actions(_a) { + var children = _a.children; + var gtMobile = useBreakpoints().gtMobile; + return (_jsx(View, { style: [ + a.w_full, + a.gap_md, + a.justify_end, + gtMobile + ? [a.flex_row, a.flex_row_reverse, a.justify_start] + : [a.flex_col], + ], children: children })); +} +export function Cancel(_a) { + var cta = _a.cta; + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var close = Dialog.useDialogContext().close; + var onPress = React.useCallback(function () { + close(); + }, [close]); + return (_jsx(Button, { variant: "solid", color: "secondary", size: gtMobile ? 'small' : 'large', label: cta || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: onPress, children: _jsx(ButtonText, { children: cta || _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Cancel"], ["Cancel"])))) }) })); +} +export function Action(_a) { + var onPress = _a.onPress, _b = _a.color, color = _b === void 0 ? 'primary' : _b, cta = _a.cta, testID = _a.testID; + var _ = useLingui()._; + var gtMobile = useBreakpoints().gtMobile; + var close = Dialog.useDialogContext().close; + var handleOnPress = React.useCallback(function (e) { + close(function () { return onPress === null || onPress === void 0 ? void 0 : onPress(e); }); + }, [close, onPress]); + return (_jsx(Button, { variant: "solid", color: color, size: gtMobile ? 'small' : 'large', label: cta || _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Confirm"], ["Confirm"])))), onPress: handleOnPress, testID: testID, children: _jsx(ButtonText, { children: cta || _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Confirm"], ["Confirm"])))) }) })); +} +export function Basic(_a) { + var control = _a.control, title = _a.title, description = _a.description, cancelButtonCta = _a.cancelButtonCta, confirmButtonCta = _a.confirmButtonCta, onConfirm = _a.onConfirm, confirmButtonColor = _a.confirmButtonColor, _b = _a.showCancel, showCancel = _b === void 0 ? true : _b; + return (_jsxs(Outer, { control: control, testID: "confirmModal", children: [_jsx(TitleText, { children: title }), description && _jsx(DescriptionText, { children: description }), _jsxs(Actions, { children: [_jsx(Action, { cta: confirmButtonCta, onPress: onConfirm, color: confirmButtonColor, testID: "confirmBtn" }), showCancel && _jsx(Cancel, { cta: cancelButtonCta })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/RichText.js b/src/components/RichText.js new file mode 100644 index 0000000000..771b497fa8 --- /dev/null +++ b/src/components/RichText.js @@ -0,0 +1,75 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React from 'react'; +import { AppBskyRichtextFacet, RichText as RichTextAPI } from '@atproto/api'; +import { toShortUrl } from '#/lib/strings/url-helpers'; +import { atoms as a, flatten } from '#/alf'; +import { isOnlyEmoji } from '#/alf/typography'; +import { InlineLinkText } from '#/components/Link'; +import { ProfileHoverCard } from '#/components/ProfileHoverCard'; +import { RichTextTag } from '#/components/RichTextTag'; +import { Text } from '#/components/Typography'; +var WORD_WRAP = { wordWrap: 1 }; +// lifted from facet detection in `RichText` impl, _without_ `gm` flags +var URL_REGEX = /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/i; +export function RichText(_a) { + var _b, _c; + var testID = _a.testID, value = _a.value, style = _a.style, numberOfLines = _a.numberOfLines, disableLinks = _a.disableLinks, selectable = _a.selectable, _d = _a.enableTags, enableTags = _d === void 0 ? false : _d, authorHandle = _a.authorHandle, onLinkPress = _a.onLinkPress, interactiveStyle = _a.interactiveStyle, _e = _a.emojiMultiplier, emojiMultiplier = _e === void 0 ? 1.85 : _e, onLayout = _a.onLayout, onTextLayout = _a.onTextLayout, shouldProxyLinks = _a.shouldProxyLinks; + var richText = React.useMemo(function () { + return value instanceof RichTextAPI ? value : new RichTextAPI({ text: value }); + }, [value]); + var plainStyles = [a.leading_snug, style]; + var interactiveStyles = [plainStyles, interactiveStyle]; + var text = richText.text, facets = richText.facets; + if (!(facets === null || facets === void 0 ? void 0 : facets.length)) { + if (isOnlyEmoji(text)) { + var flattenedStyle = (_b = flatten(style)) !== null && _b !== void 0 ? _b : {}; + var fontSize = ((_c = flattenedStyle.fontSize) !== null && _c !== void 0 ? _c : a.text_sm.fontSize) * emojiMultiplier; + return (_jsx(Text, { emoji: true, selectable: selectable, testID: testID, style: [plainStyles, { fontSize: fontSize }], onLayout: onLayout, onTextLayout: onTextLayout, + // @ts-ignore web only -prf + dataSet: WORD_WRAP, children: text })); + } + return (_jsx(Text, { emoji: true, selectable: selectable, testID: testID, style: plainStyles, numberOfLines: numberOfLines, onLayout: onLayout, onTextLayout: onTextLayout, + // @ts-ignore web only -prf + dataSet: WORD_WRAP, children: text })); + } + var els = []; + var key = 0; + // N.B. must access segments via `richText.segments`, not via destructuring + for (var _i = 0, _f = richText.segments(); _i < _f.length; _i++) { + var segment = _f[_i]; + var link = segment.link; + var mention = segment.mention; + var tag = segment.tag; + if (mention && + AppBskyRichtextFacet.validateMention(mention).success && + !disableLinks) { + els.push(_jsx(ProfileHoverCard, { did: mention.did, children: _jsx(InlineLinkText, { selectable: selectable, to: "/profile/".concat(mention.did), style: interactiveStyles, + // @ts-ignore TODO + dataSet: WORD_WRAP, shouldProxy: shouldProxyLinks, onPress: onLinkPress, children: segment.text }) }, key)); + } + else if (link && AppBskyRichtextFacet.validateLink(link).success) { + var isValidLink = URL_REGEX.test(link.uri); + if (!isValidLink || disableLinks) { + els.push(toShortUrl(segment.text)); + } + else { + els.push(_jsx(InlineLinkText, { selectable: selectable, to: link.uri, style: interactiveStyles, + // @ts-ignore TODO + dataSet: WORD_WRAP, shareOnLongPress: true, shouldProxy: shouldProxyLinks, onPress: onLinkPress, emoji: true, children: toShortUrl(segment.text) }, key)); + } + } + else if (!disableLinks && + enableTags && + tag && + AppBskyRichtextFacet.validateTag(tag).success) { + els.push(_jsx(RichTextTag, { display: segment.text, tag: tag.tag, textStyle: interactiveStyles, authorHandle: authorHandle }, key)); + } + else { + els.push(segment.text); + } + key++; + } + return (_jsx(Text, { emoji: true, selectable: selectable, testID: testID, style: plainStyles, numberOfLines: numberOfLines, onLayout: onLayout, onTextLayout: onTextLayout, + // @ts-ignore web only -prf + dataSet: WORD_WRAP, children: els })); +} diff --git a/src/components/RichTextTag.js b/src/components/RichTextTag.js new file mode 100644 index 0000000000..7a5a38d14a --- /dev/null +++ b/src/components/RichTextTag.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; +}; +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 { Text as RNText } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { isInvalidHandle } from '#/lib/strings/handles'; +import { usePreferencesQuery, useRemoveMutedWordsMutation, useUpsertMutedWordsMutation, } from '#/state/queries/preferences'; +import { MagnifyingGlass_Stroke2_Corner0_Rounded as Search } from '#/components/icons/MagnifyingGlass'; +import { Mute_Stroke2_Corner0_Rounded as Mute } from '#/components/icons/Mute'; +import { Person_Stroke2_Corner0_Rounded as Person } from '#/components/icons/Person'; +import { createStaticClick, createStaticClickIfUnmodified, InlineLinkText, } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import * as Menu from '#/components/Menu'; +import { IS_NATIVE, IS_WEB } from '#/env'; +export function RichTextTag(_a) { + var _b, _c, _d; + var tag = _a.tag, display = _a.display, authorHandle = _a.authorHandle, textStyle = _a.textStyle; + var _ = useLingui()._; + var _e = usePreferencesQuery(), isPreferencesLoading = _e.isLoading, preferences = _e.data; + var _f = useUpsertMutedWordsMutation(), upsertMutedWord = _f.mutateAsync, optimisticUpsert = _f.variables, resetUpsert = _f.reset; + var _g = useRemoveMutedWordsMutation(), removeMutedWords = _g.mutateAsync, optimisticRemove = _g.variables, resetRemove = _g.reset; + var navigation = useNavigation(); + var isCashtag = tag.startsWith('$'); + var label = isCashtag ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Cashtag ", ""], ["Cashtag ", ""])), tag)) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Hashtag ", ""], ["Hashtag ", ""])), tag)); + var hint = IS_NATIVE + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Long press to open tag menu for ", ""], ["Long press to open tag menu for ", ""])), isCashtag ? tag : "#".concat(tag))) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Click to open tag menu for ", ""], ["Click to open tag menu for ", ""])), isCashtag ? tag : "#".concat(tag))); + var isMuted = Boolean(((_c = (_b = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs.mutedWords) === null || _b === void 0 ? void 0 : _b.find(function (m) { return m.value === tag && m.targets.includes('tag'); })) !== null && _c !== void 0 ? _c : optimisticUpsert === null || optimisticUpsert === void 0 ? void 0 : optimisticUpsert.find(function (m) { return m.value === tag && m.targets.includes('tag'); })) && + !(optimisticRemove === null || optimisticRemove === void 0 ? void 0 : optimisticRemove.find(function (m) { return (m === null || m === void 0 ? void 0 : m.value) === tag; }))); + /* + * Mute word records that exactly match the tag in question. + */ + var removeableMuteWords = React.useMemo(function () { + var _a; + return (((_a = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs.mutedWords) === null || _a === void 0 ? void 0 : _a.filter(function (word) { + return word.value === tag; + })) || []); + }, [tag, (_d = preferences === null || preferences === void 0 ? void 0 : preferences.moderationPrefs) === null || _d === void 0 ? void 0 : _d.mutedWords]); + return (_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { label: label, hint: hint, children: function (_a) { + var menuProps = _a.props; + return (_jsx(InlineLinkText, __assign({ to: { + screen: 'Hashtag', + params: { tag: encodeURIComponent(tag) }, + } }, menuProps, { onPress: function (e) { + if (IS_WEB) { + return createStaticClickIfUnmodified(function () { + if (!IS_NATIVE) { + menuProps.onPress(); + } + }).onPress(e); + } + }, onLongPress: createStaticClick(menuProps.onPress).onPress, accessibilityHint: hint, label: label, style: textStyle, emoji: true, children: IS_NATIVE ? (display) : (_jsx(RNText, { ref: menuProps.ref, children: display })) }))); + } }), _jsxs(Menu.Outer, { children: [_jsxs(Menu.Group, { children: [_jsxs(Menu.Item, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["See ", " posts"], ["See ", " posts"])), isCashtag ? tag : "#".concat(tag))), onPress: function () { + navigation.push('Hashtag', { + tag: encodeURIComponent(tag), + }); + }, children: [_jsx(Menu.ItemText, { children: isCashtag ? (_jsxs(Trans, { children: ["See ", tag, " posts"] })) : (_jsxs(Trans, { children: ["See #", tag, " posts"] })) }), _jsx(Menu.ItemIcon, { icon: Search })] }), authorHandle && !isInvalidHandle(authorHandle) && (_jsxs(Menu.Item, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["See ", " posts by user"], ["See ", " posts by user"])), isCashtag ? tag : "#".concat(tag))), onPress: function () { + navigation.push('Hashtag', { + tag: encodeURIComponent(tag), + author: authorHandle, + }); + }, children: [_jsx(Menu.ItemText, { children: isCashtag ? (_jsxs(Trans, { children: ["See ", tag, " posts by user"] })) : (_jsxs(Trans, { children: ["See #", tag, " posts by user"] })) }), _jsx(Menu.ItemIcon, { icon: Person })] }))] }), _jsx(Menu.Divider, {}), _jsxs(Menu.Item, { label: isMuted ? _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Unmute ", ""], ["Unmute ", ""])), tag)) : _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Mute ", ""], ["Mute ", ""])), tag)), onPress: function () { + if (isMuted) { + resetUpsert(); + removeMutedWords(removeableMuteWords); + } + else { + resetRemove(); + upsertMutedWord([ + { value: tag, targets: ['tag'], actorTarget: 'all' }, + ]); + } + }, children: [_jsx(Menu.ItemText, { children: isMuted ? _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Unmute ", ""], ["Unmute ", ""])), tag)) : _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Mute ", ""], ["Mute ", ""])), tag)) }), _jsx(Menu.ItemIcon, { icon: isPreferencesLoading ? Loader : Mute })] })] })] })); +} +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/components/ScreenTransition.js b/src/components/ScreenTransition.js new file mode 100644 index 0000000000..53f62bbe1c --- /dev/null +++ b/src/components/ScreenTransition.js @@ -0,0 +1,13 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import Animated, { Easing, FadeIn, FadeOut, SlideInLeft, SlideInRight, } from 'react-native-reanimated'; +import { IS_WEB } from '#/env'; +export function ScreenTransition(_a) { + var direction = _a.direction, style = _a.style, children = _a.children, enabledWeb = _a.enabledWeb; + var entering = direction === 'Forward' + ? SlideInRight.easing(Easing.out(Easing.exp)) + : SlideInLeft.easing(Easing.out(Easing.exp)); + var webEntering = enabledWeb ? FadeIn.duration(90) : undefined; + var exiting = FadeOut.duration(90); // Totally vibes based + var webExiting = enabledWeb ? FadeOut.duration(90) : undefined; + return (_jsx(Animated.View, { entering: IS_WEB ? webEntering : entering, exiting: IS_WEB ? webExiting : exiting, style: style, children: children })); +} diff --git a/src/components/SearchError.js b/src/components/SearchError.js new file mode 100644 index 0000000000..1fa6d6a5b3 --- /dev/null +++ b/src/components/SearchError.js @@ -0,0 +1,28 @@ +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 { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import * as Layout from '#/components/Layout'; +import { Text } from '#/components/Typography'; +export function SearchError(_a) { + var title = _a.title, children = _a.children; + var gtMobile = useBreakpoints().gtMobile; + var t = useTheme(); + return (_jsx(Layout.Content, { children: _jsxs(View, { style: [ + a.align_center, + a.gap_4xl, + a.px_xl, + { + paddingVertical: 150, + }, + ], children: [_jsx(XIcon, { width: 32, style: [t.atoms.text_contrast_low] }), _jsxs(View, { style: [ + a.align_center, + { maxWidth: gtMobile ? 394 : 294 }, + gtMobile ? a.gap_md : a.gap_sm, + ], children: [_jsx(Text, { style: [ + a.font_semi_bold, + a.text_lg, + a.text_center, + a.leading_snug, + ], children: title }), children] })] }) })); +} diff --git a/src/components/Select/index.js b/src/components/Select/index.js new file mode 100644 index 0000000000..1181f96a3c --- /dev/null +++ b/src/components/Select/index.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 __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, useCallback, useContext, useLayoutEffect, useMemo, useState, } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useTheme } from '#/alf'; +import { atoms as a } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon } from '#/components/icons/Chevron'; +import { Text } from '#/components/Typography'; +import { BaseRadio } from '../forms/Toggle'; +var Context = createContext(null); +Context.displayName = 'SelectContext'; +var ValueTextContext = createContext([undefined, function () { }]); +ValueTextContext.displayName = 'ValueTextContext'; +function useSelectContext() { + var ctx = useContext(Context); + if (!ctx) { + throw new Error('Select components must must be used within a Select.Root'); + } + return ctx; +} +export function Root(_a) { + var children = _a.children, value = _a.value, onValueChange = _a.onValueChange, disabled = _a.disabled; + var control = Dialog.useDialogControl(); + var valueTextCtx = useState(); + var ctx = useMemo(function () { return ({ + control: control, + value: value, + onValueChange: onValueChange, + disabled: disabled, + }); }, [control, value, onValueChange, disabled]); + return (_jsx(Context.Provider, { value: ctx, children: _jsx(ValueTextContext.Provider, { value: valueTextCtx, children: children }) })); +} +export function Trigger(_a) { + var children = _a.children, label = _a.label; + var control = useSelectContext().control; + var _b = useInteractionState(), focused = _b.state, onFocus = _b.onIn, onBlur = _b.onOut; + var _c = useInteractionState(), pressed = _c.state, onPressIn = _c.onIn, onPressOut = _c.onOut; + if (typeof children === 'function') { + return children({ + IS_NATIVE: true, + control: control, + state: { + hovered: false, + focused: focused, + pressed: pressed, + }, + props: { + onPress: control.open, + onFocus: onFocus, + onBlur: onBlur, + onPressIn: onPressIn, + onPressOut: onPressOut, + accessibilityLabel: label, + }, + }); + } + else { + return (_jsx(Button, { label: label, onPress: control.open, style: [a.flex_1, a.justify_between], color: "secondary", size: "small", shape: "rectangular", children: _jsx(_Fragment, { children: children }) })); + } +} +export function ValueText(_a) { + var placeholder = _a.placeholder, _b = _a.children, children = _b === void 0 ? function (value) { return value.label; } : _b, style = _a.style; + var value = useContext(ValueTextContext)[0]; + var t = useTheme(); + var text = value && children(value); + if (!text) + text = placeholder; + return (_jsx(ButtonText, { style: [t.atoms.text, a.font_normal, style], emoji: true, children: text })); +} +export function Icon(_a) { + return _jsx(ButtonIcon, { icon: ChevronUpDownIcon }); +} +export function Content(_a) { + var items = _a.items, _b = _a.valueExtractor, valueExtractor = _b === void 0 ? defaultItemValueExtractor : _b, props = __rest(_a, ["items", "valueExtractor"]); + var _c = useSelectContext(), control = _c.control, context = __rest(_c, ["control"]); + var _d = useContext(ValueTextContext), setValue = _d[1]; + useLayoutEffect(function () { + var item = items.find(function (item) { return valueExtractor(item) === context.value; }); + if (item) { + setValue(item); + } + }, [items, context.value, valueExtractor, setValue]); + return (_jsx(Dialog.Outer, { control: control, children: _jsx(ContentInner, __assign({ control: control, items: items, valueExtractor: valueExtractor }, props, context)) })); +} +function ContentInner(_a) { + var label = _a.label, items = _a.items, renderItem = _a.renderItem, valueExtractor = _a.valueExtractor, context = __rest(_a, ["label", "items", "renderItem", "valueExtractor"]); + var _ = useLingui()._; + var _b = useState(61), headerHeight = _b[0], setHeaderHeight = _b[1]; + var render = useCallback(function (_a) { + var item = _a.item, index = _a.index; + return renderItem(item, index, context.value); + }, [renderItem, context.value]); + return (_jsxs(Context.Provider, { value: context, children: [_jsx(Dialog.Header, { onLayout: function (evt) { return setHeaderHeight(evt.nativeEvent.layout.height); }, style: [ + a.absolute, + a.top_0, + a.left_0, + a.right_0, + a.z_10, + a.pt_3xl, + a.pb_sm, + a.border_b_0, + ], children: _jsx(Dialog.HeaderText, { style: [a.flex_1, a.px_xl, a.text_left, a.font_bold, a.text_2xl], children: label !== null && label !== void 0 ? label : _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select an option"], ["Select an option"])))) }) }), _jsx(Dialog.Handle, {}), _jsx(Dialog.InnerFlatList, { headerOffset: headerHeight, data: items, renderItem: render, keyExtractor: valueExtractor })] })); +} +function defaultItemValueExtractor(item) { + return item.value; +} +var ItemContext = createContext({ + selected: false, + hovered: false, + focused: false, + pressed: false, +}); +ItemContext.displayName = 'SelectItemContext'; +export function useItemContext() { + return useContext(ItemContext); +} +export function Item(_a) { + var children = _a.children, value = _a.value, label = _a.label, style = _a.style; + var t = useTheme(); + var control = Dialog.useDialogContext(); + var _b = useSelectContext(), selected = _b.value, onValueChange = _b.onValueChange; + return (_jsx(Button, { role: "listitem", label: label, style: [a.flex_1], onPress: function () { + control.close(function () { + onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(value); + }); + }, children: function (_a) { + var hovered = _a.hovered, focused = _a.focused, pressed = _a.pressed; + return (_jsx(ItemContext.Provider, { value: { selected: value === selected, hovered: hovered, focused: focused, pressed: pressed }, children: _jsx(View, { style: [ + a.flex_1, + a.px_xl, + (focused || pressed) && t.atoms.bg_contrast_25, + a.flex_row, + a.align_center, + a.gap_sm, + a.py_md, + style, + ], children: children }) })); + } })); +} +export function ItemText(_a) { + var children = _a.children, style = _a.style, emoji = _a.emoji; + var selected = useItemContext().selected; + return (_jsx(Text, { style: [a.text_md, selected && a.font_semi_bold, style], emoji: emoji, children: children })); +} +export function ItemIndicator(_a) { + var Icon = _a.icon; + var _b = useItemContext(), selected = _b.selected, focused = _b.focused, hovered = _b.hovered; + if (Icon) { + return _jsx(View, { style: { width: 24 }, children: selected && _jsx(Icon, { size: "md" }) }); + } + return (_jsx(BaseRadio, { selected: selected, focused: focused, hovered: hovered, isInvalid: false, disabled: false })); +} +export function Separator() { + var t = useTheme(); + return (_jsx(View, { style: [ + a.flex_1, + a.border_b, + t.atoms.border_contrast_low, + a.mx_xl, + a.my_xs, + ] })); +} +var templateObject_1; diff --git a/src/components/Select/index.web.js b/src/components/Select/index.web.js new file mode 100644 index 0000000000..4e482a281c --- /dev/null +++ b/src/components/Select/index.web.js @@ -0,0 +1,218 @@ +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 { createContext, forwardRef, Fragment, useContext, useMemo } from 'react'; +import { View } from 'react-native'; +import { Select as RadixSelect } from 'radix-ui'; +import { useA11y } from '#/state/a11y'; +import { flatten, useTheme, web } from '#/alf'; +import { atoms as a } from '#/alf'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon, } from '#/components/icons/Chevron'; +import { Text } from '#/components/Typography'; +var SelectedValueContext = createContext(null); +SelectedValueContext.displayName = 'SelectSelectedValueContext'; +export function Root(props) { + return (_jsx(SelectedValueContext.Provider, { value: props.value, children: _jsx(RadixSelect.Root, __assign({}, props)) })); +} +var RadixTriggerPassThrough = forwardRef(function (props, ref) { + // @ts-expect-error Radix provides no types of this stuff + var _a; + return (_a = props.children) === null || _a === void 0 ? void 0 : _a.call(props, __assign(__assign({}, props), { ref: ref })); +}); +RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'; +export function Trigger(_a) { + var children = _a.children, label = _a.label; + var t = useTheme(); + var _b = useInteractionState(), hovered = _b.state, onMouseEnter = _b.onIn, onMouseLeave = _b.onOut; + var _c = useInteractionState(), focused = _c.state, onFocus = _c.onIn, onBlur = _c.onOut; + if (typeof children === 'function') { + return (_jsx(RadixSelect.Trigger, { asChild: true, children: _jsx(RadixTriggerPassThrough, { children: function (props) { + return children({ + IS_NATIVE: false, + state: { + hovered: hovered, + focused: focused, + pressed: false, + }, + props: __assign(__assign({}, props), { onPress: props.onClick, onFocus: onFocus, onBlur: onBlur, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, accessibilityLabel: label }), + }); + } }) })); + } + else { + return (_jsx(RadixSelect.Trigger, { onFocus: onFocus, onBlur: onBlur, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: flatten([ + a.flex, + a.relative, + t.atoms.bg_contrast_50, + a.align_center, + a.gap_sm, + a.justify_between, + a.py_sm, + a.px_md, + a.pointer, + { + borderRadius: 10, + maxWidth: 400, + outline: 0, + borderWidth: 2, + borderStyle: 'solid', + borderColor: focused + ? t.palette.primary_500 + : t.palette.contrast_50, + }, + ]), children: children })); + } +} +export function ValueText(_a) { + var children = _a.children, webOverrideValue = _a.webOverrideValue, style = _a.style, props = __rest(_a, ["children", "webOverrideValue", "style"]); + var content; + if (webOverrideValue && children) { + content = children(webOverrideValue); + } + return (_jsx(Text, { style: style, children: _jsx(RadixSelect.Value, __assign({}, props, { children: content })) })); +} +export function Icon(_a) { + var style = _a.style; + var t = useTheme(); + return (_jsx(RadixSelect.Icon, { children: _jsx(ChevronDownIcon, { style: [t.atoms.text, style], size: "xs" }) })); +} +export function Content(_a) { + var items = _a.items, renderItem = _a.renderItem, _b = _a.valueExtractor, valueExtractor = _b === void 0 ? defaultItemValueExtractor : _b; + var t = useTheme(); + var selectedValue = useContext(SelectedValueContext); + var reduceMotionEnabled = useA11y().reduceMotionEnabled; + var scrollBtnStyles = [ + a.absolute, + a.flex, + a.align_center, + a.justify_center, + a.rounded_sm, + a.z_10, + ]; + var up = __spreadArray(__spreadArray([], scrollBtnStyles, true), [ + a.pt_sm, + a.pb_lg, + { + top: 0, + left: 0, + right: 0, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + background: "linear-gradient(to bottom, ".concat(t.atoms.bg.backgroundColor, " 0%, transparent 100%)"), + }, + ], false); + var down = __spreadArray(__spreadArray([], scrollBtnStyles, true), [ + a.pt_lg, + a.pb_sm, + { + bottom: 0, + left: 0, + right: 0, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + background: "linear-gradient(to top, ".concat(t.atoms.bg.backgroundColor, " 0%, transparent 100%)"), + }, + ], false); + return (_jsx(RadixSelect.Portal, { children: _jsx(RadixSelect.Content, { style: flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden]), position: "popper", align: "center", sideOffset: 5, className: "radix-select-content", + // prevent the keyboard shortcut for opening the composer + onKeyDown: function (evt) { return evt.stopPropagation(); }, children: _jsxs(View, { style: [ + a.flex_1, + a.border, + t.atoms.border_contrast_low, + a.rounded_sm, + a.overflow_hidden, + !reduceMotionEnabled && a.zoom_fade_in, + ], children: [_jsx(RadixSelect.ScrollUpButton, { style: flatten(up), children: _jsx(ChevronUpIcon, { style: [t.atoms.text], size: "xs" }) }), _jsx(RadixSelect.Viewport, { style: flatten([a.p_xs]), children: items.map(function (item, index) { return (_jsx(Fragment, { children: renderItem(item, index, selectedValue) }, valueExtractor(item))); }) }), _jsx(RadixSelect.ScrollDownButton, { style: flatten(down), children: _jsx(ChevronDownIcon, { style: [t.atoms.text], size: "xs" }) })] }) }) })); +} +function defaultItemValueExtractor(item) { + return item.value; +} +var ItemContext = createContext({ + hovered: false, + focused: false, + pressed: false, + selected: false, +}); +ItemContext.displayName = 'SelectItemContext'; +export function useItemContext() { + return useContext(ItemContext); +} +export function Item(_a) { + var ref = _a.ref, value = _a.value, style = _a.style, children = _a.children; + var t = useTheme(); + var _b = useInteractionState(), hovered = _b.state, onMouseEnter = _b.onIn, onMouseLeave = _b.onOut; + var selected = useContext(SelectedValueContext) === value; + var _c = useInteractionState(), focused = _c.state, onFocus = _c.onIn, onBlur = _c.onOut; + var ctx = useMemo(function () { return ({ hovered: hovered, focused: focused, pressed: false, selected: selected }); }, [hovered, focused, selected]); + return (_jsx(RadixSelect.Item, { ref: ref, value: value, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onFocus: onFocus, onBlur: onBlur, style: flatten([ + t.atoms.text, + a.relative, + a.flex, + { minHeight: 25, paddingLeft: 30, paddingRight: 8 }, + a.user_select_none, + a.align_center, + a.rounded_xs, + a.py_2xs, + a.text_sm, + { outline: 0 }, + (hovered || focused) && { backgroundColor: t.palette.primary_50 }, + selected && [a.font_semi_bold], + a.transition_color, + style, + ]), children: _jsx(ItemContext.Provider, { value: ctx, children: children }) })); +} +export var ItemText = function ItemText(_a) { + var children = _a.children, style = _a.style; + return (_jsx(RadixSelect.ItemText, { asChild: true, children: _jsx(Text, { style: flatten([style, web({ pointerEvents: 'inherit' })]), children: children }) })); +}; +export function ItemIndicator(_a) { + var _b = _a.icon, Icon = _b === void 0 ? CheckIcon : _b; + return (_jsx(RadixSelect.ItemIndicator, { style: flatten([ + a.absolute, + { left: 0, width: 30 }, + a.flex, + a.align_center, + a.justify_center, + ]), children: _jsx(Icon, { size: "sm" }) })); +} +export function Separator() { + var t = useTheme(); + return (_jsx(RadixSelect.Separator, { style: flatten([ + { + height: 1, + backgroundColor: t.atoms.border_contrast_low.borderColor, + }, + a.my_xs, + a.w_full, + ]) })); +} diff --git a/src/components/Select/types.js b/src/components/Select/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/Select/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/Skeleton.js b/src/components/Skeleton.js new file mode 100644 index 0000000000..fab9fd0b49 --- /dev/null +++ b/src/components/Skeleton.js @@ -0,0 +1,72 @@ +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 { View } from 'react-native'; +import { atoms as a, flatten, useAlf, useTheme, } from '#/alf'; +import { normalizeTextStyles } from '#/alf/typography'; +export function Text(_a) { + var blend = _a.blend, style = _a.style; + var _b = useAlf(), fonts = _b.fonts, flags = _b.flags, t = _b.theme; + var _c = flatten(style), width = _c.width, flattened = __rest(_c, ["width"]); + var _d = normalizeTextStyles([a.text_sm, a.leading_snug, flattened], { + fontScale: fonts.scaleMultiplier, + fontFamily: fonts.family, + flags: flags, + }), _e = _d.lineHeight, lineHeight = _e === void 0 ? 14 : _e, rest = __rest(_d, ["lineHeight"]); + return (_jsx(View, { style: [a.flex_1, { maxWidth: width, paddingVertical: lineHeight * 0.15 }], children: _jsx(View, { style: [ + a.rounded_md, + t.atoms.bg_contrast_50, + { + height: lineHeight * 0.7, + opacity: blend ? 0.6 : 1, + }, + rest, + ] }) })); +} +export function Circle(_a) { + var children = _a.children, size = _a.size, blend = _a.blend, style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.justify_center, + a.align_center, + a.rounded_full, + t.atoms.bg_contrast_50, + { + width: size, + height: size, + opacity: blend ? 0.6 : 1, + }, + style, + ], children: children })); +} +export function Pill(_a) { + var size = _a.size, blend = _a.blend, style = _a.style; + var t = useTheme(); + return (_jsx(View, { style: [ + a.rounded_full, + t.atoms.bg_contrast_50, + { + width: size * 1.618, + height: size, + opacity: blend ? 0.6 : 1, + }, + style, + ] })); +} +export function Col(_a) { + var children = _a.children, style = _a.style; + return _jsx(View, { style: [a.flex_1, style], children: children }); +} +export function Row(_a) { + var children = _a.children, style = _a.style; + return _jsx(View, { style: [a.flex_row, style], children: children }); +} diff --git a/src/components/StarterPack/Main/FeedsList.js b/src/components/StarterPack/Main/FeedsList.js new file mode 100644 index 0000000000..036d4eb38d --- /dev/null +++ b/src/components/StarterPack/Main/FeedsList.js @@ -0,0 +1,36 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React, { useCallback } from 'react'; +import { View } from 'react-native'; +import { useBottomBarOffset } from '#/lib/hooks/useBottomBarOffset'; +import { List } from '#/view/com/util/List'; +import { atoms as a, useTheme } from '#/alf'; +import * as FeedCard from '#/components/FeedCard'; +import { IS_NATIVE, IS_WEB } from '#/env'; +function keyExtractor(item) { + return item.uri; +} +export var FeedsList = React.forwardRef(function FeedsListImpl(_a, ref) { + var feeds = _a.feeds, headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + var initialHeaderHeight = React.useState(headerHeight)[0]; + var bottomBarOffset = useBottomBarOffset(20); + 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]); + React.useImperativeHandle(ref, function () { return ({ + scrollToTop: onScrollToTop, + }); }); + var renderItem = function (_a) { + var item = _a.item, index = _a.index; + return (_jsx(View, { style: [ + a.p_lg, + (IS_WEB || index !== 0) && a.border_t, + t.atoms.border_contrast_low, + ], children: _jsx(FeedCard.Default, { view: item }) })); + }; + return (_jsx(List, { data: feeds, renderItem: renderItem, keyExtractor: keyExtractor, ref: scrollElRef, headerOffset: headerHeight, ListFooterComponent: _jsx(View, { style: [{ height: initialHeaderHeight + bottomBarOffset }] }), showsVerticalScrollIndicator: false, desktopFixedHeight: true })); +}); diff --git a/src/components/StarterPack/Main/PostsList.js b/src/components/StarterPack/Main/PostsList.js new file mode 100644 index 0000000000..3710bfece9 --- /dev/null +++ b/src/components/StarterPack/Main/PostsList.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 } from "react/jsx-runtime"; +import React, { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { PostFeed } from '#/view/com/posts/PostFeed'; +import { EmptyState } from '#/view/com/util/EmptyState'; +import { HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon } from '#/components/icons/Hashtag'; +import { IS_NATIVE } from '#/env'; +export var PostsList = React.forwardRef(function PostsListImpl(_a, ref) { + var listUri = _a.listUri, headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + var feed = "list|".concat(listUri); + var _ = useLingui()._; + var onScrollToTop = useCallback(function () { + var _a; + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: -headerHeight, + }); + }, [scrollElRef, headerHeight]); + React.useImperativeHandle(ref, function () { return ({ + scrollToTop: onScrollToTop, + }); }); + var renderPostsEmpty = useCallback(function () { + return (_jsx(EmptyState, { icon: HashtagWideIcon, iconSize: "2xl", message: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["This feed is empty."], ["This feed is empty."])))) })); + }, [_]); + return (_jsx(View, { children: _jsx(PostFeed, { feed: feed, pollInterval: 60e3, scrollElRef: scrollElRef, renderEmptyState: renderPostsEmpty, headerOffset: headerHeight }) })); +}); +var templateObject_1; diff --git a/src/components/StarterPack/Main/ProfilesList.js b/src/components/StarterPack/Main/ProfilesList.js new file mode 100644 index 0000000000..c1b690e1f8 --- /dev/null +++ b/src/components/StarterPack/Main/ProfilesList.js @@ -0,0 +1,124 @@ +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 React, { useCallback } from 'react'; +import { View } from 'react-native'; +import { AtUri, } from '@atproto/api'; +import { useBottomBarOffset } from '#/lib/hooks/useBottomBarOffset'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { isBlockedOrBlocking } from '#/lib/moderation/blocked-and-muted'; +import { useAllListMembersQuery } from '#/state/queries/list-members'; +import { useSession } from '#/state/session'; +import { List } from '#/view/com/util/List'; +import { atoms as a, useTheme } from '#/alf'; +import { ListFooter, ListMaybePlaceholder } from '#/components/Lists'; +import { Default as ProfileCard } from '#/components/ProfileCard'; +import { IS_NATIVE, IS_WEB } from '#/env'; +function keyExtractor(item, index) { + return "".concat(item.did, "-").concat(index); +} +export var ProfilesList = React.forwardRef(function ProfilesListImpl(_a, ref) { + var _this = this; + var listUri = _a.listUri, moderationOpts = _a.moderationOpts, headerHeight = _a.headerHeight, scrollElRef = _a.scrollElRef; + var t = useTheme(); + var bottomBarOffset = useBottomBarOffset(headerHeight); + var initialNumToRender = useInitialNumToRender(); + var currentAccount = useSession().currentAccount; + var _b = useAllListMembersQuery(listUri), data = _b.data, refetch = _b.refetch, isError = _b.isError; + var _c = React.useState(false), isPTRing = _c[0], setIsPTRing = _c[1]; + // The server returns these sorted by descending creation date, so we want to invert + var profiles = data === null || data === void 0 ? void 0 : data.filter(function (p) { var _a; return !isBlockedOrBlocking(p.subject) && !((_a = p.subject.associated) === null || _a === void 0 ? void 0 : _a.labeler); }).map(function (p) { return p.subject; }).reverse(); + var isOwn = new AtUri(listUri).host === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var getSortedProfiles = function () { + if (!profiles) + return; + if (!isOwn) + return profiles; + var myIndex = profiles.findIndex(function (p) { return p.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); }); + return myIndex !== -1 + ? __spreadArray(__spreadArray([ + profiles[myIndex] + ], profiles.slice(0, myIndex), true), profiles.slice(myIndex + 1), true) : profiles; + }; + var onScrollToTop = useCallback(function () { + var _a; + (_a = scrollElRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ + animated: IS_NATIVE, + offset: -headerHeight, + }); + }, [scrollElRef, headerHeight]); + React.useImperativeHandle(ref, function () { return ({ + scrollToTop: onScrollToTop, + }); }); + var renderItem = function (_a) { + var item = _a.item, index = _a.index; + return (_jsx(View, { style: [ + a.p_lg, + t.atoms.border_contrast_low, + (IS_WEB || index !== 0) && a.border_t, + ], children: _jsx(ProfileCard, { profile: item, moderationOpts: moderationOpts, logContext: "StarterPackProfilesList" }) })); + }; + if (!data) { + return (_jsx(View, { style: [ + a.h_full_vh, + { marginTop: headerHeight, marginBottom: bottomBarOffset }, + ], children: _jsx(ListMaybePlaceholder, { isLoading: true, isError: isError, onRetry: refetch }) })); + } + if (data) + return (_jsx(List, { data: getSortedProfiles(), renderItem: renderItem, keyExtractor: keyExtractor, ref: scrollElRef, headerOffset: headerHeight, ListFooterComponent: _jsx(ListFooter, { style: { paddingBottom: bottomBarOffset, borderTopWidth: 0 } }), showsVerticalScrollIndicator: false, desktopFixedHeight: true, initialNumToRender: initialNumToRender, refreshing: isPTRing, onRefresh: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setIsPTRing(true); + return [4 /*yield*/, refetch()]; + case 1: + _a.sent(); + setIsPTRing(false); + return [2 /*return*/]; + } + }); + }); } })); +}); diff --git a/src/components/StarterPack/ProfileStarterPacks.js b/src/components/StarterPack/ProfileStarterPacks.js new file mode 100644 index 0000000000..f6691d9423 --- /dev/null +++ b/src/components/StarterPack/ProfileStarterPacks.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 __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, useImperativeHandle, useState } from 'react'; +import { findNodeHandle, useWindowDimensions, View, } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { useGenerateStarterPackMutation } from '#/lib/generate-starterpack'; +import { useBottomBarOffset } from '#/lib/hooks/useBottomBarOffset'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { useWebMediaQueries } from '#/lib/hooks/useWebMediaQueries'; +import { parseStarterPackUri } from '#/lib/strings/starter-pack'; +import { logger } from '#/logger'; +import { useActorStarterPacksQuery } from '#/state/queries/actor-starter-packs'; +import { EmptyState, } from '#/view/com/util/EmptyState'; +import { List } from '#/view/com/util/List'; +import { FeedLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { atoms as a, ios, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { PlusSmall_Stroke2_Corner0_Rounded as Plus } from '#/components/icons/Plus'; +import { LinearGradientBackground } from '#/components/LinearGradientBackground'; +import { Loader } from '#/components/Loader'; +import * as Prompt from '#/components/Prompt'; +import { Default as StarterPackCard } from '#/components/StarterPack/StarterPackCard'; +import { Text } from '#/components/Typography'; +import { IS_IOS } from '#/env'; +function keyExtractor(item) { + return item.uri; +} +export function ProfileStarterPacks(_a) { + var _this = this; + var ref = _a.ref, scrollElRef = _a.scrollElRef, did = _a.did, headerOffset = _a.headerOffset, enabled = _a.enabled, style = _a.style, testID = _a.testID, setScrollViewTag = _a.setScrollViewTag, isMe = _a.isMe, emptyStateMessage = _a.emptyStateMessage, emptyStateButton = _a.emptyStateButton, emptyStateIcon = _a.emptyStateIcon; + var t = useTheme(); + var bottomBarOffset = useBottomBarOffset(100); + var height = useWindowDimensions().height; + var _b = useState(false), isPTRing = _b[0], setIsPTRing = _b[1]; + var _c = useActorStarterPacksQuery({ did: did, enabled: enabled }), data = _c.data, refetch = _c.refetch, isError = _c.isError, hasNextPage = _c.hasNextPage, isFetchingNextPage = _c.isFetchingNextPage, fetchNextPage = _c.fetchNextPage; + var isTabletOrDesktop = useWebMediaQueries().isTabletOrDesktop; + var items = data === null || data === void 0 ? void 0 : data.pages.flatMap(function (page) { return page.starterPacks; }); + var _ = useLingui()._; + var EmptyComponent = useCallback(function () { + if (emptyStateMessage || emptyStateButton || emptyStateIcon) { + return (_jsx(View, { style: [a.px_lg, a.align_center, a.justify_center], children: _jsx(EmptyState, { icon: emptyStateIcon, iconSize: "3xl", message: emptyStateMessage !== null && emptyStateMessage !== void 0 ? emptyStateMessage : _('Starter packs let you share your favorite feeds and people with your friends.'), button: emptyStateButton }) })); + } + return _jsx(Empty, {}); + }, [_, emptyStateMessage, emptyStateButton, emptyStateIcon]); + useImperativeHandle(ref, function () { return ({ + scrollToTop: function () { }, + }); }); + 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 starter packs', { 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 starter packs', { message: err_2 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]); + useEffect(function () { + if (IS_IOS && enabled && scrollElRef.current) { + var nativeTag = findNodeHandle(scrollElRef.current); + setScrollViewTag(nativeTag); + } + }, [enabled, scrollElRef, setScrollViewTag]); + var renderItem = useCallback(function (_a) { + var item = _a.item, index = _a.index; + return (_jsx(View, { style: [ + a.p_lg, + (isTabletOrDesktop || index !== 0) && a.border_t, + t.atoms.border_contrast_low, + ], children: _jsx(StarterPackCard, { starterPack: item }) })); + }, [isTabletOrDesktop, t.atoms.border_contrast_low]); + return (_jsx(View, { testID: testID, style: style, children: _jsx(List, { testID: testID ? "".concat(testID, "-flatlist") : undefined, ref: scrollElRef, data: items, renderItem: renderItem, keyExtractor: keyExtractor, refreshing: isPTRing, headerOffset: headerOffset, progressViewOffset: ios(0), contentContainerStyle: { + minHeight: height + headerOffset, + paddingBottom: bottomBarOffset, + }, removeClippedSubviews: true, desktopFixedHeight: true, onEndReached: onEndReached, onRefresh: onRefresh, ListEmptyComponent: data ? (isMe ? EmptyComponent : undefined) : FeedLoadingPlaceholder, ListFooterComponent: !!data && (items === null || items === void 0 ? void 0 : items.length) !== 0 && isMe ? CreateAnother : undefined }) })); +} +function CreateAnother() { + var _ = useLingui()._; + var t = useTheme(); + var navigation = useNavigation(); + return (_jsx(View, { style: [ + a.pr_md, + a.pt_lg, + a.gap_lg, + a.border_t, + t.atoms.border_contrast_low, + ], children: _jsxs(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Create a starter pack"], ["Create a starter pack"])))), variant: "solid", color: "secondary", size: "small", style: [a.self_center], onPress: function () { return navigation.navigate('StarterPackWizard', {}); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Create another" }) }), _jsx(ButtonIcon, { icon: Plus, position: "right" })] }) })); +} +function Empty() { + var _ = useLingui()._; + var navigation = useNavigation(); + var confirmDialogControl = useDialogControl(); + var followersDialogControl = useDialogControl(); + var errorDialogControl = useDialogControl(); + var requireEmailVerification = useRequireEmailVerification(); + var _a = useState(false), isGenerating = _a[0], setIsGenerating = _a[1]; + var generateStarterPack = useGenerateStarterPackMutation({ + onSuccess: function (_a) { + var uri = _a.uri; + var parsed = parseStarterPackUri(uri); + if (parsed) { + navigation.push('StarterPack', { + name: parsed.name, + rkey: parsed.rkey, + }); + } + setIsGenerating(false); + }, + onError: function (e) { + logger.error('Failed to generate starter pack', { safeMessage: e }); + setIsGenerating(false); + if (e.message.includes('NOT_ENOUGH_FOLLOWERS')) { + followersDialogControl.open(); + } + else { + errorDialogControl.open(); + } + }, + }).mutate; + var generate = function () { + setIsGenerating(true); + generateStarterPack(); + }; + var openConfirmDialog = useCallback(function () { + confirmDialogControl.open(); + }, [confirmDialogControl]); + var wrappedOpenConfirmDialog = requireEmailVerification(openConfirmDialog, { + instructions: [ + _jsx(Trans, { children: "Before creating a starter pack, you must first verify your email." }, "confirm"), + ], + }); + var navToWizard = useCallback(function () { + navigation.navigate('StarterPackWizard', {}); + }, [navigation]); + var wrappedNavToWizard = requireEmailVerification(navToWizard, { + instructions: [ + _jsx(Trans, { children: "Before creating a starter pack, you must first verify your email." }, "nav"), + ], + }); + return (_jsxs(LinearGradientBackground, { style: [ + a.px_lg, + a.py_lg, + a.justify_between, + a.gap_lg, + a.shadow_lg, + { marginTop: a.border.borderWidth }, + ], children: [_jsxs(View, { style: [a.gap_xs], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_lg, { color: 'white' }], children: _jsx(Trans, { children: "You haven't created a starter pack yet!" }) }), _jsx(Text, { style: [a.text_md, { color: 'white' }], children: _jsx(Trans, { children: "Starter packs let you easily share your favorite feeds and people with your friends." }) })] }), _jsxs(View, { style: [a.flex_row, a.gap_md, { marginLeft: 'auto' }], children: [_jsxs(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Create a starter pack for me"], ["Create a starter pack for me"])))), variant: "ghost", color: "primary", size: "small", disabled: isGenerating, onPress: wrappedOpenConfirmDialog, style: { backgroundColor: 'transparent' }, children: [_jsx(ButtonText, { style: { color: 'white' }, children: _jsx(Trans, { children: "Make one for me" }) }), isGenerating && _jsx(Loader, { size: "md" })] }), _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Create a starter pack"], ["Create a starter pack"])))), variant: "ghost", color: "primary", size: "small", disabled: isGenerating, onPress: wrappedNavToWizard, style: { + backgroundColor: 'white', + borderColor: 'white', + width: 100, + }, hoverStyle: [{ backgroundColor: '#dfdfdf' }], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Create" }) }) })] }), _jsxs(Prompt.Outer, { control: confirmDialogControl, children: [_jsx(Prompt.TitleText, { children: _jsx(Trans, { children: "Generate a starter pack" }) }), _jsx(Prompt.DescriptionText, { children: _jsx(Trans, { children: "Bluesky will choose a set of recommended accounts from people in your network." }) }), _jsxs(Prompt.Actions, { children: [_jsx(Prompt.Action, { color: "primary", cta: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Choose for me"], ["Choose for me"])))), onPress: generate }), _jsx(Prompt.Action, { color: "secondary", cta: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Let me choose"], ["Let me choose"])))), onPress: function () { + navigation.navigate('StarterPackWizard', {}); + } })] })] }), _jsx(Prompt.Basic, { control: followersDialogControl, title: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Oops!"], ["Oops!"])))), description: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["You must be following at least seven other people to generate a starter pack."], ["You must be following at least seven other people to generate a starter pack."])))), onConfirm: function () { }, showCancel: false }), _jsx(Prompt.Basic, { control: errorDialogControl, title: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Oops!"], ["Oops!"])))), description: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["An error occurred while generating your starter pack. Want to try again?"], ["An error occurred while generating your starter pack. Want to try again?"])))), onConfirm: generate, confirmButtonCta: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Retry"], ["Retry"])))) })] })); +} +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/components/StarterPack/QrCode.js b/src/components/StarterPack/QrCode.js new file mode 100644 index 0000000000..60341abcb6 --- /dev/null +++ b/src/components/StarterPack/QrCode.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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { lazy, useState } from 'react'; +import { View } from 'react-native'; +// @ts-expect-error missing types +import QRCode from 'react-native-qrcode-styled'; +import { AppBskyGraphStarterpack } from '@atproto/api'; +import { Trans } from '@lingui/macro'; +import { Logo } from '#/view/icons/Logo'; +import { Logotype } from '#/view/icons/Logotype'; +import { useTheme } from '#/alf'; +import { atoms as a } from '#/alf'; +import { LinearGradientBackground } from '#/components/LinearGradientBackground'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +import * as bsky from '#/types/bsky'; +var LazyViewShot = lazy( +// @ts-expect-error dynamic import +function () { return import('react-native-view-shot/src/index'); }); +export function QrCode(_a) { + var starterPack = _a.starterPack, link = _a.link, ref = _a.ref; + var record = starterPack.record; + if (!bsky.dangerousIsType(record, AppBskyGraphStarterpack.isRecord)) { + return null; + } + return (_jsx(LazyViewShot, { ref: ref, children: _jsxs(LinearGradientBackground, { style: [ + { width: 300, minHeight: 390 }, + a.align_center, + a.px_sm, + a.py_xl, + a.rounded_sm, + a.justify_between, + a.gap_md, + ], children: [_jsx(View, { style: [a.gap_sm], children: _jsx(Text, { style: [ + a.font_semi_bold, + a.text_3xl, + a.text_center, + { color: 'white' }, + ], children: record.name }) }), _jsxs(View, { style: [a.gap_xl, a.align_center], children: [_jsx(Text, { style: [ + a.font_semi_bold, + a.text_center, + { color: 'white', fontSize: 18 }, + ], children: _jsx(Trans, { children: "Join the conversation" }) }), _jsx(View, { style: [a.rounded_sm, a.overflow_hidden], children: _jsx(QrCodeInner, { link: link }) }), _jsx(Text, { style: [ + a.flex, + a.flex_row, + a.align_center, + a.font_semi_bold, + { color: 'white', fontSize: 18, gap: 6 }, + ], children: _jsxs(Trans, { children: ["on", _jsxs(View, { style: [a.flex_row, a.align_center, { gap: 6 }], children: [_jsx(Logo, { width: 25, fill: "white" }), _jsx(View, { style: [{ marginTop: 3.5 }], children: _jsx(Logotype, { width: 72, fill: "white" }) })] })] }) })] })] }) })); +} +export function QrCodeInner(_a) { + var link = _a.link; + var t = useTheme(); + var _b = useState(null), logoArea = _b[0], setLogoArea = _b[1]; + var onLogoAreaChange = function (area) { + setLogoArea(area); + }; + return (_jsxs(View, { style: { position: 'relative' }, children: [IS_WEB && logoArea && (_jsx(View, { style: { + position: 'absolute', + left: logoArea.x, + top: logoArea.y + 1, + zIndex: 1, + padding: 4, + }, children: _jsx(Logo, { width: logoArea.width - 14, height: logoArea.height - 14 }) })), _jsx(QRCode, { data: link, style: [ + a.rounded_sm, + { height: 225, width: 225, backgroundColor: '#f3f3f3' }, + ], pieceSize: IS_WEB ? 8 : 6, padding: 20, pieceBorderRadius: IS_WEB ? 4.5 : 3.5, outerEyesOptions: { + topLeft: { + borderRadius: [12, 12, 0, 12], + color: t.palette.primary_500, + }, + topRight: { + borderRadius: [12, 12, 12, 0], + color: t.palette.primary_500, + }, + bottomLeft: { + borderRadius: [12, 0, 12, 12], + color: t.palette.primary_500, + }, + }, innerEyesOptions: { borderRadius: 3 }, logo: __assign(__assign(__assign({ href: require('../../../assets/logo.png') }, (IS_WEB && { + onChange: onLogoAreaChange, + padding: 28, + })), (!IS_WEB && { + padding: 2, + scale: 0.95, + })), { hidePieces: true }) })] })); +} diff --git a/src/components/StarterPack/QrCodeDialog.js b/src/components/StarterPack/QrCodeDialog.js new file mode 100644 index 0000000000..c3db255f03 --- /dev/null +++ b/src/components/StarterPack/QrCodeDialog.js @@ -0,0 +1,220 @@ +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 { Suspense, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { requestMediaLibraryPermissionsAsync } from 'expo-image-picker'; +import { createAssetAsync } from 'expo-media-library'; +import * as Sharing from 'expo-sharing'; +import { AppBskyGraphStarterpack } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { logger } from '#/logger'; +import { atoms as a, useBreakpoints } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon } from '#/components/icons/ArrowOutOfBox'; +import { ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon } from '#/components/icons/ChainLink'; +import { FloppyDisk_Stroke2_Corner0_Rounded as FloppyDiskIcon } from '#/components/icons/FloppyDisk'; +import { Loader } from '#/components/Loader'; +import { QrCode } from '#/components/StarterPack/QrCode'; +import * as Toast from '#/components/Toast'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE, IS_WEB } from '#/env'; +import * as bsky from '#/types/bsky'; +export function QrCodeDialog(_a) { + var _this = this; + var starterPack = _a.starterPack, link = _a.link, control = _a.control; + var _ = useLingui()._; + var ax = useAnalytics(); + var gtMobile = useBreakpoints().gtMobile; + var _b = useState(false), isSaveProcessing = _b[0], setIsSaveProcessing = _b[1]; + var _c = useState(false), isCopyProcessing = _c[0], setIsCopyProcessing = _c[1]; + var ref = useRef(null); + var getCanvas = function (base64) { + return new Promise(function (resolve) { + var image = new Image(); + image.onload = function () { + var canvas = document.createElement('canvas'); + canvas.width = image.width; + canvas.height = image.height; + var ctx = canvas.getContext('2d'); + ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(image, 0, 0); + resolve(canvas); + }; + image.src = base64; + }); + }; + var onSavePress = function () { return __awaiter(_this, void 0, void 0, function () { + var _this = this; + var _a, _b; + return __generator(this, function (_c) { + (_b = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.capture) === null || _b === void 0 ? void 0 : _b.call(_a).then(function (uri) { return __awaiter(_this, void 0, void 0, function () { + var res, e_1, canvas, imgHref, link_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!IS_NATIVE) return [3 /*break*/, 6]; + return [4 /*yield*/, requestMediaLibraryPermissionsAsync()]; + case 1: + res = _a.sent(); + if (!res.granted) { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You must grant access to your photo library to save a QR code"], ["You must grant access to your photo library to save a QR code"]))))); + return [2 /*return*/]; + } + _a.label = 2; + case 2: + _a.trys.push([2, 4, , 5]); + return [4 /*yield*/, createAssetAsync("file://".concat(uri))]; + case 3: + _a.sent(); + return [3 /*break*/, 5]; + case 4: + e_1 = _a.sent(); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["An error occurred while saving the QR code!"], ["An error occurred while saving the QR code!"])))), { + type: 'error', + }); + logger.error('Failed to save QR code', { error: e_1 }); + return [2 /*return*/]; + case 5: return [3 /*break*/, 8]; + case 6: + setIsSaveProcessing(true); + if (!bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)) { + return [2 /*return*/]; + } + return [4 /*yield*/, getCanvas(uri)]; + case 7: + canvas = _a.sent(); + imgHref = canvas + .toDataURL('image/png') + .replace('image/png', 'image/octet-stream'); + link_1 = document.createElement('a'); + link_1.setAttribute('download', "".concat(starterPack.record.name.replaceAll(' ', '_'), "_Share_Card.png")); + link_1.setAttribute('href', imgHref); + link_1.click(); + _a.label = 8; + case 8: + ax.metric('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'qrcode', + qrShareType: 'save', + }); + setIsSaveProcessing(false); + Toast.show(IS_WEB + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["QR code has been downloaded!"], ["QR code has been downloaded!"])))) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["QR code saved to your camera roll!"], ["QR code saved to your camera roll!"]))))); + control.close(); + return [2 /*return*/]; + } + }); + }); }); + return [2 /*return*/]; + }); + }); }; + var onCopyPress = function () { return __awaiter(_this, void 0, void 0, function () { + var _this = this; + var _a, _b; + return __generator(this, function (_c) { + setIsCopyProcessing(true); + (_b = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.capture) === null || _b === void 0 ? void 0 : _b.call(_a).then(function (uri) { return __awaiter(_this, void 0, void 0, function () { + var canvas; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getCanvas(uri) + // @ts-expect-error web only + ]; + case 1: + canvas = _a.sent(); + // @ts-expect-error web only + canvas.toBlob(function (blob) { + var item = new ClipboardItem({ 'image/png': blob }); + navigator.clipboard.write([item]); + }); + ax.metric('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'qrcode', + qrShareType: 'copy', + }); + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["QR code copied to your clipboard!"], ["QR code copied to your clipboard!"]))))); + setIsCopyProcessing(false); + control.close(); + return [2 /*return*/]; + } + }); + }); }); + return [2 /*return*/]; + }); + }); }; + var onSharePress = function () { return __awaiter(_this, void 0, void 0, function () { + var _this = this; + var _a, _b; + return __generator(this, function (_c) { + (_b = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.capture) === null || _b === void 0 ? void 0 : _b.call(_a).then(function (uri) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + control.close(function () { + Sharing.shareAsync(uri, { mimeType: 'image/png', UTI: 'image/png' }).then(function () { + ax.metric('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'qrcode', + qrShareType: 'share', + }); + }); + }); + return [2 /*return*/]; + }); + }); }); + return [2 /*return*/]; + }); + }); }; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Create a QR code for a starter pack"], ["Create a QR code for a starter pack"])))), children: [_jsx(View, { style: [a.flex_1, a.align_center, a.gap_5xl], children: _jsx(Suspense, { fallback: _jsx(Loading, {}), children: !link ? (_jsx(Loading, {})) : (_jsxs(_Fragment, { children: [_jsx(QrCode, { starterPack: starterPack, link: link, ref: ref }), _jsxs(View, { style: [ + a.w_full, + a.gap_md, + gtMobile && [a.flex_row, a.justify_center, a.flex_wrap], + ], children: [_jsxs(Button, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Copy QR code"], ["Copy QR code"])))), color: "primary_subtle", size: "large", onPress: IS_WEB ? onCopyPress : onSharePress, children: [_jsx(ButtonIcon, { icon: isCopyProcessing + ? Loader + : IS_WEB + ? ChainLinkIcon + : ShareIcon }), _jsx(ButtonText, { children: IS_WEB ? _jsx(Trans, { children: "Copy" }) : _jsx(Trans, { children: "Share" }) })] }), _jsxs(Button, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Save QR code"], ["Save QR code"])))), color: "secondary", size: "large", onPress: onSavePress, children: [_jsx(ButtonIcon, { icon: isSaveProcessing ? Loader : FloppyDiskIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Save" }) })] })] })] })) }) }), _jsx(Dialog.Close, {})] })] })); +} +function Loading() { + return (_jsx(View, { style: [a.align_center, a.justify_center, { minHeight: 400 }], children: _jsx(Loader, { size: "xl" }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/components/StarterPack/ShareDialog.js b/src/components/StarterPack/ShareDialog.js new file mode 100644 index 0000000000..bd263b1963 --- /dev/null +++ b/src/components/StarterPack/ShareDialog.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 __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 { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useSaveImageToMediaLibrary } from '#/lib/media/save-image'; +import { shareUrl } from '#/lib/sharing'; +import { getStarterPackOgCard } from '#/lib/strings/starter-pack'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon } from '#/components/icons/ChainLink'; +import { Download_Stroke2_Corner0_Rounded as DownloadIcon } from '#/components/icons/Download'; +import { QrCode_Stroke2_Corner0_Rounded as QrCodeIcon } from '#/components/icons/QrCode'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE, IS_WEB } from '#/env'; +export function ShareDialog(props) { + return (_jsxs(Dialog.Outer, { control: props.control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(ShareDialogInner, __assign({}, props))] })); +} +function ShareDialogInner(_a) { + var _this = this; + var starterPack = _a.starterPack, link = _a.link, imageLoaded = _a.imageLoaded, qrDialogControl = _a.qrDialogControl, control = _a.control; + var _ = useLingui()._; + var ax = useAnalytics(); + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var imageUrl = getStarterPackOgCard(starterPack); + var onShareLink = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (!link) + return [2 /*return*/]; + shareUrl(link); + ax.metric('starterPack:share', { + starterPack: starterPack.uri, + shareType: 'link', + }); + control.close(); + return [2 /*return*/]; + }); + }); }; + var saveImageToAlbum = useSaveImageToMediaLibrary(); + var onSave = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, saveImageToAlbum(imageUrl)]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }; + return (_jsx(_Fragment, { children: _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Share link dialog"], ["Share link dialog"])))), children: [!imageLoaded || !link ? (_jsx(View, { style: [a.align_center, a.justify_center, { minHeight: 350 }], children: _jsx(Loader, { size: "xl" }) })) : (_jsxs(View, { style: [!gtMobile && a.gap_lg], children: [_jsxs(View, { style: [a.gap_sm, gtMobile && a.pb_lg], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_2xl], children: _jsx(Trans, { children: "Invite people to this starter pack!" }) }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Share this starter pack and help people join your community on Bluesky." }) })] }), _jsx(Image, { source: { uri: imageUrl }, style: [ + a.rounded_sm, + a.aspect_card, + { + transform: [{ scale: gtMobile ? 0.85 : 1 }], + marginTop: gtMobile ? -20 : 0, + }, + ], accessibilityIgnoresInvertColors: true }), _jsxs(View, { style: [ + a.gap_md, + gtMobile && [ + a.gap_sm, + a.justify_center, + a.flex_row, + a.flex_wrap, + ], + ], children: [_jsxs(Button, { label: IS_WEB ? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Copy link"], ["Copy link"])))) : _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Share link"], ["Share link"])))), color: "primary_subtle", size: "large", onPress: onShareLink, children: [_jsx(ButtonIcon, { icon: ChainLinkIcon }), _jsx(ButtonText, { children: IS_WEB ? (_jsx(Trans, { children: "Copy Link" })) : (_jsx(Trans, { children: "Share link" })) })] }), _jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Share QR code"], ["Share QR code"])))), color: "primary_subtle", size: "large", onPress: function () { + control.close(function () { + qrDialogControl.open(); + }); + }, children: [_jsx(ButtonIcon, { icon: QrCodeIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Share QR code" }) })] }), IS_NATIVE && (_jsxs(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Save image"], ["Save image"])))), color: "secondary", size: "large", onPress: onSave, children: [_jsx(ButtonIcon, { icon: DownloadIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Save image" }) })] }))] })] })), _jsx(Dialog.Close, {})] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/StarterPack/StarterPackCard.js b/src/components/StarterPack/StarterPackCard.js new file mode 100644 index 0000000000..e2f39c7509 --- /dev/null +++ b/src/components/StarterPack/StarterPackCard.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 } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { AppBskyGraphStarterpack, AtUri } from '@atproto/api'; +import { msg, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { getStarterPackOgCard } from '#/lib/strings/starter-pack'; +import { precacheResolvedUri } from '#/state/queries/resolve-uri'; +import { precacheStarterPack } from '#/state/queries/starter-packs'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { StarterPack as StarterPackIcon } from '#/components/icons/StarterPack'; +import { Link as BaseLink, } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import * as bsky from '#/types/bsky'; +export function Default(_a) { + var starterPack = _a.starterPack; + if (!starterPack) + return null; + return (_jsx(Link, { starterPack: starterPack, children: _jsx(Card, { starterPack: starterPack }) })); +} +export function Notification(_a) { + var starterPack = _a.starterPack; + if (!starterPack) + return null; + return (_jsx(Link, { starterPack: starterPack, children: _jsx(Card, { starterPack: starterPack, noIcon: true, noDescription: true }) })); +} +export function Card(_a) { + var starterPack = _a.starterPack, noIcon = _a.noIcon, noDescription = _a.noDescription; + var record = starterPack.record, creator = starterPack.creator, joinedAllTimeCount = starterPack.joinedAllTimeCount; + var _ = useLingui()._; + var t = useTheme(); + var currentAccount = useSession().currentAccount; + if (!bsky.dangerousIsType(record, AppBskyGraphStarterpack.isRecord)) { + return null; + } + return (_jsxs(View, { style: [a.w_full, a.gap_md], children: [_jsxs(View, { style: [a.flex_row, a.gap_sm, a.w_full], children: [!noIcon ? _jsx(StarterPackIcon, { width: 40, gradient: "sky" }) : null, _jsxs(View, { style: [a.flex_1], children: [_jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_snug], numberOfLines: 2, children: record.name }), _jsx(Text, { emoji: true, style: [a.leading_snug, t.atoms.text_contrast_medium], numberOfLines: 1, children: (creator === null || creator === void 0 ? void 0 : creator.did) === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Starter pack by you"], ["Starter pack by you"])))) + : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Starter pack by ", ""], ["Starter pack by ", ""])), sanitizeHandle(creator.handle, '@'))) })] })] }), !noDescription && record.description ? (_jsx(Text, { emoji: true, numberOfLines: 3, style: [a.leading_snug], children: record.description })) : null, !!joinedAllTimeCount && joinedAllTimeCount >= 50 && (_jsx(Text, { style: [a.font_semi_bold, t.atoms.text_contrast_medium], children: _jsxs(Trans, { comment: "Number of users (always at least 50) who have joined Bluesky using a specific starter pack", children: [_jsx(Plural, { value: joinedAllTimeCount, other: "# users have" }), " joined!"] }) }))] })); +} +export function useStarterPackLink(_a) { + var view = _a.view; + var _ = useLingui()._; + var qc = useQueryClient(); + var _b = React.useMemo(function () { + var rkey = new AtUri(view.uri).rkey; + var creator = view.creator; + return { rkey: rkey, handleOrDid: creator.handle || creator.did }; + }, [view]), rkey = _b.rkey, handleOrDid = _b.handleOrDid; + var precache = function () { + precacheResolvedUri(qc, view.creator.handle, view.creator.did); + precacheStarterPack(qc, view); + }; + return { + to: "/starter-pack/".concat(handleOrDid, "/").concat(rkey), + label: AppBskyGraphStarterpack.isRecord(view.record) + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Navigate to ", ""], ["Navigate to ", ""])), view.record.name)) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Navigate to starter pack"], ["Navigate to starter pack"])))), + precache: precache, + }; +} +export function Link(_a) { + var starterPack = _a.starterPack, children = _a.children; + var _ = useLingui()._; + var queryClient = useQueryClient(); + var record = starterPack.record; + var _b = React.useMemo(function () { + var rkey = new AtUri(starterPack.uri).rkey; + var creator = starterPack.creator; + return { rkey: rkey, handleOrDid: creator.handle || creator.did }; + }, [starterPack]), rkey = _b.rkey, handleOrDid = _b.handleOrDid; + if (!AppBskyGraphStarterpack.isRecord(record)) { + return null; + } + return (_jsx(BaseLink, { to: "/starter-pack/".concat(handleOrDid, "/").concat(rkey), label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Navigate to ", ""], ["Navigate to ", ""])), record.name)), onPress: function () { + precacheResolvedUri(queryClient, starterPack.creator.handle, starterPack.creator.did); + precacheStarterPack(queryClient, starterPack); + }, style: [a.flex_col, a.align_start], children: children })); +} +export function Embed(_a) { + var starterPack = _a.starterPack; + var t = useTheme(); + var imageUri = getStarterPackOgCard(starterPack); + return (_jsx(View, { style: [ + a.border, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border_contrast_low, + ], children: _jsxs(Link, { starterPack: starterPack, children: [_jsx(Image, { source: imageUri, style: [a.w_full, a.aspect_card], accessibilityIgnoresInvertColors: true }), _jsx(View, { style: [a.px_sm, a.py_md], children: _jsx(Card, { starterPack: starterPack }) })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.js b/src/components/StarterPack/Wizard/WizardEditListDialog.js new file mode 100644 index 0000000000..42695e1cdf --- /dev/null +++ b/src/components/StarterPack/Wizard/WizardEditListDialog.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; +}; +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 { useRef } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender'; +import { atoms as a, native, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { WizardFeedCard, WizardProfileCard, } from '#/components/StarterPack/Wizard/WizardListCard'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +function keyExtractor(item, index) { + return "".concat(item.did, "-").concat(index); +} +export function WizardEditListDialog(_a) { + var control = _a.control, state = _a.state, dispatch = _a.dispatch, moderationOpts = _a.moderationOpts, profile = _a.profile; + var _ = useLingui()._; + var t = useTheme(); + var initialNumToRender = useInitialNumToRender(); + var listRef = useRef(null); + var getData = function () { + if (state.currentStep === 'Feeds') + return state.feeds; + return __spreadArray([profile], state.profiles.filter(function (p) { return p.did !== profile.did; }), true); + }; + var renderItem = function (_a) { + var item = _a.item; + return state.currentStep === 'Profiles' ? (_jsx(WizardProfileCard, { profile: item, btnType: "remove", state: state, dispatch: dispatch, moderationOpts: moderationOpts })) : (_jsx(WizardFeedCard, { generator: item, btnType: "remove", state: state, dispatch: dispatch, moderationOpts: moderationOpts })); + }; + return (_jsxs(Dialog.Outer, { control: control, testID: "newChatDialog", children: [_jsx(Dialog.Handle, {}), _jsx(Dialog.InnerFlatList, { ref: listRef, data: getData(), renderItem: renderItem, keyExtractor: keyExtractor, ListHeaderComponent: _jsxs(View, { style: [ + native(a.pt_4xl), + a.flex_row, + a.justify_between, + a.border_b, + a.px_sm, + a.mb_sm, + t.atoms.bg, + t.atoms.border_contrast_medium, + IS_WEB + ? [ + a.align_center, + { + height: 48, + }, + ] + : [a.pb_sm, a.align_end], + ], children: [_jsx(View, { style: { width: 60 } }), _jsx(Text, { style: [a.font_semi_bold, a.text_xl], children: state.currentStep === 'Profiles' ? (_jsx(Trans, { children: "Edit People" })) : (_jsx(Trans, { children: "Edit Feeds" })) }), _jsx(View, { style: { width: 60 }, children: IS_WEB && (_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close"], ["Close"])))), variant: "ghost", color: "primary", size: "small", onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })) })] }), stickyHeaderIndices: [0], style: [ + web([a.py_0, { height: '100vh', maxHeight: 600 }, a.px_0]), + native({ + height: '100%', + paddingHorizontal: 0, + marginTop: 0, + paddingTop: 0, + }), + ], webInnerStyle: [a.py_0, { maxWidth: 500, minWidth: 200 }], keyboardDismissMode: "on-drag", removeClippedSubviews: true, initialNumToRender: initialNumToRender })] })); +} +var templateObject_1; diff --git a/src/components/StarterPack/Wizard/WizardListCard.js b/src/components/StarterPack/Wizard/WizardListCard.js new file mode 100644 index 0000000000..b5a7738bb1 --- /dev/null +++ b/src/components/StarterPack/Wizard/WizardListCard.js @@ -0,0 +1,93 @@ +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 { Keyboard, View } from 'react-native'; +import { moderateFeedGenerator, moderateProfile, } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE } from '#/lib/constants'; +import { sanitizeDisplayName } from '#/lib/strings/display-names'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { useSession } from '#/state/session'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Toggle from '#/components/forms/Toggle'; +import { Checkbox } from '#/components/forms/Toggle'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +function WizardListCard(_a) { + var type = _a.type, btnType = _a.btnType, displayName = _a.displayName, subtitle = _a.subtitle, onPress = _a.onPress, avatar = _a.avatar, included = _a.included, disabled = _a.disabled, moderationUi = _a.moderationUi; + var t = useTheme(); + var _ = useLingui()._; + return (_jsxs(Toggle.Item, { name: type === 'user' ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Person toggle"], ["Person toggle"])))) : _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Feed toggle"], ["Feed toggle"])))), label: included + ? _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Remove ", " from starter pack"], ["Remove ", " from starter pack"])), displayName)) + : _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Add ", " to starter pack"], ["Add ", " to starter pack"])), displayName)), value: included, disabled: btnType === 'remove' || disabled, onChange: onPress, style: [ + a.flex_row, + a.align_center, + a.px_lg, + a.py_md, + a.gap_md, + a.border_b, + t.atoms.border_contrast_low, + ], children: [_jsx(UserAvatar, { size: 45, avatar: avatar, moderation: moderationUi, type: type }), _jsxs(View, { style: [a.flex_1, a.gap_2xs], children: [_jsx(Text, { emoji: true, style: [ + a.flex_1, + a.font_semi_bold, + a.text_md, + a.leading_tight, + a.self_start, + ], numberOfLines: 1, children: displayName }), _jsx(Text, { style: [a.flex_1, a.leading_tight, t.atoms.text_contrast_medium], numberOfLines: 1, children: subtitle })] }), btnType === 'checkbox' ? (_jsx(Checkbox, {})) : !disabled ? (_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Remove"], ["Remove"])))), variant: "solid", color: "secondary", size: "small", style: [a.self_center, { marginLeft: 'auto' }], onPress: onPress, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Remove" }) }) })) : null] })); +} +export function WizardProfileCard(_a) { + var btnType = _a.btnType, state = _a.state, dispatch = _a.dispatch, profile = _a.profile, moderationOpts = _a.moderationOpts; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + // Determine the "main" profile for this starter pack - either targetDid or current account + var targetProfileDid = state.targetDid || (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); + var isTarget = profile.did === targetProfileDid; + var included = isTarget || state.profiles.some(function (p) { return p.did === profile.did; }); + var disabled = isTarget || + (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1); + var moderationUi = moderateProfile(profile, moderationOpts).ui('avatar'); + var displayName = profile.displayName + ? sanitizeDisplayName(profile.displayName) + : "@".concat(sanitizeHandle(profile.handle)); + var onPress = function () { + if (disabled) + return; + Keyboard.dismiss(); + if (profile.did === targetProfileDid) + return; + if (!included) { + ax.metric('starterPack:addUser', {}); + dispatch({ type: 'AddProfile', profile: profile }); + } + else { + ax.metric('starterPack:removeUser', {}); + dispatch({ type: 'RemoveProfile', profileDid: profile.did }); + } + }; + return (_jsx(WizardListCard, { type: "user", btnType: btnType, displayName: displayName, subtitle: "@".concat(sanitizeHandle(profile.handle)), onPress: onPress, avatar: profile.avatar, included: included, disabled: disabled, moderationUi: moderationUi })); +} +export function WizardFeedCard(_a) { + var btnType = _a.btnType, generator = _a.generator, state = _a.state, dispatch = _a.dispatch, moderationOpts = _a.moderationOpts; + var isDiscover = generator.uri === DISCOVER_FEED_URI; + var included = isDiscover || state.feeds.some(function (f) { return f.uri === generator.uri; }); + var disabled = isDiscover || (!included && state.feeds.length >= 3); + var moderationUi = moderateFeedGenerator(generator, moderationOpts).ui('avatar'); + var onPress = function () { + if (disabled) + return; + Keyboard.dismiss(); + if (included) { + dispatch({ type: 'RemoveFeed', feedUri: generator.uri }); + } + else { + dispatch({ type: 'AddFeed', feed: generator }); + } + }; + return (_jsx(WizardListCard, { type: "algo", btnType: btnType, displayName: sanitizeDisplayName(generator.displayName), subtitle: "Feed by @".concat(sanitizeHandle(generator.creator.handle)), onPress: onPress, avatar: generator.avatar, included: included, disabled: disabled, moderationUi: moderationUi })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/SubtleHover.js b/src/components/SubtleHover.js new file mode 100644 index 0000000000..8a8a4ac835 --- /dev/null +++ b/src/components/SubtleHover.js @@ -0,0 +1,36 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { atoms as a, useTheme } from '#/alf'; +import { IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE } from '#/env'; +export function SubtleHover(_a) { + var style = _a.style, hover = _a.hover, _b = _a.web, web = _b === void 0 ? true : _b, _c = _a.native, native = _c === void 0 ? false : _c; + var t = useTheme(); + var opacity; + switch (t.name) { + case 'dark': + opacity = 0.4; + break; + case 'dim': + opacity = 0.45; + break; + case 'light': + opacity = 0.5; + break; + } + var el = (_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.pointer_events_none, + a.transition_opacity, + t.atoms.bg_contrast_50, + style, + { opacity: hover ? opacity : 0 }, + ] })); + if (IS_WEB && web) { + return IS_WEB_TOUCH_DEVICE ? null : el; + } + else if (IS_NATIVE && native) { + return el; + } + return null; +} diff --git a/src/components/Toast/Toast.js b/src/components/Toast/Toast.js new file mode 100644 index 0000000000..37deb547c0 --- /dev/null +++ b/src/components/Toast/Toast.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); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { createContext, useContext, useMemo } from 'react'; +import { View } from 'react-native'; +import { atoms as a, select, useAlf, useTheme } from '#/alf'; +import { Button, } from '#/components/Button'; +import { CircleCheck_Stroke2_Corner0_Rounded as CircleCheck } from '#/components/icons/CircleCheck'; +import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfo } from '#/components/icons/CircleInfo'; +import { CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon } from '#/components/icons/CircleInfo'; +import { Warning_Stroke2_Corner0_Rounded as WarningIcon } from '#/components/icons/Warning'; +import { dismiss } from '#/components/Toast/sonner'; +import { Text as BaseText } from '#/components/Typography'; +export var ICONS = { + default: CircleCheck, + success: CircleCheck, + error: ErrorIcon, + warning: WarningIcon, + info: CircleInfo, +}; +var ToastConfigContext = createContext({ + id: '', + type: 'default', +}); +ToastConfigContext.displayName = 'ToastConfigContext'; +export function ToastConfigProvider(_a) { + var children = _a.children, id = _a.id, type = _a.type; + return (_jsx(ToastConfigContext.Provider, { value: useMemo(function () { return ({ id: id, type: type }); }, [id, type]), children: children })); +} +export function Outer(_a) { + var children = _a.children; + var t = useTheme(); + var type = useContext(ToastConfigContext).type; + var styles = useToastStyles({ type: type }); + return (_jsx(View, { style: [ + a.flex_1, + a.p_lg, + a.rounded_md, + a.border, + a.flex_row, + a.gap_sm, + t.atoms.shadow_sm, + { + paddingVertical: 14, // 16 seems too big + backgroundColor: styles.backgroundColor, + borderColor: styles.borderColor, + }, + ], children: children })); +} +export function Icon(_a) { + var icon = _a.icon; + var type = useContext(ToastConfigContext).type; + var styles = useToastStyles({ type: type }); + var IconComponent = icon || ICONS[type]; + return _jsx(IconComponent, { size: "md", fill: styles.iconColor }); +} +export function Text(_a) { + var children = _a.children; + var type = useContext(ToastConfigContext).type; + var textColor = useToastStyles({ type: type }).textColor; + var fontScaleCompensation = useToastFontScaleCompensation().fontScaleCompensation; + return (_jsx(View, { style: [ + a.flex_1, + a.pr_lg, + { + top: fontScaleCompensation, + }, + ], children: _jsx(BaseText, { selectable: false, style: [ + a.text_md, + a.font_medium, + a.leading_snug, + a.pointer_events_none, + { + color: textColor, + }, + ], children: children }) })); +} +export function Action(props) { + var t = useTheme(); + var fontScaleCompensation = useToastFontScaleCompensation().fontScaleCompensation; + var type = useContext(ToastConfigContext).type; + var id = useContext(ToastConfigContext).id; + var styles = useMemo(function () { + var base = { + base: { + textColor: t.palette.contrast_600, + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + }, + interacted: { + textColor: t.atoms.text.color, + backgroundColor: t.atoms.bg_contrast_50.backgroundColor, + }, + }; + return { + default: base, + success: { + base: { + textColor: select(t.name, { + light: t.palette.primary_800, + dim: t.palette.primary_900, + dark: t.palette.primary_900, + }), + backgroundColor: t.palette.primary_25, + }, + interacted: { + textColor: select(t.name, { + light: t.palette.primary_900, + dim: t.palette.primary_975, + dark: t.palette.primary_975, + }), + backgroundColor: t.palette.primary_50, + }, + }, + error: { + base: { + textColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_900, + dark: t.palette.negative_900, + }), + backgroundColor: t.palette.negative_25, + }, + interacted: { + textColor: select(t.name, { + light: t.palette.negative_900, + dim: t.palette.negative_975, + dark: t.palette.negative_975, + }), + backgroundColor: t.palette.negative_50, + }, + }, + warning: base, + info: base, + }[type]; + }, [t, type]); + var onPress = function (e) { + var _a; + console.log('Toast Action pressed, dismissing toast', id); + dismiss(id); + (_a = props.onPress) === null || _a === void 0 ? void 0 : _a.call(props, e); + }; + return (_jsx(View, { style: { top: fontScaleCompensation }, children: _jsx(Button, __assign({}, props, { onPress: onPress, children: function (s) { + var interacted = s.pressed || s.hovered || s.focused; + return (_jsxs(_Fragment, { children: [_jsx(View, { style: [ + a.absolute, + a.curve_continuous, + { + // tiny button styles + top: -5, + bottom: -5, + left: -9, + right: -9, + borderRadius: 6, + backgroundColor: interacted + ? styles.interacted.backgroundColor + : styles.base.backgroundColor, + }, + ] }), _jsx(BaseText, { style: [ + a.text_md, + a.font_medium, + a.leading_snug, + { + color: interacted + ? styles.interacted.textColor + : styles.base.textColor, + }, + ], children: props.children })] })); + } })) })); +} +/** + * Vibes-based number, provides t `top` value to wrap the text to compensate + * for different type sizes and keep the first line of text aligned with the + * icon. - esb + */ +function useToastFontScaleCompensation() { + var fonts = useAlf().fonts; + var fontScaleCompensation = useMemo(function () { return parseInt(fonts.scale) * -1 * 0.65; }, [fonts.scale]); + return useMemo(function () { return ({ + fontScaleCompensation: fontScaleCompensation, + }); }, [fontScaleCompensation]); +} +function useToastStyles(_a) { + var type = _a.type; + var t = useTheme(); + return useMemo(function () { + return { + default: { + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + borderColor: t.atoms.border_contrast_low.borderColor, + iconColor: t.atoms.text.color, + textColor: t.atoms.text.color, + }, + success: { + backgroundColor: t.palette.primary_25, + borderColor: select(t.name, { + light: t.palette.primary_300, + dim: t.palette.primary_200, + dark: t.palette.primary_100, + }), + iconColor: select(t.name, { + light: t.palette.primary_600, + dim: t.palette.primary_700, + dark: t.palette.primary_700, + }), + textColor: select(t.name, { + light: t.palette.primary_600, + dim: t.palette.primary_700, + dark: t.palette.primary_700, + }), + }, + error: { + backgroundColor: t.palette.negative_25, + borderColor: select(t.name, { + light: t.palette.negative_200, + dim: t.palette.negative_200, + dark: t.palette.negative_100, + }), + iconColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_900, + dark: t.palette.negative_900, + }), + textColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_900, + dark: t.palette.negative_900, + }), + }, + warning: { + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + borderColor: t.atoms.border_contrast_low.borderColor, + iconColor: t.atoms.text.color, + textColor: t.atoms.text.color, + }, + info: { + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + borderColor: t.atoms.border_contrast_low.borderColor, + iconColor: t.atoms.text.color, + textColor: t.atoms.text.color, + }, + }[type]; + }, [t, type]); +} diff --git a/src/components/Toast/const.js b/src/components/Toast/const.js new file mode 100644 index 0000000000..689f800bb6 --- /dev/null +++ b/src/components/Toast/const.js @@ -0,0 +1 @@ +export var DURATION = 3e3; diff --git a/src/components/Toast/index.e2e.js b/src/components/Toast/index.e2e.js new file mode 100644 index 0000000000..1b02173b34 --- /dev/null +++ b/src/components/Toast/index.e2e.js @@ -0,0 +1,20 @@ +export var DURATION = 0; +export var Action = function () { return null; }; +export var Icon = function () { return null; }; +export var Outer = function () { return null; }; +export var Text = function () { return null; }; +export var ToastConfigProvider = function () { return null; }; +export function ToastOutlet() { + return null; +} +export var api = function () { }; +api.success = function () { }; +api.wiggle = function () { }; +api.error = function () { }; +api.warning = function () { }; +api.info = function () { }; +api.promise = function () { }; +api.custom = function () { }; +api.loading = function () { }; +api.dismiss = function () { }; +export function show() { } diff --git a/src/components/Toast/index.js b/src/components/Toast/index.js new file mode 100644 index 0000000000..dc4c7270a6 --- /dev/null +++ b/src/components/Toast/index.js @@ -0,0 +1,65 @@ +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 { nanoid } from 'nanoid/non-secure'; +import { toast as sonner, Toaster } from 'sonner-native'; +import { atoms as a } from '#/alf'; +import { DURATION } from '#/components/Toast/const'; +import { Icon as ToastIcon, Outer as BaseOuter, Text as ToastText, ToastConfigProvider, } from '#/components/Toast/Toast'; +export { DURATION } from '#/components/Toast/const'; +export { Action, Icon, Text, ToastConfigProvider } from '#/components/Toast/Toast'; +/** + * Toasts are rendered in a global outlet, which is placed at the top of the + * component tree. + */ +export function ToastOutlet() { + return _jsx(Toaster, { pauseWhenPageIsHidden: true, gap: a.gap_sm.gap }); +} +export function Outer(_a) { + var children = _a.children; + return (_jsx(View, { style: [a.px_xl, a.w_full], children: _jsx(BaseOuter, { children: children }) })); +} +/** + * Access the full Sonner API + */ +export var api = sonner; +/** + * Our base toast API, using the `Toast` export of this file. + */ +export function show(content, _a) { + var _b, _c; + if (_a === void 0) { _a = {}; } + var _d = _a.type, type = _d === void 0 ? 'default' : _d, options = __rest(_a, ["type"]); + var id = nanoid(); + if (typeof content === 'string') { + sonner.custom(_jsx(ToastConfigProvider, { id: id, type: type, children: _jsxs(Outer, { children: [_jsx(ToastIcon, {}), _jsx(ToastText, { children: content })] }) }), __assign(__assign({}, options), { id: id, duration: (_b = options === null || options === void 0 ? void 0 : options.duration) !== null && _b !== void 0 ? _b : DURATION })); + } + else if (React.isValidElement(content)) { + sonner.custom(_jsx(ToastConfigProvider, { id: id, type: type, children: content }), __assign(__assign({}, options), { id: id, duration: (_c = options === null || options === void 0 ? void 0 : options.duration) !== null && _c !== void 0 ? _c : DURATION })); + } + else { + throw new Error("Toast can be a string or a React element, got ".concat(typeof content)); + } +} diff --git a/src/components/Toast/index.web.js b/src/components/Toast/index.web.js new file mode 100644 index 0000000000..7e795e2d45 --- /dev/null +++ b/src/components/Toast/index.web.js @@ -0,0 +1,62 @@ +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 { nanoid } from 'nanoid/non-secure'; +import { toast as sonner, Toaster } from 'sonner'; +import { atoms as a } from '#/alf'; +import { DURATION } from '#/components/Toast/const'; +import { Icon as ToastIcon, Outer as ToastOuter, Text as ToastText, ToastConfigProvider, } from '#/components/Toast/Toast'; +export { DURATION } from '#/components/Toast/const'; +export * from '#/components/Toast/Toast'; +/** + * Toasts are rendered in a global outlet, which is placed at the top of the + * component tree. + */ +export function ToastOutlet() { + return (_jsx(Toaster, { position: "bottom-left", gap: a.gap_sm.gap, offset: a.p_xl.padding, mobileOffset: a.p_xl.padding })); +} +/** + * Access the full Sonner API + */ +export var api = sonner; +/** + * Our base toast API, using the `Toast` export of this file. + */ +export function show(content, _a) { + var _b, _c; + if (_a === void 0) { _a = {}; } + var _d = _a.type, type = _d === void 0 ? 'default' : _d, options = __rest(_a, ["type"]); + var id = nanoid(); + if (typeof content === 'string') { + sonner(_jsx(ToastConfigProvider, { id: id, type: type, children: _jsxs(ToastOuter, { children: [_jsx(ToastIcon, {}), _jsx(ToastText, { children: content })] }) }), __assign(__assign({}, options), { unstyled: true, // required on web + id: id, duration: (_b = options === null || options === void 0 ? void 0 : options.duration) !== null && _b !== void 0 ? _b : DURATION })); + } + else if (React.isValidElement(content)) { + sonner(_jsx(ToastConfigProvider, { id: id, type: type, children: content }), __assign(__assign({}, options), { unstyled: true, // required on web + id: id, duration: (_c = options === null || options === void 0 ? void 0 : options.duration) !== null && _c !== void 0 ? _c : DURATION })); + } + else { + throw new Error("Toast can be a string or a React element, got ".concat(typeof content)); + } +} diff --git a/src/components/Toast/sonner/index.js b/src/components/Toast/sonner/index.js new file mode 100644 index 0000000000..88bfc192df --- /dev/null +++ b/src/components/Toast/sonner/index.js @@ -0,0 +1,2 @@ +import { toast } from 'sonner-native'; +export var dismiss = toast.dismiss; diff --git a/src/components/Toast/sonner/index.web.js b/src/components/Toast/sonner/index.web.js new file mode 100644 index 0000000000..2993312b49 --- /dev/null +++ b/src/components/Toast/sonner/index.web.js @@ -0,0 +1,2 @@ +import { toast } from 'sonner'; +export var dismiss = toast.dismiss; diff --git a/src/components/Toast/types.js b/src/components/Toast/types.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/src/components/Toast/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/Tooltip/const.js b/src/components/Tooltip/const.js new file mode 100644 index 0000000000..33a6652bcc --- /dev/null +++ b/src/components/Tooltip/const.js @@ -0,0 +1,5 @@ +import { atoms as a } from '#/alf'; +export var BUBBLE_MAX_WIDTH = 240; +export var ARROW_SIZE = 12; +export var ARROW_HALF_SIZE = ARROW_SIZE / 2; +export var MIN_EDGE_SPACE = a.px_lg.paddingLeft; diff --git a/src/components/Tooltip/index.e2e.js b/src/components/Tooltip/index.e2e.js new file mode 100644 index 0000000000..dfd5f28a95 --- /dev/null +++ b/src/components/Tooltip/index.e2e.js @@ -0,0 +1,18 @@ +export function SheetCompatProvider(_a) { + var children = _a.children; + return children; +} +export function Outer(_a) { + var children = _a.children; + return children; +} +export function Target(_a) { + var children = _a.children; + return children; +} +export function Content() { + return null; +} +export function TextBubble() { + return null; +} diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js new file mode 100644 index 0000000000..0d9fd8c925 --- /dev/null +++ b/src/components/Tooltip/index.js @@ -0,0 +1,284 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Children, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; +import { useWindowDimensions, View } from 'react-native'; +import Animated, { Easing, ZoomIn } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useIsKeyboardVisible } from '#/lib/hooks/useIsKeyboardVisible'; +import { GlobalGestureEventsProvider } from '#/state/global-gesture-events'; +import { atoms as a, select, useTheme } from '#/alf'; +import { useOnGesture } from '#/components/hooks/useOnGesture'; +import { createPortalGroup, Portal as RootPortal } from '#/components/Portal'; +import { ARROW_HALF_SIZE, ARROW_SIZE, BUBBLE_MAX_WIDTH, MIN_EDGE_SPACE, } from '#/components/Tooltip/const'; +import { Text } from '#/components/Typography'; +var TooltipPortal = createPortalGroup(); +var TooltipProviderContext = createContext(null); +/** + * Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container, + * such as in the composer sheet. + * + * Only really necessary on iOS but can work on Android. + */ +export function SheetCompatProvider(_a) { + var children = _a.children; + var ref = useRef(null); + return (_jsx(GlobalGestureEventsProvider, { style: [a.flex_1], children: _jsxs(TooltipPortal.Provider, { children: [_jsx(View, { ref: ref, collapsable: false, style: [a.flex_1], children: _jsx(TooltipProviderContext, { value: ref, children: children }) }), _jsx(TooltipPortal.Outlet, {})] }) })); +} +SheetCompatProvider.displayName = 'TooltipSheetCompatProvider'; +/** + * These are native specific values, not shared with web + */ +var ARROW_VISUAL_OFFSET = ARROW_SIZE / 1.25; // vibes-based, slightly off the target +var BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3; // vibes-based, provide more shadow beneath tip +var TooltipContext = createContext({ + position: 'bottom', + visible: false, + onVisibleChange: function () { }, +}); +TooltipContext.displayName = 'TooltipContext'; +var TargetContext = createContext({ + targetMeasurements: undefined, + setTargetMeasurements: function () { }, + shouldMeasure: false, +}); +TargetContext.displayName = 'TargetContext'; +export function Outer(_a) { + var children = _a.children, _b = _a.position, position = _b === void 0 ? 'bottom' : _b, requestVisible = _a.visible, onVisibleChange = _a.onVisibleChange; + /** + * Lagging state to track the externally-controlled visibility of the + * tooltip, which needs to wait for the target to be measured before + * actually being shown. + */ + var _c = useState(false), visible = _c[0], setVisible = _c[1]; + var _d = useState(undefined), targetMeasurements = _d[0], setTargetMeasurements = _d[1]; + if (requestVisible && !visible && targetMeasurements) { + setVisible(true); + } + else if (!requestVisible && visible) { + setVisible(false); + setTargetMeasurements(undefined); + } + var ctx = useMemo(function () { return ({ position: position, visible: visible, onVisibleChange: onVisibleChange }); }, [position, visible, onVisibleChange]); + var targetCtx = useMemo(function () { return ({ + targetMeasurements: targetMeasurements, + setTargetMeasurements: setTargetMeasurements, + shouldMeasure: requestVisible, + }); }, [requestVisible, targetMeasurements, setTargetMeasurements]); + return (_jsx(TooltipContext.Provider, { value: ctx, children: _jsx(TargetContext.Provider, { value: targetCtx, children: children }) })); +} +export function Target(_a) { + var children = _a.children; + var _b = useContext(TargetContext), shouldMeasure = _b.shouldMeasure, setTargetMeasurements = _b.setTargetMeasurements; + var _c = useState(false), hasLayedOut = _c[0], setHasLayedOut = _c[1]; + var targetRef = useRef(null); + var containerRef = useContext(TooltipProviderContext); + var keyboardIsOpen = useIsKeyboardVisible(); + useEffect(function () { + var _a, _b; + if (!shouldMeasure || !hasLayedOut) + return; + /* + * Once opened, measure the dimensions and position of the target + */ + if (containerRef === null || containerRef === void 0 ? void 0 : containerRef.current) { + (_a = targetRef.current) === null || _a === void 0 ? void 0 : _a.measureLayout(containerRef.current, function (x, y, width, height) { + if (x !== undefined && y !== undefined && width && height) { + setTargetMeasurements({ x: x, y: y, width: width, height: height }); + } + }); + } + else { + (_b = targetRef.current) === null || _b === void 0 ? void 0 : _b.measure(function (_x, _y, width, height, x, y) { + if (x !== undefined && y !== undefined && width && height) { + setTargetMeasurements({ x: x, y: y, width: width, height: height }); + } + }); + } + }, [ + shouldMeasure, + setTargetMeasurements, + hasLayedOut, + containerRef, + keyboardIsOpen, + ]); + return (_jsx(View, { collapsable: false, ref: targetRef, onLayout: function () { return setHasLayedOut(true); }, children: children })); +} +export function Content(_a) { + var children = _a.children, label = _a.label; + var _b = useContext(TooltipContext), position = _b.position, visible = _b.visible, onVisibleChange = _b.onVisibleChange; + var targetMeasurements = useContext(TargetContext).targetMeasurements; + var isWithinProvider = !!useContext(TooltipProviderContext); + var requestClose = useCallback(function () { + onVisibleChange(false); + }, [onVisibleChange]); + if (!visible || !targetMeasurements) + return null; + var Portal = isWithinProvider ? TooltipPortal.Portal : RootPortal; + return (_jsx(Portal, { children: _jsx(Bubble, { label: label, position: position, + /* + * Gotta pass these in here. Inside the Bubble, we're Potal-ed outside + * the context providers. + */ + targetMeasurements: targetMeasurements, requestClose: requestClose, children: children }) })); +} +function Bubble(_a) { + var children = _a.children, label = _a.label, position = _a.position, requestClose = _a.requestClose, targetMeasurements = _a.targetMeasurements; + var t = useTheme(); + var insets = useSafeAreaInsets(); + var dimensions = useWindowDimensions(); + var _b = useState(undefined), bubbleMeasurements = _b[0], setBubbleMeasurements = _b[1]; + var coords = useMemo(function () { + if (!bubbleMeasurements) + return { + top: 0, + bottom: 0, + left: 0, + right: 0, + tipTop: 0, + tipLeft: 0, + }; + var ww = dimensions.width, wh = dimensions.height; + var maxTop = insets.top; + var maxBottom = wh - insets.bottom; + var cw = bubbleMeasurements.width, ch = bubbleMeasurements.height; + var minLeft = MIN_EDGE_SPACE; + var maxLeft = ww - minLeft; + var computedPosition = position; + var top = targetMeasurements.y + targetMeasurements.height; + var left = Math.max(minLeft, targetMeasurements.x + targetMeasurements.width / 2 - cw / 2); + var tipTranslate = ARROW_HALF_SIZE * -1; + var tipTop = tipTranslate; + if (left + cw > maxLeft) { + left -= left + cw - maxLeft; + } + var tipLeft = targetMeasurements.x - + left + + targetMeasurements.width / 2 - + ARROW_HALF_SIZE; + var bottom = top + ch; + function positionTop() { + top = top - ch - targetMeasurements.height; + bottom = top + ch; + tipTop = tipTop + ch; + computedPosition = 'top'; + } + function positionBottom() { + top = targetMeasurements.y + targetMeasurements.height; + bottom = top + ch; + tipTop = tipTranslate; + computedPosition = 'bottom'; + } + if (position === 'top') { + positionTop(); + if (top < maxTop) { + positionBottom(); + } + } + else { + if (bottom > maxBottom) { + positionTop(); + } + } + if (computedPosition === 'bottom') { + top += ARROW_VISUAL_OFFSET; + bottom += ARROW_VISUAL_OFFSET; + } + else { + top -= ARROW_VISUAL_OFFSET; + bottom -= ARROW_VISUAL_OFFSET; + } + return { + computedPosition: computedPosition, + top: top, + bottom: bottom, + left: left, + right: left + cw, + tipTop: tipTop, + tipLeft: tipLeft, + }; + }, [position, targetMeasurements, bubbleMeasurements, insets, dimensions]); + var requestCloseWrapped = useCallback(function () { + setBubbleMeasurements(undefined); + requestClose(); + }, [requestClose]); + useOnGesture(useCallback(function (e) { + var x = e.x, y = e.y; + var isInside = x > coords.left && + x < coords.right && + y > coords.top && + y < coords.bottom; + if (!isInside) { + requestCloseWrapped(); + } + }, [coords, requestCloseWrapped])); + return (_jsx(View, { accessible: true, role: "alert", accessibilityHint: "", accessibilityLabel: label, + // android + importantForAccessibility: "yes", + // ios + accessibilityViewIsModal: true, style: [ + a.absolute, + a.align_start, + { + width: BUBBLE_MAX_WIDTH, + opacity: bubbleMeasurements ? 1 : 0, + top: coords.top, + left: coords.left, + }, + ], children: _jsxs(Animated.View, { entering: ZoomIn.easing(Easing.out(Easing.exp)), style: { transformOrigin: oppposite(position) }, children: [_jsx(View, { style: [ + a.absolute, + a.top_0, + a.z_10, + t.atoms.bg, + select(t.name, { + light: t.atoms.bg, + dark: t.atoms.bg_contrast_100, + dim: t.atoms.bg_contrast_100, + }), + { + borderTopLeftRadius: a.rounded_2xs.borderRadius, + borderBottomRightRadius: a.rounded_2xs.borderRadius, + width: ARROW_SIZE, + height: ARROW_SIZE, + transform: [{ rotate: '45deg' }], + top: coords.tipTop, + left: coords.tipLeft, + }, + ] }), _jsx(View, { style: [ + a.px_md, + a.py_sm, + a.rounded_sm, + select(t.name, { + light: t.atoms.bg, + dark: t.atoms.bg_contrast_100, + dim: t.atoms.bg_contrast_100, + }), + t.atoms.shadow_md, + { + shadowOpacity: 0.2, + shadowOffset: { + width: 0, + height: BUBBLE_SHADOW_OFFSET * + (coords.computedPosition === 'bottom' ? -1 : 1), + }, + }, + ], onLayout: function (e) { + setBubbleMeasurements({ + width: e.nativeEvent.layout.width, + height: e.nativeEvent.layout.height, + }); + }, children: children })] }) })); +} +function oppposite(position) { + switch (position) { + case 'top': + return 'center bottom'; + case 'bottom': + return 'center top'; + default: + return 'center'; + } +} +export function TextBubble(_a) { + var children = _a.children; + var c = Children.toArray(children); + return (_jsx(Content, { label: c.join(' '), children: _jsx(View, { style: [a.gap_xs], children: c.map(function (child, i) { return (_jsx(Text, { style: [a.text_sm, a.leading_snug], children: child }, i)); }) }) })); +} diff --git a/src/components/Tooltip/index.web.js b/src/components/Tooltip/index.web.js new file mode 100644 index 0000000000..76abc63ed2 --- /dev/null +++ b/src/components/Tooltip/index.web.js @@ -0,0 +1,61 @@ +import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Children, createContext, useContext, useMemo } from 'react'; +import { View } from 'react-native'; +import { utils } from '@bsky.app/alf'; +import { Popover } from 'radix-ui'; +import { atoms as a, flatten, select, useTheme } from '#/alf'; +import { ARROW_SIZE, BUBBLE_MAX_WIDTH, MIN_EDGE_SPACE, } from '#/components/Tooltip/const'; +import { Text } from '#/components/Typography'; +// Portal Provider on native, but we actually don't need to do anything here +export function Provider(_a) { + var children = _a.children; + return _jsx(_Fragment, { children: children }); +} +Provider.displayName = 'TooltipProvider'; +var TooltipContext = createContext({ + position: 'bottom', +}); +TooltipContext.displayName = 'TooltipContext'; +export function Outer(_a) { + var children = _a.children, _b = _a.position, position = _b === void 0 ? 'bottom' : _b, visible = _a.visible, onVisibleChange = _a.onVisibleChange; + var ctx = useMemo(function () { return ({ position: position }); }, [position]); + return (_jsx(Popover.Root, { open: visible, onOpenChange: onVisibleChange, children: _jsx(TooltipContext.Provider, { value: ctx, children: children }) })); +} +export function Target(_a) { + var children = _a.children; + return (_jsx(Popover.Trigger, { asChild: true, children: _jsx(View, { collapsable: false, children: children }) })); +} +export function Content(_a) { + var children = _a.children, label = _a.label; + var t = useTheme(); + var position = useContext(TooltipContext).position; + return (_jsx(Popover.Portal, { children: _jsxs(Popover.Content, { className: "radix-popover-content", "aria-label": label, side: position, sideOffset: 4, collisionPadding: MIN_EDGE_SPACE, onInteractOutside: function (evt) { + if (evt.type === 'dismissableLayer.focusOutside') { + evt.preventDefault(); + } + }, style: flatten([ + a.rounded_sm, + select(t.name, { + light: t.atoms.bg, + dark: t.atoms.bg_contrast_100, + dim: t.atoms.bg_contrast_100, + }), + { + minWidth: 'max-content', + boxShadow: select(t.name, { + light: "0 0 24px ".concat(utils.alpha(t.palette.black, 0.2)), + dark: "0 0 24px ".concat(utils.alpha(t.palette.black, 0.2)), + dim: "0 0 24px ".concat(utils.alpha(t.palette.black, 0.2)), + }), + }, + ]), children: [_jsx(Popover.Arrow, { width: ARROW_SIZE, height: ARROW_SIZE / 2, fill: select(t.name, { + light: t.atoms.bg.backgroundColor, + dark: t.atoms.bg_contrast_100.backgroundColor, + dim: t.atoms.bg_contrast_100.backgroundColor, + }) }), _jsx(View, { style: [a.px_md, a.py_sm, { maxWidth: BUBBLE_MAX_WIDTH }], children: children })] }) })); +} +export function TextBubble(_a) { + var children = _a.children; + var c = Children.toArray(children); + return (_jsx(Content, { label: c.join(' '), children: _jsx(View, { style: [a.gap_xs], children: c.map(function (child, i) { return (_jsx(Text, { style: [a.text_sm, a.leading_snug], children: child }, i)); }) }) })); +} diff --git a/src/components/TrendingTopics.js b/src/components/TrendingTopics.js new file mode 100644 index 0000000000..d4746f66dc --- /dev/null +++ b/src/components/TrendingTopics.js @@ -0,0 +1,156 @@ +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/macro'; +import { useLingui } from '@lingui/react'; +import { PressableScale } from '#/lib/custom-animations/PressableScale'; +import { atoms as a, native, useTheme } from '#/alf'; +import { StarterPack as StarterPackIcon } from '#/components/icons/StarterPack'; +import { Link as InternalLink } from '#/components/Link'; +import { Text } from '#/components/Typography'; +export function TrendingTopic(_a) { + var raw = _a.topic, size = _a.size, style = _a.style, hovered = _a.hovered; + var topic = useTopic(raw); + var isSmall = size === 'small'; + var hasIcon = topic.type === 'starter-pack' && !isSmall; + var iconSize = 20; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + isSmall + ? [ + { + paddingVertical: 2, + paddingHorizontal: 4, + }, + ] + : [a.py_xs, a.px_sm], + hasIcon && { gap: 6 }, + style, + ], children: [hasIcon && topic.type === 'starter-pack' && (_jsx(StarterPackIcon, { gradient: "sky", width: iconSize, style: { marginLeft: -3, marginVertical: -1 } })), _jsx(Text, { style: [ + a.font_semi_bold, + a.leading_tight, + isSmall ? [a.text_sm] : [a.text_md, { paddingBottom: 1 }], + hovered && { textDecorationLine: 'underline' }, + ], numberOfLines: 1, children: topic.displayName })] })); +} +export function TrendingTopicSkeleton(_a) { + var _b = _a.size, size = _b === void 0 ? 'large' : _b, _c = _a.index, index = _c === void 0 ? 0 : _c; + var t = useTheme(); + var isSmall = size === 'small'; + return (_jsx(View, { style: [ + a.rounded_full, + a.border, + t.atoms.border_contrast_medium, + t.atoms.bg_contrast_25, + isSmall + ? { + width: index % 2 === 0 ? 75 : 90, + height: 27, + } + : { + width: index % 2 === 0 ? 90 : 110, + height: 36, + }, + ] })); +} +export function TrendingTopicLink(_a) { + var raw = _a.topic, children = _a.children, rest = __rest(_a, ["topic", "children"]); + var topic = useTopic(raw); + return (_jsx(InternalLink, __assign({ label: topic.label, to: topic.url, PressableComponent: native(PressableScale) }, rest, { children: children }))); +} +export function useTopic(raw) { + var _ = useLingui()._; + return React.useMemo(function () { + var displayName = raw.topic, link = raw.link; + if (link.startsWith('/search')) { + return { + type: 'topic', + label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Browse posts about ", ""], ["Browse posts about ", ""])), displayName)), + displayName: displayName, + uri: undefined, + url: link, + }; + } + else if (link.startsWith('/hashtag')) { + return { + type: 'tag', + label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Browse posts tagged with ", ""], ["Browse posts tagged with ", ""])), displayName)), + displayName: displayName, + // displayName: displayName.replace(/^#/, ''), + uri: undefined, + url: link, + }; + } + else if (link.startsWith('/starter-pack')) { + return { + type: 'starter-pack', + label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Browse starter pack ", ""], ["Browse starter pack ", ""])), displayName)), + displayName: displayName, + uri: undefined, + url: link, + }; + } + /* + if (!link.startsWith('at://')) { + // above logic + } else { + const urip = new AtUri(link) + switch (urip.collection) { + case 'app.bsky.actor.profile': { + return { + type: 'profile', + label: _(msg`View ${displayName}'s profile`), + displayName, + uri: urip, + url: makeProfileLink({did: urip.host, handle: urip.host}), + } + } + case 'app.bsky.feed.generator': { + return { + type: 'feed', + label: _(msg`Browse the ${displayName} feed`), + displayName, + uri: urip, + url: feedUriToHref(link), + } + } + } + } + */ + return { + type: 'unknown', + label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Browse topic ", ""], ["Browse topic ", ""])), displayName)), + displayName: displayName, + uri: undefined, + url: link, + }; + }, [_, raw]); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/Typography.js b/src/components/Typography.js new file mode 100644 index 0000000000..27e2e6d022 --- /dev/null +++ b/src/components/Typography.js @@ -0,0 +1,77 @@ +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 { UITextView } from 'react-native-uitextview'; +import { logger } from '#/logger'; +import { atoms, useAlf, useTheme, web } from '#/alf'; +import { childHasEmoji, normalizeTextStyles, renderChildrenWithEmoji, } from '#/alf/typography'; +export { Text as Span } from 'react-native'; +/** + * Our main text component. Use this most of the time. + */ +export function Text(_a) { + var children = _a.children, emoji = _a.emoji, style = _a.style, selectable = _a.selectable, title = _a.title, dataSet = _a.dataSet, rest = __rest(_a, ["children", "emoji", "style", "selectable", "title", "dataSet"]); + var _b = useAlf(), fonts = _b.fonts, flags = _b.flags; + var t = useTheme(); + var s = normalizeTextStyles([atoms.text_sm, t.atoms.text, style], { + fontScale: fonts.scaleMultiplier, + fontFamily: fonts.family, + flags: flags, + }); + if (__DEV__) { + if (!emoji && childHasEmoji(children)) { + logger.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string + "Text: emoji detected but emoji not enabled: \"".concat(children, "\"\n\nPlease add '")); + } + } + var shared = __assign({ uiTextView: true, selectable: selectable, style: s, dataSet: Object.assign({ tooltip: title }, dataSet || {}) }, rest); + return (_jsx(UITextView, __assign({}, shared, { children: renderChildrenWithEmoji(children, shared, emoji !== null && emoji !== void 0 ? emoji : false) }))); +} +function createHeadingElement(_a) { + var level = _a.level; + return function HeadingElement(_a) { + var style = _a.style, rest = __rest(_a, ["style"]); + var attr = web({ + role: 'heading', + 'aria-level': level, + }) || {}; + return _jsx(Text, __assign({}, attr, rest, { style: style })); + }; +} +/* + * Use semantic components when it's beneficial to the user or to a web scraper + */ +export var H1 = createHeadingElement({ level: 1 }); +export var H2 = createHeadingElement({ level: 2 }); +export var H3 = createHeadingElement({ level: 3 }); +export var H4 = createHeadingElement({ level: 4 }); +export var H5 = createHeadingElement({ level: 5 }); +export var H6 = createHeadingElement({ level: 6 }); +export function P(_a) { + var style = _a.style, rest = __rest(_a, ["style"]); + var attr = web({ + role: 'paragraph', + }) || {}; + return (_jsx(Text, __assign({}, attr, rest, { style: [atoms.text_md, atoms.leading_relaxed, style] }))); +} diff --git a/src/components/VideoPostCard.js b/src/components/VideoPostCard.js new file mode 100644 index 0000000000..75d55ef41e --- /dev/null +++ b/src/components/VideoPostCard.js @@ -0,0 +1,284 @@ +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 } 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 { AppBskyEmbedVideo, AppBskyFeedPost, } from '@atproto/api'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { formatCount } from '#/view/com/util/numeric/format'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { atoms as a, useTheme } from '#/alf'; +import { BLUE_HUE } from '#/alf/util/colorGeneration'; +import { select } from '#/alf/util/themeSelector'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { EyeSlash_Stroke2_Corner0_Rounded as Eye } from '#/components/icons/EyeSlash'; +import { Heart2_Stroke2_Corner0_Rounded as Heart } from '#/components/icons/Heart2'; +import { Repost_Stroke2_Corner2_Rounded as Repost } from '#/components/icons/Repost'; +import { Link } from '#/components/Link'; +import { MediaInsetBorder } from '#/components/MediaInsetBorder'; +import * as Hider from '#/components/moderation/Hider'; +import { Text } from '#/components/Typography'; +import * as bsky from '#/types/bsky'; +function getBlackColor(t) { + return select(t.name, { + light: t.palette.black, + dark: t.atoms.bg_contrast_25.backgroundColor, + dim: "hsl(".concat(BLUE_HUE, ", 28%, 6%)"), + }); +} +export function VideoPostCard(_a) { + var _b, _c, _d; + var post = _a.post, sourceContext = _a.sourceContext, moderation = _a.moderation, onInteract = _a.onInteract; + var t = useTheme(); + var _e = useLingui(), _ = _e._, i18n = _e.i18n; + var embed = post.embed; + var _f = useInteractionState(), pressed = _f.state, onPressIn = _f.onIn, onPressOut = _f.onOut; + var listModUi = moderation.ui('contentList'); + var mergedModui = useMemo(function () { + var modui = moderation.ui('contentList'); + 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]); + /** + * Filtering should be done at a higher level, such as `PostFeed` or + * `PostFeedVideoGridRow`, but we need to protect here as well. + */ + if (!AppBskyEmbedVideo.isView(embed)) + return null; + var author = post.author; + var text = bsky.dangerousIsType(post.record, AppBskyFeedPost.isRecord) + ? (_b = post.record) === null || _b === void 0 ? void 0 : _b.text + : ''; + var likeCount = (_c = post === null || post === void 0 ? void 0 : post.likeCount) !== null && _c !== void 0 ? _c : 0; + var repostCount = (_d = post === null || post === void 0 ? void 0 : post.repostCount) !== null && _d !== void 0 ? _d : 0; + var thumbnail = embed.thumbnail; + var black = getBlackColor(t); + var textAndAuthor = (_jsxs(View, { style: [a.pr_xs, { paddingTop: 6, gap: 4 }], children: [text && (_jsx(Text, { style: [a.text_md, a.leading_snug], numberOfLines: 2, emoji: true, children: text })), _jsxs(View, { style: [a.flex_row, a.gap_xs, a.align_center], children: [_jsxs(View, { style: [a.relative, a.rounded_full, { width: 20, height: 20 }], children: [_jsx(UserAvatar, { type: "user", size: 20, avatar: post.author.avatar }), _jsx(MediaInsetBorder, {})] }), _jsx(Text, { style: [ + a.flex_1, + a.text_sm, + a.leading_tight, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: sanitizeHandle(post.author.handle, '@') })] })] })); + return (_jsx(Link, { accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Views video in immersive mode"], ["Views video in immersive mode"])))), label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Video from ", ": ", ""], ["Video from ", ": ", ""])), author.handle, text)), to: { + screen: 'VideoFeed', + params: __assign(__assign({}, sourceContext), { initialPostUri: post.uri }), + }, onPress: function () { + onInteract === null || onInteract === void 0 ? void 0 : onInteract(); + }, onPressIn: onPressIn, onPressOut: onPressOut, style: [ + a.flex_col, + { + alignItems: undefined, + justifyContent: undefined, + }, + ], children: _jsxs(Hider.Outer, { modui: mergedModui, children: [_jsxs(Hider.Mask, { children: [_jsxs(View, { style: [ + a.justify_center, + a.rounded_md, + a.overflow_hidden, + { + backgroundColor: black, + aspectRatio: 9 / 16, + }, + ], children: [_jsx(Image, { source: { uri: thumbnail }, style: [a.w_full, a.h_full, { opacity: pressed ? 0.8 : 1 }], accessibilityIgnoresInvertColors: true, blurRadius: 100 }), _jsx(MediaInsetBorder, {}), _jsxs(View, { style: [a.absolute, a.inset_0, a.justify_center, a.align_center], children: [_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.justify_center, + a.align_center, + { + backgroundColor: 'black', + opacity: 0.2, + }, + ] }), _jsxs(View, { style: [a.align_center, a.gap_xs], children: [_jsx(Eye, { size: "lg", fill: "white" }), _jsx(Text, { style: [a.text_sm, { color: 'white' }], children: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Hidden"], ["Hidden"])))) })] })] })] }), listModUi.blur ? (_jsx(VideoPostCardTextPlaceholder, { author: post.author })) : (textAndAuthor)] }), _jsxs(Hider.Content, { children: [_jsxs(View, { style: [ + a.justify_center, + a.rounded_md, + a.overflow_hidden, + { + backgroundColor: black, + aspectRatio: 9 / 16, + }, + ], children: [_jsx(Image, { source: { uri: thumbnail }, style: [a.w_full, a.h_full, { opacity: pressed ? 0.8 : 1 }], accessibilityIgnoresInvertColors: true }), _jsx(MediaInsetBorder, {}), _jsx(View, { style: [a.absolute, a.inset_0], children: _jsxs(View, { style: [ + a.absolute, + a.inset_0, + a.pt_2xl, + { + top: 'auto', + }, + ], children: [_jsx(LinearGradient, { colors: [black, 'rgba(0, 0, 0, 0)'], locations: [0.02, 1], start: { x: 0, y: 1 }, end: { x: 0, y: 0 }, style: [a.absolute, a.inset_0, { opacity: 0.9 }] }), _jsxs(View, { style: [a.relative, a.z_10, a.p_md, a.flex_row, a.gap_md], children: [likeCount > 0 && (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(Heart, { size: "sm", fill: "white" }), _jsx(Text, { style: [a.text_sm, a.font_semi_bold, { color: 'white' }], children: formatCount(i18n, likeCount) })] })), repostCount > 0 && (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(Repost, { size: "sm", fill: "white" }), _jsx(Text, { style: [a.text_sm, a.font_semi_bold, { color: 'white' }], children: formatCount(i18n, repostCount) })] }))] })] }) })] }), textAndAuthor] })] }) })); +} +export function VideoPostCardPlaceholder() { + var t = useTheme(); + var black = getBlackColor(t); + return (_jsxs(View, { style: [a.flex_1], children: [_jsx(View, { style: [ + a.rounded_md, + a.overflow_hidden, + { + backgroundColor: black, + aspectRatio: 9 / 16, + }, + ], children: _jsx(MediaInsetBorder, {}) }), _jsx(VideoPostCardTextPlaceholder, {})] })); +} +export function VideoPostCardTextPlaceholder(_a) { + var author = _a.author; + var t = useTheme(); + return (_jsx(View, { style: [a.flex_1], children: _jsxs(View, { style: [a.pr_xs, { paddingTop: 8, gap: 6 }], children: [_jsx(View, { style: [ + a.w_full, + a.rounded_xs, + t.atoms.bg_contrast_50, + { + height: 14, + }, + ] }), _jsx(View, { style: [ + a.w_full, + a.rounded_xs, + t.atoms.bg_contrast_50, + { + height: 14, + width: '70%', + }, + ] }), author ? (_jsxs(View, { style: [a.flex_row, a.gap_xs, a.align_center], children: [_jsxs(View, { style: [a.relative, a.rounded_full, { width: 20, height: 20 }], children: [_jsx(UserAvatar, { type: "user", size: 20, avatar: author.avatar }), _jsx(MediaInsetBorder, {})] }), _jsx(Text, { style: [ + a.flex_1, + a.text_sm, + a.leading_tight, + t.atoms.text_contrast_medium, + ], numberOfLines: 1, children: sanitizeHandle(author.handle, '@') })] })) : (_jsxs(View, { style: [a.flex_row, a.gap_xs, a.align_center], children: [_jsx(View, { style: [ + a.rounded_full, + t.atoms.bg_contrast_50, + { + width: 20, + height: 20, + }, + ] }), _jsx(View, { style: [ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + height: 12, + width: '75%', + }, + ] })] }))] }) })); +} +export function CompactVideoPostCard(_a) { + var _b; + var post = _a.post, sourceContext = _a.sourceContext, moderation = _a.moderation, onInteract = _a.onInteract; + var t = useTheme(); + var _c = useLingui(), _ = _c._, i18n = _c.i18n; + var embed = post.embed; + var _d = useInteractionState(), pressed = _d.state, onPressIn = _d.onIn, onPressOut = _d.onOut; + var mergedModui = useMemo(function () { + var modui = moderation.ui('contentList'); + 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]); + /** + * Filtering should be done at a higher level, such as `PostFeed` or + * `PostFeedVideoGridRow`, but we need to protect here as well. + */ + if (!AppBskyEmbedVideo.isView(embed)) + return null; + var likeCount = (_b = post === null || post === void 0 ? void 0 : post.likeCount) !== null && _b !== void 0 ? _b : 0; + var showLikeCount = false; + var thumbnail = embed.thumbnail; + var black = getBlackColor(t); + return (_jsx(Link, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["View video"], ["View video"])))), to: { + screen: 'VideoFeed', + params: __assign(__assign({}, sourceContext), { initialPostUri: post.uri }), + }, onPress: function () { + onInteract === null || onInteract === void 0 ? void 0 : onInteract(); + }, onPressIn: onPressIn, onPressOut: onPressOut, style: [ + a.flex_col, + t.atoms.shadow_sm, + { + alignItems: undefined, + justifyContent: undefined, + }, + ], children: _jsxs(Hider.Outer, { modui: mergedModui, children: [_jsx(Hider.Mask, { children: _jsxs(View, { style: [ + a.justify_center, + a.rounded_lg, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + { + backgroundColor: black, + aspectRatio: 9 / 16, + }, + ], children: [_jsx(Image, { source: { uri: thumbnail }, style: [a.w_full, a.h_full, { opacity: pressed ? 0.8 : 1 }], accessibilityIgnoresInvertColors: true, blurRadius: 100 }), _jsx(MediaInsetBorder, {}), _jsxs(View, { style: [a.absolute, a.inset_0, a.justify_center, a.align_center], children: [_jsx(View, { style: [ + a.absolute, + a.inset_0, + a.justify_center, + a.align_center, + a.border, + t.atoms.border_contrast_low, + { + backgroundColor: 'black', + opacity: 0.2, + }, + ] }), _jsxs(View, { style: [a.align_center, a.gap_xs], children: [_jsx(Eye, { size: "lg", fill: "white" }), _jsx(Text, { style: [a.text_sm, { color: 'white' }], children: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Hidden"], ["Hidden"])))) })] })] })] }) }), _jsx(Hider.Content, { children: _jsxs(View, { style: [ + a.justify_center, + a.rounded_lg, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + { + backgroundColor: black, + aspectRatio: 9 / 16, + }, + ], children: [_jsx(Image, { source: { uri: thumbnail }, style: [a.w_full, a.h_full, { opacity: pressed ? 0.8 : 1 }], accessibilityIgnoresInvertColors: true }), _jsx(MediaInsetBorder, {}), _jsxs(View, { style: [a.absolute, a.inset_0, t.atoms.shadow_sm], children: [_jsx(View, { style: [a.absolute, a.inset_0, a.p_sm, { bottom: 'auto' }], children: _jsxs(View, { style: [a.relative, a.rounded_full, { width: 24, height: 24 }], children: [_jsx(UserAvatar, { type: "user", size: 24, avatar: post.author.avatar }), _jsx(MediaInsetBorder, {})] }) }), showLikeCount && (_jsxs(View, { style: [ + a.absolute, + a.inset_0, + a.pt_2xl, + { + top: 'auto', + }, + ], children: [_jsx(LinearGradient, { colors: [black, 'rgba(0, 0, 0, 0)'], locations: [0.02, 1], start: { x: 0, y: 1 }, end: { x: 0, y: 0 }, style: [a.absolute, a.inset_0, { opacity: 0.9 }] }), _jsx(View, { style: [a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md], children: likeCount > 0 && (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(Heart, { size: "sm", fill: "white" }), _jsx(Text, { style: [ + a.text_sm, + a.font_semi_bold, + { color: 'white' }, + ], children: formatCount(i18n, likeCount) })] })) })] }))] })] }) })] }) })); +} +export function CompactVideoPostCardPlaceholder() { + var t = useTheme(); + var black = getBlackColor(t); + return (_jsx(View, { style: [a.flex_1, t.atoms.shadow_sm], children: _jsx(View, { style: [ + a.rounded_lg, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + { + backgroundColor: black, + aspectRatio: 9 / 16, + }, + ], children: _jsx(MediaInsetBorder, {}) }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/WelcomeModal.js b/src/components/WelcomeModal.js new file mode 100644 index 0000000000..815133fa34 --- /dev/null +++ b/src/components/WelcomeModal.js @@ -0,0 +1,142 @@ +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, useState } from 'react'; +import { Pressable, View } from 'react-native'; +import { ImageBackground } from 'expo-image'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { FocusGuards, FocusScope } from 'radix-ui/internal'; +import { useLoggedOutViewControls } from '#/state/shell/logged-out'; +import { Logo } from '#/view/icons/Logo'; +import { atoms as a, flatten, useBreakpoints, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +var welcomeModalBg = require('../../assets/images/welcome-modal-bg.jpg'); +export function WelcomeModal(_a) { + var control = _a.control; + var _ = useLingui()._; + var ax = useAnalytics(); + var requestSwitchToAccount = useLoggedOutViewControls().requestSwitchToAccount; + var gtMobile = useBreakpoints().gtMobile; + var _b = useState(false), isExiting = _b[0], setIsExiting = _b[1]; + var _c = useState(false), signInLinkHovered = _c[0], setSignInLinkHovered = _c[1]; + var fadeOutAndClose = function (callback) { + setIsExiting(true); + setTimeout(function () { + control.close(); + if (callback) + callback(); + }, 150); + }; + useEffect(function () { + if (control.isOpen) { + ax.metric('welcomeModal:presented', {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [control.isOpen]); + var onPressCreateAccount = function () { + ax.metric('welcomeModal:signupClicked', {}); + control.close(); + requestSwitchToAccount({ requestedAccount: 'new' }); + }; + var onPressExplore = function () { + ax.metric('welcomeModal:exploreClicked', {}); + fadeOutAndClose(); + }; + var onPressSignIn = function () { + ax.metric('welcomeModal:signinClicked', {}); + control.close(); + requestSwitchToAccount({ requestedAccount: 'existing' }); + }; + FocusGuards.useFocusGuards(); + return (_jsx(View, { role: "dialog", "aria-modal": true, style: [ + a.fixed, + a.inset_0, + a.justify_center, + a.align_center, + { zIndex: 9999, backgroundColor: 'rgba(0,0,0,0.2)' }, + web({ backdropFilter: 'blur(15px)' }), + isExiting ? a.fade_out : a.fade_in, + ], children: _jsx(FocusScope.FocusScope, { asChild: true, loop: true, trapped: true, children: _jsx(View, { style: flatten([ + { + maxWidth: 800, + maxHeight: 600, + width: '90%', + height: '90%', + backgroundColor: '#C0DCF0', + }, + a.rounded_lg, + a.overflow_hidden, + a.zoom_in, + ]), children: _jsxs(ImageBackground, { source: welcomeModalBg, style: [a.flex_1, a.justify_center], contentFit: "cover", children: [_jsxs(View, { style: [a.gap_2xl, a.align_center, a.p_4xl], children: [_jsx(View, { style: [ + a.flex_row, + a.align_center, + a.justify_center, + a.w_full, + a.p_0, + ], children: _jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(Logo, { width: 26 }), _jsx(Text, { style: [ + a.text_2xl, + a.font_semi_bold, + a.user_select_none, + { color: '#354358', letterSpacing: -0.5 }, + ], children: "Bluesky" })] }) }), _jsx(View, { style: [ + a.gap_sm, + a.align_center, + a.pt_5xl, + a.pb_3xl, + a.mt_2xl, + ], children: _jsxs(Text, { style: [ + gtMobile ? a.text_4xl : a.text_3xl, + a.font_semi_bold, + a.text_center, + { color: '#354358' }, + web({ + backgroundImage: 'linear-gradient(180deg, #313F54 0%, #667B99 83.65%, rgba(102, 123, 153, 0.50) 100%)', + backgroundClip: 'text', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + color: 'transparent', + lineHeight: 1.2, + letterSpacing: -0.5, + }), + ], children: [_jsx(Trans, { children: "Real people." }), '\n', _jsx(Trans, { children: "Real conversations." }), '\n', _jsx(Trans, { children: "Social media you control." })] }) }), _jsxs(View, { style: [a.gap_md, a.align_center], children: [_jsxs(View, { children: [_jsx(Button, { onPress: onPressCreateAccount, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Create account"], ["Create account"])))), size: "large", color: "primary", style: { + width: 200, + backgroundColor: '#006AFF', + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Create account" }) }) }), _jsx(Button, { onPress: onPressExplore, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Explore the app"], ["Explore the app"])))), size: "large", color: "primary", variant: "ghost", style: [a.bg_transparent, { width: 200 }], hoverStyle: [a.bg_transparent], children: function (_a) { + var hovered = _a.hovered; + return (_jsx(ButtonText, { style: [hovered && [a.underline], { color: '#006AFF' }], children: _jsx(Trans, { children: "Explore the app" }) })); + } })] }), _jsx(View, { style: [a.align_center, { minWidth: 200 }], children: _jsxs(Text, { style: [ + a.text_md, + a.text_center, + { color: '#405168', lineHeight: 24 }, + ], children: [_jsx(Trans, { children: "Already have an account?" }), ' ', _jsx(Pressable, { onPointerEnter: function () { return setSignInLinkHovered(true); }, onPointerLeave: function () { return setSignInLinkHovered(false); }, accessibilityRole: "button", accessibilityLabel: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Sign in"], ["Sign in"])))), accessibilityHint: "", children: _jsx(Text, { style: [ + a.font_medium, + { + color: '#006AFF', + fontSize: undefined, + }, + signInLinkHovered && a.underline, + ], onPress: onPressSignIn, children: _jsx(Trans, { children: "Sign in" }) }) })] }) })] })] }), _jsx(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Close welcome modal"], ["Close welcome modal"])))), style: [ + a.absolute, + { + top: 8, + right: 8, + }, + a.bg_transparent, + ], hoverStyle: [a.bg_transparent], onPress: function () { + ax.metric('welcomeModal:dismissed', {}); + fadeOutAndClose(); + }, color: "secondary", size: "small", variant: "ghost", shape: "round", children: function (_a) { + var hovered = _a.hovered, pressed = _a.pressed, focused = _a.focused; + return (_jsx(XIcon, { size: "md", style: { + color: '#354358', + opacity: hovered || pressed || focused ? 1 : 0.7, + } })); + } })] }) }) }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/components/WhoCanReply.js b/src/components/WhoCanReply.js new file mode 100644 index 0000000000..e4453c6fa9 --- /dev/null +++ b/src/components/WhoCanReply.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 __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 { Fragment, useMemo, useRef } from 'react'; +import { Keyboard, Platform, View, } from 'react-native'; +import { AppBskyFeedPost, AtUri, } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_10 } from '#/lib/constants'; +import { makeListLink, makeProfileLink } from '#/lib/routes/links'; +import { threadgateViewToAllowUISetting, } from '#/state/queries/threadgate'; +import { atoms as a, native, useTheme, web } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useDialogControl } from '#/components/Dialog'; +import { PostInteractionSettingsDialog, usePrefetchPostInteractionSettings, } from '#/components/dialogs/PostInteractionSettingsDialog'; +import { TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronDownIcon } from '#/components/icons/Chevron'; +import { CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSignIcon } from '#/components/icons/CircleBanSign'; +import { Earth_Stroke2_Corner0_Rounded as EarthIcon } from '#/components/icons/Globe'; +import { Group3_Stroke2_Corner0_Rounded as GroupIcon } from '#/components/icons/Group'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +import * as bsky from '#/types/bsky'; +export function WhoCanReply(_a) { + var _b, _c; + var post = _a.post, isThreadAuthor = _a.isThreadAuthor, style = _a.style; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var infoDialogControl = useDialogControl(); + var editDialogControl = useDialogControl(); + /* + * `WhoCanReply` is only used for root posts atm, in case this changes + * unexpectedly, we should check to make sure it's for sure the root URI. + */ + var rootUri = bsky.dangerousIsType(post.record, AppBskyFeedPost.isRecord) && ((_b = post.record.reply) === null || _b === void 0 ? void 0 : _b.root) + ? post.record.reply.root.uri + : post.uri; + var settings = useMemo(function () { + return threadgateViewToAllowUISetting(post.threadgate); + }, [post.threadgate]); + var prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({ + postUri: post.uri, + rootPostUri: rootUri, + }); + var prefetchPromise = useRef(Promise.resolve()); + var prefetch = function () { + prefetchPromise.current = prefetchPostInteractionSettings(); + }; + var anyoneCanReply = settings.length === 1 && settings[0].type === 'everybody'; + var noOneCanReply = settings.length === 1 && settings[0].type === 'nobody'; + var description = anyoneCanReply + ? _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Everybody can reply"], ["Everybody can reply"])))) + : noOneCanReply + ? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Replies disabled"], ["Replies disabled"])))) + : _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Some people can reply"], ["Some people can reply"])))); + var onPressOpen = function () { + if (IS_NATIVE && Keyboard.isVisible()) { + Keyboard.dismiss(); + } + if (isThreadAuthor) { + ax.metric('thread:click:editOwnThreadgate', {}); + // wait on prefetch if it manages to resolve in under 200ms + // otherwise, proceed immediately and show the spinner -sfn + Promise.race([ + prefetchPromise.current, + new Promise(function (res) { return setTimeout(res, 200); }), + ]).finally(function () { + editDialogControl.open(); + }); + } + else { + ax.metric('thread:click:viewSomeoneElsesThreadgate', {}); + infoDialogControl.open(); + } + }; + return (_jsxs(_Fragment, { children: [_jsx(Button, __assign({ label: isThreadAuthor ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Edit who can reply"], ["Edit who can reply"])))) : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Who can reply"], ["Who can reply"])))), onPress: onPressOpen }, (isThreadAuthor + ? Platform.select({ + web: { + onHoverIn: prefetch, + }, + native: { + onPressIn: prefetch, + }, + }) + : {}), { hitSlop: HITSLOP_10, children: function (_a) { + var hovered = _a.hovered, focused = _a.focused, pressed = _a.pressed; + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.gap_xs, + (hovered || focused || pressed) && native({ opacity: 0.5 }), + style, + ], children: [_jsx(Icon, { color: isThreadAuthor ? t.palette.primary_500 : t.palette.contrast_400, width: 16, settings: settings }), _jsx(Text, { style: [ + a.text_sm, + a.leading_tight, + isThreadAuthor + ? { color: t.palette.primary_500 } + : t.atoms.text_contrast_medium, + (hovered || focused || pressed) && web(a.underline), + ], children: description }), isThreadAuthor && (_jsx(TinyChevronDownIcon, { width: 8, fill: t.palette.primary_500 }))] })); + } })), isThreadAuthor ? (_jsx(PostInteractionSettingsDialog, { postUri: post.uri, rootPostUri: rootUri, control: editDialogControl, initialThreadgateView: post.threadgate })) : (_jsx(WhoCanReplyDialog, { control: infoDialogControl, post: post, settings: settings, embeddingDisabled: Boolean((_c = post.viewer) === null || _c === void 0 ? void 0 : _c.embeddingDisabled) }))] })); +} +function Icon(_a) { + var color = _a.color, width = _a.width, settings = _a.settings; + var isEverybody = settings.length === 0 || + settings.every(function (setting) { return setting.type === 'everybody'; }); + var isNobody = !!settings.find(function (gate) { return gate.type === 'nobody'; }); + var IconComponent = isEverybody + ? EarthIcon + : isNobody + ? CircleBanSignIcon + : GroupIcon; + return _jsx(IconComponent, { fill: color, width: width }); +} +function WhoCanReplyDialog(_a) { + var control = _a.control, post = _a.post, settings = _a.settings, embeddingDisabled = _a.embeddingDisabled; + var _ = useLingui()._; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Dialog: adjust who can interact with this post"], ["Dialog: adjust who can interact with this post"])))), style: web({ maxWidth: 400 }), children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_xl, a.pb_sm], children: _jsx(Trans, { children: "Who can interact with this post?" }) }), _jsx(Rules, { post: post, settings: settings, embeddingDisabled: embeddingDisabled })] }), IS_NATIVE && (_jsx(Button, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Close"], ["Close"])))), onPress: function () { return control.close(); }, size: "small", variant: "solid", color: "secondary", style: [a.mt_5xl], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) })), _jsx(Dialog.Close, {})] })] })); +} +function Rules(_a) { + var post = _a.post, settings = _a.settings, embeddingDisabled = _a.embeddingDisabled; + var t = useTheme(); + return (_jsxs(_Fragment, { children: [_jsxs(Text, { style: [ + a.text_sm, + a.leading_snug, + a.flex_wrap, + t.atoms.text_contrast_medium, + ], children: [settings.length === 0 ? (_jsx(Trans, { children: "This post has an unknown type of threadgate on it. Your app may be out of date." })) : settings[0].type === 'everybody' ? (_jsx(Trans, { children: "Everybody can reply to this post." })) : settings[0].type === 'nobody' ? (_jsx(Trans, { children: "Replies to this post are disabled." })) : (_jsxs(Trans, { children: ["Only", ' ', settings.map(function (rule, i) { return (_jsxs(Fragment, { children: [_jsx(Rule, { rule: rule, post: post, lists: post.threadgate.lists }), _jsx(Separator, { i: i, length: settings.length })] }, "rule-".concat(i))); }), ' ', "can reply."] })), ' '] }), embeddingDisabled && (_jsx(Text, { style: [ + a.text_sm, + a.leading_snug, + a.flex_wrap, + t.atoms.text_contrast_medium, + ], children: _jsx(Trans, { children: "No one but the author can quote this post." }) }))] })); +} +function Rule(_a) { + var rule = _a.rule, post = _a.post, lists = _a.lists; + if (rule.type === 'mention') { + return _jsx(Trans, { children: "mentioned users" }); + } + if (rule.type === 'followers') { + return (_jsxs(Trans, { children: ["users following", ' ', _jsxs(InlineLinkText, { label: "@".concat(post.author.handle), to: makeProfileLink(post.author), style: [a.text_sm, a.leading_snug], children: ["@", post.author.handle] })] })); + } + if (rule.type === 'following') { + return (_jsxs(Trans, { children: ["users followed by", ' ', _jsxs(InlineLinkText, { label: "@".concat(post.author.handle), to: makeProfileLink(post.author), style: [a.text_sm, a.leading_snug], children: ["@", post.author.handle] })] })); + } + if (rule.type === 'list') { + var list = lists === null || lists === void 0 ? void 0 : lists.find(function (l) { return l.uri === rule.list; }); + if (list) { + var listUrip = new AtUri(list.uri); + return (_jsxs(Trans, { children: [_jsx(InlineLinkText, { label: list.name, to: makeListLink(listUrip.hostname, listUrip.rkey), style: [a.text_sm, a.leading_snug], children: list.name }), ' ', "members"] })); + } + } +} +function Separator(_a) { + var i = _a.i, length = _a.length; + if (length < 2 || i === length - 1) { + return null; + } + if (i === length - 2) { + return (_jsxs(_Fragment, { children: [length > 2 ? ',' : '', " ", _jsx(Trans, { children: "and" }), ' '] })); + } + return _jsx(_Fragment, { children: ", " }); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/activity-notifications/SubscribeProfileButton.js b/src/components/activity-notifications/SubscribeProfileButton.js new file mode 100644 index 0000000000..6a4942b002 --- /dev/null +++ b/src/components/activity-notifications/SubscribeProfileButton.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 { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useEffect, useState } from 'react'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { createSanitizedDisplayName } from '#/lib/moderation/create-sanitized-display-name'; +import { Button, ButtonIcon } from '#/components/Button'; +import { useDialogControl } from '#/components/Dialog'; +import { BellPlus_Stroke2_Corner0_Rounded as BellPlusIcon } from '#/components/icons/BellPlus'; +import { BellRinging_Filled_Corner0_Rounded as BellRingingIcon } from '#/components/icons/BellRinging'; +import * as Tooltip from '#/components/Tooltip'; +import { Text } from '#/components/Typography'; +import { useActivitySubscriptionsNudged } from '#/storage/hooks/activity-subscriptions-nudged'; +import { SubscribeProfileDialog } from './SubscribeProfileDialog'; +export function SubscribeProfileButton(_a) { + var _b, _c, _d, _e; + var profile = _a.profile, moderationOpts = _a.moderationOpts, disableHint = _a.disableHint; + var _ = useLingui()._; + var requireEmailVerification = useRequireEmailVerification(); + var subscribeDialogControl = useDialogControl(); + var _f = useActivitySubscriptionsNudged(), activitySubscriptionsNudged = _f[0], setActivitySubscriptionsNudged = _f[1]; + var _g = useState(false), showTooltip = _g[0], setShowTooltip = _g[1]; + useEffect(function () { + if (!activitySubscriptionsNudged) { + var timeout_1 = setTimeout(function () { + setShowTooltip(true); + }, 500); + return function () { return clearTimeout(timeout_1); }; + } + }, [activitySubscriptionsNudged]); + var onDismissTooltip = function (visible) { + if (visible) + return; + setShowTooltip(false); + setActivitySubscriptionsNudged(true); + }; + var onPress = useCallback(function () { + subscribeDialogControl.open(); + }, [subscribeDialogControl]); + var name = createSanitizedDisplayName(profile, true); + var wrappedOnPress = requireEmailVerification(onPress, { + instructions: [ + _jsxs(Trans, { children: ["Before you can get notifications for ", name, "'s posts, you must first verify your email."] }, "message"), + ], + }); + var isSubscribed = ((_c = (_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.activitySubscription) === null || _c === void 0 ? void 0 : _c.post) || + ((_e = (_d = profile.viewer) === null || _d === void 0 ? void 0 : _d.activitySubscription) === null || _e === void 0 ? void 0 : _e.reply); + var Icon = isSubscribed ? BellRingingIcon : BellPlusIcon; + var tooltipVisible = showTooltip && !disableHint; + return (_jsxs(_Fragment, { children: [_jsxs(Tooltip.Outer, { visible: tooltipVisible, onVisibleChange: onDismissTooltip, position: "bottom", children: [_jsx(Tooltip.Target, { children: _jsx(Button, { accessibilityRole: "button", testID: "dmBtn", size: "small", color: tooltipVisible ? 'primary_subtle' : 'secondary', shape: "round", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Get notified when ", " posts"], ["Get notified when ", " posts"])), name)), onPress: wrappedOnPress, children: _jsx(ButtonIcon, { icon: Icon, size: "md" }) }) }), _jsx(Tooltip.TextBubble, { children: _jsx(Text, { children: _jsx(Trans, { children: "Get notified about new posts" }) }) })] }), _jsx(SubscribeProfileDialog, { control: subscribeDialogControl, profile: profile, moderationOpts: moderationOpts })] })); +} +var templateObject_1; diff --git a/src/components/activity-notifications/SubscribeProfileDialog.js b/src/components/activity-notifications/SubscribeProfileDialog.js new file mode 100644 index 0000000000..b90e52b259 --- /dev/null +++ b/src/components/activity-notifications/SubscribeProfileDialog.js @@ -0,0 +1,219 @@ +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 { useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation, useQueryClient, } from '@tanstack/react-query'; +import { createSanitizedDisplayName } from '#/lib/moderation/create-sanitized-display-name'; +import { cleanError } from '#/lib/strings/errors'; +import { sanitizeHandle } from '#/lib/strings/handles'; +import { updateProfileShadow } from '#/state/cache/profile-shadow'; +import { RQKEY_getActivitySubscriptions } from '#/state/queries/activity-subscriptions'; +import { useAgent } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +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 Toggle from '#/components/forms/Toggle'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +export function SubscribeProfileDialog(_a) { + var control = _a.control, profile = _a.profile, moderationOpts = _a.moderationOpts, includeProfile = _a.includeProfile; + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsx(DialogInner, { profile: profile, moderationOpts: moderationOpts, includeProfile: includeProfile })] })); +} +function DialogInner(_a) { + var _this = this; + var _b; + var profile = _a.profile, moderationOpts = _a.moderationOpts, includeProfile = _a.includeProfile; + var ax = useAnalytics(); + var _ = useLingui()._; + var t = useTheme(); + var agent = useAgent(); + var control = Dialog.useDialogContext(); + var queryClient = useQueryClient(); + var initialState = parseActivitySubscription((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.activitySubscription); + var _c = useState(initialState), state = _c[0], setState = _c[1]; + var values = useMemo(function () { + var post = state.post, reply = state.reply; + var res = []; + if (post) + res.push('post'); + if (reply) + res.push('reply'); + return res; + }, [state]); + var onChange = function (newValues) { + setState(function (oldValues) { + // ensure you can't have reply without post + if (!oldValues.reply && newValues.includes('reply')) { + return { + post: true, + reply: true, + }; + } + if (oldValues.post && !newValues.includes('post')) { + return { + post: false, + reply: false, + }; + } + return { + post: newValues.includes('post'), + reply: newValues.includes('reply'), + }; + }); + }; + var _d = useMutation({ + mutationFn: function (activitySubscription) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.app.bsky.notification.putActivitySubscription({ + subject: profile.did, + activitySubscription: activitySubscription, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_data, activitySubscription) { + control.close(function () { + updateProfileShadow(queryClient, profile.did, { + activitySubscription: activitySubscription, + }); + if (!activitySubscription.post && !activitySubscription.reply) { + ax.metric('activitySubscription:disable', {}); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You will no longer receive notifications for ", ""], ["You will no longer receive notifications for ", ""])), sanitizeHandle(profile.handle, '@'))), 'check'); + // filter out the subscription + queryClient.setQueryData(RQKEY_getActivitySubscriptions, function (old) { + if (!old) + return old; + return __assign(__assign({}, old), { pages: old.pages.map(function (page) { return (__assign(__assign({}, page), { subscriptions: page.subscriptions.filter(function (item) { return item.did !== profile.did; }) })); }) }); + }); + } + else { + ax.metric('activitySubscription:enable', { + setting: activitySubscription.reply ? 'posts_and_replies' : 'posts', + }); + if (!initialState.post && !initialState.reply) { + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You'll start receiving notifications for ", "!"], ["You'll start receiving notifications for ", "!"])), sanitizeHandle(profile.handle, '@'))), 'check'); + } + else { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Changes saved"], ["Changes saved"])))), 'check'); + } + } + }); + }, + onError: function (err) { + ax.logger.error('Could not save activity subscription', { message: err }); + }, + }), saveChanges = _d.mutate, isSaving = _d.isPending, error = _d.error; + var buttonProps = useMemo(function () { + var isDirty = state.post !== initialState.post || state.reply !== initialState.reply; + var hasAny = state.post || state.reply; + if (isDirty) { + return { + label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Save changes"], ["Save changes"])))), + color: hasAny ? 'primary' : 'negative', + onPress: function () { return saveChanges(state); }, + disabled: isSaving, + }; + } + else { + // on web, a disabled save button feels more natural than a massive close button + if (IS_WEB) { + return { + label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Save changes"], ["Save changes"])))), + color: 'secondary', + disabled: true, + }; + } + else { + return { + label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Cancel"], ["Cancel"])))), + color: 'secondary', + onPress: function () { return control.close(); }, + }; + } + } + }, [state, initialState, control, _, isSaving, saveChanges]); + var name = createSanitizedDisplayName(profile, false); + return (_jsxs(Dialog.ScrollableInner, { style: web({ maxWidth: 400 }), label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Get notified of new posts from ", ""], ["Get notified of new posts from ", ""])), name)), children: [_jsxs(View, { style: [a.gap_lg], children: [_jsxs(View, { style: [a.gap_xs], children: [_jsx(Text, { style: [a.font_bold, a.text_2xl], children: _jsx(Trans, { children: "Keep me posted" }) }), _jsx(Text, { style: [t.atoms.text_contrast_medium, a.text_md], children: _jsx(Trans, { children: "Get notified of this account\u2019s activity" }) })] }), includeProfile && (_jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts, disabledPreview: true }), _jsx(ProfileCard.NameAndHandle, { profile: profile, moderationOpts: moderationOpts })] })), _jsx(Toggle.Group, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Subscribe to account activity"], ["Subscribe to account activity"])))), values: values, onChange: onChange, children: _jsxs(View, { style: [a.gap_sm], children: [_jsxs(Toggle.Item, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Posts"], ["Posts"])))), name: "post", style: [ + a.flex_1, + 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: "Posts" }) }), _jsx(Toggle.Switch, {})] }), _jsxs(Toggle.Item, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Replies"], ["Replies"])))), name: "reply", style: [ + a.flex_1, + 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: "Replies" }) }), _jsx(Toggle.Switch, {})] })] }) }), error && (_jsx(Admonition, { type: "error", children: _jsxs(Trans, { children: ["Could not save changes: ", cleanError(error)] }) })), _jsxs(Button, __assign({}, buttonProps, { size: "large", variant: "solid", children: [_jsx(ButtonText, { children: buttonProps.label }), isSaving && _jsx(ButtonIcon, { icon: Loader })] }))] }), _jsx(Dialog.Close, {})] })); +} +function parseActivitySubscription(sub) { + if (!sub) + return { post: false, reply: false }; + var post = sub.post, reply = sub.reply; + return { post: post, reply: reply }; +} +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/components/ageAssurance/AgeAssuranceAccountCard.js b/src/components/ageAssurance/AgeAssuranceAccountCard.js new file mode 100644 index 0000000000..4a1c3f4ea6 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceAccountCard.js @@ -0,0 +1,120 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { dateDiff, useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { AgeAssuranceAppealDialog } from '#/components/ageAssurance/AgeAssuranceAppealDialog'; +import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge'; +import { AgeAssuranceConfigUnavailableError } from '#/components/ageAssurance/AgeAssuranceErrors'; +import { AgeAssuranceInitDialog, useDialogControl, } from '#/components/ageAssurance/AgeAssuranceInitDialog'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { DeviceLocationRequestDialog } from '#/components/dialogs/DeviceLocationRequestDialog'; +import { Divider } from '#/components/Divider'; +import { createStaticClick, InlineLinkText } from '#/components/Link'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useComputeAgeAssuranceRegionAccess } from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'; +import { useAnalytics } from '#/analytics'; +import { IS_NATIVE } from '#/env'; +import { useDeviceGeolocationApi } from '#/geolocation'; +export function AgeAssuranceAccountCard(_a) { + var style = _a.style; + var aa = useAgeAssurance(); + if (aa.state.access === aa.Access.Full) + return null; + if (aa.state.error === 'config') { + return (_jsx(View, { style: style, children: _jsx(AgeAssuranceConfigUnavailableError, {}) })); + } + return _jsx(Inner, { style: style }); +} +function Inner(_a) { + var style = _a.style; + var t = useTheme(); + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var ax = useAnalytics(); + var control = useDialogControl(); + var appealControl = Dialog.useDialogControl(); + var locationControl = Dialog.useDialogControl(); + var getTimeAgo = useGetTimeAgo(); + var gtPhone = useBreakpoints().gtPhone; + var setDeviceGeolocation = useDeviceGeolocationApi().setDeviceGeolocation; + var computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess(); + var copy = useAgeAssuranceCopy(); + var aa = useAgeAssurance(); + var _c = aa.state, status = _c.status, lastInitiatedAt = _c.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 }), _jsx(View, { style: style, children: _jsxs(View, { style: [a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low], children: [_jsx(View, { style: [ + a.flex_row, + a.justify_between, + a.align_center, + a.gap_lg, + a.pb_md, + a.z_10, + ], children: _jsx(View, { style: [a.align_start], children: _jsx(AgeAssuranceBadge, {}) }) }), _jsxs(View, { style: [a.pb_md, a.gap_xs], children: [_jsx(Text, { style: [a.text_sm, a.leading_snug], children: copy.notice }), IS_NATIVE && (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_sm, a.leading_snug], children: _jsxs(Trans, { children: ["Is your location not accurate?", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __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_2 || (templateObject_2 = __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_3 || (templateObject_3 = __makeTemplateObject(["Thanks! You're all set."], ["Thanks! You're all set."])))), { + type: 'success', + }); + }); + } + } })] }))] }), isBlocked ? (_jsx(Admonition, { type: "warning", children: _jsxs(Trans, { children: ["You are currently unable to access Bluesky's Age Assurance flow. Please", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_4 || (templateObject_4 = __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."] }) })) : (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(View, { style: [ + a.pt_md, + gtPhone + ? [ + a.flex_row_reverse, + a.gap_xl, + a.justify_between, + a.align_center, + ] + : [a.gap_md], + ], children: [_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Verify now"], ["Verify now"])))), size: "small", variant: "solid", color: hasInitiated ? 'secondary' : 'primary', onPress: function () { + control.open(); + ax.metric('ageAssurance:initDialogOpen', { + hasInitiatedPreviously: hasInitiated, + }); + }, children: _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" }) }))] })] }))] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/ageAssurance/AgeAssuranceAdmonition.js b/src/components/ageAssurance/AgeAssuranceAdmonition.js new file mode 100644 index 0000000000..48ccbfa39a --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceAdmonition.js @@ -0,0 +1,69 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, select, useTheme } from '#/alf'; +import { AgeAssuranceConfigUnavailableError } from '#/components/ageAssurance/AgeAssuranceErrors'; +import { useDialogControl } from '#/components/ageAssurance/AgeAssuranceInitDialog'; +import { ShieldCheck_Stroke2_Corner0_Rounded as Shield } from '#/components/icons/Shield'; +import { InlineLinkText } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +export function AgeAssuranceAdmonition(_a) { + var children = _a.children, style = _a.style; + var control = useDialogControl(); + var aa = useAgeAssurance(); + if (aa.state.access === aa.Access.Full) + return null; + if (aa.state.error === 'config') { + return _jsx(AgeAssuranceConfigUnavailableError, { style: style }); + } + return (_jsx(Inner, { style: style, control: control, children: children })); +} +function Inner(_a) { + var children = _a.children, style = _a.style; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + return (_jsx(_Fragment, { children: _jsx(View, { style: style, children: _jsxs(View, { style: [ + a.p_md, + a.rounded_md, + a.border, + a.flex_row, + a.align_start, + a.gap_sm, + { + backgroundColor: select(t.name, { + light: t.palette.primary_25, + dark: t.palette.primary_25, + dim: t.palette.primary_25, + }), + borderColor: select(t.name, { + light: t.palette.primary_100, + dark: t.palette.primary_100, + dim: t.palette.primary_100, + }), + }, + ], children: [_jsx(View, { style: [ + a.align_center, + a.justify_center, + a.rounded_full, + { + width: 32, + height: 32, + backgroundColor: select(t.name, { + light: t.palette.primary_100, + dark: t.palette.primary_100, + dim: t.palette.primary_100, + }), + }, + ], children: _jsx(Shield, { size: "md" }) }), _jsxs(View, { style: [a.flex_1, a.gap_xs, a.pr_4xl], children: [_jsx(Text, { style: [a.text_sm, a.leading_snug], children: children }), _jsx(Text, { style: [a.text_sm, a.leading_snug, a.font_semi_bold], children: _jsxs(Trans, { children: ["Learn more in your", ' ', _jsx(InlineLinkText, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go to account settings"], ["Go to account settings"])))), to: '/settings/account', style: [a.text_sm, a.leading_snug, a.font_semi_bold], onPress: function () { + ax.metric('ageAssurance:navigateToSettings', {}); + }, children: "account settings." })] }) })] })] }) }) })); +} +var templateObject_1; diff --git a/src/components/ageAssurance/AgeAssuranceAppealDialog.js b/src/components/ageAssurance/AgeAssuranceAppealDialog.js new file mode 100644 index 0000000000..17887cdd95 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceAppealDialog.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 } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { ToolsOzoneReportDefs } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation } from '@tanstack/react-query'; +import { BLUESKY_MOD_SERVICE_HEADERS } from '#/lib/constants'; +import { useAgent, useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, useBreakpoints, web } from '#/alf'; +import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { logger } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +export function AgeAssuranceAppealDialog(_a) { + var control = _a.control; + var _ = useLingui()._; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Contact our moderation team"], ["Contact our moderation team"])))), style: [web({ maxWidth: 400 })], children: [_jsx(Inner, { control: control }), _jsx(Dialog.Close, {})] })] })); +} +function Inner(_a) { + var _this = this; + var control = _a.control; + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var gtPhone = useBreakpoints().gtPhone; + var agent = useAgent(); + var _b = React.useState(''), details = _b[0], setDetails = _b[1]; + var isInvalid = details.length > 1000; + var _c = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + ax.metric('ageAssurance:appealDialogSubmit', {}); + return [4 /*yield*/, agent.createModerationReport({ + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, + }, + reason: "AGE_ASSURANCE_INQUIRY: " + details, + }, { + encoding: 'application/json', + headers: BLUESKY_MOD_SERVICE_HEADERS, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onError: function (err) { + logger.error('AgeAssuranceAppealDialog failed', { safeMessage: err }); + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Age assurance inquiry failed to send, please try again."], ["Age assurance inquiry failed to send, please try again."])))), 'xmark'); + }, + onSuccess: function () { + control.close(); + Toast.show(_(msg({ + message: 'Age assurance inquiry was submitted', + context: 'toast', + }))); + }, + }), mutate = _c.mutate, isPending = _c.isPending; + return (_jsxs(View, { children: [_jsx(View, { style: [a.align_start], children: _jsx(AgeAssuranceBadge, {}) }), _jsx(Text, { style: [a.text_2xl, a.font_bold, a.pt_md, a.leading_tight], children: _jsx(Trans, { children: "Contact us" }) }), _jsx(Text, { style: [a.text_sm, a.pt_sm, a.leading_snug], children: _jsx(Trans, { children: "Please provide any additional details you feel moderators may need in order to properly assess your Age Assurance status." }) }), _jsxs(View, { style: [a.pt_md], children: [_jsx(Dialog.Input, { multiline: true, isInvalid: isInvalid, value: details, onChangeText: function (details) { + setDetails(details); + }, label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Additional details (limit 1000 characters)"], ["Additional details (limit 1000 characters)"])))), numberOfLines: 4, onSubmitEditing: function () { return mutate(); } }), _jsxs(View, { style: [a.pt_md, a.gap_sm, gtPhone && [a.flex_row_reverse]], children: [_jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Submit"], ["Submit"])))), size: "small", variant: "solid", color: "primary", onPress: function () { return mutate(); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Submit" }) }), isPending && _jsx(ButtonIcon, { icon: Loader, position: "right" })] }), _jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Cancel"], ["Cancel"])))), size: "small", variant: "solid", color: "secondary", onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/ageAssurance/AgeAssuranceBadge.js b/src/components/ageAssurance/AgeAssuranceBadge.js new file mode 100644 index 0000000000..a6911ecd6c --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceBadge.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/macro'; +import { atoms as a, select, useTheme } from '#/alf'; +import { ShieldCheck_Stroke2_Corner0_Rounded as Shield } from '#/components/icons/Shield'; +import { Text } from '#/components/Typography'; +export function AgeAssuranceBadge() { + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_row, + a.align_center, + a.gap_xs, + a.px_sm, + a.py_xs, + a.pr_sm, + a.rounded_full, + { + backgroundColor: select(t.name, { + light: t.palette.primary_100, + dark: t.palette.primary_100, + dim: t.palette.primary_100, + }), + }, + ], children: [_jsx(Shield, { size: "sm" }), _jsx(Text, { style: [ + a.font_semi_bold, + a.leading_snug, + { + color: select(t.name, { + light: t.palette.primary_800, + dark: t.palette.primary_800, + dim: t.palette.primary_800, + }), + }, + ], children: _jsx(Trans, { children: "Age Assurance" }) })] })); +} diff --git a/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.js b/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.js new file mode 100644 index 0000000000..a1eb34912c --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.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 { useMemo } from 'react'; +import { View } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { Nux, useNux, useSaveNux } from '#/state/queries/nuxs'; +import { atoms as a, select, useTheme } from '#/alf'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { Button } from '#/components/Button'; +import { ShieldCheck_Stroke2_Corner0_Rounded as Shield } from '#/components/icons/Shield'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +export function useInternalState() { + var aa = useAgeAssurance(); + var nux = useNux(Nux.AgeAssuranceDismissibleFeedBanner).nux; + var _a = useSaveNux(), save = _a.mutate, variables = _a.variables; + var hidden = !!variables; + var visible = useMemo(function () { + if (aa.state.access === aa.Access.Full) + return false; + if (aa.state.lastInitiatedAt) + return false; + if (aa.state.error === 'config') + return false; + if (hidden) + return false; + if (nux && nux.completed) + return false; + return true; + }, [aa, hidden, nux]); + var close = function () { + save({ + id: Nux.AgeAssuranceDismissibleFeedBanner, + completed: true, + data: undefined, + }); + }; + return { visible: visible, close: close }; +} +export function AgeAssuranceDismissibleFeedBanner() { + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var _a = useInternalState(), visible = _a.visible, close = _a.close; + var copy = useAgeAssuranceCopy(); + if (!visible) + return null; + return (_jsxs(View, { style: [ + a.px_lg, + { + paddingVertical: 10, + backgroundColor: select(t.name, { + light: t.palette.primary_25, + dark: t.palette.primary_25, + dim: t.palette.primary_25, + }), + }, + ], children: [_jsxs(Link, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Learn more about age assurance"], ["Learn more about age assurance"])))), to: "/settings/account", onPress: function () { + close(); + ax.metric('ageAssurance:navigateToSettings', {}); + }, style: [a.w_full, a.justify_between, a.align_center, a.gap_md], children: [_jsx(View, { style: [ + a.align_center, + a.justify_center, + a.rounded_full, + { + width: 42, + height: 42, + backgroundColor: select(t.name, { + light: t.palette.primary_100, + dark: t.palette.primary_100, + dim: t.palette.primary_100, + }), + }, + ], children: _jsx(Shield, { size: "lg" }) }), _jsx(View, { style: [ + a.flex_1, + { + paddingRight: 40, + }, + ], children: _jsx(View, { style: { maxWidth: 400 }, children: _jsx(Text, { style: [a.leading_snug], children: copy.banner }) }) })] }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Don't show again"], ["Don't show again"])))), size: "small", onPress: function () { + close(); + ax.metric('ageAssurance:dismissFeedBanner', {}); + }, style: [ + a.absolute, + a.justify_center, + a.align_center, + { + top: 0, + bottom: 0, + right: 0, + paddingRight: a.px_md.paddingLeft, + }, + ], children: _jsx(X, { width: 20, fill: select(t.name, { + light: t.palette.primary_600, + dark: t.palette.primary_600, + dim: t.palette.primary_600, + }) }) })] })); +} +var templateObject_1, templateObject_2; diff --git a/src/components/ageAssurance/AgeAssuranceDismissibleNotice.js b/src/components/ageAssurance/AgeAssuranceDismissibleNotice.js new file mode 100644 index 0000000000..ccb540e1c4 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceDismissibleNotice.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 { View } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { Nux, useNux, useSaveNux } from '#/state/queries/nuxs'; +import { atoms as a } from '#/alf'; +import { AgeAssuranceAdmonition } from '#/components/ageAssurance/AgeAssuranceAdmonition'; +import { AgeAssuranceConfigUnavailableError } from '#/components/ageAssurance/AgeAssuranceErrors'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { Button, ButtonIcon } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +export function AgeAssuranceDismissibleNotice(_a) { + var style = _a.style; + var _ = useLingui()._; + var ax = useAnalytics(); + var aa = useAgeAssurance(); + var nux = useNux(Nux.AgeAssuranceDismissibleNotice).nux; + var copy = useAgeAssuranceCopy(); + var _b = useSaveNux(), save = _b.mutate, variables = _b.variables; + var hidden = !!variables; + if (aa.state.access === aa.Access.Full) + return null; + if (aa.state.lastInitiatedAt) + return null; + if (hidden) + return null; + if (nux && nux.completed) + return null; + return (_jsx(View, { style: style, children: aa.state.error === 'config' ? (_jsx(AgeAssuranceConfigUnavailableError, {})) : (_jsxs(View, { children: [_jsx(AgeAssuranceAdmonition, { children: copy.notice }), _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Don't show again"], ["Don't show again"])))), size: "tiny", variant: "solid", color: "secondary_inverted", shape: "round", onPress: function () { + save({ + id: Nux.AgeAssuranceDismissibleNotice, + completed: true, + data: undefined, + }); + ax.metric('ageAssurance:dismissSettingsNotice', {}); + }, style: [ + a.absolute, + { + top: 12, + right: 12, + }, + ], children: _jsx(ButtonIcon, { icon: X }) })] })) })); +} +var templateObject_1; diff --git a/src/components/ageAssurance/AgeAssuranceErrors.js b/src/components/ageAssurance/AgeAssuranceErrors.js new file mode 100644 index 0000000000..c441be7556 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceErrors.js @@ -0,0 +1,16 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import * as Admonition from '#/components/Admonition'; +import { ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon } from '#/components/icons/ArrowRotate'; +import { refetchConfig } from '#/ageAssurance/data'; +export function AgeAssuranceConfigUnavailableError(props) { + var _ = useLingui()._; + return (_jsx(Admonition.Outer, { type: "error", style: props.style, children: _jsxs(Admonition.Row, { children: [_jsx(Admonition.Icon, {}), _jsx(Admonition.Content, { children: _jsx(Admonition.Text, { children: _jsx(Trans, { children: "We were unable to load the age assurance configuration for your region, probably due to a network error. Some content and features may be unavailable temporarily. Please try again later." }) }) }), _jsxs(Admonition.Button, { color: "negative_subtle", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Retry"], ["Retry"])))), onPress: function () { return refetchConfig().catch(function () { }); }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }), _jsx(ButtonIcon, { icon: RetryIcon })] })] }) })); +} +var templateObject_1; diff --git a/src/components/ageAssurance/AgeAssuranceInitDialog.js b/src/components/ageAssurance/AgeAssuranceInitDialog.js new file mode 100644 index 0000000000..d97722e192 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceInitDialog.js @@ -0,0 +1,198 @@ +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 { XRPCError } from '@atproto/xrpc'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { validate as validateEmail } from 'email-validator'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { SupportCode, useCreateSupportLink, } from '#/lib/hooks/useCreateSupportLink'; +import { useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { useTLDs } from '#/lib/hooks/useTLDs'; +import { isEmailMaybeInvalid } from '#/lib/strings/email'; +import { useLanguagePrefs } from '#/state/preferences'; +import { useSession } from '#/state/session'; +import { atoms as a, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge'; +import { KWS_SUPPORTED_LANGS, urls } from '#/components/ageAssurance/const'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { Divider } from '#/components/Divider'; +import * as TextField from '#/components/forms/TextField'; +import { ShieldCheck_Stroke2_Corner0_Rounded as Shield } from '#/components/icons/Shield'; +import { LanguageSelect } from '#/components/LanguageSelect'; +import { SimpleInlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useBeginAgeAssurance } from '#/ageAssurance/useBeginAgeAssurance'; +import { useAnalytics } from '#/analytics'; +export { useDialogControl } from '#/components/Dialog/context'; +export function AgeAssuranceInitDialog(_a) { + var control = _a.control; + var _ = useLingui()._; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Begin the age assurance process by completing the fields below."], ["Begin the age assurance process by completing the fields below."])))), style: [ + web({ + maxWidth: 400, + }), + ], children: [_jsx(Inner, {}), _jsx(Dialog.Close, {})] })] })); +} +function Inner() { + var _this = this; + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var langPrefs = useLanguagePrefs(); + var cleanError = useCleanError(); + var close = Dialog.useDialogContext().close; + var aa = useAgeAssurance(); + var lastInitiatedAt = aa.state.lastInitiatedAt; + var getTimeAgo = useGetTimeAgo(); + var tlds = useTLDs(); + var createSupportLink = useCreateSupportLink(); + var wasRecentlyInitiated = lastInitiatedAt && + new Date(lastInitiatedAt).getTime() > Date.now() - 5 * 60 * 1000; // 5 minutes + var _a = useState(false), success = _a[0], setSuccess = _a[1]; + var _b = useState((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email) || ''), email = _b[0], setEmail = _b[1]; + var _c = useState(''), emailError = _c[0], setEmailError = _c[1]; + var _d = useState(false), languageError = _d[0], setLanguageError = _d[1]; + var _e = useState(false), disabled = _e[0], setDisabled = _e[1]; + var _f = useState(convertToKWSSupportedLanguage(langPrefs.appLanguage)), language = _f[0], setLanguage = _f[1]; + var _g = useState(null), error = _g[0], setError = _g[1]; + var _h = useBeginAgeAssurance(), begin = _h.mutateAsync, isPending = _h.isPending; + var runEmailValidation = function () { + if (validateEmail(email)) { + setEmailError(''); + setDisabled(false); + if (tlds && isEmailMaybeInvalid(email, tlds)) { + setEmailError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please double-check that you have entered your email address correctly."], ["Please double-check that you have entered your email address correctly."]))))); + return { status: 'maybe' }; + } + return { status: 'valid' }; + } + setEmailError(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Please enter a valid email address."], ["Please enter a valid email address."]))))); + setDisabled(true); + return { status: 'invalid' }; + }; + var onSubmit = function () { return __awaiter(_this, void 0, void 0, function () { + var status_1, e_1, error_1, _a, clean, raw; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + setLanguageError(false); + ax.metric('ageAssurance:initDialogSubmit', {}); + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + status_1 = runEmailValidation().status; + if (status_1 === 'invalid') + return [2 /*return*/]; + if (!language) { + setLanguageError(true); + return [2 /*return*/]; + } + return [4 /*yield*/, begin({ + email: email, + language: language, + })]; + case 2: + _b.sent(); + setSuccess(true); + return [3 /*break*/, 4]; + case 3: + e_1 = _b.sent(); + error_1 = _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Something went wrong, please try again"], ["Something went wrong, please try again"])))); + if (e_1 instanceof XRPCError) { + if (e_1.error === 'InvalidEmail') { + error_1 = _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Please enter a valid, non-temporary email address. You may need to access this email in the future."], ["Please enter a valid, non-temporary email address. You may need to access this email in the future."])))); + ax.metric('ageAssurance:initDialogError', { code: 'InvalidEmail' }); + } + else if (e_1.error === 'DidTooLong') { + error_1 = (_jsx(_Fragment, { children: _jsxs(Trans, { children: ["We're having issues initializing the age assurance process for your account. Please", ' ', _jsx(SimpleInlineLinkText, { to: createSupportLink({ code: SupportCode.AA_DID, email: email }), label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Contact support"], ["Contact support"])))), children: "contact support" }), ' ', "for assistance."] }) })); + ax.metric('ageAssurance:initDialogError', { code: 'DidTooLong' }); + } + else { + ax.metric('ageAssurance:initDialogError', { code: 'other' }); + } + } + else { + _a = cleanError(e_1), clean = _a.clean, raw = _a.raw; + error_1 = clean || raw || error_1; + ax.metric('ageAssurance:initDialogError', { code: 'other' }); + } + setError(error_1); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + return (_jsx(View, { children: _jsxs(View, { style: [a.align_start], children: [_jsx(AgeAssuranceBadge, {}), _jsx(Text, { style: [a.text_xl, a.font_bold, a.pt_xl, a.pb_md], children: success ? _jsx(Trans, { children: "Success!" }) : _jsx(Trans, { children: "Verify your age" }) }), _jsx(View, { style: [a.pb_xl, a.gap_sm], children: success ? (_jsx(Text, { style: [a.text_sm, a.leading_snug], children: _jsx(Trans, { children: "Please check your email inbox for further instructions. It may take a minute or two to arrive." }) })) : (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_sm, a.leading_snug], children: _jsxs(Trans, { children: ["We have partnered with", ' ', _jsx(SimpleInlineLinkText, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["KWS website"], ["KWS website"])))), to: urls.kwsHome, style: [a.text_sm, a.leading_snug], children: "KWS" }), ' ', "to handle age verification. When you click \"Begin\" below, KWS will email you instructions to complete the verification process. If your email address has already been used to verify your age for another game or service that uses KWS, you won\u2019t need to do it again. When you\u2019re done, you'll be brought back to continue using Bluesky."] }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug], children: _jsx(Trans, { children: "This should only take a few minutes." }) })] })) }), success ? (_jsx(View, { style: [a.w_full], children: _jsx(Button, { label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Close dialog"], ["Close dialog"])))), size: "large", variant: "solid", color: "secondary", onPress: function () { return close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close dialog" }) }) }) })) : (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(View, { style: [a.w_full, a.pt_xl, a.gap_lg, a.pb_lg], children: [wasRecentlyInitiated && (_jsx(Admonition, { type: "warning", children: _jsxs(Trans, { children: ["You initiated this flow already,", ' ', getTimeAgo(lastInitiatedAt, new Date(), { format: 'long' }), ' ', "ago. It may take up to 5 minutes for emails to reach your inbox. Please consider waiting a few minutes before trying again."] }) })), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Your email" }) }), _jsx(TextField.Root, { isInvalid: !!emailError, children: _jsx(TextField.Input, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Your email"], ["Your email"])))), placeholder: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Your email"], ["Your email"])))), value: email, onChangeText: setEmail, onFocus: function () { return setEmailError(''); }, onBlur: function () { + runEmailValidation(); + }, returnKeyType: "done", autoCapitalize: "none", autoComplete: "off", autoCorrect: false, onSubmitEditing: onSubmit }) }), emailError ? (_jsx(Admonition, { type: "error", style: [a.mt_sm], children: emailError })) : (_jsx(Admonition, { type: "tip", style: [a.mt_sm], children: _jsx(Trans, { children: "Use your account email address, or another real email address you control, in case KWS or Bluesky needs to contact you." }) }))] }), _jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Your preferred language" }) }), _jsx(LanguageSelect, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Preferred language"], ["Preferred language"])))), value: language, onChange: function (value) { + setLanguage(value); + setLanguageError(false); + }, items: KWS_SUPPORTED_LANGS }), languageError && (_jsx(Admonition, { type: "error", style: [a.mt_sm], children: _jsx(Trans, { children: "Please select a language" }) }))] }), error && _jsx(Admonition, { type: "error", children: error }), _jsxs(Button, { disabled: disabled, label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Begin age assurance process"], ["Begin age assurance process"])))), size: "large", variant: "solid", color: "primary", onPress: onSubmit, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Begin" }) }), _jsx(ButtonIcon, { icon: isPending ? Loader : Shield, position: "right" })] })] })] }))] }) })); +} +// best-effort mapping of our languages to KWS supported languages +function convertToKWSSupportedLanguage(appLanguage) { + var _a; + // `${Enum}` is how you get a type of string union of the enum values (???) -sfn + switch (appLanguage) { + // only en is supported + case 'en-GB': + return 'en'; + // pt-PT is pt (pt-BR is supported independently) + case 'pt-PT': + return 'pt'; + // only chinese (simplified) is supported, map all chinese variants + case 'zh-Hans-CN': + case 'zh-Hant-HK': + case 'zh-Hant-TW': + return 'zh-Hans'; + default: + // try and map directly - if undefined, they will have to pick from the dropdown + return (_a = KWS_SUPPORTED_LANGS.find(function (v) { return v.value === appLanguage; })) === null || _a === void 0 ? void 0 : _a.value; + } +} +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/ageAssurance/AgeAssuranceRedirectDialog.js b/src/components/ageAssurance/AgeAssuranceRedirectDialog.js new file mode 100644 index 0000000000..f3b3cf638b --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceRedirectDialog.js @@ -0,0 +1,172 @@ +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, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { retry } from '#/lib/async/retry'; +import { wait } from '#/lib/async/wait'; +import { useAgent } from '#/state/session'; +import { atoms as a, useTheme, web } from '#/alf'; +import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +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_NATIVE } 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 parseAgeAssuranceRedirectDialogState(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 (result && actorDid) { + return { + result: result, + actorDid: actorDid, + }; + } +} +export function useAgeAssuranceRedirectDialogControl() { + return useGlobalDialogsControlContext().ageAssuranceRedirectDialogControl; +} +export function AgeAssuranceRedirectDialog() { + var _ = useLingui()._; + var control = useAgeAssuranceRedirectDialogControl(); + // for testing + // Dialog.useAutoOpen(control.control, 3e3) + return (_jsxs(Dialog.Outer, { control: control.control, onClose: function () { return control.clear(); }, children: [_jsx(Dialog.Handle, {}), _jsx(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Verifying your age assurance status"], ["Verifying your age assurance status"])))), style: [web({ maxWidth: 400 })], children: _jsx(Inner, { optimisticState: control.value }) })] })); +} +export function Inner(_a) { + var _this = this; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var agent = useAgent(); + var polling = useRef(false); + var unmounted = useRef(false); + var control = useAgeAssuranceRedirectDialogControl(); + var _b = useState(false), error = _b[0], setError = _b[1]; + var _c = useState(false), success = _c[0], setSuccess = _c[1]; + 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, control]); + if (success) { + return (_jsxs(_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_xl, 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." }) }), IS_NATIVE && (_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 control.control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }) }))] }), _jsx(Dialog.Close, {})] })); + } + return (_jsxs(_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: "md", fill: t.palette.negative_500 }), _jsx(Text, { style: [a.text_xl, a.font_bold], children: error ? _jsx(Trans, { children: "Connection issue" }) : _jsx(Trans, { children: "Verifying" }) }), !error && _jsx(Loader, { size: "md" })] }), _jsx(Text, { style: [a.text_md, 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 && IS_NATIVE && (_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 control.control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }) }))] }), error && _jsx(Dialog.Close, {})] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/ageAssurance/AgeRestrictedScreen.js b/src/components/ageAssurance/AgeRestrictedScreen.js new file mode 100644 index 0000000000..4c34405fca --- /dev/null +++ b/src/components/ageAssurance/AgeRestrictedScreen.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 { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge'; +import { AgeAssuranceConfigUnavailableError } from '#/components/ageAssurance/AgeAssuranceErrors'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { ButtonIcon, ButtonText } from '#/components/Button'; +import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRight } from '#/components/icons/Chevron'; +import * as Layout from '#/components/Layout'; +import { Link } from '#/components/Link'; +import { Text } from '#/components/Typography'; +import { useAgeAssurance } from '#/ageAssurance'; +import { useAnalytics } from '#/analytics'; +export function AgeRestrictedScreen(_a) { + var children = _a.children, screenTitle = _a.screenTitle, infoText = _a.infoText, rightHeaderSlot = _a.rightHeaderSlot; + var _ = useLingui()._; + var ax = useAnalytics(); + var copy = useAgeAssuranceCopy(); + var aa = useAgeAssurance(); + if (aa.state.access === aa.Access.Full) + return children; + 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: screenTitle !== null && screenTitle !== void 0 ? screenTitle : _jsx(Trans, { children: "Unavailable" }) }) }), rightHeaderSlot !== null && rightHeaderSlot !== void 0 ? rightHeaderSlot : _jsx(Layout.Header.Slot, {})] }), _jsx(Layout.Content, { children: _jsxs(View, { style: [a.p_lg], children: [aa.state.error === 'config' && (_jsx(View, { style: [a.pb_lg], children: _jsx(AgeAssuranceConfigUnavailableError, {}) })), _jsx(View, { style: [a.align_start, a.pb_lg], children: _jsx(AgeAssuranceBadge, {}) }), _jsxs(View, { style: [a.gap_sm, a.pb_lg], children: [_jsx(Text, { style: [a.text_xl, a.leading_snug, a.font_bold], children: _jsx(Trans, { children: "You must complete age assurance in order to access this screen." }) }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: copy.notice })] }), _jsx(View, { style: [a.flex_row, a.justify_between, a.align_center, a.pb_xl], children: _jsxs(Link, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Go to account settings"], ["Go to account settings"])))), to: "/settings/account", size: "small", variant: "solid", color: "primary", onPress: function () { + ax.metric('ageAssurance:navigateToSettings', {}); + }, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Go to account settings" }) }), _jsx(ButtonIcon, { icon: ChevronRight, position: "right" })] }) }), infoText && _jsx(Admonition, { type: "tip", children: infoText })] }) })] })); +} +var templateObject_1; diff --git a/src/components/ageAssurance/const.js b/src/components/ageAssurance/const.js new file mode 100644 index 0000000000..0a71eb14f8 --- /dev/null +++ b/src/components/ageAssurance/const.js @@ -0,0 +1,25 @@ +export var urls = { + kwsHome: 'https://www.kidswebservices.com/en-US', + kwsTermsOfUse: 'https://www.kidswebservices.com/en-US/terms-of-use', + kwsPrivacyPolicy: 'https://www.kidswebservices.com/en-US/privacy-policy', +}; +export var KWS_SUPPORTED_LANGS = [ + { value: 'en', label: 'English' }, + { value: 'ar', label: 'العربية' }, + { value: 'zh-Hans', label: '简体中文' }, + { value: 'nl', label: 'Nederlands' }, + { value: 'tl', label: 'Filipino' }, + { value: 'fr', label: 'Français' }, + { value: 'de', label: 'Deutsch' }, + { value: 'id', label: 'Bahasa Indonesia' }, + { value: 'it', label: 'Italiano' }, + { value: 'ja', label: '日本語' }, + { value: 'ko', label: '한국어' }, + { value: 'pt', label: 'Português' }, + { value: 'pt-BR', label: 'Português (Brasil)' }, + { value: 'ru', label: 'Русский' }, + { value: 'es', label: 'Español' }, + { value: 'tr', label: 'Türkçe' }, + { value: 'th', label: 'ภาษาไทย' }, + { value: 'vi', label: 'Tiếng Việt' }, +]; diff --git a/src/components/ageAssurance/useAgeAssuranceCopy.js b/src/components/ageAssurance/useAgeAssuranceCopy.js new file mode 100644 index 0000000000..6fb8299cd3 --- /dev/null +++ b/src/components/ageAssurance/useAgeAssuranceCopy.js @@ -0,0 +1,18 @@ +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/macro'; +import { useLingui } from '@lingui/react'; +export function useAgeAssuranceCopy() { + var _ = useLingui()._; + return useMemo(function () { + return { + notice: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Due to laws in your region, certain features on Bluesky are currently restricted until you're able to verify you're an adult."], ["Due to laws in your region, certain features on Bluesky are currently restricted until you're able to verify you're an adult."])))), + banner: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["The laws in your location require you to verify you're an adult to access certain features. Tap to learn more."], ["The laws in your location require you to verify you're an adult to access certain features. Tap to learn more."])))), + chatsInfoText: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Don't worry! All existing messages and settings are saved and will be available after you verify you're an adult."], ["Don't worry! All existing messages and settings are saved and will be available after you verify you're an adult."])))), + }; + }, [_]); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/anim/AnimatedCheck.js b/src/components/anim/AnimatedCheck.js new file mode 100644 index 0000000000..6329dd398d --- /dev/null +++ b/src/components/anim/AnimatedCheck.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 React from 'react'; +import Animated, { Easing, useAnimatedProps, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated'; +import Svg, { Circle, Path } from 'react-native-svg'; +import { useCommonSVGProps } from '#/components/icons/common'; +var AnimatedPath = Animated.createAnimatedComponent(Path); +var AnimatedCircle = Animated.createAnimatedComponent(Circle); +var PATH = 'M14.1 27.2l7.1 7.2 16.7-16.8'; +export var AnimatedCheck = React.forwardRef(function AnimatedCheck(_a, ref) { + var playOnMount = _a.playOnMount, props = __rest(_a, ["playOnMount"]); + var _b = useCommonSVGProps(props), fill = _b.fill, size = _b.size, style = _b.style, rest = __rest(_b, ["fill", "size", "style"]); + var circleAnim = useSharedValue(0); + var checkAnim = useSharedValue(0); + var circleAnimatedProps = useAnimatedProps(function () { return ({ + strokeDashoffset: 166 - circleAnim.get() * 166, + }); }); + var checkAnimatedProps = useAnimatedProps(function () { return ({ + strokeDashoffset: 48 - 48 * checkAnim.get(), + }); }); + var play = React.useCallback(function (cb) { + circleAnim.set(0); + checkAnim.set(0); + circleAnim.set(function () { + return withTiming(1, { duration: 500, easing: Easing.linear }); + }); + checkAnim.set(function () { + return withDelay(500, withTiming(1, { duration: 300, easing: Easing.linear }, cb)); + }); + }, [circleAnim, checkAnim]); + React.useImperativeHandle(ref, function () { return ({ + play: play, + }); }); + React.useEffect(function () { + if (playOnMount) { + play(); + } + }, [play, playOnMount]); + return (_jsxs(Svg, __assign({ fill: "none" }, rest, { viewBox: "0 0 52 52", width: size, height: size, style: style, children: [_jsx(AnimatedCircle, { animatedProps: circleAnimatedProps, cx: "26", cy: "26", r: "24", fill: "none", stroke: fill, strokeWidth: 4, strokeDasharray: 166 }), _jsx(AnimatedPath, { animatedProps: checkAnimatedProps, stroke: fill, d: PATH, strokeWidth: 4, strokeDasharray: 48 })] }))); +}); diff --git a/src/components/contacts/FindContactsBannerNUX.js b/src/components/contacts/FindContactsBannerNUX.js new file mode 100644 index 0000000000..ffa4a640f5 --- /dev/null +++ b/src/components/contacts/FindContactsBannerNUX.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 { useMemo } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_10 } from '#/lib/constants'; +import { Nux, useNux, useSaveNux } from '#/state/queries/nuxs'; +import { atoms as a, useTheme } from '#/alf'; +import { Button } from '#/components/Button'; +import { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { IS_WEB } from '#/env'; +import { Link } from '../Link'; +import { useIsFindContactsFeatureEnabledBasedOnGeolocation } from './country-allowlist'; +export function FindContactsBannerNUX() { + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var _a = useInternalState(), visible = _a.visible, close = _a.close; + if (!visible) + return null; + return (_jsx(View, { style: [a.w_full, a.p_lg, a.border_b, t.atoms.border_contrast_low], children: _jsxs(View, { style: a.w_full, children: [_jsx(Link, { to: { screen: 'FindContactsFlow' }, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Import contacts to find your friends"], ["Import contacts to find your friends"])))), onPress: function () { + ax.metric('contacts:nux:bannerPressed', {}); + }, style: [ + a.w_full, + a.rounded_xl, + a.curve_continuous, + a.overflow_hidden, + ], children: _jsxs(LinearGradient, { colors: [t.palette.primary_200, t.palette.primary_50], start: { x: 0, y: 0.5 }, end: { x: 1, y: 0.5 }, style: [ + a.w_full, + a.h_full, + a.flex_row, + a.align_center, + a.gap_lg, + a.pl_lg, + ], children: [_jsx(Image, { source: require('../../../assets/images/find_friends_illustration_small.webp'), accessibilityIgnoresInvertColors: true, style: [ + { height: 70, aspectRatio: 573 / 286 }, + a.self_end, + a.mt_sm, + ] }), _jsx(View, { style: [a.flex_1, a.justify_center, a.py_xl, a.pr_5xl], children: _jsx(Text, { style: [ + a.text_md, + a.font_bold, + { color: t.palette.primary_900 }, + ], children: _jsx(Trans, { children: "Import contacts to find your friends" }) }) })] }) }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Dismiss banner"], ["Dismiss banner"])))), hitSlop: HITSLOP_10, onPress: close, style: [a.absolute, { top: 14, right: 14 }], hoverStyle: [a.bg_transparent, { opacity: 0.5 }], children: _jsx(XIcon, { size: "xs", style: [t.atoms.text_contrast_low] }) })] }) })); +} +function useInternalState() { + var ax = useAnalytics(); + var nux = useNux(Nux.FindContactsDismissibleBanner).nux; + var _a = useSaveNux(), save = _a.mutate, variables = _a.variables; + var hidden = !!variables; + var isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation(); + var visible = useMemo(function () { + if (IS_WEB) + return false; + if (hidden) + return false; + if (nux && nux.completed) + return false; + if (!isFeatureEnabled) + return false; + return true; + }, [hidden, nux, isFeatureEnabled]); + var close = function () { + save({ + id: Nux.FindContactsDismissibleBanner, + completed: true, + data: undefined, + }); + ax.metric('contacts:nux:bannerDismissed', {}); + }; + return { visible: visible, close: close }; +} +var templateObject_1, templateObject_2; diff --git a/src/components/contacts/FindContactsFlow.js b/src/components/contacts/FindContactsFlow.js new file mode 100644 index 0000000000..7e194c2fa5 --- /dev/null +++ b/src/components/contacts/FindContactsFlow.js @@ -0,0 +1,10 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { GetContacts } from './screens/GetContacts'; +import { PhoneInput } from './screens/PhoneInput'; +import { VerifyNumber } from './screens/VerifyNumber'; +import { ViewMatches } from './screens/ViewMatches'; +import { FindContactsGoBackContext } from './state'; +export function FindContactsFlow(_a) { + var state = _a.state, dispatch = _a.dispatch, onBack = _a.onBack, onCancel = _a.onCancel, _b = _a.context, context = _b === void 0 ? 'Standalone' : _b; + return (_jsxs(FindContactsGoBackContext, { value: onBack, children: [state.step === '1: phone input' && (_jsx(PhoneInput, { state: state, dispatch: dispatch, context: context, onSkip: onCancel })), state.step === '2: verify number' && (_jsx(VerifyNumber, { state: state, dispatch: dispatch, context: context, onSkip: onCancel })), state.step === '3: get contacts' && (_jsx(GetContacts, { state: state, dispatch: dispatch, onCancel: onCancel, context: context })), state.step === '4: view matches' && (_jsx(ViewMatches, { state: state, dispatch: dispatch, context: context, onNext: onCancel }))] })); +} diff --git a/src/components/contacts/FindContactsFlow.web.js b/src/components/contacts/FindContactsFlow.web.js new file mode 100644 index 0000000000..038aca3a50 --- /dev/null +++ b/src/components/contacts/FindContactsFlow.web.js @@ -0,0 +1,3 @@ +export function FindContactsFlow() { + throw new Error('FindContactsFlow is not available on web'); +} diff --git a/src/components/contacts/components/HeroImage.js b/src/components/contacts/components/HeroImage.js new file mode 100644 index 0000000000..6bd59dc13c --- /dev/null +++ b/src/components/contacts/components/HeroImage.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 { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useTheme } from '#/alf'; +export function ContactsHeroImage() { + var t = useTheme(); + var _ = useLingui()._; + return (_jsx(View, { style: [ + a.w_full, + a.pl_3xl, + a.pr_2xl, + a.pt_4xl, + a.pb_3xl, + a.rounded_lg, + { backgroundColor: t.palette.primary_50 }, + ], children: _jsx(Image, { source: require('../../../../assets/images/find_friends_illustration.webp'), accessibilityIgnoresInvertColors: true, style: [a.w_full, { aspectRatio: 1278 / 661 }], alt: _(msg(templateObject_1 || (templateObject_1 = __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"])))) }) })); +} +var templateObject_1; diff --git a/src/components/contacts/components/InviteInfo.js b/src/components/contacts/components/InviteInfo.js new file mode 100644 index 0000000000..4db6138a6f --- /dev/null +++ b/src/components/contacts/components/InviteInfo.js @@ -0,0 +1,21 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { HITSLOP_20 } from '#/lib/constants'; +import { android, atoms as a } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { CircleInfo_Stroke2_Corner0_Rounded as InfoIcon } from '#/components/icons/CircleInfo'; +import { Text } from '#/components/Typography'; +export function InviteInfo(_a) { + var iconStyle = _a.iconStyle, _b = _a.iconOffset, iconOffset = _b === void 0 ? 0 : _b; + var _ = useLingui()._; + var control = Dialog.useDialogControl(); + var style = [a.text_md, a.leading_snug, a.mt_xs]; + return (_jsxs(_Fragment, { children: [_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Learn more about how inviting friends works"], ["Learn more about how inviting friends works"])))), onPress: control.open, hitSlop: HITSLOP_20, style: android({ transform: [{ translateY: iconOffset }] }), children: _jsx(InfoIcon, { style: iconStyle, size: "sm" }) }), _jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Invite Friends"], ["Invite Friends"])))), children: [_jsx(Text, { style: [a.text_2xl, a.font_bold], children: _jsx(Trans, { children: "Invite Friends" }) }), _jsx(Text, { style: style, children: _jsx(Trans, { children: "It looks like some of your contacts have not tried to find you here yet. You can personally invite them by customizing a draft message we will provide." }) }), _jsx(Text, { style: [style, a.font_medium, a.mt_lg], children: _jsx(Trans, { children: "How it works:" }) }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "Choose who to invite" })] }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "Personalize the message" })] }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "Send the message from your phone" })] }), _jsxs(Text, { style: style, children: ["\u2022", ' ', _jsx(Trans, { children: "We don't store your friends' phone numbers or send any messages" })] }), _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Done"], ["Done"])))), onPress: function () { return control.close(); }, size: "large", color: "primary", style: [a.mt_2xl], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Done" }) }) })] })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/contacts/components/OTPInput.js b/src/components/contacts/components/OTPInput.js new file mode 100644 index 0000000000..f193c00011 --- /dev/null +++ b/src/components/contacts/components/OTPInput.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; +}; +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 { useRef, useState } from 'react'; +import { Pressable, TextInput, View, } from 'react-native'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { mergeRefs } from '#/lib/merge-refs'; +import { atoms as a, ios, platform, useTheme } from '#/alf'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Text } from '#/components/Typography'; +import { IS_ANDROID, IS_IOS } from '#/env'; +export function OTPInput(_a) { + var label = _a.label, value = _a.value, onChange = _a.onChange, ref = _a.ref, _b = _a.numberOfDigits, numberOfDigits = _b === void 0 ? 6 : _b, onComplete = _a.onComplete; + var t = useTheme(); + var _ = useLingui()._; + var innerRef = useRef(null); + var _c = useInteractionState(), focused = _c.state, onFocus = _c.onIn, onBlur = _c.onOut; + var _d = useState({ start: 0, end: 0 }), selection = _d[0], setSelection = _d[1]; + var onChangeText = function (text) { + var _a; + // only numbers + text = text.replace(/[^0-9]/g, ''); + text = text.slice(0, numberOfDigits); + onChange(text); + if (text.length === numberOfDigits) { + onComplete === null || onComplete === void 0 ? void 0 : onComplete(text); + (_a = innerRef.current) === null || _a === void 0 ? void 0 : _a.blur(); + } + }; + var onSelectionChange = function (evt) { + setSelection(evt.nativeEvent.selection); + }; + return (_jsxs(Pressable, { accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Focus code input"], ["Focus code input"])))), accessibilityRole: "button", accessibilityHint: "", style: [a.w_full, a.relative], onPress: function () { + var _a, _b; + (_a = innerRef.current) === null || _a === void 0 ? void 0 : _a.focus(); + (_b = innerRef.current) === null || _b === void 0 ? void 0 : _b.clear(); + }, children: [_jsx(View, { style: [a.w_full, a.flex_row, a.gap_sm], children: __spreadArray([], value.padEnd(numberOfDigits, ' '), true).map(function (digit, index) { + var selected = focused + ? selection.start === selection.end + ? selection.start === index + : index >= selection.start && index < selection.end + : false; + return (_jsx(View, { style: [ + a.flex_1, + a.align_center, + a.justify_center, + t.atoms.bg_contrast_50, + { + height: 64, + borderWidth: 1, + borderRadius: 10, + borderColor: selected + ? t.palette.primary_500 + : t.atoms.bg_contrast_50.backgroundColor, + }, + ], children: _jsx(Text, { style: [a.text_2xl, a.text_center, a.font_bold], children: digit }) }, index)); + }) }), _jsx(TextInput + // SMS autofill is borked on iOS if you open the keyboard immediately -sfn + , { + // SMS autofill is borked on iOS if you open the keyboard immediately -sfn + onLayout: ios(function () { return setTimeout(function () { var _a; return (_a = innerRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 100); }), autoFocus: IS_ANDROID, accessible: true, accessibilityLabel: label, accessibilityHint: "", accessibilityRole: "text", ref: mergeRefs(ref ? [ref, innerRef] : [innerRef]), value: value, onChangeText: onChangeText, onSelectionChange: onSelectionChange, keyboardAppearance: t.scheme, inputMode: "numeric", keyboardType: "number-pad", textContentType: "oneTimeCode", autoComplete: platform({ + android: 'sms-otp', + ios: 'one-time-code', + }), onFocus: onFocus, onBlur: onBlur, maxLength: numberOfDigits, style: [ + a.absolute, + a.inset_0, + // roughly vibe align the characters + // with the visible ones so that + // moving the caret via long press + // still kinda sorta works + { + fontVariant: ['tabular-nums'], + textAlignVertical: 'center', + letterSpacing: 24, + fontSize: 60, + paddingLeft: 6, + }, + platform({ + // completely transparent inputs on iOS cannot be pasted into + ios: { opacity: 0.02, color: 'transparent' }, + android: { opacity: 0 }, + }), + ], caretHidden: IS_IOS, clearTextOnFocus: true })] })); +} +var templateObject_1; diff --git a/src/components/contacts/contacts.js b/src/components/contacts/contacts.js new file mode 100644 index 0000000000..79a90004f7 --- /dev/null +++ b/src/components/contacts/contacts.js @@ -0,0 +1,71 @@ +import { normalizePhoneNumber } from './phone-number'; +/** + * Filters out contacts that do not have any associated phone numbers, + * as well as businesses + */ +export function contactsWithPhoneNumbersOnly(contacts) { + return contacts.filter(function (contact) { + return contact.phoneNumbers && + contact.phoneNumbers.length > 0 && + contact.contactType !== 'company'; + }); +} +/** + * Takes the raw contact book and returns a plain list of numbers in E.164 format, along + * with a mapping to retrieve the contact ID when we get the results back. + * + * `countryCode` is used as a fallback for local numbers that don't have a country code associated with them. + * I'm making the assumption that most local numbers in someone's phone book will be the same as theirs. + */ +export function normalizeContactBook(contacts, countryCode, ownNumber) { + var _a; + var phoneNumbers = []; + var indexToContactId = new Map(); + for (var _i = 0, contacts_1 = contacts; _i < contacts_1.length; _i++) { + var contact = contacts_1[_i]; + for (var _b = 0, _c = (_a = contact.phoneNumbers) !== null && _a !== void 0 ? _a : []; _b < _c.length; _b++) { + var number = _c[_b]; + var rawNumber = void 0; + if (number.number) { + rawNumber = number.number; + } + else if (number.digits) { + rawNumber = number.digits; + } + else { + continue; + } + var normalized = normalizePhoneNumber(rawNumber, number.countryCode, countryCode); + if (normalized === null) + continue; + // skip if it's your own number + if (normalized === ownNumber) + continue; + phoneNumbers.push(normalized); + indexToContactId.set(phoneNumbers.length - 1, contact.id); + } + } + return { + phoneNumbers: phoneNumbers, + indexToContactId: indexToContactId, + }; +} +export function filterMatchedNumbers(contacts, results, mapping) { + var filteredIds = new Set(); + for (var _i = 0, results_1 = results; _i < results_1.length; _i++) { + var result = results_1[_i]; + var id = mapping.get(result.contactIndex); + if (id !== undefined) { + filteredIds.add(id); + } + } + return contacts.filter(function (contact) { return !filteredIds.has(contact.id); }); +} +export function getMatchedContacts(contacts, results, mapping) { + var contactsById = new Map(contacts.map(function (c) { return [c.id, c]; })); + return results.map(function (result) { + var id = mapping.get(result.contactIndex); + var contact = id !== undefined ? contactsById.get(id) : undefined; + return { profile: result.match, contact: contact }; + }); +} diff --git a/src/components/contacts/country-allowlist.js b/src/components/contacts/country-allowlist.js new file mode 100644 index 0000000000..5c587dcbc6 --- /dev/null +++ b/src/components/contacts/country-allowlist.js @@ -0,0 +1,33 @@ +import { IS_DEV } from '#/env'; +import { useGeolocation } from '#/geolocation'; +var FIND_CONTACTS_FEATURE_COUNTRY_ALLOWLIST = [ + 'US', + 'GB', + 'JP', + 'CA', + 'DE', + 'FR', + 'ES', + 'BR', + 'KR', + 'NL', + 'AU', + 'SE', + 'IT', +]; +export function isFindContactsFeatureEnabled(countryCode) { + if (IS_DEV) + return true; + /* + * This should never happen unless geolocation fails entirely. In that + * case, let the user try, since it should work as long as they have a + * phone number from one of the allow-listed countries. + */ + if (!countryCode) + return true; + return FIND_CONTACTS_FEATURE_COUNTRY_ALLOWLIST.includes(countryCode.toUpperCase()); +} +export function useIsFindContactsFeatureEnabledBasedOnGeolocation() { + var location = useGeolocation(); + return isFindContactsFeatureEnabled(location.countryCode); +} diff --git a/src/components/contacts/phone-number.js b/src/components/contacts/phone-number.js new file mode 100644 index 0000000000..7a8887b92d --- /dev/null +++ b/src/components/contacts/phone-number.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 { t } from '@lingui/macro'; +import { isSupportedCountry, ParseError, parsePhoneNumber, parsePhoneNumberWithError, } from 'libphonenumber-js/max'; +/** + * Intended for after the user has finished inputting their phone number. + */ +export function processPhoneNumber(number, country) { + try { + var phoneNumber = parsePhoneNumberWithError(number, { + defaultCountry: country, + }); + if (!phoneNumber.isValid()) { + return { valid: false, reason: t(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Invalid phone number"], ["Invalid phone number"]))) }; + } + var type = phoneNumber.getType(); + if (type !== 'MOBILE' && + type !== 'FIXED_LINE_OR_MOBILE' && + type !== 'PERSONAL_NUMBER') { + return { + valid: false, + reason: t(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Number should be a mobile number"], ["Number should be a mobile number"]))), + }; + } + var countryCode = country; + if (phoneNumber.country && phoneNumber.country !== country) { + if (phoneNumber.country === 'AC' || phoneNumber.country === 'TA') { + countryCode = 'SH'; + } + else { + countryCode = phoneNumber.country; + } + } + return { + valid: true, + formatted: formatE164lWithoutCountryCode(phoneNumber), + countryCode: countryCode, + }; + } + catch (error) { + if (error instanceof ParseError) { + return { valid: false, reason: error.message }; + } + else { + return { valid: false }; + } + } +} +/** + * Format a phone number as the international format with the prefix + * removed. + */ +function formatE164lWithoutCountryCode(phoneNumber) { + var intl = phoneNumber.format('E.164'); + var prefix = '+' + phoneNumber.countryCallingCode; + return intl.replace(prefix, '').trim(); +} +/** + * Takes a country code and a prefix-less phone number and constructs a full phone number. + * + * Does not have nice error handling - if you're unsure if the number is valid, use + * `processPhoneNumber` instead + */ +export function constructFullPhoneNumber(countryCode, phoneNumber) { + var result = parsePhoneNumber(phoneNumber, { defaultCountry: countryCode }); + if (!result.isValid()) + throw new Error('Invalid phone number passed to constructFullPhoneNumber'); + return result.format('E.164'); +} +/** + * Takes a phone number and applies human-readable formatting. Do not sent to the API - they + * expect E.164 format. + */ +export function prettyPhoneNumber(phoneNumber) { + var result = parsePhoneNumber(phoneNumber); + return result.formatNational(); +} +/** + * Attempts to parse a phone number from a string, and returns the country code + * and the rest of the number if possible. If the number is invalid, returns undefined. + */ +export function getCountryCodeFromPastedNumber(text) { + try { + var phoneNumber = parsePhoneNumber(text); + if (!phoneNumber.isValid()) { + return undefined; + } + var countryCode = phoneNumber.country; + // we don't have AC and TA in our dropdown - see `#/lib/international-telephone-codes` + if (countryCode && countryCode !== 'AC' && countryCode !== 'TA') { + return { + countryCode: countryCode, + rest: formatE164lWithoutCountryCode(phoneNumber), + }; + } + else { + return undefined; + } + } + catch (error) { + return undefined; + } +} +/** + * Normalizes a phone number into E.164 format + */ +export function normalizePhoneNumber(rawNumber, countryCode, fallbackCountryCode) { + try { + var result = parsePhoneNumber(rawNumber, { + defaultCountry: countryCode && isSupportedCountry(countryCode) + ? countryCode + : fallbackCountryCode, + }); + if (!result.isValid()) + return null; + var type = result.getType(); + if (type !== 'MOBILE' && + type !== 'FIXED_LINE_OR_MOBILE' && + type !== 'PERSONAL_NUMBER') { + return null; + } + return result.format('E.164'); + } + catch (error) { + console.log('Failed to normalize phone number:', error); + return null; + } +} +var templateObject_1, templateObject_2; diff --git a/src/components/contacts/screens/GetContacts.js b/src/components/contacts/screens/GetContacts.js new file mode 100644 index 0000000000..8c5e77dc01 --- /dev/null +++ b/src/components/contacts/screens/GetContacts.js @@ -0,0 +1,295 @@ +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 { useContext } from 'react'; +import { Alert, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import * as Contacts from 'expo-contacts'; +import { AppBskyContactImportContacts, } from '@atproto/api'; +import { msg, t, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { uploadBlob } from '#/lib/api'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { findContactsStatusQueryKey } from '#/state/queries/find-contacts'; +import { useAgent } from '#/state/session'; +import { Context as OnboardingContext, } from '#/screens/Onboarding/state'; +import { atoms as a, ios, tokens, useGutters } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { contactsWithPhoneNumbersOnly, filterMatchedNumbers, getMatchedContacts, normalizeContactBook, } from '../contacts'; +import { constructFullPhoneNumber } from '../phone-number'; +var MAX_UPLOAD_COUNT = 1000; +export function GetContacts(_a) { + var _this = this; + var state = _a.state, dispatch = _a.dispatch, onCancel = _a.onCancel, context = _a.context; + var _ = useLingui()._; + var ax = useAnalytics(); + var agent = useAgent(); + var insets = useSafeAreaInsets(); + var gutters = useGutters([0, 'wide']); + var queryClient = useQueryClient(); + var maybeOnboardingContext = useContext(OnboardingContext); + var _b = useMutation({ + mutationFn: function (contacts) { return __awaiter(_this, void 0, void 0, function () { + var error_1, _a, phoneNumbers, indexToContactId, res; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!(context === 'Onboarding' && maybeOnboardingContext)) return [3 /*break*/, 4]; + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, createProfileRecord(agent, maybeOnboardingContext)]; + case 2: + _b.sent(); + return [3 /*break*/, 4]; + case 3: + error_1 = _b.sent(); + logger.debug('Error creating profile record:', { safeMessage: error_1 }); + return [3 /*break*/, 4]; + case 4: + _a = normalizeContactBook(contacts, state.phoneCountryCode, constructFullPhoneNumber(state.phoneCountryCode, state.phoneNumber)), phoneNumbers = _a.phoneNumbers, indexToContactId = _a.indexToContactId; + if (!(phoneNumbers.length > 0)) return [3 /*break*/, 6]; + return [4 /*yield*/, agent.app.bsky.contact.importContacts({ + token: state.token, + contacts: phoneNumbers.slice(0, MAX_UPLOAD_COUNT), + })]; + case 5: + res = _b.sent(); + return [2 /*return*/, { + matches: res.data.matchesAndContactIndexes, + indexToContactId: indexToContactId, + }]; + case 6: return [2 /*return*/, { + matches: [], + indexToContactId: indexToContactId, + }]; + } + }); + }); }, + onSuccess: function (result, contacts) { + if (context === 'Onboarding') { + ax.metric('onboarding:contacts:contactsShared', {}); + } + if (result.matches.length > 0) { + ax.metric('contacts:import:success', { + contactCount: contacts.length, + matchCount: result.matches.length, + entryPoint: context, + }); + } + else { + ax.metric('contacts:import:failure', { + reason: 'noValidNumbers', + entryPoint: context, + }); + } + dispatch({ + type: 'SYNC_CONTACTS_SUCCESS', + payload: { + matches: getMatchedContacts(contacts, result.matches, result.indexToContactId), + contacts: filterMatchedNumbers(contacts, result.matches, result.indexToContactId), + }, + }); + queryClient.invalidateQueries({ + queryKey: findContactsStatusQueryKey, + }); + }, + onError: function (err) { + ax.metric('contacts:import:failure', { + reason: isNetworkError(err) ? 'networkError' : 'unknown', + entryPoint: context, + }); + if (isNetworkError(err)) { + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["There was a problem with your internet connection, please try again"], ["There was a problem with your internet connection, please try again"])))), { type: 'error' }); + } + else if (err instanceof AppBskyContactImportContacts.TooManyContactsError) { + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Too many contacts - you've exceeded the number of contacts you can import to find your friends"], ["Too many contacts - you've exceeded the number of contacts you can import to find your friends"])))), { type: 'error' }); + } + else if (err instanceof AppBskyContactImportContacts.InvalidTokenError) { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Could not upload contacts. You need to re-verify your phone number to proceed"], ["Could not upload contacts. You need to re-verify your phone number to proceed"])))), { type: 'error' }); + } + else { + logger.error('Error uploading contacts', { safeMessage: err }); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Could not upload contacts. ", ""], ["Could not upload contacts. ", ""])), cleanError(err))), { + type: 'error', + }); + } + }, + }), uploadContacts = _b.mutate, isUploadPending = _b.isPending; + var _c = useMutation({ + mutationFn: function () { return __awaiter(_this, void 0, void 0, function () { + var permissions, contacts; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, Contacts.getPermissionsAsync()]; + case 1: + permissions = _a.sent(); + if (!(!permissions.granted && permissions.canAskAgain)) return [3 /*break*/, 3]; + return [4 /*yield*/, Contacts.requestPermissionsAsync()]; + case 2: + permissions = _a.sent(); + _a.label = 3; + case 3: + ax.metric('contacts:permission:request', { + status: permissions.granted ? 'granted' : 'denied', + accessLevelIOS: ios(permissions.accessPrivileges), + }); + if (!permissions.granted) { + throw new PermissionDeniedError(); + } + return [4 /*yield*/, Contacts.getContactsAsync({ + fields: [ + Contacts.Fields.FirstName, + Contacts.Fields.LastName, + Contacts.Fields.PhoneNumbers, + Contacts.Fields.Image, + ], + })]; + case 4: + contacts = _a.sent(); + return [2 /*return*/, contactsWithPhoneNumbersOnly(contacts.data)]; + } + }); + }); }, + onSuccess: function (contacts) { + dispatch({ + type: 'GET_CONTACTS_SUCCESS', + payload: { contacts: contacts }, + }); + uploadContacts(contacts); + }, + onError: function (err) { + if (err instanceof PermissionDeniedError) { + showPermissionDeniedAlert(); + } + else { + logger.error('Error getting contacts', { safeMessage: err }); + } + }, + }), getContacts = _c.mutate, isGetContactsPending = _c.isPending; + var isPending = isUploadPending || isGetContactsPending; + var style = [a.text_md, a.leading_snug, a.mt_md]; + return (_jsxs(View, { style: [a.h_full], children: [_jsxs(Layout.Content, { contentContainerStyle: [gutters, a.flex_1, a.pt_xl], bounces: false, children: [_jsx(Text, { style: [a.font_bold, a.text_3xl], children: _jsx(Trans, { children: "Share your contacts to find friends" }) }), _jsx(Text, { style: style, children: _jsx(Trans, { children: "Bluesky helps friends find each other by creating an encoded digital fingerprint, called a \"hash\", and then looking for matching hashes." }) }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "We never keep plain phone numbers" })] }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "We delete hashes after matches are made" })] }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "We only suggest follows if both people consent" })] }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "You can always opt out and delete your data" })] }), _jsx(Text, { style: [style, a.mt_lg], children: _jsx(Trans, { children: "We apply the highest privacy standards, and never share or sell your contact information." }) })] }), _jsxs(View, { style: [ + gutters, + a.pt_xs, + { paddingBottom: Math.max(insets.bottom, tokens.space.xl) }, + a.gap_md, + ], children: [_jsx(Text, { style: [a.text_sm, a.pb_xs], children: _jsx(Trans, { children: "I consent to Bluesky using my contacts for mutual friend discovery and to retain hashed data for matching until I opt out." }) }), _jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Find my friends"], ["Find my friends"])))), size: "large", color: "primary", onPress: function () { return getContacts(); }, disabled: isPending, children: isUploadPending ? (_jsxs(_Fragment, { children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Finding friends..." }) }), _jsx(ButtonIcon, { icon: Loader })] })) : (_jsx(ButtonText, { children: _jsx(Trans, { children: "Find my friends" }) })) }), _jsx(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Cancel"], ["Cancel"])))), size: "large", color: "secondary", onPress: onCancel, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] })] })); +} +var PermissionDeniedError = /** @class */ (function (_super) { + __extends(PermissionDeniedError, _super); + function PermissionDeniedError() { + return _super.call(this, 'Permission denied') || this; + } + return PermissionDeniedError; +}(Error)); +function showPermissionDeniedAlert() { + Alert.alert(t(templateObject_7 || (templateObject_7 = __makeTemplateObject(["You've denied access to your contacts"], ["You've denied access to your contacts"]))), t(templateObject_8 || (templateObject_8 = __makeTemplateObject(["You'll need to go to the System Settings for Bluesky and give permission if you want to use this feature."], ["You'll need to go to the System Settings for Bluesky and give permission if you want to use this feature."]))), [ + { + text: t(templateObject_9 || (templateObject_9 = __makeTemplateObject(["OK"], ["OK"]))), + style: 'default', + }, + ]); +} +/** + * Copied from `#/screens/Onboarding/StepFinished/index.tsx` + */ +function createProfileRecord(agent, onboardingContext) { + return __awaiter(this, void 0, void 0, function () { + var profileStepResults, imageUri, imageMime, blobPromise; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + profileStepResults = onboardingContext.state.profileStepResults; + imageUri = profileStepResults.imageUri, imageMime = profileStepResults.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: + next.displayName = ''; + next.createdAt = new Date().toISOString(); + return [2 /*return*/, next]; + } + }); + }); })]; + 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/components/contacts/screens/PhoneInput.js b/src/components/contacts/screens/PhoneInput.js new file mode 100644 index 0000000000..6b8d79b921 --- /dev/null +++ b/src/components/contacts/screens/PhoneInput.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 __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 { Keyboard, View } from 'react-native'; +import { KeyboardAvoidingView } from 'react-native-keyboard-controller'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { AppBskyContactStartPhoneVerification } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation } from '@tanstack/react-query'; +import { urls } from '#/lib/constants'; +import { getDefaultCountry, } from '#/lib/international-telephone-codes'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { OnboardingPosition } from '#/screens/Onboarding/Layout'; +import { android, atoms as a, platform, tokens, useGutters, useTheme, } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as TextField from '#/components/forms/TextField'; +import { InternationalPhoneCodeSelect } from '#/components/InternationalPhoneCodeSelect'; +import * as Layout from '#/components/Layout'; +import { InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { useGeolocation } from '#/geolocation'; +import { isFindContactsFeatureEnabled } from '../country-allowlist'; +import { constructFullPhoneNumber, getCountryCodeFromPastedNumber, processPhoneNumber, } from '../phone-number'; +import { useOnPressBackButton } from '../state'; +export function PhoneInput(_a) { + var _this = this; + var _b; + var state = _a.state, dispatch = _a.dispatch, context = _a.context, onSkip = _a.onSkip; + var _ = useLingui()._; + var ax = useAnalytics(); + var t = useTheme(); + var agent = useAgent(); + var location = useGeolocation(); + var _c = useState(function () { var _a; return (_a = state.phoneCountryCode) !== null && _a !== void 0 ? _a : getDefaultCountry(location); }), countryCode = _c[0], setCountryCode = _c[1]; + var _d = useState((_b = state.phoneNumber) !== null && _b !== void 0 ? _b : ''), phoneNumber = _d[0], setPhoneNumber = _d[1]; + var gutters = useGutters([0, 'wide']); + var insets = useSafeAreaInsets(); + // for API/generic errors + var _e = useState(''), error = _e[0], setError = _e[1]; + // for issues with parsing the number + var _f = useState(''), formatError = _f[0], setFormatError = _f[1]; + var _g = useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var phoneCountryCode = _b.phoneCountryCode, phoneNumber = _b.phoneNumber; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + // sends a onetime code to the user's phone number + return [4 /*yield*/, agent.app.bsky.contact.startPhoneVerification({ + phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber), + })]; + case 1: + // sends a onetime code to the user's phone number + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function (_data, _a) { + var phoneCountryCode = _a.phoneCountryCode, phoneNumber = _a.phoneNumber; + dispatch({ + type: 'SUBMIT_PHONE_NUMBER', + payload: { phoneCountryCode: phoneCountryCode, phoneNumber: phoneNumber }, + }); + ax.metric('contacts:phone:phoneEntered', { entryPoint: context }); + }, + onMutate: function () { + Keyboard.dismiss(); + setError(''); + setFormatError(''); + }, + onError: function (err) { + if (isNetworkError(err)) { + setError(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["A network error occurred. Please check your internet connection"], ["A network error occurred. Please check your internet connection"]))))); + } + else if (err instanceof + AppBskyContactStartPhoneVerification.RateLimitExceededError) { + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Rate limit exceeded. Please try again later."], ["Rate limit exceeded. Please try again later."]))))); + } + else if (err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError) { + setError(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["The verification provider was unable to send a code to your phone number. Please check your phone number and try again."], ["The verification provider was unable to send a code to your phone number. Please check your phone number and try again."]))))); + } + else { + logger.error('Verify phone number failed', { safeMessage: err }); + setError(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["An error occurred. ", ""], ["An error occurred. ", ""])), cleanError(err)))); + } + }, + }), submit = _g.mutate, isPending = _g.isPending; + var isFeatureEnabled = isFindContactsFeatureEnabled(countryCode); + var onSubmitNumber = function () { + var _a; + if (!isFeatureEnabled) + return; + if (!phoneNumber) + return; + var result = processPhoneNumber(phoneNumber, countryCode); + if (result.valid) { + setPhoneNumber(result.formatted); + setCountryCode(result.countryCode); + if (!isFindContactsFeatureEnabled(result.countryCode)) + return; + submit({ + phoneCountryCode: result.countryCode, + phoneNumber: result.formatted, + }); + } + else { + setFormatError((_a = result.reason) !== null && _a !== void 0 ? _a : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Invalid phone number"], ["Invalid phone number"]))))); + } + }; + var paddingBottom = Math.max(insets.bottom, tokens.space.xl); + var onPressBack = useOnPressBackButton(); + return (_jsxs(View, { style: [a.h_full], children: [_jsxs(Layout.Header.Outer, { noBottomBorder: true, children: [_jsx(Layout.Header.BackButton, { onPress: onPressBack }), _jsx(Layout.Header.Content, {}), context === 'Onboarding' ? (_jsx(Button, { size: "small", color: "secondary", variant: "ghost", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Skip contact sharing and continue to the app"], ["Skip contact sharing and continue to the app"])))), onPress: onSkip, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Skip" }) }) })) : (_jsx(Layout.Header.Slot, {}))] }), _jsxs(Layout.Content, { contentContainerStyle: [gutters, a.pt_sm, a.flex_1], keyboardShouldPersistTaps: "handled", children: [context === 'Onboarding' && _jsx(OnboardingPosition, {}), _jsx(Text, { style: [a.font_bold, a.text_3xl], children: _jsx(Trans, { children: "Verify phone number" }) }), _jsx(Text, { style: [ + a.text_md, + t.atoms.text_contrast_medium, + a.leading_snug, + a.mt_sm, + ], children: _jsx(Trans, { children: "We need to verify your number before we can look for your friends. A verification code will be sent to this number." }) }), _jsxs(View, { style: [a.mt_2xl], children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Phone number" }) }), _jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center], children: [_jsx(View, { children: _jsx(InternationalPhoneCodeSelect, { value: countryCode, onChange: function (value) { return setCountryCode(value); } }) }), _jsx(View, { style: [a.flex_1], children: _jsx(TextField.Root, { isInvalid: !!formatError || !isFeatureEnabled, children: _jsx(TextField.Input, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Phone number"], ["Phone number"])))), value: phoneNumber, onChangeText: function (text) { + if (formatError) + setFormatError(''); + if (Math.abs(text.length - phoneNumber.length) > 1) { + // possibly pasted/autocompleted? auto-switch + // country code if possible + var result = getCountryCodeFromPastedNumber(text); + if (result) { + setCountryCode(result.countryCode); + setPhoneNumber(result.rest); + return; + } + } + setPhoneNumber(text); + }, placeholder: null, keyboardType: platform({ + ios: 'number-pad', + android: 'phone-pad', + }), autoComplete: "tel", returnKeyType: android('next'), onSubmitEditing: onSubmitNumber }) }) })] })] }), !isFeatureEnabled && (_jsx(ErrorText, { children: _jsx(Trans, { children: "Support for this feature in your country has not been enabled yet! Please check back later." }) })), error && _jsx(ErrorText, { children: error }), formatError && _jsx(ErrorText, { children: formatError }), _jsx(View, { style: [a.mt_auto, a.py_xl], children: _jsx(LegalDisclaimer, {}) })] }), _jsx(KeyboardAvoidingView, { behavior: "padding", keyboardVerticalOffset: insets.top - paddingBottom + tokens.space.xl, children: _jsx(View, { style: [gutters, { paddingBottom: paddingBottom }], children: _jsxs(Button, { disabled: !phoneNumber || isPending, label: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Send code"], ["Send code"])))), size: "large", color: "primary", onPress: onSubmitNumber, children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Send code" }) }), isPending && _jsx(ButtonIcon, { icon: Loader })] }) }) })] })); +} +function LegalDisclaimer() { + var t = useTheme(); + var _ = useLingui()._; + var style = [a.text_xs, t.atoms.text_contrast_medium, a.leading_snug]; + return (_jsxs(View, { style: [a.gap_xs], children: [_jsx(Text, { style: [style, a.font_medium], children: _jsx(Trans, { children: "How we use your number:" }) }), _jsxs(Text, { style: style, children: ["\u2022", ' ', _jsx(Trans, { children: "Sent to our phone number verification provider Plivo" })] }), _jsxs(Text, { style: style, children: ["\u2022 ", _jsx(Trans, { children: "Deleted by Plivo after verification" })] }), _jsxs(Text, { style: style, children: ["\u2022", ' ', _jsx(Trans, { children: "Held by Bluesky for 7 days to prevent abuse, then deleted" })] }), _jsxs(Text, { style: style, children: ["\u2022", ' ', _jsx(Trans, { children: "Stored as part of a secure code for matching with others" })] }), _jsx(Text, { style: [style, a.mt_xs], children: _jsxs(Trans, { children: ["By continuing, you consent to this use. You may change your mind any time by visiting settings.", ' ', _jsx(InlineLinkText, { to: urls.website.support.findFriendsPrivacyPolicy, label: _(msg({ + message: "Learn more about importing contacts", + context: "english-only-resource", + })), style: [a.text_xs, a.leading_snug], children: _jsx(Trans, { context: "english-only-resource", children: "Learn more" }) })] }) })] })); +} +function ErrorText(_a) { + var children = _a.children; + var t = useTheme(); + return (_jsx(Text, { style: [ + a.text_md, + { color: t.palette.negative_500 }, + a.leading_snug, + a.mt_md, + ], children: children })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/components/contacts/screens/VerifyNumber.js b/src/components/contacts/screens/VerifyNumber.js new file mode 100644 index 0000000000..115548015b --- /dev/null +++ b/src/components/contacts/screens/VerifyNumber.js @@ -0,0 +1,270 @@ +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 { useEffect, useMemo, useState } from 'react'; +import { Text as NestedText, View } from 'react-native'; +import { AppBskyContactStartPhoneVerification, AppBskyContactVerifyPhone, } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation } from '@tanstack/react-query'; +import { clamp } from '#/lib/numbers'; +import { cleanError, isNetworkError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { useAgent } from '#/state/session'; +import { OnboardingPosition } from '#/screens/Onboarding/Layout'; +import { atoms as a, useGutters, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon } from '#/components/icons/ArrowRotate'; +import { CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon } from '#/components/icons/CircleCheck'; +import { Warning_Stroke2_Corner0_Rounded as WarningIcon } from '#/components/icons/Warning'; +import * as Layout from '#/components/Layout'; +import { Loader } from '#/components/Loader'; +import * as Toast from '#/components/Toast'; +import { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +import { OTPInput } from '../components/OTPInput'; +import { constructFullPhoneNumber, prettyPhoneNumber } from '../phone-number'; +import { useOnPressBackButton } from '../state'; +export function VerifyNumber(_a) { + var _this = this; + var state = _a.state, dispatch = _a.dispatch, context = _a.context, onSkip = _a.onSkip; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var agent = useAgent(); + var gutters = useGutters([0, 'wide']); + var _b = useState(''), otpCode = _b[0], setOtpCode = _b[1]; + var _c = useState(null), error = _c[0], setError = _c[1]; + var _d = useState(otpCode), prevOtpCode = _d[0], setPrevOtpCode = _d[1]; + if (otpCode !== prevOtpCode) { + setPrevOtpCode(otpCode); + setError(null); + } + var phone = useMemo(function () { return constructFullPhoneNumber(state.phoneCountryCode, state.phoneNumber); }, [state.phoneCountryCode, state.phoneNumber]); + var prettyNumber = useMemo(function () { return prettyPhoneNumber(phone); }, [phone]); + var _e = useMutation({ + mutationFn: function (code) { 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.contact.verifyPhone({ code: code, phone: phone })]; + case 1: + res = _a.sent(); + return [2 /*return*/, res.data.token]; + } + }); + }); }, + onSuccess: function (token) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + // let the success state show for a moment + setTimeout(function () { + dispatch({ + type: 'VERIFY_PHONE_NUMBER_SUCCESS', + payload: { + token: token, + }, + }); + }, 1000); + ax.metric('contacts:phone:phoneVerified', { entryPoint: context }); + return [2 /*return*/]; + }); + }); }, + onMutate: function () { return setError(null); }, + onError: function (err) { + setOtpCode(''); + if (isNetworkError(err)) { + setError({ + retryable: true, + isResendError: false, + message: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["A network error occurred. Please check your internet connection."], ["A network error occurred. Please check your internet connection."])))), + }); + } + else if (err instanceof AppBskyContactVerifyPhone.InvalidCodeError) { + setError({ + retryable: true, + isResendError: true, + message: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["This code is invalid. Resend to get a new code."], ["This code is invalid. Resend to get a new code."])))), + }); + } + else if (err instanceof AppBskyContactVerifyPhone.InvalidPhoneError) { + setError({ + retryable: false, + isResendError: false, + message: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["The verification provider was unable to send a code to your phone number. Please check your phone number and try again."], ["The verification provider was unable to send a code to your phone number. Please check your phone number and try again."])))), + }); + } + else if (err instanceof AppBskyContactVerifyPhone.RateLimitExceededError) { + setError({ + retryable: true, + isResendError: false, + message: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Too many attempts. Please wait a few minutes and try again."], ["Too many attempts. Please wait a few minutes and try again."])))), + }); + } + else { + logger.error('Verify phone number failed', { safeMessage: err }); + setError({ + retryable: true, + isResendError: false, + message: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["An error occurred. ", ""], ["An error occurred. ", ""])), cleanError(err))), + }); + } + }, + }), verifyNumber = _e.mutate, isPending = _e.isPending, isSuccess = _e.isSuccess; + var _f = 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.startPhoneVerification({ phone: phone })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: function () { + dispatch({ type: 'RESEND_VERIFICATION_CODE' }); + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["A new code has been sent"], ["A new code has been sent"]))))); + }, + onMutate: function () { + setOtpCode(''); + setError(null); + }, + onError: function (err) { + if (isNetworkError(err)) { + setError({ + retryable: true, + isResendError: true, + message: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["A network error occurred. Please check your internet connection."], ["A network error occurred. Please check your internet connection."])))), + }); + } + else if (err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError) { + setError({ + retryable: false, + isResendError: true, + message: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["The verification provider was unable to send a code to your phone number. Please check your phone number and try again."], ["The verification provider was unable to send a code to your phone number. Please check your phone number and try again."])))), + }); + } + else if (err instanceof + AppBskyContactStartPhoneVerification.RateLimitExceededError) { + setError({ + retryable: true, + isResendError: true, + message: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Too many codes sent. Please wait a few minutes and try again."], ["Too many codes sent. Please wait a few minutes and try again."])))), + }); + } + else { + logger.error('Resend failed', { safeMessage: err }); + setError({ + retryable: true, + isResendError: true, + message: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["An error occurred. ", ""], ["An error occurred. ", ""])), cleanError(err))), + }); + } + }, + }), resendCode = _f.mutate, isResendingCode = _f.isPending; + var onPressBack = useOnPressBackButton(); + return (_jsxs(View, { style: [a.h_full], children: [_jsxs(Layout.Header.Outer, { noBottomBorder: true, children: [_jsx(Layout.Header.BackButton, { onPress: onPressBack }), _jsx(Layout.Header.Content, {}), context === 'Onboarding' ? (_jsx(Button, { size: "small", color: "secondary", variant: "ghost", label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Skip contact sharing and continue to the app"], ["Skip contact sharing and continue to the app"])))), onPress: onSkip, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Skip" }) }) })) : (_jsx(Layout.Header.Slot, {}))] }), _jsxs(Layout.Content, { contentContainerStyle: [gutters, a.pt_sm, a.flex_1], keyboardShouldPersistTaps: "always", children: [context === 'Onboarding' && _jsx(OnboardingPosition, {}), _jsx(Text, { style: [a.font_bold, a.text_3xl], children: _jsx(Trans, { children: "Verify phone number" }) }), _jsx(Text, { style: [ + a.text_md, + t.atoms.text_contrast_medium, + a.leading_snug, + a.mt_sm, + ], children: _jsxs(Trans, { children: ["Enter the 6-digit code sent to ", prettyNumber] }) }), _jsx(View, { style: [a.mt_2xl], children: _jsx(OTPInput, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Enter 6-digit code that was sent to your phone number"], ["Enter 6-digit code that was sent to your phone number"])))), value: otpCode, onChange: setOtpCode, onComplete: function (code) { return verifyNumber(code); } }) }), _jsx(View, { style: [a.mt_sm], children: _jsx(OTPStatus, { error: error, isPending: isPending, isResendingCode: isResendingCode, isSuccess: isSuccess, onResend: function () { return resendCode(); }, onRetry: function () { return verifyNumber(otpCode); }, lastCodeSentAt: state.lastSentAt }) })] })] })); +} +/** + * Horrible component that takes all the state above and figures out what messages + * and buttons to display. + */ +function OTPStatus(_a) { + var _b; + var error = _a.error, isPending = _a.isPending, isResendingCode = _a.isResendingCode, isSuccess = _a.isSuccess, onResend = _a.onResend, onRetry = _a.onRetry, lastCodeSentAt = _a.lastCodeSentAt; + var _ = useLingui()._; + var t = useTheme(); + var _c = useState(Date.now()), time = _c[0], setTime = _c[1]; + useEffect(function () { + var interval = setInterval(function () { + setTime(Date.now()); + }, 1000); + return function () { return clearInterval(interval); }; + }, []); + var timeUntilCanResend = Math.max(0, 30000 - (time - ((_b = lastCodeSentAt === null || lastCodeSentAt === void 0 ? void 0 : lastCodeSentAt.getTime()) !== null && _b !== void 0 ? _b : 0))); + var isWaiting = timeUntilCanResend > 0; + var Icon = null; + var text = ''; + var textColor = t.atoms.text_contrast_medium.color; + var showResendButton = false; + var showRetryButton = false; + if (isSuccess) { + Icon = CircleCheckIcon; + text = _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Phone number verified"], ["Phone number verified"])))); + textColor = t.palette.positive_500; + } + else if (isPending) { + text = _(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Verifying..."], ["Verifying..."])))); + } + else if (error) { + Icon = WarningIcon; + text = error.message; + textColor = t.palette.negative_500; + if (error.retryable) { + if (error.isResendError) { + showResendButton = true; + } + else { + showRetryButton = true; + } + } + } + else { + showResendButton = true; + } + return (_jsxs(View, { style: [a.w_full, a.align_center], children: [text && (_jsxs(View, { style: [ + a.gap_xs, + a.flex_row, + a.align_center, + (isSuccess || isPending) && a.mt_lg, + ], children: [Icon && _jsx(Icon, { size: "xs", style: { color: textColor } }), _jsx(Text, { style: [ + { color: textColor }, + a.text_sm, + a.leading_snug, + a.text_center, + ], children: text })] })), showRetryButton && (_jsxs(Button, { size: "small", color: "secondary_inverted", label: _(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Retry"], ["Retry"])))), onPress: onRetry, style: [a.mt_2xl], children: [_jsx(ButtonIcon, { icon: RetryIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) })] })), showResendButton && (_jsxs(Button, { size: "large", color: "secondary", variant: "ghost", label: _(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Resend code"], ["Resend code"])))), disabled: isResendingCode || isWaiting, onPress: onResend, style: [a.mt_2xl], children: [isResendingCode && _jsx(ButtonIcon, { icon: Loader }), _jsx(ButtonText, { children: isWaiting ? (_jsxs(Trans, { children: ["Resend code in", ' ', _jsxs(NestedText, { style: { fontVariant: ['tabular-nums'] }, children: ["00:", String(clamp(Math.round(timeUntilCanResend / 1000), 0, 30)).padStart(2, '0')] })] })) : (_jsx(Trans, { children: "Resend code" })) })] }))] })); +} +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/contacts/screens/ViewMatches.js b/src/components/contacts/screens/ViewMatches.js new file mode 100644 index 0000000000..a84cdde542 --- /dev/null +++ b/src/components/contacts/screens/ViewMatches.js @@ -0,0 +1,427 @@ +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, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import * as SMS from 'expo-sms'; +import { msg, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { wait } from '#/lib/async/wait'; +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 { optimisticRemoveMatch, useMatchesPassthroughQuery, } from '#/state/queries/find-contacts'; +import { useAgent, useSession } from '#/state/session'; +import { List } from '#/view/com/util/List'; +import { UserAvatar } from '#/view/com/util/UserAvatar'; +import { OnboardingPosition } from '#/screens/Onboarding/Layout'; +import { bulkWriteFollows } from '#/screens/Onboarding/util'; +import { atoms as a, tokens, useGutters, useTheme } from '#/alf'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { SearchInput } from '#/components/forms/SearchInput'; +import { useInteractionState } from '#/components/hooks/useInteractionState'; +import { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { MagnifyingGlassX_Stroke2_Corner0_Rounded_Large as SearchFailedIcon } from '#/components/icons/MagnifyingGlass'; +import { PersonX_Stroke2_Corner0_Rounded_Large as PersonXIcon } from '#/components/icons/Person'; +import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus'; +import { TimesLarge_Stroke2_Corner0_Rounded as XIcon } from '#/components/icons/Times'; +import * as Layout from '#/components/Layout'; +import { ListFooter } from '#/components/Lists'; +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 { InviteInfo } from '../components/InviteInfo'; +export function ViewMatches(_a) { + var _this = this; + var _b, _c; + var state = _a.state, dispatch = _a.dispatch, context = _a.context, onNext = _a.onNext; + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var gutter = useGutters([0, 'wide']); + var moderationOpts = useModerationOpts(); + var queryClient = useQueryClient(); + var agent = useAgent(); + var insets = useSafeAreaInsets(); + var listRef = useRef(null); + var _d = useState(''), search = _d[0], setSearch = _d[1]; + var _e = useInteractionState(), searchFocused = _e.state, onFocus = _e.onIn, onBlur = _e.onOut; + // HACK: Although we already have the match data, we need to pass it through + // a query to get it into the shadow state + var allMatches = useMatchesPassthroughQuery(state.matches); + var matches = allMatches.filter(function (match) { return !state.dismissedMatches.includes(match.profile.did); }); + var followableDids = matches.map(function (match) { return match.profile.did; }); + var _f = useState(followableDids.length === 0), didFollowAll = _f[0], setDidFollowAll = _f[1]; + var cumulativeFollowCount = useRef(0); + var onFollow = useCallback(function () { + ax.metric('contacts:matches:follow', { entryPoint: context }); + cumulativeFollowCount.current += 1; + }, [ax, context]); + var _g = useMutation({ + 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(500, 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]; + } + }); + }); }, + onMutate: function () { + return ax.metric('contacts:matches:followAll', { + followCount: followableDids.length, + entryPoint: context, + }); + }, + onSuccess: function () { + setDidFollowAll(true); + Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["All friends followed!"], ["All friends followed!"])))), { type: 'success' }); + cumulativeFollowCount.current += followableDids.length; + }, + onError: function (_err) { + Toast.show(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to follow all your friends, please try again"], ["Failed to follow all your friends, please try again"])))), { + type: 'error', + }); + for (var _i = 0, followableDids_3 = followableDids; _i < followableDids_3.length; _i++) { + var did = followableDids_3[_i]; + updateProfileShadow(queryClient, did, { + followingUri: undefined, + }); + } + }, + }), followAll = _g.mutate, isFollowingAll = _g.isPending; + var items = useMemo(function () { + var _a; + var all = []; + if (searchFocused || search.length > 0) { + for (var _i = 0, matches_1 = matches; _i < matches_1.length; _i++) { + var match = matches_1[_i]; + if (search.length === 0 || + ((_a = match.profile.displayName) !== null && _a !== void 0 ? _a : '') + .toLocaleLowerCase() + .includes(search.toLocaleLowerCase()) || + match.profile.handle + .toLocaleLowerCase() + .includes(search.toLocaleLowerCase())) { + all.push({ type: 'match', match: match }); + } + } + for (var _b = 0, _c = state.contacts; _b < _c.length; _b++) { + var contact = _c[_b]; + if (search.length === 0 || + [contact.firstName, contact.lastName] + .filter(Boolean) + .join(' ') + .toLocaleLowerCase() + .includes(search.toLocaleLowerCase())) { + all.push({ type: 'contact', contact: contact }); + } + } + if (all.length === 0) { + all.push({ type: 'search empty state', query: search }); + } + } + else { + if (matches.length > 0) { + all.push({ type: 'matches header', count: matches.length }); + for (var _d = 0, matches_2 = matches; _d < matches_2.length; _d++) { + var match = matches_2[_d]; + all.push({ type: 'match', match: match }); + } + if (state.contacts.length > 0) { + all.push({ type: 'contacts header' }); + } + } + else if (state.contacts.length > 0) { + all.push({ type: 'no matches header' }); + } + for (var _e = 0, _f = state.contacts; _e < _f.length; _e++) { + var contact = _f[_e]; + all.push({ type: 'contact', contact: contact }); + } + if (all.length === 0) { + all.push({ type: 'totally empty state' }); + } + } + return all; + }, [matches, state.contacts, search, searchFocused]); + 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) { + ax.metric('contacts:matches:dismiss', { entryPoint: context }); + dispatch({ type: 'DISMISS_MATCH', payload: { did: did } }); + }, + onSuccess: function (_res, did) { + // for the other screen + optimisticRemoveMatch(queryClient, did); + }, + onError: function (err, did) { + dispatch({ type: 'DISMISS_MATCH_FAILED', payload: { did: did } }); + if (isNetworkError(err)) { + Toast.show(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to hide suggestion, please check your internet connection"], ["Failed to hide suggestion, please check your internet connection"])))), { type: 'error' }); + } + else { + logger.error('Dismissing match failed', { safeMessage: err }); + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["An error occurred while hiding suggestion. ", ""], ["An error occurred while hiding suggestion. ", ""])), cleanError(err))), { type: 'error' }); + } + }, + }).mutate; + var renderItem = function (_a) { + var item = _a.item; + switch (item.type) { + case 'match': + return (_jsx(MatchItem, { profile: item.match.profile, contact: item.match.contact, moderationOpts: moderationOpts, onRemoveSuggestion: dismissMatch, onFollow: onFollow })); + case 'contact': + return _jsx(ContactItem, { contact: item.contact, context: context }); + case 'matches header': + return (_jsx(Header, { titleText: _jsx(Plural, { value: item.count, one: "# friend found!", other: "# friends found!" }), children: item.count > 1 && (_jsxs(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Follow all"], ["Follow all"])))), size: "small", color: "primary_subtle", onPress: function () { return followAll(); }, disabled: isFollowingAll || didFollowAll, children: [_jsx(ButtonIcon, { icon: isFollowingAll + ? Loader + : !didFollowAll + ? PlusIcon + : CheckIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Follow all" }) })] })) })); + case 'contacts header': + return (_jsx(Header, { titleText: _jsxs(Trans, { children: ["Invite friends", ' ', _jsx(InviteInfo, { iconStyle: t.atoms.text, iconOffset: 1 })] }), hasContentAbove: true })); + case 'no matches header': + return (_jsx(Header, { titleText: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["You got here first"], ["You got here first"])))), largeTitle: true, subtitleText: _jsxs(Trans, { children: ["Bluesky is more fun with friends. Do you want to invite some of yours?", ' ', _jsx(InviteInfo, { iconStyle: t.atoms.text_contrast_medium, iconOffset: 2 })] }) })); + case 'search empty state': + return _jsx(SearchEmptyState, { query: item.query }); + case 'totally empty state': + return _jsx(TotallyEmptyState, {}); + } + }; + var isSearchEmpty = ((_b = items === null || items === void 0 ? void 0 : items[0]) === null || _b === void 0 ? void 0 : _b.type) === 'search empty state'; + var isTotallyEmpty = ((_c = items === null || items === void 0 ? void 0 : items[0]) === null || _c === void 0 ? void 0 : _c.type) === 'totally empty state'; + var isEmpty = isSearchEmpty || isTotallyEmpty; + return (_jsxs(View, { style: [a.h_full], children: [context === 'Standalone' && (_jsxs(Layout.Header.Outer, { noBottomBorder: true, children: [_jsx(Layout.Header.BackButton, {}), _jsx(Layout.Header.Content, {}), _jsx(Layout.Header.Slot, {})] })), !isTotallyEmpty && (_jsxs(View, { style: [ + gutter, + a.mb_md, + context === 'Onboarding' && [a.mt_sm, a.gap_sm], + ], children: [context === 'Onboarding' && _jsx(OnboardingPosition, {}), _jsx(SearchInput, { placeholder: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Search contacts"], ["Search contacts"])))), value: search, onFocus: function () { + var _a; + onFocus(); + (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ offset: 0, animated: false }); + }, onBlur: function () { + var _a; + onBlur(); + (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ offset: 0, animated: false }); + }, onChangeText: function (text) { + var _a; + setSearch(text); + (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollToOffset({ offset: 0, animated: false }); + }, onClearText: function () { return setSearch(''); } })] })), _jsx(List, { ref: listRef, data: items, renderItem: renderItem, ListFooterComponent: !isEmpty ? _jsx(ListFooter, { height: 20 }) : null, keyExtractor: keyExtractor, keyboardDismissMode: "interactive", automaticallyAdjustKeyboardInsets: true }), _jsx(View, { style: [ + t.atoms.bg, + t.atoms.border_contrast_low, + a.border_t, + a.align_center, + a.align_stretch, + gutter, + a.pt_md, + { paddingBottom: insets.bottom + tokens.space.md }, + ], children: _jsx(Button, { label: context === 'Onboarding' ? _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Next"], ["Next"])))) : _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Done"], ["Done"])))), onPress: function () { + if (context === 'Onboarding') { + ax.metric('onboarding:contacts:nextPressed', { + matchCount: allMatches.length, + followCount: cumulativeFollowCount.current, + dismissedMatchCount: state.dismissedMatches.length, + }); + } + onNext(); + }, size: "large", color: "primary", children: _jsx(ButtonText, { children: context === 'Onboarding' ? (_jsx(Trans, { children: "Next" })) : (_jsx(Trans, { children: "Done" })) }) }) })] })); +} +function keyExtractor(item) { + switch (item.type) { + case 'contact': + return item.contact.id; + case 'match': + return item.match.profile.did; + default: + return item.type; + } +} +function MatchItem(_a) { + var _b; + var profile = _a.profile, contact = _a.contact, moderationOpts = _a.moderationOpts, onRemoveSuggestion = _a.onRemoveSuggestion, onFollow = _a.onFollow; + var gutter = useGutters([0, 'wide']); + var t = useTheme(); + var _ = useLingui()._; + var shadow = useProfileShadow(profile); + var contactName = useMemo(function () { + var _a, _b, _c, _d, _e; + if (!contact) + return null; + var name = (_b = (_a = contact.name) !== null && _a !== void 0 ? _a : contact.firstName) !== null && _b !== void 0 ? _b : contact.lastName; + if (name) + return _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Your contact ", ""], ["Your contact ", ""])), name)); + var phone = (_d = (_c = contact.phoneNumbers) === null || _c === void 0 ? void 0 : _c.find(function (p) { return p.isPrimary; })) !== null && _d !== void 0 ? _d : (_e = contact.phoneNumbers) === null || _e === void 0 ? void 0 : _e[0]; + if (phone === null || phone === void 0 ? void 0 : phone.number) + return phone.number; + return null; + }, [contact, _]); + if (!moderationOpts) + return null; + return (_jsx(View, { style: [gutter, a.py_md, a.border_t, t.atoms.border_contrast_low], children: _jsxs(ProfileCard.Header, { children: [_jsx(ProfileCard.Avatar, { profile: profile, moderationOpts: moderationOpts, size: 48 }), _jsxs(View, { style: [a.flex_1], children: [_jsx(ProfileCard.Name, { profile: profile, moderationOpts: moderationOpts, textStyle: [a.leading_tight] }), _jsx(ProfileCard.Handle, { profile: profile, textStyle: [contactName && a.text_xs] }), contactName && (_jsx(Text, { emoji: true, style: [a.leading_snug, t.atoms.text_contrast_medium, a.text_xs], numberOfLines: 1, children: contactName }))] }), _jsx(ProfileCard.FollowButton, { profile: profile, moderationOpts: moderationOpts, logContext: "FindContacts", onFollow: onFollow }), !((_b = shadow.viewer) === null || _b === void 0 ? void 0 : _b.following) && (_jsx(Button, { color: "secondary", variant: "ghost", label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Remove suggestion"], ["Remove suggestion"])))), onPress: function () { return onRemoveSuggestion(profile.did); }, hoverStyle: [a.bg_transparent, { opacity: 0.5 }], hitSlop: 8, children: _jsx(ButtonIcon, { icon: XIcon }) }))] }) })); +} +function ContactItem(_a) { + var _this = this; + var _b, _c, _d, _e, _f, _g; + var contact = _a.contact, context = _a.context; + var gutter = useGutters([0, 'wide']); + var t = useTheme(); + var _ = useLingui()._; + var ax = useAnalytics(); + var currentAccount = useSession().currentAccount; + var name = (_c = (_b = contact.name) !== null && _b !== void 0 ? _b : contact.firstName) !== null && _c !== void 0 ? _c : contact.lastName; + var phone = (_e = (_d = contact.phoneNumbers) === null || _d === void 0 ? void 0 : _d.find(function (phone) { return phone.isPrimary; })) !== null && _e !== void 0 ? _e : (_f = contact.phoneNumbers) === null || _f === void 0 ? void 0 : _f[0]; + var phoneNumber = phone === null || phone === void 0 ? void 0 : phone.number; + return (_jsx(View, { style: [gutter, a.py_md, a.border_t, t.atoms.border_contrast_low], children: _jsxs(ProfileCard.Header, { children: [contact.image ? (_jsx(UserAvatar, { size: 40, avatar: contact.image.uri, type: "user" })) : (_jsx(View, { style: [ + { width: 40, height: 40 }, + a.rounded_full, + a.justify_center, + a.align_center, + t.atoms.bg_contrast_400, + ], children: _jsx(Text, { style: [ + a.text_lg, + a.font_semi_bold, + { color: t.palette.contrast_0 }, + ], children: (_g = name === null || name === void 0 ? void 0 : name[0]) === null || _g === void 0 ? void 0 : _g.toLocaleUpperCase() }) })), _jsx(Text, { style: [ + a.flex_1, + a.text_md, + a.font_medium, + !name && [t.atoms.text_contrast_medium, a.italic], + ], numberOfLines: 2, children: name !== null && name !== void 0 ? name : _jsx(Trans, { children: "No name" }) }), phoneNumber && currentAccount && (_jsx(Button, { label: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Invite ", " to join Bluesky"], ["Invite ", " to join Bluesky"])), name)), color: "secondary", size: "small", onPress: function () { return __awaiter(_this, void 0, void 0, function () { + var err_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + ax.metric('contacts:matches:invite', { + entryPoint: context, + }); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, SMS.sendSMSAsync([phoneNumber], _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["I'm on Bluesky as ", " - come find me! https://bsky.app/download"], ["I'm on Bluesky as ", " - come find me! https://bsky.app/download"])), currentAccount.handle)))]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _a.sent(); + Toast.show(_(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Failed to launch SMS app"], ["Failed to launch SMS app"])))), { type: 'error' }); + logger.error('Could not launch SMS', { safeMessage: err_1 }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Invite" }) }) }))] }) })); +} +function Header(_a) { + var titleText = _a.titleText, largeTitle = _a.largeTitle, subtitleText = _a.subtitleText, children = _a.children, hasContentAbove = _a.hasContentAbove; + var gutter = useGutters([0, 'wide']); + var t = useTheme(); + return (_jsxs(View, { style: [ + gutter, + a.pb_md, + a.gap_sm, + hasContentAbove + ? [a.pt_4xl, a.border_t, t.atoms.border_contrast_low] + : a.pt_md, + ], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.justify_between], children: [_jsx(Text, { style: [largeTitle ? a.text_3xl : a.text_xl, a.font_bold], children: titleText }), children] }), subtitleText && (_jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium, a.leading_snug], children: subtitleText }))] })); +} +function SearchEmptyState(_a) { + var query = _a.query; + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_1, + a.flex_col, + a.align_center, + a.justify_center, + a.gap_lg, + a.pt_5xl, + a.px_5xl, + ], children: [_jsx(SearchFailedIcon, { width: 64, style: [t.atoms.text_contrast_low] }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + t.atoms.text_contrast_medium, + a.text_center, + ], children: _jsxs(Trans, { children: ["No contacts with the name \u201C", query, "\u201D found"] }) })] })); +} +function TotallyEmptyState() { + var t = useTheme(); + return (_jsxs(View, { style: [ + a.flex_1, + a.flex_col, + a.align_center, + a.justify_center, + a.gap_lg, + { paddingTop: 140 }, + a.px_5xl, + ], children: [_jsx(PersonXIcon, { width: 64, style: [t.atoms.text_contrast_low] }), _jsx(Text, { style: [a.text_xl, a.font_bold, a.leading_snug, a.text_center], children: _jsx(Trans, { children: "No contacts found" }) })] })); +} +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/components/contacts/state.js b/src/components/contacts/state.js new file mode 100644 index 0000000000..0e02bd21ef --- /dev/null +++ b/src/components/contacts/state.js @@ -0,0 +1,117 @@ +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 __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, useContext, useReducer } from 'react'; +function reducer(state, action) { + switch (action.type) { + case 'SUBMIT_PHONE_NUMBER': { + assertCurrentStep(state, '1: phone input'); + return __assign(__assign({ step: '2: verify number' }, action.payload), { lastSentAt: null }); + } + case 'RESEND_VERIFICATION_CODE': { + assertCurrentStep(state, '2: verify number'); + return __assign(__assign({}, state), { lastSentAt: new Date() }); + } + case 'VERIFY_PHONE_NUMBER_SUCCESS': { + assertCurrentStep(state, '2: verify number'); + return { + step: '3: get contacts', + token: action.payload.token, + phoneCountryCode: state.phoneCountryCode, + phoneNumber: state.phoneNumber, + }; + } + case 'BACK': { + assertCurrentStep(state, '2: verify number'); + return { + step: '1: phone input', + phoneNumber: state.phoneNumber, + phoneCountryCode: state.phoneCountryCode, + }; + } + case 'GET_CONTACTS_SUCCESS': { + assertCurrentStep(state, '3: get contacts'); + return __assign(__assign({}, state), { contacts: action.payload.contacts }); + } + case 'SYNC_CONTACTS_SUCCESS': { + assertCurrentStep(state, '3: get contacts'); + return { + step: '4: view matches', + contacts: action.payload.contacts, + matches: action.payload.matches, + dismissedMatches: [], + }; + } + case 'DISMISS_MATCH': { + assertCurrentStep(state, '4: view matches'); + return __assign(__assign({}, state), { dismissedMatches: __spreadArray(__spreadArray([], new Set(state.dismissedMatches), true), [ + action.payload.did, + ], false) }); + } + case 'DISMISS_MATCH_FAILED': { + assertCurrentStep(state, '4: view matches'); + return __assign(__assign({}, state), { dismissedMatches: state.dismissedMatches.filter(function (did) { return did !== action.payload.did; }) }); + } + } +} +var InvalidStateTransitionError = /** @class */ (function (_super) { + __extends(InvalidStateTransitionError, _super); + function InvalidStateTransitionError(message) { + var _this = _super.call(this, message) || this; + _this.name = 'InvalidStateTransitionError'; + return _this; + } + return InvalidStateTransitionError; +}(Error)); +function assertCurrentStep(state, step) { + if (state.step !== step) { + throw new InvalidStateTransitionError("Invalid state transition: expecting ".concat(step, ", got ").concat(state.step)); + } +} +export function useFindContactsFlowState(initialState) { + if (initialState === void 0) { initialState = { step: '1: phone input' }; } + return useReducer(reducer, initialState); +} +export var FindContactsGoBackContext = createContext(undefined); +export function useOnPressBackButton() { + var goBack = useContext(FindContactsGoBackContext); + if (!goBack) { + return undefined; + } + return function (evt) { + evt.preventDefault(); + goBack(); + }; +} diff --git a/src/components/dialogs/BirthDateSettings.js b/src/components/dialogs/BirthDateSettings.js new file mode 100644 index 0000000000..3183266e74 --- /dev/null +++ b/src/components/dialogs/BirthDateSettings.js @@ -0,0 +1,120 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { isAppPassword } from '#/lib/jwt'; +import { getAge, getDateAgo } from '#/lib/strings/time'; +import { logger } from '#/logger'; +import { useBirthdateMutation, useIsBirthdateUpdateAllowed, } from '#/state/birthdate'; +import { usePreferencesQuery, } from '#/state/queries/preferences'; +import { useSession } from '#/state/session'; +import { ErrorMessage } from '#/view/com/util/error/ErrorMessage'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { DateField } from '#/components/forms/DateField'; +import { SimpleInlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Span, Text } from '#/components/Typography'; +import { IS_IOS, IS_WEB } from '#/env'; +export function BirthDateSettingsDialog(_a) { + var control = _a.control; + var t = useTheme(); + var _ = useLingui()._; + var _b = usePreferencesQuery(), isLoading = _b.isLoading, error = _b.error, preferences = _b.data; + var isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed(); + var currentAccount = useSession().currentAccount; + var isUsingAppPassword = isAppPassword((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.accessJwt) || ''); + return (_jsxs(Dialog.Outer, { control: control, nativeOptions: { preventExpansion: true }, children: [_jsx(Dialog.Handle, {}), isBirthdateUpdateAllowed ? (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["My Birthdate"], ["My Birthdate"])))), style: web({ maxWidth: 400 }), children: [_jsxs(View, { style: [a.gap_md], children: [_jsx(Text, { style: [a.text_xl, a.font_semi_bold], children: _jsx(Trans, { children: "My Birthdate" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "This information is private and not shared with other users." }) }), isLoading ? (_jsx(Loader, { size: "xl" })) : error || !preferences ? (_jsx(ErrorMessage, { message: (error === null || error === void 0 ? void 0 : error.toString()) || + _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["We were unable to load your birthdate preferences. Please try again."], ["We were unable to load your birthdate preferences. Please try again."])))), style: [a.rounded_sm] })) : isUsingAppPassword ? (_jsx(Admonition, { type: "info", children: _jsxs(Trans, { children: ["Hmm, it looks like you're logged in with an", ' ', _jsx(Span, { style: [a.italic], children: "App Password" }), ". To set your birthdate, you'll need to log in with your main account password, or ask whomever controls this account to do so."] }) })) : (_jsx(BirthdayInner, { control: control, preferences: preferences }))] }), _jsx(Dialog.Close, {})] })) : (_jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You recently changed your birthdate"], ["You recently changed your birthdate"])))), style: web({ maxWidth: 400 }), children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [ + a.text_xl, + a.font_semi_bold, + a.leading_snug, + { paddingRight: 32 }, + ], children: _jsx(Trans, { children: "You recently changed your birthdate" }) }), _jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "There is a limit to how often you can change your birthdate. You may need to wait a day or two before updating it again." }) })] }), _jsx(Dialog.Close, {})] }))] })); +} +function BirthdayInner(_a) { + var _this = this; + var control = _a.control, preferences = _a.preferences; + var _ = useLingui()._; + var cleanError = useCleanError(); + var _b = React.useState(preferences.birthDate || getDateAgo(18)), date = _b[0], setDate = _b[1]; + var _c = useBirthdateMutation(), isPending = _c.isPending, error = _c.error, setBirthDate = _c.mutateAsync; + var hasChanged = date !== preferences.birthDate; + var errorMessage = React.useMemo(function () { + if (error) { + var _a = cleanError(error), raw = _a.raw, clean = _a.clean; + return clean || raw || error.toString(); + } + }, [error, cleanError]); + var age = getAge(new Date(date)); + var isUnder13 = age < 13; + var isUnder18 = age >= 13 && age < 18; + var onSave = 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, 3, , 4]); + if (!hasChanged) return [3 /*break*/, 2]; + return [4 /*yield*/, setBirthDate({ birthDate: date })]; + case 1: + _a.sent(); + _a.label = 2; + case 2: + control.close(); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + logger.error("setBirthDate failed", { message: e_1.message }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }, [date, setBirthDate, control, hasChanged]); + return (_jsxs(View, { style: a.gap_lg, testID: "birthDateSettingsDialog", children: [_jsx(View, { style: IS_IOS && [a.w_full, a.align_center], children: _jsx(DateField, { testID: "birthdayInput", value: date, onChangeDate: function (newDate) { return setDate(new Date(newDate)); }, label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Birthdate"], ["Birthdate"])))), accessibilityHint: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Enter your birthdate"], ["Enter your birthdate"])))) }) }), isUnder18 && hasChanged && (_jsx(Admonition, { type: "info", children: _jsx(Trans, { children: "The birthdate you've entered means you are under 18 years old. Certain content and features may be unavailable to you." }) })), isUnder13 && (_jsx(Admonition, { type: "error", children: _jsxs(Trans, { children: ["You must be at least 13 years old to use Bluesky. Read our", ' ', _jsx(SimpleInlineLinkText, { to: "https://bsky.social/about/support/tos", label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Terms of Service"], ["Terms of Service"])))), children: "Terms of Service" }), ' ', "for more information."] }) })), errorMessage ? (_jsx(ErrorMessage, { message: errorMessage, style: [a.rounded_sm] })) : undefined, _jsx(View, { style: IS_WEB && [a.flex_row, a.justify_end], children: _jsxs(Button, { label: hasChanged ? _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Save birthdate"], ["Save birthdate"])))) : _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Done"], ["Done"])))), size: "large", onPress: onSave, variant: "solid", color: "primary", disabled: isUnder13, children: [_jsx(ButtonText, { children: hasChanged ? _jsx(Trans, { children: "Save" }) : _jsx(Trans, { children: "Done" }) }), isPending && _jsx(ButtonIcon, { icon: Loader })] }) })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/src/components/dialogs/Context.js b/src/components/dialogs/Context.js new file mode 100644 index 0000000000..06b35f14ec --- /dev/null +++ b/src/components/dialogs/Context.js @@ -0,0 +1,53 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useContext, useMemo, useState } from 'react'; +import * as Dialog from '#/components/Dialog'; +var ControlsContext = createContext(null); +ControlsContext.displayName = 'GlobalDialogControlsContext'; +export function useGlobalDialogsControlContext() { + var ctx = useContext(ControlsContext); + if (!ctx) { + throw new Error('useGlobalDialogsControlContext must be used within a Provider'); + } + return ctx; +} +export function Provider(_a) { + var children = _a.children; + var mutedWordsDialogControl = Dialog.useDialogControl(); + var signinDialogControl = Dialog.useDialogControl(); + var inAppBrowserConsentControl = useStatefulDialogControl(); + var emailDialogControl = useStatefulDialogControl(); + var linkWarningDialogControl = useStatefulDialogControl(); + var ageAssuranceRedirectDialogControl = useStatefulDialogControl(); + var reportDialogControl = useStatefulDialogControl(); + var ctx = useMemo(function () { return ({ + mutedWordsDialogControl: mutedWordsDialogControl, + signinDialogControl: signinDialogControl, + inAppBrowserConsentControl: inAppBrowserConsentControl, + emailDialogControl: emailDialogControl, + linkWarningDialogControl: linkWarningDialogControl, + ageAssuranceRedirectDialogControl: ageAssuranceRedirectDialogControl, + reportDialogControl: reportDialogControl, + }); }, [ + mutedWordsDialogControl, + signinDialogControl, + inAppBrowserConsentControl, + emailDialogControl, + linkWarningDialogControl, + ageAssuranceRedirectDialogControl, + reportDialogControl, + ]); + return (_jsx(ControlsContext.Provider, { value: ctx, children: children })); +} +export function useStatefulDialogControl(initialValue) { + var _a = useState(initialValue), value = _a[0], setValue = _a[1]; + var control = Dialog.useDialogControl(); + return useMemo(function () { return ({ + control: control, + open: function (v) { + setValue(v); + control.open(); + }, + clear: function () { return setValue(initialValue); }, + value: value, + }); }, [control, value, initialValue]); +} diff --git a/src/components/dialogs/DeviceLocationRequestDialog.js b/src/components/dialogs/DeviceLocationRequestDialog.js new file mode 100644 index 0000000000..6ff959371b --- /dev/null +++ b/src/components/dialogs/DeviceLocationRequestDialog.js @@ -0,0 +1,129 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { wait } from '#/lib/async/wait'; +import { isNetworkError, useCleanError } from '#/lib/hooks/useCleanError'; +import { logger } from '#/logger'; +import { atoms as a, useTheme, web } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import * as Dialog from '#/components/Dialog'; +import { PinLocation_Stroke2_Corner0_Rounded as LocationIcon } from '#/components/icons/PinLocation'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +import { IS_WEB } from '#/env'; +import { useRequestDeviceGeolocation } from '#/geolocation'; +export function DeviceLocationRequestDialog(_a) { + var control = _a.control, onLocationAcquired = _a.onLocationAcquired; + var _ = useLingui()._; + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Confirm your location"], ["Confirm your location"])))), style: [web({ maxWidth: 380 })], children: [_jsx(DeviceLocationRequestDialogInner, { onLocationAcquired: onLocationAcquired }), _jsx(Dialog.Close, {})] })] })); +} +function DeviceLocationRequestDialogInner(_a) { + var _this = this; + var onLocationAcquired = _a.onLocationAcquired; + var t = useTheme(); + var _ = useLingui()._; + var close = Dialog.useDialogContext().close; + var requestDeviceLocation = useRequestDeviceGeolocation(); + var cleanError = useCleanError(); + var _b = useState(false), isRequesting = _b[0], setIsRequesting = _b[1]; + var _c = useState(''), error = _c[0], setError = _c[1]; + var _d = useState(false), dialogDisabled = _d[0], setDialogDisabled = _d[1]; + var onPressConfirm = function () { return __awaiter(_this, void 0, void 0, function () { + var req, location_1, e_1, _a, clean, raw; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + setError(''); + setIsRequesting(true); + _b.label = 1; + case 1: + _b.trys.push([1, 3, 4, 5]); + return [4 /*yield*/, wait(1e3, requestDeviceLocation())]; + case 2: + req = _b.sent(); + if (req.granted) { + location_1 = req.location; + if (location_1 && location_1.countryCode) { + onLocationAcquired === null || onLocationAcquired === void 0 ? void 0 : onLocationAcquired({ + geolocation: location_1, + setDialogError: setError, + disableDialogAction: function () { return setDialogDisabled(true); }, + closeDialog: close, + }); + } + else { + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Failed to resolve location. Please try again."], ["Failed to resolve location. Please try again."]))))); + } + } + else { + setError(_(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Unable to access location. You'll need to visit your system settings to enable location services for Bluesky."], ["Unable to access location. You'll need to visit your system settings to enable location services for Bluesky."]))))); + } + return [3 /*break*/, 5]; + case 3: + e_1 = _b.sent(); + _a = cleanError(e_1), clean = _a.clean, raw = _a.raw; + setError(clean || raw || e_1.message); + if (!isNetworkError(e_1)) { + logger.error("blockedGeoOverlay: unexpected error", { + safeMessage: e_1.message, + }); + } + return [3 /*break*/, 5]; + case 4: + setIsRequesting(false); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(View, { style: [a.gap_md], children: [_jsx(Text, { style: [a.text_xl, a.font_bold], children: _jsx(Trans, { children: "Confirm your location" }) }), _jsxs(View, { style: [a.gap_sm, a.pb_xs], children: [_jsx(Text, { style: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Tap below to allow Bluesky to access your GPS location. We will then use that data to more accurately determine the content and features available in your region." }) }), _jsx(Text, { style: [ + a.text_md, + a.leading_snug, + t.atoms.text_contrast_medium, + a.pb_xs, + ], children: _jsx(Trans, { children: "Your location data is not tracked and does not leave your device." }) })] }), error && (_jsx(View, { style: [a.pb_xs], children: _jsx(Admonition, { type: "error", children: error }) })), _jsxs(View, { style: [a.gap_sm], children: [!dialogDisabled && (_jsxs(Button, { disabled: isRequesting, label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Allow location access"], ["Allow location access"])))), onPress: onPressConfirm, size: IS_WEB ? 'small' : 'large', color: "primary", children: [_jsx(ButtonIcon, { icon: isRequesting ? Loader : LocationIcon }), _jsx(ButtonText, { children: _jsx(Trans, { children: "Allow location access" }) })] })), !IS_WEB && (_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Cancel"], ["Cancel"])))), onPress: function () { return close(); }, size: IS_WEB ? 'small' : 'large', color: "secondary", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) }))] })] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/components/dialogs/EmailDialog/components/ResendEmailText.js b/src/components/dialogs/EmailDialog/components/ResendEmailText.js new file mode 100644 index 0000000000..ed90730eed --- /dev/null +++ b/src/components/dialogs/EmailDialog/components/ResendEmailText.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; +}; +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 { useState } from 'react'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { wait } from '#/lib/async/wait'; +import { atoms as a, useTheme } from '#/alf'; +import { CheckThick_Stroke2_Corner0_Rounded as Check } from '#/components/icons/Check'; +import { createStaticClick, InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Span, Text } from '#/components/Typography'; +export function ResendEmailText(_a) { + var _this = this; + var onPress = _a.onPress, style = _a.style; + var t = useTheme(); + var _ = useLingui()._; + var _b = useState(null), status = _b[0], setStatus = _b[1]; + var handleOnPress = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + setStatus('sending'); + _a.label = 1; + case 1: + _a.trys.push([1, , 3, 4]); + return [4 /*yield*/, wait(1000, onPress())]; + case 2: + _a.sent(); + setStatus('success'); + return [3 /*break*/, 4]; + case 3: + setTimeout(function () { + setStatus(null); + }, 1000); + return [7 /*endfinally*/]; + case 4: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(Text, { style: [a.italic, a.leading_snug, t.atoms.text_contrast_medium, style], children: [_jsxs(Trans, { children: ["Don't see an email?", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Resend"], ["Resend"])))) }, createStaticClick(function () { + handleOnPress(); + }), { children: "Click here to resend." }))] }), ' ', _jsx(Span, { style: { top: 1 }, children: status === 'sending' ? (_jsx(Loader, { size: "xs" })) : status === 'success' ? (_jsx(Check, { size: "xs", fill: t.palette.positive_500 })) : null })] })); +} +var templateObject_1; diff --git a/src/components/dialogs/EmailDialog/components/TokenField.js b/src/components/dialogs/EmailDialog/components/TokenField.js new file mode 100644 index 0000000000..645e2ed664 --- /dev/null +++ b/src/components/dialogs/EmailDialog/components/TokenField.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/macro'; +import { useLingui } from '@lingui/react'; +import * as TextField from '#/components/forms/TextField'; +import { Shield_Stroke2_Corner0_Rounded as Shield } from '#/components/icons/Shield'; +export function normalizeCode(value) { + var normalized = value.toUpperCase().replace(/[^A-Z2-7]/g, ''); + if (normalized.length <= 5) + return normalized; + return "".concat(normalized.slice(0, 5), "-").concat(normalized.slice(5)); +} +export function isValidCode(value) { + return Boolean(value && /^[A-Z2-7]{5}-[A-Z2-7]{5}$/.test(value)); +} +export function TokenField(_a) { + var value = _a.value, onChangeText = _a.onChangeText, onSubmitEditing = _a.onSubmitEditing; + var _ = useLingui()._; + var isInvalid = Boolean(value && value.length > 10 && !isValidCode(value)); + var handleOnChangeText = function (v) { + onChangeText === null || onChangeText === void 0 ? void 0 : onChangeText(normalizeCode(v)); + }; + return (_jsx(View, { children: _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: Shield }), _jsx(TextField.Input, { isInvalid: isInvalid, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Confirmation code"], ["Confirmation code"])))), placeholder: "XXXXX-XXXXX", value: value, onChangeText: handleOnChangeText, onSubmitEditing: onSubmitEditing })] }) })); +} +var templateObject_1; diff --git a/src/components/dialogs/EmailDialog/data/useAccountEmailState.js b/src/components/dialogs/EmailDialog/data/useAccountEmailState.js new file mode 100644 index 0000000000..68e77bdd62 --- /dev/null +++ b/src/components/dialogs/EmailDialog/data/useAccountEmailState.js @@ -0,0 +1,90 @@ +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, useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useAgent, useSessionApi } from '#/state/session'; +import { emitEmailVerified } from '#/components/dialogs/EmailDialog/events'; +export var accountEmailStateQueryKey = ['accountEmailState']; +export function useAccountEmailState() { + var _this = this; + var _a; + var agent = useAgent(); + var partialRefreshSession = useSessionApi().partialRefreshSession; + var _b = useState(!!((_a = agent.session) === null || _a === void 0 ? void 0 : _a.emailConfirmed)), prevIsEmailVerified = _b[0], setPrevEmailIsVerified = _b[1]; + var state = useMemo(function () { + var _a, _b; + return ({ + isEmailVerified: !!((_a = agent.session) === null || _a === void 0 ? void 0 : _a.emailConfirmed), + email2FAEnabled: !!((_b = agent.session) === null || _b === void 0 ? void 0 : _b.emailAuthFactor), + }); + }, [agent.session]); + /** + * Only here to refetch on focus, when necessary + */ + useQuery({ + enabled: !!agent.session, + /** + * Only refetch if the email verification s incomplete. + */ + refetchOnWindowFocus: !prevIsEmailVerified, + queryKey: accountEmailStateQueryKey, + queryFn: function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, partialRefreshSession()]; + case 1: + _a.sent(); + return [2 /*return*/, null]; + } + }); + }); }, + }); + /* + * This will emit `n` times for each instance of this hook. So the listeners + * all use `once` to prevent multiple handlers firing. + */ + useEffect(function () { + if (state.isEmailVerified && !prevIsEmailVerified) { + setPrevEmailIsVerified(true); + emitEmailVerified(); + } + else if (!state.isEmailVerified && prevIsEmailVerified) { + setPrevEmailIsVerified(false); + } + }, [state, prevIsEmailVerified]); + return state; +} diff --git a/src/components/dialogs/EmailDialog/data/useConfirmEmail.js b/src/components/dialogs/EmailDialog/data/useConfirmEmail.js new file mode 100644 index 0000000000..c7032c84aa --- /dev/null +++ b/src/components/dialogs/EmailDialog/data/useConfirmEmail.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, useSession } from '#/state/session'; +export function useConfirmEmail(_a) { + var _this = this; + var _b = _a === void 0 ? {} : _a, onSuccess = _b.onSuccess, onError = _b.onError; + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var token = _b.token; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email)) { + throw new Error('No email found for the current account'); + } + return [4 /*yield*/, agent.com.atproto.server.confirmEmail({ + email: currentAccount.email.trim(), + token: token.trim(), + }) + // will update session state at root of app + ]; + case 1: + _c.sent(); + // will update session state at root of app + return [4 /*yield*/, agent.resumeSession(agent.session)]; + case 2: + // will update session state at root of app + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + onSuccess: onSuccess, + onError: onError, + }); +} diff --git a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.js b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.js new file mode 100644 index 0000000000..d5c857c53a --- /dev/null +++ b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.js @@ -0,0 +1,71 @@ +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, useSession } from '#/state/session'; +export function useManageEmail2FA() { + var _this = this; + var agent = useAgent(); + var currentAccount = useSession().currentAccount; + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var enabled = _b.enabled, token = _b.token; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email)) { + throw new Error('No email found for the current account'); + } + return [4 /*yield*/, agent.com.atproto.server.updateEmail({ + email: currentAccount.email, + emailAuthFactor: enabled, + token: token, + }) + // will update session state at root of app + ]; + case 1: + _c.sent(); + // will update session state at root of app + return [4 /*yield*/, agent.resumeSession(agent.session)]; + case 2: + // will update session state at root of app + _c.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.js b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.js new file mode 100644 index 0000000000..29d7fddb2f --- /dev/null +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.js @@ -0,0 +1,52 @@ +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 useRequestEmailUpdate() { + var _this = this; + 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.com.atproto.server.requestEmailUpdate()]; + case 1: return [2 /*return*/, (_a.sent()).data]; + } + }); + }); }, + }); +} diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.js b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.js new file mode 100644 index 0000000000..9e97eb013c --- /dev/null +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.js @@ -0,0 +1,54 @@ +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 useRequestEmailVerification() { + var _this = this; + 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.com.atproto.server.requestEmailConfirmation()]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }, + }); +} diff --git a/src/components/dialogs/EmailDialog/data/useUpdateEmail.js b/src/components/dialogs/EmailDialog/data/useUpdateEmail.js new file mode 100644 index 0000000000..e0ae2790a0 --- /dev/null +++ b/src/components/dialogs/EmailDialog/data/useUpdateEmail.js @@ -0,0 +1,90 @@ +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'; +import { useRequestEmailUpdate } from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'; +function updateEmailAndRefreshSession(agent, email, token) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, agent.com.atproto.server.updateEmail({ email: email.trim(), token: token })]; + case 1: + _a.sent(); + return [4 /*yield*/, agent.resumeSession(agent.session)]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +export function useUpdateEmail() { + var _this = this; + var agent = useAgent(); + var requestEmailUpdate = useRequestEmailUpdate().mutateAsync; + return useMutation({ + mutationFn: function (_a) { return __awaiter(_this, [_a], void 0, function (_b) { + var tokenRequired; + var email = _b.email, token = _b.token; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!token) return [3 /*break*/, 2]; + return [4 /*yield*/, updateEmailAndRefreshSession(agent, email, token)]; + case 1: + _c.sent(); + return [2 /*return*/, { + status: 'success', + }]; + case 2: return [4 /*yield*/, requestEmailUpdate()]; + case 3: + tokenRequired = (_c.sent()).tokenRequired; + if (!tokenRequired) return [3 /*break*/, 4]; + return [2 /*return*/, { + status: 'tokenRequired', + }]; + case 4: return [4 /*yield*/, updateEmailAndRefreshSession(agent, email, token)]; + case 5: + _c.sent(); + return [2 /*return*/, { + status: 'success', + }]; + } + }); + }); }, + }); +} diff --git a/src/components/dialogs/EmailDialog/events.js b/src/components/dialogs/EmailDialog/events.js new file mode 100644 index 0000000000..a1afb191b4 --- /dev/null +++ b/src/components/dialogs/EmailDialog/events.js @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import EventEmitter from 'eventemitter3'; +var events = new EventEmitter(); +export function emitEmailVerified() { + events.emit('emailVerified'); +} +export function useOnEmailVerified(cb) { + useEffect(function () { + /* + * N.B. Use `once` here, since the event can fire multiple times for each + * instance of `useAccountEmailState` + */ + events.once('emailVerified', cb); + return function () { + events.off('emailVerified', cb); + }; + }, [cb]); +} diff --git a/src/components/dialogs/EmailDialog/index.js b/src/components/dialogs/EmailDialog/index.js new file mode 100644 index 0000000000..91e945cfb5 --- /dev/null +++ b/src/components/dialogs/EmailDialog/index.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, useState } from 'react'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { web } from '#/alf'; +import * as Dialog from '#/components/Dialog'; +import { useGlobalDialogsControlContext } from '#/components/dialogs/Context'; +import { useAccountEmailState } from '#/components/dialogs/EmailDialog/data/useAccountEmailState'; +import { Manage2FA } from '#/components/dialogs/EmailDialog/screens/Manage2FA'; +import { Update } from '#/components/dialogs/EmailDialog/screens/Update'; +import { VerificationReminder } from '#/components/dialogs/EmailDialog/screens/VerificationReminder'; +import { Verify } from '#/components/dialogs/EmailDialog/screens/Verify'; +import { ScreenID } from '#/components/dialogs/EmailDialog/types'; +export { ScreenID as EmailDialogScreenID } from '#/components/dialogs/EmailDialog/types'; +export function useEmailDialogControl() { + return useGlobalDialogsControlContext().emailDialogControl; +} +export function EmailDialog() { + var _ = useLingui()._; + var emailDialogControl = useEmailDialogControl(); + var isEmailVerified = useAccountEmailState().isEmailVerified; + var onClose = useCallback(function () { + var _a, _b, _c; + if (!isEmailVerified) { + if (((_a = emailDialogControl.value) === null || _a === void 0 ? void 0 : _a.id) === ScreenID.Verify) { + (_c = (_b = emailDialogControl.value).onCloseWithoutVerifying) === null || _c === void 0 ? void 0 : _c.call(_b); + } + } + emailDialogControl.clear(); + }, [isEmailVerified, emailDialogControl]); + return (_jsxs(Dialog.Outer, { control: emailDialogControl.control, onClose: onClose, children: [_jsx(Dialog.Handle, {}), _jsxs(Dialog.ScrollableInner, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Make adjustments to email settings for your account"], ["Make adjustments to email settings for your account"])))), style: web({ maxWidth: 400 }), children: [_jsx(Inner, { control: emailDialogControl }), _jsx(Dialog.Close, {})] })] })); +} +function Inner(_a) { + var control = _a.control; + var _b = useState(function () { return control.value; }), screen = _b[0], showScreen = _b[1]; + if (!screen) + return null; + switch (screen.id) { + case ScreenID.Update: { + return _jsx(Update, { config: screen, showScreen: showScreen }); + } + case ScreenID.Verify: { + return _jsx(Verify, { config: screen, showScreen: showScreen }); + } + case ScreenID.VerificationReminder: { + return _jsx(VerificationReminder, { config: screen, showScreen: showScreen }); + } + case ScreenID.Manage2FA: { + return _jsx(Manage2FA, { config: screen, showScreen: showScreen }); + } + default: { + return null; + } + } +} +var templateObject_1; diff --git a/src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.js b/src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.js new file mode 100644 index 0000000000..51f2b9e55a --- /dev/null +++ b/src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.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 { useReducer, useState } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { wait } from '#/lib/async/wait'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { logger } from '#/logger'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogContext } from '#/components/Dialog'; +import { ResendEmailText } from '#/components/dialogs/EmailDialog/components/ResendEmailText'; +import { isValidCode, TokenField, } from '#/components/dialogs/EmailDialog/components/TokenField'; +import { useManageEmail2FA } from '#/components/dialogs/EmailDialog/data/useManageEmail2FA'; +import { useRequestEmailUpdate } from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'; +import { Divider } from '#/components/Divider'; +import { Check_Stroke2_Corner0_Rounded as Check } from '#/components/icons/Check'; +import { Envelope_Stroke2_Corner0_Rounded as Envelope } from '#/components/icons/Envelope'; +import { createStaticClick, InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Span, Text } from '#/components/Typography'; +function reducer(state, action) { + switch (action.type) { + case 'setError': { + return __assign(__assign({}, state), { error: action.error, emailStatus: 'error', tokenStatus: 'error' }); + } + case 'setStep': { + return __assign(__assign({}, state), { error: '', step: action.step }); + } + case 'setEmailStatus': { + return __assign(__assign({}, state), { error: '', emailStatus: action.status }); + } + case 'setTokenStatus': { + return __assign(__assign({}, state), { error: '', tokenStatus: action.status }); + } + default: { + return state; + } + } +} +export function Disable() { + var _this = this; + var t = useTheme(); + var _ = useLingui()._; + var cleanError = useCleanError(); + var currentAccount = useSession().currentAccount; + var requestEmailUpdate = useRequestEmailUpdate().mutateAsync; + var manageEmail2FA = useManageEmail2FA().mutateAsync; + var control = useDialogContext(); + var _a = useState(''), token = _a[0], setToken = _a[1]; + var _b = useReducer(reducer, { + error: '', + step: 'email', + emailStatus: 'default', + tokenStatus: 'default', + }), state = _b[0], dispatch = _b[1]; + var handleSendEmail = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1, clean; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + dispatch({ type: 'setEmailStatus', status: 'pending' }); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, wait(1000, requestEmailUpdate())]; + case 2: + _a.sent(); + dispatch({ type: 'setEmailStatus', status: 'success' }); + setTimeout(function () { + dispatch({ type: 'setStep', step: 'token' }); + }, 1000); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + logger.error('Manage2FA: email update code request failed', { + safeMessage: e_1, + }); + clean = cleanError(e_1).clean; + dispatch({ + type: 'setError', + error: clean || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to send email, please try again."], ["Failed to send email, please try again."])))), + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var handleManageEmail2FA = function () { return __awaiter(_this, void 0, void 0, function () { + var e_2, clean; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!isValidCode(token)) { + dispatch({ + type: 'setError', + error: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please enter a valid code."], ["Please enter a valid code."])))), + }); + return [2 /*return*/]; + } + dispatch({ type: 'setTokenStatus', status: 'pending' }); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, wait(1000, manageEmail2FA({ enabled: false, token: token }))]; + case 2: + _a.sent(); + dispatch({ type: 'setTokenStatus', status: 'success' }); + setTimeout(function () { + control.close(); + }, 1000); + return [3 /*break*/, 4]; + case 3: + e_2 = _a.sent(); + logger.error('Manage2FA: disable email 2FA failed', { safeMessage: e_2 }); + clean = cleanError(e_2).clean; + dispatch({ + type: 'setError', + error: clean || _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to update email 2FA settings"], ["Failed to update email 2FA settings"])))), + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.text_xl, a.font_bold, a.leading_snug], children: _jsx(Trans, { children: "Disable email 2FA" }) }), state.step === 'email' ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["To disable your email 2FA method, please verify your access to", ' ', _jsx(Span, { style: [a.font_semi_bold], children: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email })] }) }), _jsxs(View, { style: [a.gap_lg, a.pt_sm], children: [state.error && _jsx(Admonition, { type: "error", children: state.error }), _jsxs(Button, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Send email"], ["Send email"])))), size: "large", variant: "solid", color: "primary", onPress: handleSendEmail, disabled: state.emailStatus === 'pending', children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Send email" }) }), _jsx(ButtonIcon, { icon: state.emailStatus === 'pending' + ? Loader + : state.emailStatus === 'success' + ? Check + : Envelope })] }), _jsx(Divider, {}), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Have a code?", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Enter code"], ["Enter code"])))) }, createStaticClick(function () { + dispatch({ type: 'setStep', step: 'token' }); + }), { children: "Click here." }))] }) })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["To disable your email 2FA method, please verify your access to", ' ', _jsx(Span, { style: [a.font_semi_bold], children: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.email })] }) }), _jsxs(View, { style: [a.gap_sm, a.py_sm], children: [_jsx(TokenField, { value: token, onChangeText: setToken, onSubmitEditing: handleManageEmail2FA }), _jsx(ResendEmailText, { onPress: handleSendEmail })] }), state.error && _jsx(Admonition, { type: "error", children: state.error }), _jsxs(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Disable 2FA"], ["Disable 2FA"])))), size: "large", variant: "solid", color: "primary", onPress: handleManageEmail2FA, disabled: !token || token.length !== 11 || state.tokenStatus === 'pending', children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Disable 2FA" }) }), state.tokenStatus === 'pending' ? (_jsx(ButtonIcon, { icon: Loader })) : state.tokenStatus === 'success' ? (_jsx(ButtonIcon, { icon: Check })) : null] })] }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6; diff --git a/src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.js b/src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.js new file mode 100644 index 0000000000..1ca0c4f5e9 --- /dev/null +++ b/src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.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 __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 { useReducer } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { wait } from '#/lib/async/wait'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { logger } from '#/logger'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { useDialogContext } from '#/components/Dialog'; +import { useManageEmail2FA } from '#/components/dialogs/EmailDialog/data/useManageEmail2FA'; +import { Check_Stroke2_Corner0_Rounded as Check } from '#/components/icons/Check'; +import { ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon } from '#/components/icons/Shield'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +function reducer(state, action) { + switch (action.type) { + case 'setError': { + return __assign(__assign({}, state), { error: action.error, status: 'error' }); + } + case 'setStatus': { + return __assign(__assign({}, state), { error: '', status: action.status }); + } + default: { + return state; + } + } +} +export function Enable() { + var _this = this; + var t = useTheme(); + var _ = useLingui()._; + var cleanError = useCleanError(); + var gtPhone = useBreakpoints().gtPhone; + var manageEmail2FA = useManageEmail2FA().mutateAsync; + var control = useDialogContext(); + var _a = useReducer(reducer, { + error: '', + status: 'default', + }), state = _a[0], dispatch = _a[1]; + var handleManageEmail2FA = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1, clean; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + dispatch({ type: 'setStatus', status: 'pending' }); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, wait(1000, manageEmail2FA({ enabled: true }))]; + case 2: + _a.sent(); + dispatch({ type: 'setStatus', status: 'success' }); + setTimeout(function () { + control.close(); + }, 1000); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + logger.error('Manage2FA: enable email 2FA failed', { safeMessage: e_1 }); + clean = cleanError(e_1).clean; + dispatch({ + type: 'setError', + error: clean || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to update email 2FA settings"], ["Failed to update email 2FA settings"])))), + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(View, { style: [a.gap_lg], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.text_xl, a.font_bold, a.leading_snug], children: _jsx(Trans, { children: "Enable email 2FA" }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Require an email code to sign in to your account." }) })] }), state.error && _jsx(Admonition, { type: "error", children: state.error }), _jsxs(View, { style: [a.gap_sm, gtPhone && [a.flex_row_reverse]], children: [_jsxs(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Enable"], ["Enable"])))), size: "large", variant: "solid", color: "primary", onPress: handleManageEmail2FA, disabled: state.status === 'pending', children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Enable" }) }), _jsx(ButtonIcon, { position: "right", icon: state.status === 'pending' + ? Loader + : state.status === 'success' + ? Check + : ShieldIcon })] }), _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Cancel"], ["Cancel"])))), size: "large", variant: "solid", color: "secondary", onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Cancel" }) }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/dialogs/EmailDialog/screens/Manage2FA/index.js b/src/components/dialogs/EmailDialog/screens/Manage2FA/index.js new file mode 100644 index 0000000000..164e941d86 --- /dev/null +++ b/src/components/dialogs/EmailDialog/screens/Manage2FA/index.js @@ -0,0 +1,57 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { useEffect, useState } from 'react'; +import { Trans } from '@lingui/macro'; +import { useAccountEmailState } from '#/components/dialogs/EmailDialog/data/useAccountEmailState'; +import { Disable } from '#/components/dialogs/EmailDialog/screens/Manage2FA/Disable'; +import { Enable } from '#/components/dialogs/EmailDialog/screens/Manage2FA/Enable'; +import { ScreenID, } from '#/components/dialogs/EmailDialog/types'; +export function Manage2FA(_a) { + var showScreen = _a.showScreen; + var _b = useAccountEmailState(), isEmailVerified = _b.isEmailVerified, email2FAEnabled = _b.email2FAEnabled; + var _c = useState(null), requestedAction = _c[0], setRequestedAction = _c[1]; + useEffect(function () { + if (!isEmailVerified) { + showScreen({ + id: ScreenID.Verify, + instructions: [ + _jsx(Trans, { children: "You need to verify your email address before you can enable email 2FA." }, "2fa"), + ], + onVerify: function () { + showScreen({ + id: ScreenID.Manage2FA, + }); + }, + }); + } + }, [isEmailVerified, showScreen]); + /* + * Wacky state handling so that once 2FA settings change, we don't show the + * wrong step of this form - esb + */ + if (email2FAEnabled) { + if (!requestedAction) { + setRequestedAction('disable'); + return _jsx(Disable, {}); + } + if (requestedAction === 'disable') { + return _jsx(Disable, {}); + } + if (requestedAction === 'enable') { + return _jsx(Enable, {}); + } + } + else { + if (!requestedAction) { + setRequestedAction('enable'); + return _jsx(Enable, {}); + } + if (requestedAction === 'disable') { + return _jsx(Disable, {}); + } + if (requestedAction === 'enable') { + return _jsx(Enable, {}); + } + } + // should never happen + return null; +} diff --git a/src/components/dialogs/EmailDialog/screens/Update.js b/src/components/dialogs/EmailDialog/screens/Update.js new file mode 100644 index 0000000000..f3012d6da3 --- /dev/null +++ b/src/components/dialogs/EmailDialog/screens/Update.js @@ -0,0 +1,214 @@ +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 { useReducer } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { validate as validateEmail } from 'email-validator'; +import { wait } from '#/lib/async/wait'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { logger } from '#/logger'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ResendEmailText } from '#/components/dialogs/EmailDialog/components/ResendEmailText'; +import { isValidCode, TokenField, } from '#/components/dialogs/EmailDialog/components/TokenField'; +import { useRequestEmailUpdate } from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'; +import { useRequestEmailVerification } from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'; +import { useUpdateEmail } from '#/components/dialogs/EmailDialog/data/useUpdateEmail'; +import { Divider } from '#/components/Divider'; +import * as TextField from '#/components/forms/TextField'; +import { CheckThick_Stroke2_Corner0_Rounded as Check } from '#/components/icons/Check'; +import { Envelope_Stroke2_Corner0_Rounded as Envelope } from '#/components/icons/Envelope'; +import { Loader } from '#/components/Loader'; +import { Text } from '#/components/Typography'; +function reducer(state, action) { + switch (action.type) { + case 'setStep': { + return __assign(__assign({}, state), { step: action.step }); + } + case 'setError': { + return __assign(__assign({}, state), { error: action.error, mutationStatus: 'error' }); + } + case 'setMutationStatus': { + return __assign(__assign({}, state), { error: '', mutationStatus: action.status }); + } + case 'setEmail': { + var emailValid = validateEmail(action.value); + return __assign(__assign({}, state), { step: 'email', token: '', email: action.value, emailValid: emailValid }); + } + case 'setToken': { + return __assign(__assign({}, state), { error: '', token: action.value }); + } + } +} +export function Update(_props) { + var _this = this; + var t = useTheme(); + var _ = useLingui()._; + var cleanError = useCleanError(); + var currentAccount = useSession().currentAccount; + var _a = useReducer(reducer, { + step: 'email', + mutationStatus: 'default', + error: '', + email: '', + emailValid: true, + token: '', + }), state = _a[0], dispatch = _a[1]; + var updateEmail = useUpdateEmail().mutateAsync; + var requestEmailUpdate = useRequestEmailUpdate().mutateAsync; + var requestEmailVerification = useRequestEmailVerification().mutateAsync; + var handleEmailChange = function (email) { + dispatch({ + type: 'setEmail', + value: email, + }); + }; + var handleUpdateEmail = function () { return __awaiter(_this, void 0, void 0, function () { + var status_1, _a, e_1, clean; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (state.step === 'token' && !isValidCode(state.token)) { + dispatch({ + type: 'setError', + error: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Please enter a valid code."], ["Please enter a valid code."])))), + }); + return [2 /*return*/]; + } + dispatch({ + type: 'setMutationStatus', + status: 'pending', + }); + if (state.emailValid === false) { + dispatch({ + type: 'setError', + error: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please enter a valid email address."], ["Please enter a valid email address."])))), + }); + return [2 /*return*/]; + } + if (state.email === currentAccount.email) { + dispatch({ + type: 'setError', + error: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["This email is already associated with your account."], ["This email is already associated with your account."])))), + }); + return [2 /*return*/]; + } + _b.label = 1; + case 1: + _b.trys.push([1, 8, , 9]); + return [4 /*yield*/, wait(1000, updateEmail({ + email: state.email, + token: state.token, + }))]; + case 2: + status_1 = (_b.sent()).status; + if (!(status_1 === 'tokenRequired')) return [3 /*break*/, 3]; + dispatch({ + type: 'setStep', + step: 'token', + }); + dispatch({ + type: 'setMutationStatus', + status: 'default', + }); + return [3 /*break*/, 7]; + case 3: + if (!(status_1 === 'success')) return [3 /*break*/, 7]; + dispatch({ + type: 'setMutationStatus', + status: 'success', + }); + _b.label = 4; + case 4: + _b.trys.push([4, 6, , 7]); + // fire off a confirmation email immediately + return [4 /*yield*/, requestEmailVerification()]; + case 5: + // fire off a confirmation email immediately + _b.sent(); + return [3 /*break*/, 7]; + case 6: + _a = _b.sent(); + return [3 /*break*/, 7]; + case 7: return [3 /*break*/, 9]; + case 8: + e_1 = _b.sent(); + logger.error('EmailDialog: update email failed', { safeMessage: e_1 }); + clean = cleanError(e_1).clean; + dispatch({ + type: 'setError', + error: clean || _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Failed to update email, please try again."], ["Failed to update email, please try again."])))), + }); + return [3 /*break*/, 9]; + case 9: return [2 /*return*/]; + } + }); + }); }; + return (_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [a.text_xl, a.font_bold], children: _jsx(Trans, { children: "Update your email" }) }), (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.emailAuthFactor) && (_jsx(Admonition, { type: "warning", children: _jsx(Trans, { children: "If you update your email address, email 2FA will be disabled." }) })), _jsxs(View, { style: [a.gap_md], children: [_jsxs(View, { children: [_jsx(Text, { style: [a.pb_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Please enter your new email address." }) }), _jsxs(TextField.Root, { children: [_jsx(TextField.Icon, { icon: Envelope }), _jsx(TextField.Input, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["New email address"], ["New email address"])))), placeholder: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["alice@example.com"], ["alice@example.com"])))), defaultValue: state.email, onChangeText: state.mutationStatus === 'success' + ? undefined + : handleEmailChange, keyboardType: "email-address", autoComplete: "email", autoCapitalize: "none", onSubmitEditing: handleUpdateEmail })] })] }), state.step === 'token' && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(View, { children: [_jsx(Text, { style: [a.text_md, a.pb_sm, a.font_semi_bold], children: _jsx(Trans, { children: "Security step required" }) }), _jsx(Text, { style: [a.pb_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Please enter the security code we sent to your previous email address." }) }), _jsx(TokenField, { value: state.token, onChangeText: state.mutationStatus === 'success' + ? undefined + : function (token) { + dispatch({ + type: 'setToken', + value: token, + }); + }, onSubmitEditing: handleUpdateEmail }), state.mutationStatus !== 'success' && (_jsx(ResendEmailText, { onPress: requestEmailUpdate, style: [a.pt_sm] }))] })] })), state.error && _jsx(Admonition, { type: "error", children: state.error })] }), state.mutationStatus === 'success' ? (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(View, { style: [a.gap_sm], children: [_jsxs(View, { style: [a.flex_row, a.gap_sm, a.align_center], children: [_jsx(Check, { fill: t.palette.positive_500, size: "xs" }), _jsx(Text, { style: [a.text_md, a.font_bold], children: _jsx(Trans, { children: "Success!" }) })] }), _jsx(Text, { style: [a.leading_snug], children: _jsx(Trans, { children: "Please click on the link in the email we just sent you to verify your new email address. This is an important step to allow you to continue enjoying all the features of Bluesky." }) })] })] })) : (_jsxs(Button, { label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Update email"], ["Update email"])))), size: "large", variant: "solid", color: "primary", onPress: handleUpdateEmail, disabled: !state.email || + (state.step === 'token' && + (!state.token || state.token.length !== 11)) || + state.mutationStatus === 'pending', children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Update email" }) }), state.mutationStatus === 'pending' && _jsx(ButtonIcon, { icon: Loader })] }))] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/dialogs/EmailDialog/screens/VerificationReminder.js b/src/components/dialogs/EmailDialog/screens/VerificationReminder.js new file mode 100644 index 0000000000..0f02cefd97 --- /dev/null +++ b/src/components/dialogs/EmailDialog/screens/VerificationReminder.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 { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, platform, tokens, useBreakpoints, useTheme } from '#/alf'; +import { Button, ButtonText } from '#/components/Button'; +import { useDialogContext } from '#/components/Dialog'; +import { ScreenID, } from '#/components/dialogs/EmailDialog/types'; +import { Divider } from '#/components/Divider'; +import { GradientFill } from '#/components/GradientFill'; +import { ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon } from '#/components/icons/Shield'; +import { Text } from '#/components/Typography'; +export function VerificationReminder(_a) { + var showScreen = _a.showScreen; + var t = useTheme(); + var _ = useLingui()._; + var _b = useBreakpoints(), gtPhone = _b.gtPhone, gtMobile = _b.gtMobile; + var control = useDialogContext(); + var dialogPadding = gtMobile ? a.p_2xl.padding : a.p_xl.padding; + return (_jsxs(View, { style: [a.gap_lg], children: [_jsx(View, { style: [ + a.absolute, + { + top: platform({ web: dialogPadding, default: a.p_2xl.padding }) * -1, + left: dialogPadding * -1, + right: dialogPadding * -1, + height: 150, + }, + ], children: _jsxs(View, { style: [ + a.absolute, + a.inset_0, + a.align_center, + a.justify_center, + a.overflow_hidden, + a.pt_md, + t.atoms.bg_contrast_100, + { + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }, + ], children: [_jsx(GradientFill, { gradient: tokens.gradients.primary }), _jsx(ShieldIcon, { width: 64, fill: "white", style: [a.z_10] })] }) }), _jsx(View, { style: [a.mb_xs, { height: 150 - dialogPadding }] }), _jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.text_xl, a.font_bold], children: _jsx(Trans, { children: "Please verify your email" }) }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Your email has not yet been verified. Please verify your email in order to enjoy all the features of Bluesky." }) })] }), _jsx(Divider, {}), _jsxs(View, { style: [a.gap_sm, gtPhone && [a.flex_row_reverse]], children: [_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Get started"], ["Get started"])))), variant: "solid", color: "primary", size: "large", onPress: function () { + return showScreen({ + id: ScreenID.Verify, + }); + }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Get started" }) }) }), _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Maybe later"], ["Maybe later"])))), accessibilityHint: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Snoozes the reminder"], ["Snoozes the reminder"])))), variant: "ghost", color: "secondary", size: "large", onPress: function () { return control.close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Maybe later" }) }) })] })] })); +} +var templateObject_1, templateObject_2, templateObject_3; diff --git a/src/components/dialogs/EmailDialog/screens/Verify.js b/src/components/dialogs/EmailDialog/screens/Verify.js new file mode 100644 index 0000000000..560a457a48 --- /dev/null +++ b/src/components/dialogs/EmailDialog/screens/Verify.js @@ -0,0 +1,229 @@ +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 { useReducer } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { wait } from '#/lib/async/wait'; +import { useCleanError } from '#/lib/hooks/useCleanError'; +import { logger } from '#/logger'; +import { useSession } from '#/state/session'; +import { atoms as a, useTheme } from '#/alf'; +import { Admonition } from '#/components/Admonition'; +import { Button, ButtonIcon, ButtonText } from '#/components/Button'; +import { ResendEmailText } from '#/components/dialogs/EmailDialog/components/ResendEmailText'; +import { isValidCode, TokenField, } from '#/components/dialogs/EmailDialog/components/TokenField'; +import { useConfirmEmail } from '#/components/dialogs/EmailDialog/data/useConfirmEmail'; +import { useRequestEmailVerification } from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'; +import { useOnEmailVerified } from '#/components/dialogs/EmailDialog/events'; +import { ScreenID, } from '#/components/dialogs/EmailDialog/types'; +import { Divider } from '#/components/Divider'; +import { CheckThick_Stroke2_Corner0_Rounded as Check } from '#/components/icons/Check'; +import { Envelope_Stroke2_Corner0_Rounded as Envelope } from '#/components/icons/Envelope'; +import { createStaticClick, InlineLinkText } from '#/components/Link'; +import { Loader } from '#/components/Loader'; +import { Span, Text } from '#/components/Typography'; +function reducer(state, action) { + switch (action.type) { + case 'setStep': { + return __assign(__assign({}, state), { error: '', mutationStatus: 'default', step: action.step }); + } + case 'setError': { + return __assign(__assign({}, state), { error: action.error, mutationStatus: 'error' }); + } + case 'setMutationStatus': { + return __assign(__assign({}, state), { error: '', mutationStatus: action.status }); + } + case 'setToken': { + return __assign(__assign({}, state), { error: '', token: action.value }); + } + } +} +export function Verify(_a) { + var _this = this; + var _b; + var config = _a.config, showScreen = _a.showScreen; + var t = useTheme(); + var _ = useLingui()._; + var cleanError = useCleanError(); + var currentAccount = useSession().currentAccount; + var _c = useReducer(reducer, { + step: 'email', + mutationStatus: 'default', + error: '', + token: '', + }), state = _c[0], dispatch = _c[1]; + var requestEmailVerification = useRequestEmailVerification().mutateAsync; + var confirmEmail = useConfirmEmail().mutateAsync; + useOnEmailVerified(function () { + if (config.onVerify) { + config.onVerify(); + } + else { + dispatch({ + type: 'setStep', + step: 'success', + }); + } + }); + var handleRequestEmailVerification = function () { return __awaiter(_this, void 0, void 0, function () { + var e_1, clean; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + dispatch({ + type: 'setMutationStatus', + status: 'pending', + }); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, wait(1000, requestEmailVerification())]; + case 2: + _a.sent(); + dispatch({ + type: 'setMutationStatus', + status: 'success', + }); + return [3 /*break*/, 4]; + case 3: + e_1 = _a.sent(); + logger.error('EmailDialog: sending verification email failed', { + safeMessage: e_1, + }); + clean = cleanError(e_1).clean; + dispatch({ + type: 'setError', + error: clean || _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to send email, please try again."], ["Failed to send email, please try again."])))), + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + var handleConfirmEmail = function () { return __awaiter(_this, void 0, void 0, function () { + var e_2, clean; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!isValidCode(state.token)) { + dispatch({ + type: 'setError', + error: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please enter a valid code."], ["Please enter a valid code."])))), + }); + return [2 /*return*/]; + } + dispatch({ + type: 'setMutationStatus', + status: 'pending', + }); + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, wait(1000, confirmEmail({ token: state.token }))]; + case 2: + _a.sent(); + dispatch({ + type: 'setStep', + step: 'success', + }); + return [3 /*break*/, 4]; + case 3: + e_2 = _a.sent(); + logger.error('EmailDialog: confirming email failed', { + safeMessage: e_2, + }); + clean = cleanError(e_2).clean; + dispatch({ + type: 'setError', + error: clean || _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Failed to verify email, please try again."], ["Failed to verify email, please try again."])))), + }); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); }; + if (state.step === 'success') { + return (_jsx(View, { style: [a.gap_lg], children: _jsxs(View, { style: [a.gap_sm], children: [_jsxs(Text, { style: [a.text_xl, a.font_bold], children: [_jsx(Span, { style: { top: 1 }, children: _jsx(Check, { size: "sm", fill: t.palette.positive_500 }) }), ' ', _jsx(Trans, { children: "Email verification complete!" })] }), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "You have successfully verified your email address. You can close this dialog." }) })] }) })); + } + return (_jsxs(View, { style: [a.gap_lg], children: [_jsxs(View, { style: [a.gap_sm], children: [_jsx(Text, { style: [a.text_xl, a.font_bold], children: state.step === 'email' ? (state.mutationStatus === 'success' ? (_jsxs(_Fragment, { children: [_jsx(Span, { style: { top: 1 }, children: _jsx(Check, { size: "sm", fill: t.palette.positive_500 }) }), ' ', _jsx(Trans, { children: "Email sent!" })] })) : (_jsx(Trans, { children: "Verify your email" }))) : (_jsx(Trans, { comment: "Dialog title when a user is verifying their email address by entering a code they have been sent", children: "Verify email code" })) }), state.step === 'email' && state.mutationStatus !== 'success' && (_jsx(_Fragment, { children: (_b = config.instructions) === null || _b === void 0 ? void 0 : _b.map(function (int, i) { return (_jsx(Text, { style: [ + a.italic, + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: int }, i)); }) })), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: state.step === 'email' ? (state.mutationStatus === 'success' ? (_jsxs(Trans, { children: ["We sent an email to", ' ', _jsx(Span, { style: [a.font_semi_bold, t.atoms.text], children: currentAccount.email }), ' ', "containing a link. Please click on it to complete the email verification process."] })) : (_jsxs(Trans, { children: ["We'll send an email to", ' ', _jsx(Span, { style: [a.font_semi_bold, t.atoms.text], children: currentAccount.email }), ' ', "containing a link. Please click on it to complete the email verification process."] }))) : (_jsxs(Trans, { children: ["Please enter the code we sent to", ' ', _jsx(Span, { style: [a.font_semi_bold, t.atoms.text], children: currentAccount.email }), ' ', "below."] })) }), state.step === 'email' && state.mutationStatus !== 'success' && (_jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["If you need to update your email,", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Click here to update your email"], ["Click here to update your email"])))) }, createStaticClick(function () { + showScreen({ id: ScreenID.Update }); + }), { children: "click here" })), "."] }) })), state.step === 'email' && state.mutationStatus === 'success' && (_jsx(ResendEmailText, { onPress: requestEmailVerification }))] }), state.step === 'email' && state.mutationStatus !== 'success' ? (_jsxs(_Fragment, { children: [state.error && _jsx(Admonition, { type: "error", children: state.error }), _jsxs(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Send verification email"], ["Send verification email"])))), size: "large", variant: "solid", color: "primary", onPress: handleRequestEmailVerification, disabled: state.mutationStatus === 'pending', children: [_jsx(ButtonText, { children: _jsx(Trans, { children: "Send email" }) }), _jsx(ButtonIcon, { icon: state.mutationStatus === 'pending' ? Loader : Envelope })] })] })) : null, state.step === 'email' && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Have a code?", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Enter code"], ["Enter code"])))) }, createStaticClick(function () { + dispatch({ + type: 'setStep', + step: 'token', + }); + }), { children: "Click here." }))] }) })] })), state.step === 'token' ? (_jsxs(_Fragment, { children: [_jsx(TokenField, { value: state.token, onChangeText: function (token) { + dispatch({ + type: 'setToken', + value: token, + }); + }, onSubmitEditing: handleConfirmEmail }), state.error && _jsx(Admonition, { type: "error", children: state.error }), _jsxs(Button, { label: _(msg({ + message: "Verify code", + context: "action", + comment: "Button text and accessibility label for action to verify the user's email address using the code entered", + })), size: "large", variant: "solid", color: "primary", onPress: handleConfirmEmail, disabled: !state.token || + state.token.length !== 11 || + state.mutationStatus === 'pending', children: [_jsx(ButtonText, { children: _jsx(Trans, { context: "action", comment: "Button text and accessibility label for action to verify the user's email address using the code entered", children: "Verify code" }) }), state.mutationStatus === 'pending' && _jsx(ButtonIcon, { icon: Loader })] }), _jsx(Divider, {}), _jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["Don't have a code or need a new one?", ' ', _jsx(InlineLinkText, __assign({ label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Click here to restart the verification process."], ["Click here to restart the verification process."])))) }, createStaticClick(function () { + dispatch({ + type: 'setStep', + step: 'email', + }); + }), { children: "Click here." }))] }) })] })) : null] })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; diff --git a/src/components/dialogs/EmailDialog/types.js b/src/components/dialogs/EmailDialog/types.js new file mode 100644 index 0000000000..1d9c32b4c2 --- /dev/null +++ b/src/components/dialogs/EmailDialog/types.js @@ -0,0 +1,7 @@ +export var ScreenID; +(function (ScreenID) { + ScreenID["Update"] = "Update"; + ScreenID["Verify"] = "Verify"; + ScreenID["VerificationReminder"] = "VerificationReminder"; + ScreenID["Manage2FA"] = "Manage2FA"; +})(ScreenID || (ScreenID = {})); diff --git a/src/components/dialogs/Embed.js b/src/components/dialogs/Embed.js new file mode 100644 index 0000000000..27370c1c68 --- /dev/null +++ b/src/components/dialogs/Embed.js @@ -0,0 +1,145 @@ +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 { memo, useEffect, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { AtUri } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { EMBED_SCRIPT } from '#/lib/constants'; +import { niceDate } from '#/lib/strings/time'; +import { toShareUrl } from '#/lib/strings/url-helpers'; +import { atoms as a, useTheme } from '#/alf'; +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 { Check_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check'; +import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon, ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon, } from '#/components/icons/Chevron'; +import { CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon } from '#/components/icons/CodeBrackets'; +import { Text } from '#/components/Typography'; +var EmbedDialog = function (_a) { + var control = _a.control, rest = __rest(_a, ["control"]); + return (_jsxs(Dialog.Outer, { control: control, children: [_jsx(Dialog.Handle, {}), _jsx(EmbedDialogInner, __assign({}, rest))] })); +}; +EmbedDialog = memo(EmbedDialog); +export { EmbedDialog }; +function EmbedDialogInner(_a) { + var postAuthor = _a.postAuthor, postCid = _a.postCid, postUri = _a.postUri, record = _a.record, timestamp = _a.timestamp; + var t = useTheme(); + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var _c = useState(false), copied = _c[0], setCopied = _c[1]; + var _d = useState(false), showCustomisation = _d[0], setShowCustomisation = _d[1]; + var _e = useState('system'), colorMode = _e[0], setColorMode = _e[1]; + // reset copied state after 2 seconds + useEffect(function () { + if (copied) { + var timeout_1 = setTimeout(function () { + setCopied(false); + }, 2000); + return function () { return clearTimeout(timeout_1); }; + } + }, [copied]); + var snippet = useMemo(function () { + function toEmbedUrl(href) { + return toShareUrl(href) + '?ref_src=embed'; + } + var lang = record.langs && record.langs.length > 0 ? record.langs[0] : ''; + var profileHref = toEmbedUrl(['/profile', postAuthor.did].join('/')); + var urip = new AtUri(postUri); + var href = toEmbedUrl(['/profile', postAuthor.did, 'post', urip.rkey].join('/')); + // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x + // DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM! + // Also, keep this code synced with the bskyembed code in landing.tsx. + // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x + return "

").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..c8a9ce587c --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { embedPlayerSources, externalEmbedLabels, } from '#/lib/strings/embed-player'; +import { useSetExternalEmbedPref } from '#/state/preferences'; +import { atoms as a, useBreakpoints, useTheme } from '#/alf'; +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 t = useTheme(); + var setExternalEmbedPref = useSetExternalEmbedPref(); + var gtMobile = useBreakpoints().gtMobile; + 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: [gtMobile ? { width: 'auto', maxWidth: 400 } : a.w_full], children: [_jsxs(View, { style: a.gap_sm, children: [_jsx(Text, { style: [a.text_2xl, a.font_semi_bold], children: _jsx(Trans, { children: "External Media" }) }), _jsxs(View, { style: [a.mt_sm, a.mb_2xl, a.gap_lg], children: [_jsx(Text, { children: _jsxs(Trans, { children: ["This content is hosted by ", externalEmbedLabels[source], ". Do you want to enable external media?"] }) }), _jsx(Text, { style: t.atoms.text_contrast_medium, 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, { style: gtMobile && a.flex_1, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Enable external media"], ["Enable external media"])))), onPress: onShowAllPress, onAccessibilityEscape: control.close, color: "primary", size: "large", variant: "solid", children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Enable external media" }) }) }), _jsx(Button, { style: gtMobile && a.flex_1, label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Enable this source only"], ["Enable this source only"])))), onPress: onShowPress, onAccessibilityEscape: control.close, color: "secondary", size: "large", variant: "solid", 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..e9f71093cc --- /dev/null +++ b/src/components/dialogs/GifSelect.js @@ -0,0 +1,142 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..0b893256e8 --- /dev/null +++ b/src/components/dialogs/InAppBrowserConsent.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 } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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/LinkWarning.js b/src/components/dialogs/LinkWarning.js new file mode 100644 index 0000000000..d07a7cc2b5 --- /dev/null +++ b/src/components/dialogs/LinkWarning.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, useMemo } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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(InAppBrowserConsentInner, { link: linkWarningDialogControl.value })] })); +} +function InAppBrowserConsentInner(_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..55bc637d49 --- /dev/null +++ b/src/components/dialogs/MutedWords.js @@ -0,0 +1,271 @@ +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 { 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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { logger } from '#/logger'; +import { usePreferencesQuery, useRemoveMutedWordMutation, 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 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 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]); + 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') && (_jsx(View, { style: [a.flex_1, a.flex_row, a.align_center, a.gap_sm], children: _jsxs(Text, { style: [ + a.flex_1, + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ], children: [expiryDate && (_jsx(_Fragment, { children: isExpired ? (_jsx(Trans, { children: "Expired" })) : (_jsxs(Trans, { children: ["Expires", ' ', formatDistance(expiryDate, new Date(), { + addSuffix: true, + })] })) })), word.actorTarget === 'exclude-following' && (_jsxs(_Fragment, { children: [' • ', _jsx(Trans, { children: "Excludes users you follow" })] }))] }) }))] }), _jsx(Button, { label: _(msg(templateObject_18 || (templateObject_18 = __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; diff --git a/src/components/dialogs/PostInteractionSettingsDialog.js b/src/components/dialogs/PostInteractionSettingsDialog.js new file mode 100644 index 0000000000..036e4f3ebe --- /dev/null +++ b/src/components/dialogs/PostInteractionSettingsDialog.js @@ -0,0 +1,385 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..afb14f1e90 --- /dev/null +++ b/src/components/dialogs/SearchablePeopleList.js @@ -0,0 +1,328 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..37d9bcc0cf --- /dev/null +++ b/src/components/dialogs/ServerInput.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..696c54947f --- /dev/null +++ b/src/components/dialogs/Signin.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 } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..7bd2298372 --- /dev/null +++ b/src/components/dialogs/StarterPackDialog.js @@ -0,0 +1,229 @@ +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 { AppBskyGraphStarterpack, } from '@atproto/api'; +import { msg, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRequireEmailVerification } from '#/lib/hooks/useRequireEmailVerification'; +import { invalidateActorStarterPacksWithMembershipQuery, useActorStarterPacksWithMembershipsQuery, } from '#/state/queries/actor-starter-packs'; +import { useListMembershipAddMutation, useListMembershipRemoveMutation, } from '#/state/queries/list-memberships'; +import * as Toast from '#/view/com/util/Toast'; +import { atoms as a, 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 { 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 _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 })); + }, [targetDid]); + var onClose = useCallback(function () { + control.close(); + }, [control]); + var listHeader = (_jsxs(_Fragment, { children: [_jsxs(View, { style: [ + { justifyContent: 'space-between', flexDirection: 'row' }, + IS_WEB ? a.mb_2xl : a.my_lg, + a.align_center, + ], 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", 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: IS_WEB ? [a.px_md, { minHeight: 500 }] : [a.px_2xl, a.pt_lg] })); +} +function StarterPackItem(_a) { + var _b, _c; + var starterPackWithMembership = _a.starterPackWithMembership, targetDid = _a.targetDid; + var t = useTheme(); + var ax = useAnalytics(); + var _ = useLingui()._; + var queryClient = useQueryClient(); + var starterPack = starterPackWithMembership.starterPack; + var isInPack = !!starterPackWithMembership.listItem; + var _d = useState(false), isPendingRefresh = _d[0], setIsPendingRefresh = _d[1]; + var addMembership = useListMembershipAddMutation({ + onSuccess: function () { + Toast.show(_(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Added to starter pack"], ["Added to starter pack"]))))); + // Use a timeout to wait for the appview to update, matching the pattern + // in list-memberships.ts + setTimeout(function () { + invalidateActorStarterPacksWithMembershipQuery({ + queryClient: queryClient, + did: targetDid, + }); + setIsPendingRefresh(false); + }, 1e3); + }, + onError: function () { + Toast.show(_(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Failed to add to starter pack"], ["Failed to add to starter pack"])))), 'xmark'); + setIsPendingRefresh(false); + }, + }).mutate; + var removeMembership = useListMembershipRemoveMutation({ + onSuccess: function () { + Toast.show(_(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Removed from starter pack"], ["Removed from starter pack"]))))); + // Use a timeout to wait for the appview to update, matching the pattern + // in list-memberships.ts + setTimeout(function () { + invalidateActorStarterPacksWithMembershipQuery({ + queryClient: queryClient, + did: targetDid, + }); + setIsPendingRefresh(false); + }, 1e3); + }, + onError: function () { + Toast.show(_(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Failed to remove from starter pack"], ["Failed to remove from starter pack"])))), 'xmark'); + setIsPendingRefresh(false); + }, + }).mutate; + var handleToggleMembership = function () { + var _a, _b; + if (!((_a = starterPack.list) === null || _a === void 0 ? void 0 : _a.uri) || isPendingRefresh) + return; + var listUri = starterPack.list.uri; + var starterPackUri = starterPack.uri; + setIsPendingRefresh(true); + 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'); + setIsPendingRefresh(false); + 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: 32, 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" }) }) }))] })) })] }), _jsx(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: isPendingRefresh, onPress: handleToggleMembership, children: _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..53078c8cb1 --- /dev/null +++ b/src/components/dialogs/SwitchAccount.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 { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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/CreateOrEditListDialog.js b/src/components/dialogs/lists/CreateOrEditListDialog.js new file mode 100644 index 0000000000..241dd85400 --- /dev/null +++ b/src/components/dialogs/lists/CreateOrEditListDialog.js @@ -0,0 +1,290 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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; + 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 }), _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 list = _a.list, purpose = _a.purpose, onSave = _a.onSave, setDirty = _a.setDirty, onPressCancel = _a.onPressCancel; + 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 _b = useListCreateMutation(), createListMutation = _b.mutateAsync, createListError = _b.error, isCreateListError = _b.isError, isCreatingList = _b.isPending; + var _c = useListMetadataMutation(), updateListMutation = _c.mutateAsync, updateListError = _c.error, isUpdateListError = _c.isError, isUpdatingList = _c.isPending; + var _d = useState(''), imageError = _d[0], setImageError = _d[1]; + var _e = useState(false), displayNameTooShort = _e[0], setDisplayNameTooShort = _e[1]; + var initialDisplayName = (list === null || list === void 0 ? void 0 : list.name) || ''; + var _f = useState(initialDisplayName), displayName = _f[0], setDisplayName = _f[1]; + var initialDescription = (list === null || list === void 0 ? void 0 : list.description) || ''; + var _g = useState(function () { + var text = list === null || list === void 0 ? void 0 : list.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 = _g[0], setDescriptionRt = _g[1]; + var _h = useState(list === null || list === void 0 ? void 0 : list.avatar), listAvatar = _h[0], setListAvatar = _h[1]; + var _j = useState(), newListAvatar = _j[0], setNewListAvatar = _j[1]; + var dirty = displayName !== initialDisplayName || + descriptionRt.text !== initialDescription || + listAvatar !== (list === null || list === void 0 ? void 0 : list.avatar); + 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..362f13fdb4 --- /dev/null +++ b/src/components/dialogs/lists/ListAddRemoveUsersDialog.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 { useCallback, useMemo } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..62f3dfc0b5 --- /dev/null +++ b/src/components/dialogs/nuxs/ActivitySubscriptions.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 { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..25e696383e --- /dev/null +++ b/src/components/dialogs/nuxs/BookmarksAnnouncement.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 } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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/FindContactsAnnouncement.js b/src/components/dialogs/nuxs/FindContactsAnnouncement.js new file mode 100644 index 0000000000..b51d306168 --- /dev/null +++ b/src/components/dialogs/nuxs/FindContactsAnnouncement.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 { useCallback } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..1c1cc6e344 --- /dev/null +++ b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.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 } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..a8ef1cceff --- /dev/null +++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..f9f12426d9 --- /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 { enabled as isLiveNowBetaDialogEnabled, LiveNowBetaDialog, } from '#/components/dialogs/nuxs/LiveNowBetaDialog'; +import { isSnoozed, snooze, unsnooze } from '#/components/dialogs/nuxs/snoozing'; +import { useAnalytics } from '#/analytics'; +import { useGeolocation } from '#/geolocation'; +var queuedNuxs = [ + { + id: Nux.LiveNowBetaDialog, + enabled: isLiveNowBetaDialogEnabled, + }, +]; +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.LiveNowBetaDialog && _jsx(LiveNowBetaDialog, {}) })); +} 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..37f92f59ce --- /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/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..ddc3c49dc4 --- /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/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 { DotGrid_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..e7ed570af7 --- /dev/null +++ b/src/components/dms/AfterReportDialog.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 { memo, useState } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..893acd9b9f --- /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/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..36bc757064 --- /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/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..dca96d2e3f --- /dev/null +++ b/src/components/dms/ConvoMenu.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); +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { DotGrid_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..fc3abac744 --- /dev/null +++ b/src/components/dms/DateDivider.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 } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..1c7b4d3ea7 --- /dev/null +++ b/src/components/dms/EmojiPopup.android.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, 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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..6e2f3406c4 --- /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/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..d61fcbd28d --- /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/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 { DotGrid_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..c4334ced2f --- /dev/null +++ b/src/components/dms/LeaveConvoPrompt.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 } from "react/jsx-runtime"; +import { msg } from '@lingui/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..dc7a2fd285 --- /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/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..0d90e840a7 --- /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/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..96bce6e836 --- /dev/null +++ b/src/components/dms/MessageProfileButton.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; +}; +import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..a9bdc27656 --- /dev/null +++ b/src/components/dms/MessagesListBlockedFooter.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 React from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..cd3d8f8556 --- /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/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..828f3d5f4b --- /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/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..e01b3e4e0d --- /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/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..50e994ff60 --- /dev/null +++ b/src/components/dms/dialogs/NewChatDialog.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, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..3e586cb1b1 --- /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/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..457bb37544 --- /dev/null +++ b/src/components/forms/DateField/index.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 { useCallback, useImperativeHandle } from 'react'; +import { Keyboard, View } from 'react-native'; +import DatePicker from 'react-native-date-picker'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..5faa120e29 --- /dev/null +++ b/src/components/forms/HostingProvider.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 React from 'react'; +import { Keyboard, View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..be504d6cfd --- /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/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..10f1c1e534 --- /dev/null +++ b/src/components/hooks/dates.js @@ -0,0 +1,82 @@ +/** + * 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); +}; +var _a; +import React from 'react'; +import { formatDistance } from 'date-fns'; +import { ca, cy, da, de, el, enGB, eo, es, eu, fi, fr, fy, gd, gl, hi, hu, id, it, ja, km, ko, nl, pl, pt, ptBR, ro, ru, sv, th, tr, uk, vi, zhCN, zhHK, zhTW, } from 'date-fns/locale'; +import { useLanguagePrefs } from '#/state/preferences'; +/** + * {@link AppLanguage} + */ +var locales = (_a = { + en: undefined, + an: undefined, + ast: undefined, + ca: ca, + cy: cy, + da: da, + de: de, + el: el + }, + _a['en-GB'] = enGB, + _a.eo = eo, + _a.es = es, + _a.eu = eu, + _a.fi = fi, + _a.fr = fr, + _a.fy = fy, + _a.ga = undefined, + _a.gd = gd, + _a.gl = gl, + _a.hi = hi, + _a.hu = hu, + _a.ia = undefined, + _a.id = id, + _a.it = it, + _a.ja = ja, + _a.km = km, + _a.ko = ko, + _a.ne = undefined, + _a.nl = nl, + _a.pl = pl, + _a['pt-PT'] = pt, + _a['pt-BR'] = ptBR, + _a.ro = ro, + _a.ru = ru, + _a.sv = sv, + _a.th = th, + _a.tr = tr, + _a.uk = uk, + _a.vi = vi, + _a['zh-Hans-CN'] = zhCN, + _a['zh-Hant-HK'] = zhHK, + _a['zh-Hant-TW'] = zhTW, + _a); +/** + * Returns a localized `formatDistance` function. + * {@link formatDistance} + */ +export function useFormatDistance() { + var appLanguage = useLanguagePrefs().appLanguage; + return React.useCallback(function (date, baseDate, options) { + var locale = locales[appLanguage]; + return formatDistance(date, baseDate, __assign(__assign({}, options), { locale: locale })); + }, [appLanguage]); +} 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..37481703e1 --- /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/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..e7e8ed8331 --- /dev/null +++ b/src/components/hooks/useHeaderOffset.js @@ -0,0 +1,14 @@ +import { useWindowDimensions } from 'react-native'; +import { useWebMediaQueries } from '#/lib/hooks/useWebMediaQueries'; +export function useHeaderOffset() { + var _a = useWebMediaQueries(), isDesktop = _a.isDesktop, isTablet = _a.isTablet; + var fontScale = useWindowDimensions().fontScale; + if (isDesktop || isTablet) { + return 0; + } + var navBarHeight = 52; + 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..c92a6cfd87 --- /dev/null +++ b/src/components/hooks/useWelcomeModal.js @@ -0,0 +1,35 @@ +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 () { + setIsOpen(false); + // Mark that user has actively closed the modal, don't show again this session + if (typeof window !== 'undefined') { + sessionStorage.setItem('welcomeModalClosed', 'true'); + } + }; + 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. User hasn't actively closed the modal in this session + if (IS_WEB && !hasSession && typeof window !== 'undefined') { + var currentPath = window.location.pathname; + var isHomePage = currentPath === '/'; + var hasUserClosedModal = sessionStorage.getItem('welcomeModalClosed') === 'true'; + if (isHomePage && !hasUserClosedModal) { + // 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..c144275038 --- /dev/null +++ b/src/components/icons/DotGrid.js @@ -0,0 +1,4 @@ +import { createSinglePathSVG } from './TEMPLATE'; +export var DotGrid_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', +}); 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/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..17b45f727f --- /dev/null +++ b/src/components/icons/Person.js @@ -0,0 +1,29 @@ +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', +}); 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..ba74a4de6f --- /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/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..a97893c811 --- /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/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..141a1ca89e --- /dev/null +++ b/src/components/intents/VerifyEmailIntentDialog.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..28dd0fe7f1 --- /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/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..0552c40cdd --- /dev/null +++ b/src/components/interstitials/TrendingVideos.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 { useCallback, useEffect, useMemo } from 'react'; +import { ScrollView, View } from 'react-native'; +import { AppBskyEmbedVideo, AtUri } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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/live/EditLiveDialog.js b/src/components/live/EditLiveDialog.js new file mode 100644 index 0000000000..e14a1c74bd --- /dev/null +++ b/src/components/live/EditLiveDialog.js @@ -0,0 +1,84 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { differenceInMinutes } from 'date-fns'; +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 { LinkPreview } from './LinkPreview'; +import { useLiveLinkMetaQuery, useRemoveLiveStatusMutation, useUpsertLiveStatusMutation, } from './queries'; +import { displayDuration, useDebouncedValue } from './utils'; +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/components/live/GoLiveDialog.js b/src/components/live/GoLiveDialog.js new file mode 100644 index 0000000000..84c2f5eb9b --- /dev/null +++ b/src/components/live/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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { cleanError } from '#/lib/strings/errors'; +import { definitelyUrl } from '#/lib/strings/url-helpers'; +import { useModerationOpts } from '#/state/preferences/moderation-opts'; +import { useLiveNowConfig } from '#/state/service-config'; +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 { displayDuration, getLiveServiceNames, useDebouncedValue, } from '#/components/live/utils'; +import { Loader } from '#/components/Loader'; +import * as ProfileCard from '#/components/ProfileCard'; +import * as Select from '#/components/Select'; +import { Text } from '#/components/Typography'; +import { LinkPreview } from './LinkPreview'; +import { useLiveLinkMetaQuery, useUpsertLiveStatusMutation } from './queries'; +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.allowedDomains).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/components/live/GoLiveDisabledDialog.js b/src/components/live/GoLiveDisabledDialog.js new file mode 100644 index 0000000000..5eb924244a --- /dev/null +++ b/src/components/live/GoLiveDisabledDialog.js @@ -0,0 +1,124 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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/components/live/LinkPreview.js b/src/components/live/LinkPreview.js new file mode 100644 index 0000000000..24e93c9fbd --- /dev/null +++ b/src/components/live/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/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/components/live/LiveIndicator.js b/src/components/live/LiveIndicator.js new file mode 100644 index 0000000000..64a1daa719 --- /dev/null +++ b/src/components/live/LiveIndicator.js @@ -0,0 +1,32 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { Trans } from '@lingui/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/components/live/LiveStatusDialog.js b/src/components/live/LiveStatusDialog.js new file mode 100644 index 0000000000..03a76bb2e1 --- /dev/null +++ b/src/components/live/LiveStatusDialog.js @@ -0,0 +1,116 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { 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 { Globe_Stroke2_Corner0_Rounded } from '../icons/Globe'; +import { SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon } from '../icons/SquareArrowTopRight'; +import { LiveIndicator } from './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/components/live/queries.js b/src/components/live/queries.js new file mode 100644 index 0000000000..70e6465a34 --- /dev/null +++ b/src/components/live/queries.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 }; + } +}; +import { ComAtprotoRepoPutRecord, } from '@atproto/api'; +import { retry } from '@atproto/common-web'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { uploadBlob } from '#/lib/api'; +import { imageToThumb } from '#/lib/api/resolve'; +import { getLinkMeta } from '#/lib/link-meta/link-meta'; +import { updateProfileShadow } from '#/state/cache/profile-shadow'; +import { useLiveNowConfig } from '#/state/service-config'; +import { useAgent, useSession } from '#/state/session'; +import * as Toast from '#/view/com/util/Toast'; +import { useDialogContext } from '#/components/Dialog'; +import { getLiveServiceNames } from '#/components/live/utils'; +import { useAnalytics } from '#/analytics'; +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 urlp, formatted; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!url) + return [2 /*return*/, undefined]; + urlp = new URL(url); + if (!liveNowConfig.allowedDomains.has(urlp.hostname)) { + formatted = getLiveServiceNames(liveNowConfig.allowedDomains).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/components/live/utils.js b/src/components/live/utils.js new file mode 100644 index 0000000000..a253fb3b67 --- /dev/null +++ b/src/components/live/utils.js @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react'; +import { plural } from '@lingui/macro'; +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; +} +// Trailing debounce +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; +} +var serviceUrlToNameMap = { + 'twitch.tv': 'Twitch', + 'www.twitch.tv': 'Twitch', + 'youtube.com': 'YouTube', + 'www.youtube.com': 'YouTube', + 'youtu.be': 'YouTube', + 'nba.com': 'NBA', + 'www.nba.com': 'NBA', + 'nba.smart.link': 'nba.smart.link', + 'espn.com': 'ESPN', + 'www.espn.com': 'ESPN', + 'stream.place': 'Streamplace', + 'skylight.social': 'Skylight', + 'bluecast.app': 'Bluecast', + 'www.bluecast.app': 'Bluecast', +}; +export function getLiveServiceNames(domains) { + var names = Array.from(new Set(Array.from(domains.values()).map(function (d) { return serviceUrlToNameMap[d] || d; }))); + return { + names: names, + formatted: names.join(', '), + }; +} diff --git a/src/components/moderation/ContentHider.js b/src/components/moderation/ContentHider.js new file mode 100644 index 0000000000..5df49fab38 --- /dev/null +++ b/src/components/moderation/ContentHider.js @@ -0,0 +1,164 @@ +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 React from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 = React.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 = React.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) { + 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..dae23fd4ff --- /dev/null +++ b/src/components/moderation/LabelPreference.js @@ -0,0 +1,134 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..7f13ba88c9 --- /dev/null +++ b/src/components/moderation/LabelsOnMe.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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..527b6358b5 --- /dev/null +++ b/src/components/moderation/LabelsOnMeDialog.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..1c454a6e6a --- /dev/null +++ b/src/components/moderation/ModerationDetailsDialog.js @@ -0,0 +1,138 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..bbe99a649c --- /dev/null +++ b/src/components/moderation/PostHider.js @@ -0,0 +1,100 @@ +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 { Pressable, StyleSheet, View, } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useModerationCauseDescription } from '#/lib/moderation/useModerationCauseDescription'; +import { addStyle } from '#/lib/styles'; +import { precacheProfile } from '#/state/queries/profile'; +// 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 = React.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 = React.useCallback(function () { + precacheProfile(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) { + setOverride(function (v) { return !v; }); + } + }, accessibilityRole: "button", accessibilityHint: 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"])))), accessibilityLabel: "", 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..eecf59431d --- /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/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..a65d0c8174 --- /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/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..7a38d2c858 --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..5b98d2f309 --- /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/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..7cb79b1e5c --- /dev/null +++ b/src/components/moderation/ScreenHider.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, Fragment as _Fragment } from "react/jsx-runtime"; +import React from 'react'; +import { TouchableWithoutFeedback, View, } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..1af4760473 --- /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/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..29615a7760 --- /dev/null +++ b/src/components/verification/VerificationCreatePrompt.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..b546cd329c --- /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/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..c6f8215007 --- /dev/null +++ b/src/components/verification/VerificationsDialog.js @@ -0,0 +1,87 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..eb8161db24 --- /dev/null +++ b/src/components/verification/VerifierDialog.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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, 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", variant: "solid", 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", variant: "solid", 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..e04059436b --- /dev/null +++ b/src/env/common.js @@ -0,0 +1,102 @@ +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; diff --git a/src/env/index.js b/src/env/index.js new file mode 100644 index 0000000000..78f3339aac --- /dev/null +++ b/src/env/index.js @@ -0,0 +1,34 @@ +import { Platform } from 'react-native'; +import { nativeBuildVersion } from 'expo-application'; +import { BUNDLE_IDENTIFIER, IS_TESTFLIGHT, 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 = "".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; diff --git a/src/env/index.web.js b/src/env/index.web.js new file mode 100644 index 0000000000..ad3e678e71 --- /dev/null +++ b/src/env/index.web.js @@ -0,0 +1,35 @@ +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; diff --git a/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.js b/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.js new file mode 100644 index 0000000000..4f08267d7e --- /dev/null +++ b/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.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, Fragment as _Fragment } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..043917a462 --- /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/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..bdd05a9e4f --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardWide.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 { useMemo } from 'react'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..e31a1a22f7 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.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 { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..cb6828189e --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.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 { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..c9f2bfeb24 --- /dev/null +++ b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.js @@ -0,0 +1,7 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { LiveEventFeedCardCompact } from '#/features/liveEvents/components/LiveEventFeedCardCompact'; +import { useLiveEvents } from '#/features/liveEvents/context'; +export function SidebarLiveEventFeedsBanner() { + var events = useLiveEvents(); + return events.feeds.map(function (feed) { return (_jsx(LiveEventFeedCardCompact, { feed: feed, metricContext: "sidebar" }, feed.id)); }); +} diff --git a/src/features/liveEvents/context.js b/src/features/liveEvents/context.js new file mode 100644 index 0000000000..01acf29307 --- /dev/null +++ b/src/features/liveEvents/context.js @@ -0,0 +1,169 @@ +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 { 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 { 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 children = _a.children; + var isDevMode = useDevMode()[0]; + var isBskyTeam = useIsBskyTeam(); + var _b = 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 = _b.data, refetch = _b.refetch; + useOnAppStateChange(function (state) { + if (state === 'active') + refetch(); + }); + var ctx = useMemo(function () { + if (!data) + return DEFAULT_LIVE_EVENTS; + var feeds = data.feeds.filter(function (f) { + if (f.preview && !isBskyTeam) + 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]); + 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/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..59f1739dec --- /dev/null +++ b/src/geolocation/util.js @@ -0,0 +1,117 @@ +var _a; +import { IS_ANDROID } from '#/env'; +import { logger } from '#/geolocation/logger'; +/** + * 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; +} 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/actor-status.js b/src/lib/actor-status.js new file mode 100644 index 0000000000..9b0ad262c1 --- /dev/null +++ b/src/lib/actor-status.js @@ -0,0 +1,72 @@ +import { useMemo } from 'react'; +import { AppBskyEmbedExternal, } from '@atproto/api'; +import { isAfter, parseISO } from 'date-fns'; +import { useMaybeProfileShadow } from '#/state/cache/profile-shadow'; +import { useLiveNowConfig } from '#/state/service-config'; +import { useTickEveryMinute } from '#/state/shell'; +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 = validateStatus(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); +} +export function validateStatus(status, config) { + if (status.status !== 'app.bsky.actor.status#live') + return false; + try { + if (AppBskyEmbedExternal.isView(status.embed)) { + var url = new URL(status.embed.external.uri); + return config.allowedDomains.has(url.hostname); + } + else { + return false; + } + } + catch (_a) { + return false; + } +} 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..ad88debd20 --- /dev/null +++ b/src/lib/api/index.js @@ -0,0 +1,524 @@ +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/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, + }]; + 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..7e75d972c3 --- /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) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, retry(retries, isNetworkError, fn)]; + }); + }); +} 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..64a7dae728 --- /dev/null +++ b/src/lib/constants.js @@ -0,0 +1,196 @@ +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_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/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..ee56b7dc8f --- /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/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..1c0b046b5e --- /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/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..41a1a9ea71 --- /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/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..e8d3d6666a --- /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/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/useDedupe.js b/src/lib/hooks/useDedupe.js new file mode 100644 index 0000000000..1d93e1d2d5 --- /dev/null +++ b/src/lib/hooks/useDedupe.js @@ -0,0 +1,16 @@ +import React from 'react'; +export var useDedupe = function (timeout) { + if (timeout === void 0) { timeout = 250; } + var canDo = React.useRef(true); + return React.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/useEnableKeyboardController.js b/src/lib/hooks/useEnableKeyboardController.js new file mode 100644 index 0000000000..4426316aff --- /dev/null +++ b/src/lib/hooks/useEnableKeyboardController.js @@ -0,0 +1,60 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from 'react'; +import { KeyboardProvider, useKeyboardController, } from 'react-native-keyboard-controller'; +import { useFocusEffect } from '@react-navigation/native'; +var KeyboardControllerRefCountContext = createContext({ + incrementRefCount: function () { }, + decrementRefCount: function () { }, +}); +KeyboardControllerRefCountContext.displayName = + 'KeyboardControllerRefCountContext'; +export function KeyboardControllerProvider(_a) { + var children = _a.children; + return (_jsx(KeyboardProvider, { enabled: false, children: _jsx(KeyboardControllerProviderInner, { children: children }) })); +} +function KeyboardControllerProviderInner(_a) { + var children = _a.children; + var setEnabled = useKeyboardController().setEnabled; + var refCount = useRef(0); + var value = useMemo(function () { return ({ + incrementRefCount: function () { + refCount.current++; + setEnabled(refCount.current > 0); + }, + decrementRefCount: function () { + refCount.current--; + setEnabled(refCount.current > 0); + if (__DEV__ && refCount.current < 0) { + console.error('KeyboardController ref count < 0'); + } + }, + }); }, [setEnabled]); + return (_jsx(KeyboardControllerRefCountContext.Provider, { value: value, children: children })); +} +export function useEnableKeyboardController(shouldEnable) { + var _a = useContext(KeyboardControllerRefCountContext), incrementRefCount = _a.incrementRefCount, decrementRefCount = _a.decrementRefCount; + useEffect(function () { + if (!shouldEnable) { + return; + } + incrementRefCount(); + return function () { + decrementRefCount(); + }; + }, [shouldEnable, incrementRefCount, decrementRefCount]); +} +/** + * Like `useEnableKeyboardController`, but using `useFocusEffect` + */ +export function useEnableKeyboardControllerScreen(shouldEnable) { + var _a = useContext(KeyboardControllerRefCountContext), incrementRefCount = _a.incrementRefCount, decrementRefCount = _a.decrementRefCount; + useFocusEffect(useCallback(function () { + if (!shouldEnable) { + return; + } + incrementRefCount(); + return function () { + decrementRefCount(); + }; + }, [shouldEnable, incrementRefCount, decrementRefCount])); +} 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..6dc5b497c1 --- /dev/null +++ b/src/lib/hooks/useIntentHandler.js @@ -0,0 +1,194 @@ +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) }, + }); + 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, + }); + }, 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..d39c3e759a --- /dev/null +++ b/src/lib/hooks/useMinimalShellTransform.js @@ -0,0 +1,51 @@ +import { interpolate, useAnimatedStyle } from 'react-native-reanimated'; +import { useMinimalShellMode } from '#/state/shell/minimal-mode'; +import { useShellLayout } from '#/state/shell/shell-layout'; +// 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 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, -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..3e10d4f8a5 --- /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/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..9e28c0dd78 --- /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/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..54308a5578 --- /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/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..0c85697953 --- /dev/null +++ b/src/lib/hooks/useTranslate.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 { useCallback } from 'react'; +import * as IntentLauncher from 'expo-intent-launcher'; +import { getTranslatorLink } from '#/locale/helpers'; +import { IS_ANDROID } from '#/env'; +import { useOpenLink } from './useOpenLink'; +export function useTranslate() { + var _this = this; + var openLink = useOpenLink(); + return useCallback(function (text, language) { 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, language); + 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..dde6c15f0e --- /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/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..a9cabc5587 --- /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/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..c09a7d1f7c --- /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/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..92423eacf3 --- /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/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..ee017fa770 --- /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/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..13243defb9 --- /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/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..962e2592cc --- /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/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..b86c5bccaa --- /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/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..b653872614 --- /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/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..6a37c0634a --- /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/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..9fedba55e0 --- /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/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/react-query.js b/src/lib/react-query.js new file mode 100644 index 0000000000..9336b0b974 --- /dev/null +++ b/src/lib/react-query.js @@ -0,0 +1,204 @@ +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 AsyncStorage from '@react-native-async-storage/async-storage'; +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 { listenNetworkConfirmed, listenNetworkLost } from '#/state/events'; +import { IS_NATIVE, IS_WEB } from '#/env'; +// any query keys in this array will be persisted to AsyncStorage +export var labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'; +var STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot]; +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) { + return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0])); + }, +}; +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 asyncPersister = createAsyncStoragePersister({ + storage: AsyncStorage, + key: 'queryClient-' + (currentDid !== null && currentDid !== void 0 ? currentDid : 'logged-out'), + }); + return { + persister: asyncPersister, + dehydrateOptions: dehydrateOptions, + }; + }), 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..710b26155c --- /dev/null +++ b/src/lib/sharing.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; +}; +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'; +// TODO: replace global i18n instance with one returned from useLingui -sfn +import { t } from '@lingui/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..f9059bfebf --- /dev/null +++ b/src/lib/strings/embed-player.js @@ -0,0 +1,478 @@ +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', +]; +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', +}; +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; + } + } +} +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 }; + 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), + }; + 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..18c3e855a9 --- /dev/null +++ b/src/lib/strings/errors.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 { XRPCError } from '@atproto/xrpc'; +import { t } from '@lingui/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.startsWith('Error: ')) { + return str.slice('Error: '.length); + } + return str; +} +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; +} +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; 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..f820aa189b --- /dev/null +++ b/src/lib/strings/time.js @@ -0,0 +1,42 @@ +import { msg } from '@lingui/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..579d7ab7c8 --- /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, lang) { + return "https://translate.google.com/?sl=auto&tl=".concat(lang, "&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..d80d5aa2ff --- /dev/null +++ b/src/locale/i18n.js @@ -0,0 +1,562 @@ +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 } from 'react'; +import { i18n } from '@lingui/core'; +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; + 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('@formatjs/intl-pluralrules/locale-data/an'), + import('@formatjs/intl-numberformat/locale-data/es'), + import('@formatjs/intl-displaynames/locale-data/es'), + ])]; + case 2: + _b.sent(); + return [3 /*break*/, 82]; + case 3: + i18n.loadAndActivate({ locale: locale, messages: messagesAst }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ast'), + import('@formatjs/intl-numberformat/locale-data/ast'), + import('@formatjs/intl-displaynames/locale-data/ast'), + ])]; + case 4: + _b.sent(); + return [3 /*break*/, 82]; + case 5: + i18n.loadAndActivate({ locale: locale, messages: messagesCa }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ca'), + import('@formatjs/intl-numberformat/locale-data/ca'), + import('@formatjs/intl-displaynames/locale-data/ca'), + ])]; + case 6: + _b.sent(); + return [3 /*break*/, 82]; + case 7: + i18n.loadAndActivate({ locale: locale, messages: messagesCy }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/cy'), + import('@formatjs/intl-numberformat/locale-data/cy'), + import('@formatjs/intl-displaynames/locale-data/cy'), + ])]; + case 8: + _b.sent(); + return [3 /*break*/, 82]; + case 9: + i18n.loadAndActivate({ locale: locale, messages: messagesDa }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/da'), + import('@formatjs/intl-numberformat/locale-data/da'), + import('@formatjs/intl-displaynames/locale-data/da'), + ])]; + case 10: + _b.sent(); + return [3 /*break*/, 82]; + case 11: + i18n.loadAndActivate({ locale: locale, messages: messagesDe }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/de'), + import('@formatjs/intl-numberformat/locale-data/de'), + import('@formatjs/intl-displaynames/locale-data/de'), + ])]; + case 12: + _b.sent(); + return [3 /*break*/, 82]; + case 13: + i18n.loadAndActivate({ locale: locale, messages: messagesEl }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/el'), + import('@formatjs/intl-numberformat/locale-data/el'), + import('@formatjs/intl-displaynames/locale-data/el'), + ])]; + case 14: + _b.sent(); + return [3 /*break*/, 82]; + case 15: + i18n.loadAndActivate({ locale: locale, messages: messagesEn_GB }); + return [4 /*yield*/, Promise.all([ + 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: + _b.sent(); + return [3 /*break*/, 82]; + case 17: + i18n.loadAndActivate({ locale: locale, messages: messagesEo }); + return [4 /*yield*/, Promise.all([ + 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: + _b.sent(); + return [3 /*break*/, 82]; + case 19: + i18n.loadAndActivate({ locale: locale, messages: messagesEs }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/es'), + import('@formatjs/intl-numberformat/locale-data/es'), + import('@formatjs/intl-displaynames/locale-data/es'), + ])]; + case 20: + _b.sent(); + return [3 /*break*/, 82]; + case 21: + i18n.loadAndActivate({ locale: locale, messages: messagesEu }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/eu'), + import('@formatjs/intl-numberformat/locale-data/eu'), + import('@formatjs/intl-displaynames/locale-data/eu'), + ])]; + case 22: + _b.sent(); + return [3 /*break*/, 82]; + case 23: + i18n.loadAndActivate({ locale: locale, messages: messagesFi }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/fi'), + import('@formatjs/intl-numberformat/locale-data/fi'), + import('@formatjs/intl-displaynames/locale-data/fi'), + ])]; + case 24: + _b.sent(); + return [3 /*break*/, 82]; + case 25: + i18n.loadAndActivate({ locale: locale, messages: messagesFr }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/fr'), + import('@formatjs/intl-numberformat/locale-data/fr'), + import('@formatjs/intl-displaynames/locale-data/fr'), + ])]; + case 26: + _b.sent(); + return [3 /*break*/, 82]; + case 27: + i18n.loadAndActivate({ locale: locale, messages: messagesFy }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/fy'), + import('@formatjs/intl-numberformat/locale-data/fy'), + import('@formatjs/intl-displaynames/locale-data/fy'), + ])]; + case 28: + _b.sent(); + return [3 /*break*/, 82]; + 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 [3 /*break*/, 82]; + case 31: + i18n.loadAndActivate({ locale: locale, messages: messagesGd }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/gd'), + import('@formatjs/intl-numberformat/locale-data/gd'), + import('@formatjs/intl-displaynames/locale-data/gd'), + ])]; + case 32: + _b.sent(); + return [3 /*break*/, 82]; + case 33: + i18n.loadAndActivate({ locale: locale, messages: messagesGl }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/gl'), + import('@formatjs/intl-numberformat/locale-data/gl'), + import('@formatjs/intl-displaynames/locale-data/gl'), + ])]; + case 34: + _b.sent(); + return [3 /*break*/, 82]; + case 35: + i18n.loadAndActivate({ locale: locale, messages: messagesHi }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/hi'), + import('@formatjs/intl-numberformat/locale-data/hi'), + import('@formatjs/intl-displaynames/locale-data/hi'), + ])]; + case 36: + _b.sent(); + return [3 /*break*/, 82]; + case 37: + i18n.loadAndActivate({ locale: locale, messages: messagesHu }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/hu'), + import('@formatjs/intl-numberformat/locale-data/hu'), + import('@formatjs/intl-displaynames/locale-data/hu'), + ])]; + case 38: + _b.sent(); + return [3 /*break*/, 82]; + 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 [3 /*break*/, 82]; + case 41: + i18n.loadAndActivate({ locale: locale, messages: messagesId }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/id'), + import('@formatjs/intl-numberformat/locale-data/id'), + import('@formatjs/intl-displaynames/locale-data/id'), + ])]; + case 42: + _b.sent(); + return [3 /*break*/, 82]; + case 43: + i18n.loadAndActivate({ locale: locale, messages: messagesIt }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/it'), + import('@formatjs/intl-numberformat/locale-data/it'), + import('@formatjs/intl-displaynames/locale-data/it'), + ])]; + case 44: + _b.sent(); + return [3 /*break*/, 82]; + case 45: + i18n.loadAndActivate({ locale: locale, messages: messagesJa }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ja'), + import('@formatjs/intl-numberformat/locale-data/ja'), + import('@formatjs/intl-displaynames/locale-data/ja'), + ])]; + case 46: + _b.sent(); + return [3 /*break*/, 82]; + case 47: + i18n.loadAndActivate({ locale: locale, messages: messagesKm }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/km'), + import('@formatjs/intl-numberformat/locale-data/km'), + import('@formatjs/intl-displaynames/locale-data/km'), + ])]; + case 48: + _b.sent(); + return [3 /*break*/, 82]; + case 49: + i18n.loadAndActivate({ locale: locale, messages: messagesKo }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ko'), + import('@formatjs/intl-numberformat/locale-data/ko'), + import('@formatjs/intl-displaynames/locale-data/ko'), + ])]; + case 50: + _b.sent(); + return [3 /*break*/, 82]; + 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 [3 /*break*/, 82]; + case 53: + i18n.loadAndActivate({ locale: locale, messages: messagesNl }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/nl'), + import('@formatjs/intl-numberformat/locale-data/nl'), + import('@formatjs/intl-displaynames/locale-data/nl'), + ])]; + case 54: + _b.sent(); + return [3 /*break*/, 82]; + case 55: + i18n.loadAndActivate({ locale: locale, messages: messagesPl }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/pl'), + import('@formatjs/intl-numberformat/locale-data/pl'), + import('@formatjs/intl-displaynames/locale-data/pl'), + ])]; + case 56: + _b.sent(); + return [3 /*break*/, 82]; + case 57: + i18n.loadAndActivate({ locale: locale, messages: messagesPt_BR }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/pt'), + import('@formatjs/intl-numberformat/locale-data/pt'), + import('@formatjs/intl-displaynames/locale-data/pt'), + ])]; + case 58: + _b.sent(); + return [3 /*break*/, 82]; + case 59: + i18n.loadAndActivate({ locale: locale, messages: messagesPt_PT }); + return [4 /*yield*/, Promise.all([ + 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: + _b.sent(); + return [3 /*break*/, 82]; + case 61: + i18n.loadAndActivate({ locale: locale, messages: messagesRo }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ro'), + import('@formatjs/intl-numberformat/locale-data/ro'), + import('@formatjs/intl-displaynames/locale-data/ro'), + ])]; + case 62: + _b.sent(); + return [3 /*break*/, 82]; + case 63: + i18n.loadAndActivate({ locale: locale, messages: messagesRu }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/ru'), + import('@formatjs/intl-numberformat/locale-data/ru'), + import('@formatjs/intl-displaynames/locale-data/ru'), + ])]; + case 64: + _b.sent(); + return [3 /*break*/, 82]; + case 65: + i18n.loadAndActivate({ locale: locale, messages: messagesSv }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/sv'), + import('@formatjs/intl-numberformat/locale-data/sv'), + import('@formatjs/intl-displaynames/locale-data/sv'), + ])]; + case 66: + _b.sent(); + return [3 /*break*/, 82]; + case 67: + i18n.loadAndActivate({ locale: locale, messages: messagesTh }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/th'), + import('@formatjs/intl-numberformat/locale-data/th'), + import('@formatjs/intl-displaynames/locale-data/th'), + ])]; + case 68: + _b.sent(); + return [3 /*break*/, 82]; + case 69: + i18n.loadAndActivate({ locale: locale, messages: messagesTr }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/tr'), + import('@formatjs/intl-numberformat/locale-data/tr'), + import('@formatjs/intl-displaynames/locale-data/tr'), + ])]; + case 70: + _b.sent(); + return [3 /*break*/, 82]; + case 71: + i18n.loadAndActivate({ locale: locale, messages: messagesUk }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/uk'), + import('@formatjs/intl-numberformat/locale-data/uk'), + import('@formatjs/intl-displaynames/locale-data/uk'), + ])]; + case 72: + _b.sent(); + return [3 /*break*/, 82]; + case 73: + i18n.loadAndActivate({ locale: locale, messages: messagesVi }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/vi'), + import('@formatjs/intl-numberformat/locale-data/vi'), + import('@formatjs/intl-displaynames/locale-data/vi'), + ])]; + case 74: + _b.sent(); + return [3 /*break*/, 82]; + case 75: + i18n.loadAndActivate({ locale: locale, messages: messagesZh_CN }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/zh'), + import('@formatjs/intl-numberformat/locale-data/zh'), + import('@formatjs/intl-displaynames/locale-data/zh'), + ])]; + case 76: + _b.sent(); + return [3 /*break*/, 82]; + case 77: + i18n.loadAndActivate({ locale: locale, messages: messagesZh_HK }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/zh'), + import('@formatjs/intl-numberformat/locale-data/zh'), + import('@formatjs/intl-displaynames/locale-data/zh'), + ])]; + case 78: + _b.sent(); + return [3 /*break*/, 82]; + case 79: + i18n.loadAndActivate({ locale: locale, messages: messagesZh_TW }); + return [4 /*yield*/, Promise.all([ + import('@formatjs/intl-pluralrules/locale-data/zh'), + import('@formatjs/intl-numberformat/locale-data/zh'), + import('@formatjs/intl-displaynames/locale-data/zh'), + ])]; + case 80: + _b.sent(); + return [3 /*break*/, 82]; + case 81: + { + i18n.loadAndActivate({ locale: locale, messages: messagesEn }); + return [3 /*break*/, 82]; + } + _b.label = 82; + case 82: return [2 /*return*/]; + } + }); + }); +} +export function useLocaleLanguage() { + var appLanguage = useLanguagePrefs().appLanguage; + useEffect(function () { + dynamicActivate(sanitizeAppLanguageSetting(appLanguage)); + }, [appLanguage]); +} diff --git a/src/locale/i18n.web.js b/src/locale/i18n.web.js new file mode 100644 index 0000000000..d1d06f862c --- /dev/null +++ b/src/locale/i18n.web.js @@ -0,0 +1,274 @@ +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 { i18n } from '@lingui/core'; +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, _a; + 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: return [4 /*yield*/, import("./locales/an/messages")]; + case 2: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 3: return [4 /*yield*/, import("./locales/ast/messages")]; + case 4: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 5: return [4 /*yield*/, import("./locales/ca/messages")]; + case 6: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 7: return [4 /*yield*/, import("./locales/cy/messages")]; + case 8: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 9: return [4 /*yield*/, import("./locales/da/messages")]; + case 10: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 11: return [4 /*yield*/, import("./locales/de/messages")]; + case 12: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 13: return [4 /*yield*/, import("./locales/el/messages")]; + case 14: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 15: return [4 /*yield*/, import("./locales/en-GB/messages")]; + case 16: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 17: return [4 /*yield*/, import("./locales/eo/messages")]; + case 18: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 19: return [4 /*yield*/, import("./locales/es/messages")]; + case 20: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 21: return [4 /*yield*/, import("./locales/eu/messages")]; + case 22: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 23: return [4 /*yield*/, import("./locales/fi/messages")]; + case 24: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 25: return [4 /*yield*/, import("./locales/fr/messages")]; + case 26: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 27: return [4 /*yield*/, import("./locales/fy/messages")]; + case 28: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 29: return [4 /*yield*/, import("./locales/ga/messages")]; + case 30: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 31: return [4 /*yield*/, import("./locales/gd/messages")]; + case 32: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 33: return [4 /*yield*/, import("./locales/gl/messages")]; + case 34: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 35: return [4 /*yield*/, import("./locales/hi/messages")]; + case 36: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 37: return [4 /*yield*/, import("./locales/hu/messages")]; + case 38: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 39: return [4 /*yield*/, import("./locales/ia/messages")]; + case 40: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 41: return [4 /*yield*/, import("./locales/id/messages")]; + case 42: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 43: return [4 /*yield*/, import("./locales/it/messages")]; + case 44: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 45: return [4 /*yield*/, import("./locales/ja/messages")]; + case 46: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 47: return [4 /*yield*/, import("./locales/km/messages")]; + case 48: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 49: return [4 /*yield*/, import("./locales/ko/messages")]; + case 50: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 51: return [4 /*yield*/, import("./locales/ne/messages")]; + case 52: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 53: return [4 /*yield*/, import("./locales/nl/messages")]; + case 54: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 55: return [4 /*yield*/, import("./locales/pl/messages")]; + case 56: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 57: return [4 /*yield*/, import("./locales/pt-BR/messages")]; + case 58: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 59: return [4 /*yield*/, import("./locales/pt-PT/messages")]; + case 60: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 61: return [4 /*yield*/, import("./locales/ro/messages")]; + case 62: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 63: return [4 /*yield*/, import("./locales/ru/messages")]; + case 64: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 65: return [4 /*yield*/, import("./locales/sv/messages")]; + case 66: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 67: return [4 /*yield*/, import("./locales/th/messages")]; + case 68: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 69: return [4 /*yield*/, import("./locales/tr/messages")]; + case 70: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 71: return [4 /*yield*/, import("./locales/uk/messages")]; + case 72: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 73: return [4 /*yield*/, import("./locales/vi/messages")]; + case 74: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 75: return [4 /*yield*/, import("./locales/zh-CN/messages")]; + case 76: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 77: return [4 /*yield*/, import("./locales/zh-HK/messages")]; + case 78: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 79: return [4 /*yield*/, import("./locales/zh-TW/messages")]; + case 80: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 81: return [4 /*yield*/, import("./locales/en/messages")]; + case 82: + mod = _b.sent(); + return [3 /*break*/, 83]; + case 83: + i18n.load(locale, mod.messages); + i18n.activate(locale); + return [2 /*return*/]; + } + }); + }); +} +export function useLocaleLanguage() { + var appLanguage = useLanguagePrefs().appLanguage; + useEffect(function () { + var sanitizedLanguage = sanitizeAppLanguageSetting(appLanguage); + document.documentElement.lang = sanitizedLanguage; + dynamicActivate(sanitizedLanguage); + }, [appLanguage]); +} diff --git a/src/locale/i18nProvider.js b/src/locale/i18nProvider.js new file mode 100644 index 0000000000..99c1a77557 --- /dev/null +++ b/src/locale/i18nProvider.js @@ -0,0 +1,9 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { i18n } from '@lingui/core'; +import { I18nProvider as DefaultI18nProvider } from '@lingui/react'; +import { useLocaleLanguage } from './i18n'; +export default function I18nProvider(_a) { + var children = _a.children; + useLocaleLanguage(); + return _jsx(DefaultI18nProvider, { i18n: i18n, children: children }); +} diff --git a/src/locale/languages.js b/src/locale/languages.js new file mode 100644 index 0000000000..bd1c991c38 --- /dev/null +++ b/src/locale/languages.js @@ -0,0 +1,631 @@ +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' }, +]; +export var LANGUAGES = [ + { code3: 'aar', code2: 'aa', name: 'Afar' }, + { code3: 'abk', code2: 'ab', name: 'Abkhazian' }, + { code3: 'ace', code2: '', name: 'Achinese' }, + { code3: 'ach', code2: '', name: 'Acoli' }, + { code3: 'ada', code2: '', name: 'Adangme' }, + { code3: 'ady', code2: '', name: 'Adyghe; Adygei' }, + { code3: 'afa', code2: '', name: 'Afro-Asiatic languages' }, + { code3: 'afh', code2: '', name: 'Afrihili' }, + { code3: 'afr', code2: 'af', name: 'Afrikaans' }, + { code3: 'ain', code2: '', name: 'Ainu' }, + { code3: 'aka', code2: 'ak', name: 'Akan' }, + { code3: 'akk', code2: '', name: 'Akkadian' }, + { code3: 'alb', code2: 'sq', name: 'Albanian' }, + { code3: 'ale', code2: '', name: 'Aleut' }, + { code3: 'alg', code2: '', name: 'Algonquian languages' }, + { code3: 'alt', code2: '', name: 'Southern Altai' }, + { code3: 'amh', code2: 'am', name: 'Amharic' }, + { code3: 'ang', code2: '', name: 'English, Old (ca.450-1100)' }, + { code3: 'anp', code2: '', name: 'Angika' }, + { code3: 'apa', code2: '', name: 'Apache languages' }, + { code3: 'ara', code2: 'ar', name: 'Arabic' }, + { + code3: 'arc', + code2: '', + name: 'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', + }, + { code3: 'arg', code2: 'an', name: 'Aragonese' }, + { code3: 'arm', code2: 'hy', name: 'Armenian' }, + { code3: 'arn', code2: '', name: 'Mapudungun; Mapuche' }, + { code3: 'arp', code2: '', name: 'Arapaho' }, + { code3: 'art', code2: '', name: 'Artificial languages' }, + { code3: 'arw', code2: '', name: 'Arawak' }, + { code3: 'asm', code2: 'as', name: 'Assamese' }, + { code3: 'ast', code2: '', name: 'Asturian; Bable; Leonese; Asturleonese' }, + { code3: 'ath', code2: '', name: 'Athapascan languages' }, + { code3: 'aus', code2: '', name: 'Australian languages' }, + { code3: 'ava', code2: 'av', name: 'Avaric' }, + { code3: 'ave', code2: 'ae', name: 'Avestan' }, + { code3: 'awa', code2: '', name: 'Awadhi' }, + { code3: 'aym', code2: 'ay', name: 'Aymara' }, + { code3: 'aze', code2: 'az', name: 'Azerbaijani' }, + { code3: 'bad', code2: '', name: 'Banda languages' }, + { code3: 'bai', code2: '', name: 'Bamileke languages' }, + { code3: 'bak', code2: 'ba', name: 'Bashkir' }, + { code3: 'bal', code2: '', name: 'Baluchi' }, + { code3: 'bam', code2: 'bm', name: 'Bambara' }, + { code3: 'ban', code2: '', name: 'Balinese' }, + { code3: 'baq', code2: 'eu', name: 'Basque' }, + { code3: 'bas', code2: '', name: 'Basa' }, + { code3: 'bat', code2: '', name: 'Baltic languages' }, + { code3: 'bej', code2: '', name: 'Beja; Bedawiyet' }, + { code3: 'bel', code2: 'be', name: 'Belarusian' }, + { code3: 'bem', code2: '', name: 'Bemba' }, + { code3: 'ben', code2: 'bn', name: 'Bengali' }, + { code3: 'ber', code2: '', name: 'Berber languages' }, + { code3: 'bho', code2: '', name: 'Bhojpuri' }, + { code3: 'bih', code2: 'bh', name: 'Bihari languages' }, + { code3: 'bik', code2: '', name: 'Bikol' }, + { code3: 'bin', code2: '', name: 'Bini; Edo' }, + { code3: 'bis', code2: 'bi', name: 'Bislama' }, + { code3: 'bla', code2: '', name: 'Siksika' }, + { code3: 'bnt', code2: '', name: 'Bantu languages' }, + { code3: 'bod', code2: 'bo', name: 'Tibetan' }, + { code3: 'bos', code2: 'bs', name: 'Bosnian' }, + { code3: 'bra', code2: '', name: 'Braj' }, + { code3: 'bre', code2: 'br', name: 'Breton' }, + { code3: 'btk', code2: '', name: 'Batak languages' }, + { code3: 'bua', code2: '', name: 'Buriat' }, + { code3: 'bug', code2: '', name: 'Buginese' }, + { code3: 'bul', code2: 'bg', name: 'Bulgarian' }, + { code3: 'bur', code2: 'my', name: 'Burmese' }, + { code3: 'byn', code2: '', name: 'Blin; Bilin' }, + { code3: 'cad', code2: '', name: 'Caddo' }, + { code3: 'cai', code2: '', name: 'Central American Indian languages' }, + { code3: 'car', code2: '', name: 'Galibi Carib' }, + { code3: 'cat', code2: 'ca', name: 'Catalan-Valencian' }, + { code3: 'cau', code2: '', name: 'Caucasian languages' }, + { code3: 'ceb', code2: '', name: 'Cebuano' }, + { code3: 'cel', code2: '', name: 'Celtic languages' }, + { code3: 'ces', code2: 'cs', name: 'Czech' }, + { code3: 'cha', code2: 'ch', name: 'Chamorro' }, + { code3: 'chb', code2: '', name: 'Chibcha' }, + { code3: 'che', code2: 'ce', name: 'Chechen' }, + { code3: 'chg', code2: '', name: 'Chagatai' }, + { code3: 'chi', code2: 'zh', name: 'Chinese' }, + { code3: 'chk', code2: '', name: 'Chuukese' }, + { code3: 'chm', code2: '', name: 'Mari' }, + { code3: 'chn', code2: '', name: 'Chinook jargon' }, + { code3: 'cho', code2: '', name: 'Choctaw' }, + { code3: 'chp', code2: '', name: 'Chipewyan; Dene Suline' }, + { code3: 'chr', code2: '', name: 'Cherokee' }, + { code3: 'chu', code2: 'cu', name: 'Church Slavic' }, + { code3: 'chv', code2: 'cv', name: 'Chuvash' }, + { code3: 'chy', code2: '', name: 'Cheyenne' }, + { code3: 'cmc', code2: '', name: 'Chamic languages' }, + { code3: 'cnr', code2: '', name: 'Montenegrin' }, + { code3: 'cop', code2: '', name: 'Coptic' }, + { code3: 'cor', code2: 'kw', name: 'Cornish' }, + { code3: 'cos', code2: 'co', name: 'Corsican' }, + { code3: 'cpe', code2: '', name: 'Creoles and pidgins, English based' }, + { code3: 'cpf', code2: '', name: 'Creoles and pidgins, French-based' }, + { code3: 'cpp', code2: '', name: 'Creoles and pidgins, Portuguese-based' }, + { code3: 'cre', code2: 'cr', name: 'Cree' }, + { code3: 'crh', code2: '', name: 'Crimean Tatar; Crimean Turkish' }, + { code3: 'crp', code2: '', name: 'Creoles and pidgins' }, + { code3: 'csb', code2: '', name: 'Kashubian' }, + { code3: 'cus', code2: '', name: 'Cushitic languages' }, + { code3: 'cym', code2: 'cy', name: 'Welsh' }, + { code3: 'cze', code2: 'cs', name: 'Czech' }, + { code3: 'dak', code2: '', name: 'Dakota' }, + { code3: 'dan', code2: 'da', name: 'Danish' }, + { code3: 'dar', code2: '', name: 'Dargwa' }, + { code3: 'day', code2: '', name: 'Land Dayak languages' }, + { code3: 'del', code2: '', name: 'Delaware' }, + { code3: 'den', code2: '', name: 'Slave (Athapascan)' }, + { code3: 'deu', code2: 'de', name: 'German' }, + { code3: 'dgr', code2: '', name: 'Dogrib' }, + { code3: 'din', code2: '', name: 'Dinka' }, + { code3: 'div', code2: 'dv', name: 'Divehi; Dhivehi; Maldivian' }, + { code3: 'doi', code2: '', name: 'Dogri' }, + { code3: 'dra', code2: '', name: 'Dravidian languages' }, + { code3: 'dsb', code2: '', name: 'Lower Sorbian' }, + { code3: 'dua', code2: '', name: 'Duala' }, + { code3: 'dum', code2: '', name: 'Dutch, Middle (ca.1050-1350)' }, + { code3: 'dut', code2: 'nl', name: 'Dutch; Flemish' }, + { code3: 'dyu', code2: '', name: 'Dyula' }, + { code3: 'dzo', code2: 'dz', name: 'Dzongkha' }, + { code3: 'efi', code2: '', name: 'Efik' }, + { code3: 'egy', code2: '', name: 'Egyptian (Ancient)' }, + { code3: 'eka', code2: '', name: 'Ekajuk' }, + { code3: 'ell', code2: 'el', name: 'Greek' }, + { code3: 'elx', code2: '', name: 'Elamite' }, + { code3: 'eng', code2: 'en', name: 'English' }, + { code3: 'enm', code2: '', name: 'English, Middle (1100-1500)' }, + { code3: 'epo', code2: 'eo', name: 'Esperanto' }, + { code3: 'est', code2: 'et', name: 'Estonian' }, + { code3: 'eus', code2: 'eu', name: 'Basque' }, + { code3: 'ewe', code2: 'ee', name: 'Ewe' }, + { code3: 'ewo', code2: '', name: 'Ewondo' }, + { code3: 'fan', code2: '', name: 'Fang' }, + { code3: 'fao', code2: 'fo', name: 'Faroese' }, + { code3: 'fas', code2: 'fa', name: 'Persian' }, + { code3: 'fat', code2: '', name: 'Fanti' }, + { code3: 'fij', code2: 'fj', name: 'Fijian' }, + { code3: 'fil', code2: '', name: 'Filipino; Pilipino' }, + { code3: 'fin', code2: 'fi', name: 'Finnish' }, + { code3: 'fiu', code2: '', name: 'Finno-Ugrian languages' }, + { code3: 'fon', code2: '', name: 'Fon' }, + { code3: 'fra', code2: 'fr', name: 'French' }, + { code3: 'fre', code2: 'fr', name: 'French' }, + { code3: 'frm', code2: '', name: 'French, Middle (ca.1400-1600)' }, + { code3: 'fro', code2: '', name: 'French, Old (842-ca.1400)' }, + { code3: 'frr', code2: '', name: 'Northern Frisian' }, + { code3: 'frs', code2: '', name: 'Eastern Frisian' }, + { code3: 'fry', code2: 'fy', name: 'Western Frisian' }, + { code3: 'ful', code2: 'ff', name: 'Fulah' }, + { code3: 'fur', code2: '', name: 'Friulian' }, + { code3: 'gaa', code2: '', name: 'Ga' }, + { code3: 'gay', code2: '', name: 'Gayo' }, + { code3: 'gba', code2: '', name: 'Gbaya' }, + { code3: 'gem', code2: '', name: 'Germanic languages' }, + { code3: 'geo', code2: 'ka', name: 'Georgian' }, + { code3: 'ger', code2: 'de', name: 'German' }, + { code3: 'gez', code2: '', name: 'Geez' }, + { code3: 'gil', code2: '', name: 'Gilbertese' }, + { code3: 'gla', code2: 'gd', name: 'Gaelic; Scottish Gaelic' }, + { code3: 'gle', code2: 'ga', name: 'Irish' }, + { code3: 'glg', code2: 'gl', name: 'Galician' }, + { code3: 'glv', code2: 'gv', name: 'Manx' }, + { code3: 'gmh', code2: '', name: 'German, Middle High (ca.1050-1500)' }, + { code3: 'goh', code2: '', name: 'German, Old High (ca.750-1050)' }, + { code3: 'gon', code2: '', name: 'Gondi' }, + { code3: 'gor', code2: '', name: 'Gorontalo' }, + { code3: 'got', code2: '', name: 'Gothic' }, + { code3: 'grb', code2: '', name: 'Grebo' }, + { code3: 'grc', code2: '', name: 'Ancient Greek' }, + { code3: 'gre', code2: 'el', name: 'Greek' }, + { code3: 'grn', code2: 'gn', name: 'Guarani' }, + { code3: 'gsw', code2: '', name: 'Swiss German; Alemannic; Alsatian' }, + { code3: 'guj', code2: 'gu', name: 'Gujarati' }, + { code3: 'gwi', code2: '', name: "Gwich'in" }, + { code3: 'hai', code2: '', name: 'Haida' }, + { code3: 'hat', code2: 'ht', name: 'Haitian; Haitian Creole' }, + { code3: 'hau', code2: 'ha', name: 'Hausa' }, + { code3: 'haw', code2: '', name: 'Hawaiian' }, + { code3: 'heb', code2: 'he', name: 'Hebrew' }, + { code3: 'her', code2: 'hz', name: 'Herero' }, + { code3: 'hil', code2: '', name: 'Hiligaynon' }, + { + code3: 'him', + code2: '', + name: 'Himachali languages; Western Pahari languages', + }, + { code3: 'hin', code2: 'hi', name: 'Hindi' }, + { code3: 'hit', code2: '', name: 'Hittite' }, + { code3: 'hmn', code2: '', name: 'Hmong; Mong' }, + { code3: 'hmo', code2: 'ho', name: 'Hiri Motu' }, + { code3: 'hrv', code2: 'hr', name: 'Croatian' }, + { code3: 'hsb', code2: '', name: 'Upper Sorbian' }, + { code3: 'hun', code2: 'hu', name: 'Hungarian' }, + { code3: 'hup', code2: '', name: 'Hupa' }, + { code3: 'hye', code2: 'hy', name: 'Armenian' }, + { code3: 'iba', code2: '', name: 'Iban' }, + { code3: 'ibo', code2: 'ig', name: 'Igbo' }, + { code3: 'ice', code2: 'is', name: 'Icelandic' }, + { code3: 'ido', code2: 'io', name: 'Ido' }, + { code3: 'iii', code2: 'ii', name: 'Sichuan Yi; Nuosu' }, + { code3: 'ijo', code2: '', name: 'Ijo languages' }, + { code3: 'iku', code2: 'iu', name: 'Inuktitut' }, + { code3: 'ile', code2: 'ie', name: 'Interlingue' }, + { code3: 'ilo', code2: '', name: 'Iloko' }, + { code3: 'ina', code2: 'ia', name: 'Interlingua' }, + { code3: 'inc', code2: '', name: 'Indic languages' }, + { code3: 'ind', code2: 'id', name: 'Indonesian' }, + { code3: 'ine', code2: '', name: 'Indo-European languages' }, + { code3: 'inh', code2: '', name: 'Ingush' }, + { code3: 'ipk', code2: 'ik', name: 'Inupiaq' }, + { code3: 'ira', code2: '', name: 'Iranian languages' }, + { code3: 'iro', code2: '', name: 'Iroquoian languages' }, + { code3: 'isl', code2: 'is', name: 'Icelandic' }, + { code3: 'ita', code2: 'it', name: 'Italian' }, + { code3: 'jav', code2: 'jv', name: 'Javanese' }, + { code3: 'jbo', code2: '', name: 'Lojban' }, + { code3: 'jpn', code2: 'ja', name: 'Japanese' }, + { code3: 'jpr', code2: '', name: 'Judeo-Persian' }, + { code3: 'jrb', code2: '', name: 'Judeo-Arabic' }, + { code3: 'kaa', code2: '', name: 'Kara-Kalpak' }, + { code3: 'kab', code2: '', name: 'Kabyle' }, + { code3: 'kac', code2: '', name: 'Kachin; Jingpho' }, + { code3: 'kal', code2: 'kl', name: 'Kalaallisut' }, + { code3: 'kam', code2: '', name: 'Kamba' }, + { code3: 'kan', code2: 'kn', name: 'Kannada' }, + { code3: 'kar', code2: '', name: 'Karen languages' }, + { code3: 'kas', code2: 'ks', name: 'Kashmiri' }, + { code3: 'kat', code2: 'ka', name: 'Georgian' }, + { code3: 'kau', code2: 'kr', name: 'Kanuri' }, + { code3: 'kaw', code2: '', name: 'Kawi' }, + { code3: 'kaz', code2: 'kk', name: 'Kazakh' }, + { code3: 'kbd', code2: '', name: 'Kabardian' }, + { code3: 'kha', code2: '', name: 'Khasi' }, + { code3: 'khi', code2: '', name: 'Khoisan languages' }, + { code3: 'khm', code2: 'km', name: 'Central Khmer' }, + { code3: 'kho', code2: '', name: 'Khotanese; Sakan' }, + { code3: 'kik', code2: 'ki', name: 'Kikuyu; Gikuyu' }, + { code3: 'kin', code2: 'rw', name: 'Kinyarwanda' }, + { code3: 'kir', code2: 'ky', name: 'Kirghiz; Kyrgyz' }, + { code3: 'kmb', code2: '', name: 'Kimbundu' }, + { code3: 'kok', code2: '', name: 'Konkani' }, + { code3: 'kom', code2: 'kv', name: 'Komi' }, + { code3: 'kon', code2: 'kg', name: 'Kongo' }, + { code3: 'kor', code2: 'ko', name: 'Korean' }, + { code3: 'kos', code2: '', name: 'Kosraean' }, + { code3: 'kpe', code2: '', name: 'Kpelle' }, + { code3: 'krc', code2: '', name: 'Karachay-Balkar' }, + { code3: 'krl', code2: '', name: 'Karelian' }, + { code3: 'kro', code2: '', name: 'Kru languages' }, + { code3: 'kru', code2: '', name: 'Kurukh' }, + { code3: 'kua', code2: 'kj', name: 'Kuanyama; Kwanyama' }, + { code3: 'kum', code2: '', name: 'Kumyk' }, + { code3: 'kur', code2: 'ku', name: 'Kurdish' }, + { code3: 'kut', code2: '', name: 'Kutenai' }, + { code3: 'lad', code2: '', name: 'Ladino' }, + { code3: 'lah', code2: '', name: 'Lahnda' }, + { code3: 'lam', code2: '', name: 'Lamba' }, + { code3: 'lao', code2: 'lo', name: 'Lao' }, + { code3: 'lat', code2: 'la', name: 'Latin' }, + { code3: 'lav', code2: 'lv', name: 'Latvian' }, + { code3: 'lez', code2: '', name: 'Lezghian' }, + { code3: 'lim', code2: 'li', name: 'Limburgish' }, + { code3: 'lin', code2: 'ln', name: 'Lingala' }, + { code3: 'lit', code2: 'lt', name: 'Lithuanian' }, + { code3: 'lol', code2: '', name: 'Mongo' }, + { code3: 'loz', code2: '', name: 'Lozi' }, + { code3: 'ltz', code2: 'lb', name: 'Luxembourgish' }, + { code3: 'lua', code2: '', name: 'Luba-Lulua' }, + { code3: 'lub', code2: 'lu', name: 'Luba-Katanga' }, + { code3: 'lug', code2: 'lg', name: 'Ganda' }, + { code3: 'lui', code2: '', name: 'Luiseno' }, + { code3: 'lun', code2: '', name: 'Lunda' }, + { + code3: 'luo', + code2: '', + name: 'Luo (Kenya and Tanzania)', + }, + { code3: 'lus', code2: '', name: 'Lushai' }, + { code3: 'mac', code2: 'mk', name: 'Macedonian' }, + { code3: 'mad', code2: '', name: 'Madurese' }, + { code3: 'mag', code2: '', name: 'Magahi' }, + { code3: 'mah', code2: 'mh', name: 'Marshallese' }, + { code3: 'mai', code2: '', name: 'Maithili' }, + { code3: 'mak', code2: '', name: 'Makasar' }, + { code3: 'mal', code2: 'ml', name: 'Malayalam' }, + { code3: 'man', code2: '', name: 'Mandingo' }, + { code3: 'mao', code2: 'mi', name: 'Maori' }, + { code3: 'map', code2: '', name: 'Austronesian languages' }, + { code3: 'mar', code2: 'mr', name: 'Marathi' }, + { code3: 'mas', code2: '', name: 'Masai' }, + { code3: 'may', code2: 'ms', name: 'Malay' }, + { code3: 'mdf', code2: '', name: 'Moksha' }, + { code3: 'mdr', code2: '', name: 'Mandar' }, + { code3: 'men', code2: '', name: 'Mende' }, + { code3: 'mga', code2: '', name: 'Irish, Middle (900-1200)' }, + { code3: 'mic', code2: '', name: "Mi'kmaq; Micmac" }, + { code3: 'min', code2: '', name: 'Minangkabau' }, + { code3: 'mis', code2: '', name: 'Uncoded languages' }, + { code3: 'mkd', code2: 'mk', name: 'Macedonian' }, + { code3: 'mkh', code2: '', name: 'Mon-Khmer languages' }, + { code3: 'mlg', code2: 'mg', name: 'Malagasy' }, + { code3: 'mlt', code2: 'mt', name: 'Maltese' }, + { code3: 'mnc', code2: '', name: 'Manchu' }, + { code3: 'mni', code2: '', name: 'Manipuri' }, + { code3: 'mno', code2: '', name: 'Manobo languages' }, + { code3: 'moh', code2: '', name: 'Mohawk' }, + { code3: 'mon', code2: 'mn', name: 'Mongolian' }, + { code3: 'mos', code2: '', name: 'Mossi' }, + { code3: 'mri', code2: 'mi', name: 'Maori' }, + { code3: 'msa', code2: 'ms', name: 'Malay' }, + { code3: 'mul', code2: '', name: 'Multiple languages' }, + { code3: 'mun', code2: '', name: 'Munda languages' }, + { code3: 'mus', code2: '', name: 'Creek' }, + { code3: 'mwl', code2: '', name: 'Mirandese' }, + { code3: 'mwr', code2: '', name: 'Marwari' }, + { code3: 'mya', code2: 'my', name: 'Burmese' }, + { code3: 'myn', code2: '', name: 'Mayan languages' }, + { code3: 'myv', code2: '', name: 'Erzya' }, + { code3: 'nah', code2: '', name: 'Nahuatl languages' }, + { code3: 'nai', code2: '', name: 'North American Indian languages' }, + { code3: 'nap', code2: '', name: 'Neapolitan' }, + { code3: 'nau', code2: 'na', name: 'Nauru' }, + { code3: 'nav', code2: 'nv', name: 'Navajo' }, + { code3: 'nbl', code2: 'nr', name: 'South Ndebele' }, + { code3: 'nde', code2: 'nd', name: 'North Ndebele' }, + { code3: 'ndo', code2: 'ng', name: 'Ndonga' }, + { + code3: 'nds', + code2: '', + name: 'Low German; Low Saxon; German, Low; Saxon, Low', + }, + { code3: 'nep', code2: 'ne', name: 'Nepali' }, + { code3: 'new', code2: '', name: 'Nepal Bhasa; Newari' }, + { code3: 'nia', code2: '', name: 'Nias' }, + { code3: 'nic', code2: '', name: 'Niger-Kordofanian languages' }, + { code3: 'niu', code2: '', name: 'Niuean' }, + { code3: 'nld', code2: 'nl', name: 'Dutch; Flemish' }, + { code3: 'nno', code2: 'nn', name: 'Norwegian Nynorsk' }, + { code3: 'nob', code2: 'nb', name: 'Norwegian Bokmål' }, + { code3: 'nog', code2: '', name: 'Nogai' }, + { code3: 'non', code2: '', name: 'Norse, Old' }, + { code3: 'nor', code2: 'no', name: 'Norwegian' }, + { code3: 'nqo', code2: '', name: "N'Ko" }, + { code3: 'nso', code2: '', name: 'Pedi; Sepedi; Northern Sotho' }, + { code3: 'nub', code2: '', name: 'Nubian languages' }, + { + code3: 'nwc', + code2: '', + name: 'Classical Newari; Old Newari; Classical Nepal Bhasa', + }, + { code3: 'nya', code2: 'ny', name: 'Chichewa; Chewa; Nyanja' }, + { code3: 'nym', code2: '', name: 'Nyamwezi' }, + { code3: 'nyn', code2: '', name: 'Nyankole' }, + { code3: 'nyo', code2: '', name: 'Nyoro' }, + { code3: 'nzi', code2: '', name: 'Nzima' }, + { code3: 'oci', code2: 'oc', name: 'Occitan (post 1500)' }, + { code3: 'oji', code2: 'oj', name: 'Ojibwa' }, + { code3: 'ori', code2: 'or', name: 'Oriya' }, + { code3: 'orm', code2: 'om', name: 'Oromo' }, + { code3: 'osa', code2: '', name: 'Osage' }, + { code3: 'oss', code2: 'os', name: 'Ossetic' }, + { code3: 'ota', code2: '', name: 'Turkish, Ottoman (1500-1928)' }, + { code3: 'oto', code2: '', name: 'Otomian languages' }, + { code3: 'paa', code2: '', name: 'Papuan languages' }, + { code3: 'pag', code2: '', name: 'Pangasinan' }, + { code3: 'pal', code2: '', name: 'Pahlavi' }, + { code3: 'pam', code2: '', name: 'Pampanga; Kapampangan' }, + { code3: 'pan', code2: 'pa', name: 'Panjabi; Punjabi' }, + { code3: 'pap', code2: '', name: 'Papiamento' }, + { code3: 'pau', code2: '', name: 'Palauan' }, + { code3: 'peo', code2: '', name: 'Persian, Old (ca.600-400 B.C.)' }, + { code3: 'per', code2: 'fa', name: 'Persian' }, + { code3: 'phi', code2: '', name: 'Philippine languages' }, + { code3: 'phn', code2: '', name: 'Phoenician' }, + { code3: 'pli', code2: 'pi', name: 'Pali' }, + { code3: 'pol', code2: 'pl', name: 'Polish' }, + { code3: 'pon', code2: '', name: 'Pohnpeian' }, + { code3: 'por', code2: 'pt', name: 'Portuguese' }, + { code3: 'pra', code2: '', name: 'Prakrit languages' }, + { + code3: 'pro', + code2: '', + name: 'Provençal, Old (to 1500);Occitan, Old (to 1500)', + }, + { code3: 'pus', code2: 'ps', name: 'Pushto; Pashto' }, + { code3: 'que', code2: 'qu', name: 'Quechua' }, + { code3: 'raj', code2: '', name: 'Rajasthani' }, + { code3: 'rap', code2: '', name: 'Rapanui' }, + { code3: 'rar', code2: '', name: 'Rarotongan; Cook Islands Maori' }, + { code3: 'roa', code2: '', name: 'Romance languages' }, + { code3: 'roh', code2: 'rm', name: 'Romansh' }, + { code3: 'rom', code2: '', name: 'Romany' }, + { code3: 'rum', code2: 'ro', name: 'Romanian' }, + { code3: 'ron', code2: 'ro', name: 'Romanian' }, + { code3: 'run', code2: 'rn', name: 'Rundi' }, + { code3: 'rup', code2: '', name: 'Aromanian; Arumanian; Macedo-Romanian' }, + { code3: 'rus', code2: 'ru', name: 'Russian' }, + { code3: 'sad', code2: '', name: 'Sandawe' }, + { code3: 'sag', code2: 'sg', name: 'Sango' }, + { code3: 'sah', code2: '', name: 'Yakut' }, + { code3: 'sai', code2: '', name: 'South American Indian languages' }, + { code3: 'sal', code2: '', name: 'Salishan languages' }, + { code3: 'sam', code2: '', name: 'Samaritan Aramaic' }, + { code3: 'san', code2: 'sa', name: 'Sanskrit' }, + { code3: 'sas', code2: '', name: 'Sasak' }, + { code3: 'sat', code2: '', name: 'Santali' }, + { code3: 'scn', code2: '', name: 'Sicilian' }, + { code3: 'sco', code2: '', name: 'Scots' }, + { code3: 'sel', code2: '', name: 'Selkup' }, + { code3: 'sem', code2: '', name: 'Semitic languages' }, + { code3: 'sga', code2: '', name: 'Irish, Old (to 900)' }, + { code3: 'sgn', code2: '', name: 'Sign Languages' }, + { code3: 'shn', code2: '', name: 'Shan' }, + { code3: 'sid', code2: '', name: 'Sidamo' }, + { code3: 'sin', code2: 'si', name: 'Sinhala; Sinhalese' }, + { code3: 'sio', code2: '', name: 'Siouan languages' }, + { code3: 'sit', code2: '', name: 'Sino-Tibetan languages' }, + { code3: 'sla', code2: '', name: 'Slavic languages' }, + { code3: 'slo', code2: 'sk', name: 'Slovak' }, + { code3: 'slk', code2: 'sk', name: 'Slovak' }, + { code3: 'slv', code2: 'sl', name: 'Slovenian' }, + { code3: 'sma', code2: '', name: 'Southern Sami' }, + { code3: 'sme', code2: 'se', name: 'Northern Sami' }, + { code3: 'smi', code2: '', name: 'Sami languages' }, + { code3: 'smj', code2: '', name: 'Lule Sami' }, + { code3: 'smn', code2: '', name: 'Inari Sami' }, + { code3: 'smo', code2: 'sm', name: 'Samoan' }, + { code3: 'sms', code2: '', name: 'Skolt Sami' }, + { code3: 'sna', code2: 'sn', name: 'Shona' }, + { code3: 'snd', code2: 'sd', name: 'Sindhi' }, + { code3: 'snk', code2: '', name: 'Soninke' }, + { code3: 'sog', code2: '', name: 'Sogdian' }, + { code3: 'som', code2: 'so', name: 'Somali' }, + { code3: 'son', code2: '', name: 'Songhai languages' }, + { code3: 'sot', code2: 'st', name: 'Sotho, Southern' }, + { code3: 'spa', code2: 'es', name: 'Spanish' }, + { code3: 'sqi', code2: 'sq', name: 'Albanian' }, + { code3: 'srd', code2: 'sc', name: 'Sardinian' }, + { code3: 'srn', code2: '', name: 'Sranan Tongo' }, + { code3: 'srp', code2: 'sr', name: 'Serbian' }, + { code3: 'srr', code2: '', name: 'Serer' }, + { code3: 'ssa', code2: '', name: 'Nilo-Saharan languages' }, + { code3: 'ssw', code2: 'ss', name: 'Swati' }, + { code3: 'suk', code2: '', name: 'Sukuma' }, + { code3: 'sun', code2: 'su', name: 'Sundanese' }, + { code3: 'sus', code2: '', name: 'Susu' }, + { code3: 'sux', code2: '', name: 'Sumerian' }, + { code3: 'swa', code2: 'sw', name: 'Swahili' }, + { code3: 'swe', code2: 'sv', name: 'Swedish' }, + { code3: 'syc', code2: '', name: 'Classical Syriac' }, + { code3: 'syr', code2: '', name: 'Syriac' }, + { code3: 'tah', code2: 'ty', name: 'Tahitian' }, + { code3: 'tai', code2: '', name: 'Tai languages' }, + { code3: 'tam', code2: 'ta', name: 'Tamil' }, + { code3: 'tat', code2: 'tt', name: 'Tatar' }, + { code3: 'tel', code2: 'te', name: 'Telugu' }, + { code3: 'tem', code2: '', name: 'Timne' }, + { code3: 'ter', code2: '', name: 'Tereno' }, + { code3: 'tet', code2: '', name: 'Tetum' }, + { code3: 'tgk', code2: 'tg', name: 'Tajik' }, + { code3: 'tgl', code2: 'tl', name: 'Tagalog' }, + { code3: 'tha', code2: 'th', name: 'Thai' }, + { code3: 'tib', code2: 'bo', name: 'Tibetan' }, + { code3: 'tig', code2: '', name: 'Tigre' }, + { code3: 'tir', code2: 'ti', name: 'Tigrinya' }, + { code3: 'tiv', code2: '', name: 'Tiv' }, + { code3: 'tkl', code2: '', name: 'Tokelau' }, + { code3: 'tlh', code2: '', name: 'Klingon; tlhIngan-Hol' }, + { code3: 'tli', code2: '', name: 'Tlingit' }, + { code3: 'tmh', code2: '', name: 'Tamashek' }, + { code3: 'tog', code2: '', name: 'Tonga (Nyasa)' }, + { code3: 'ton', code2: 'to', name: 'Tonga (Tonga Islands)' }, + { code3: 'tpi', code2: '', name: 'Tok Pisin' }, + { code3: 'tsi', code2: '', name: 'Tsimshian' }, + { code3: 'tsn', code2: 'tn', name: 'Tswana' }, + { code3: 'tso', code2: 'ts', name: 'Tsonga' }, + { code3: 'tuk', code2: 'tk', name: 'Turkmen' }, + { code3: 'tum', code2: '', name: 'Tumbuka' }, + { code3: 'tup', code2: '', name: 'Tupi languages' }, + { code3: 'tur', code2: 'tr', name: 'Turkish' }, + { code3: 'tut', code2: '', name: 'Altaic languages' }, + { code3: 'tvl', code2: '', name: 'Tuvalu' }, + { code3: 'twi', code2: 'tw', name: 'Twi' }, + { code3: 'tyv', code2: '', name: 'Tuvinian' }, + { code3: 'udm', code2: '', name: 'Udmurt' }, + { code3: 'uga', code2: '', name: 'Ugaritic' }, + { code3: 'uig', code2: 'ug', name: 'Uighur; Uyghur' }, + { code3: 'ukr', code2: 'uk', name: 'Ukrainian' }, + { code3: 'umb', code2: '', name: 'Umbundu' }, + { code3: 'und', code2: '', name: 'Undetermined' }, + { code3: 'urd', code2: 'ur', name: 'Urdu' }, + { code3: 'uzb', code2: 'uz', name: 'Uzbek' }, + { code3: 'vai', code2: '', name: 'Vai' }, + { code3: 'ven', code2: 've', name: 'Venda' }, + { code3: 'vie', code2: 'vi', name: 'Vietnamese' }, + { code3: 'vol', code2: 'vo', name: 'Volapük' }, + { code3: 'vot', code2: '', name: 'Votic' }, + { code3: 'wak', code2: '', name: 'Wakashan languages' }, + { code3: 'wal', code2: '', name: 'Wolaitta; Wolaytta' }, + { code3: 'war', code2: '', name: 'Waray' }, + { code3: 'was', code2: '', name: 'Washo' }, + { code3: 'wel', code2: 'cy', name: 'Welsh' }, + { code3: 'wen', code2: '', name: 'Sorbian languages' }, + { code3: 'wln', code2: 'wa', name: 'Walloon' }, + { code3: 'wol', code2: 'wo', name: 'Wolof' }, + { code3: 'xal', code2: '', name: 'Kalmyk; Oirat' }, + { code3: 'xho', code2: 'xh', name: 'Xhosa' }, + { code3: 'yao', code2: '', name: 'Yao' }, + { code3: 'yap', code2: '', name: 'Yapese' }, + { code3: 'yid', code2: 'yi', name: 'Yiddish' }, + { code3: 'yor', code2: 'yo', name: 'Yoruba' }, + { code3: 'ypk', code2: '', name: 'Yupik languages' }, + { code3: 'zap', code2: '', name: 'Zapotec' }, + { code3: 'zbl', code2: '', name: 'Blissymbols; Blissymbolics; Bliss' }, + { code3: 'zen', code2: '', name: 'Zenaga' }, + { code3: 'zgh', code2: '', name: 'Standard Moroccan Tamazight' }, + { code3: 'zha', code2: 'za', name: 'Zhuang; Chuang' }, + { code3: 'zho', code2: 'zh', name: 'Chinese' }, + { code3: 'znd', code2: '', name: 'Zande languages' }, + { code3: 'zul', code2: 'zu', name: 'Zulu' }, + { code3: 'zun', code2: '', name: 'Zuni' }, + { + code3: 'zza', + code2: '', + name: 'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', + }, +]; +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..f91c56975c --- /dev/null +++ b/src/logger/transports/console.js @@ -0,0 +1,74 @@ +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; + var hasMetadata = Object.keys(metadata).length; + var colorize = withColor((_a = {}, + _a[LogLevel.Debug] = colors.magenta, + _a[LogLevel.Info] = colors.blue, + _a[LogLevel.Log] = colors.green, + _a[LogLevel.Warn] = colors.yellow, + _a[LogLevel.Error] = colors.red, + _a)[level]); + var msg = "".concat(colorize(format(timestamp, 'HH:mm:ss'))); + if (context) { + msg += " ".concat(colorize("(".concat(context, ")"))); + } + if (message) { + msg += " ".concat(message.toString()); + } + if (IS_WEB) { + if (hasMetadata) { + console.groupCollapsed(msg); + console.log(prepareMetadata(metadata)); + console.groupEnd(); + } + else { + console.log(msg); + } + if (message instanceof Error) { + // for stacktrace + console.error(message); + } + } + else { + 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..26d3c883d5 --- /dev/null +++ b/src/logger/types.js @@ -0,0 +1,32 @@ +/** + * 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"; + /** + * 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..90d1dce9f2 --- /dev/null +++ b/src/platform/polyfills.js @@ -0,0 +1,2 @@ +import 'react-native-url-polyfill/auto'; +import 'fast-text-encoding'; diff --git a/src/platform/polyfills.web.js b/src/platform/polyfills.web.js new file mode 100644 index 0000000000..5baea350a0 --- /dev/null +++ b/src/platform/polyfills.web.js @@ -0,0 +1,29 @@ +import 'array.prototype.findlast/auto'; +/// +// @ts-ignore whatever typescript wants to complain about here, I dont care about -prf +window.setImmediate = function (cb) { return setTimeout(cb, 0); }; +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..ebc689e6d4 --- /dev/null +++ b/src/screens/Bookmarks/components/EmptyState.js @@ -0,0 +1,27 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..58bf1d455b --- /dev/null +++ b/src/screens/Bookmarks/index.js @@ -0,0 +1,268 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..2d53d1e370 --- /dev/null +++ b/src/screens/Deactivated.js @@ -0,0 +1,149 @@ +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 { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAccountSwitcher } from '#/lib/hooks/useAccountSwitcher'; +import { logger } from '#/logger'; +import { useAgent, useSession, useSessionApi, } from '#/state/session'; +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 logoutCurrentAccount = useSessionApi().logoutCurrentAccount; + var agent = useAgent(); + var _c = React.useState(false), pending = _c[0], setPending = _c[1]; + var _d = React.useState(), error = _d[0], setError = _d[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 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(); + return [4 /*yield*/, agent.resumeSession(agent.session)]; + 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, setPending, setError, queryClient]); + 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..d381a629fe --- /dev/null +++ b/src/screens/Feeds/NoFollowingFeed.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..e20da2c2a0 --- /dev/null +++ b/src/screens/Feeds/NoSavedFeedsOfAnyType.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..0fc5c3818a --- /dev/null +++ b/src/screens/FindContactsFlowScreen.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 } from "react/jsx-runtime"; +import { useCallback, useLayoutEffect, useState } from 'react'; +import { LayoutAnimationConfig } from 'react-native-reanimated'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { usePreventRemove } from '@react-navigation/native'; +import { useEnableKeyboardControllerScreen } from '#/lib/hooks/useEnableKeyboardController'; +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'); + }); + }); + useEnableKeyboardControllerScreen(true); + 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..b29c23098b --- /dev/null +++ b/src/screens/Hashtag.js @@ -0,0 +1,198 @@ +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, Trans } from '@lingui/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 { 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..a8ed220ece --- /dev/null +++ b/src/screens/Home/NoFeedsPinned.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..2083920832 --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 } from '#/state/queries/list'; +import { 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..04b407a418 --- /dev/null +++ b/src/screens/Log.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; +}; +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, View } from 'react-native'; +import { Pressable } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect } from '@react-navigation/native'; +import { useGetTimeAgo } from '#/lib/hooks/useTimeAgo'; +import { getEntries } from '#/logger/logDump'; +import { useTickEveryMinute } from '#/state/shell'; +import { useSetMinimalShellMode } 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..255d7632c3 --- /dev/null +++ b/src/screens/Login/ChooseAccountForm.js @@ -0,0 +1,120 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { FormContainer } from './FormContainer'; +export var ChooseAccountForm = function (_a) { + var onSelectAccount = _a.onSelectAccount, onPressBack = _a.onPressBack; + var _b = React.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 = React.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.message, + }); + // 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, + _, + ]); + return (_jsxs(FormContainer, { testID: "chooseAccountForm", titleText: _jsx(Trans, { children: "Select account" }), style: web([a.py_2xl]), children: [_jsxs(View, { children: [_jsx(TextField.LabelText, { children: _jsx(Trans, { children: "Sign in as..." }) }), _jsx(AccountList, { onSelectAccount: onSelect, onSelectOther: function () { return onSelectAccount(); }, pendingDid: pendingDid })] }), _jsxs(View, { style: [a.flex_row], children: [_jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Back"], ["Back"])))), variant: "solid", 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..6706335704 --- /dev/null +++ b/src/screens/Login/ForgotPasswordForm.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 __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, { useState } from 'react'; +import { ActivityIndicator, Keyboard, View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import * as EmailValidator from 'email-validator'; +import { isNetworkError } from '#/lib/strings/errors'; +import { cleanError } from '#/lib/strings/errors'; +import { logger } from '#/logger'; +import { Agent } from '#/state/session/agent'; +import { atoms as a, useTheme } from '#/alf'; +import { Button, 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 { Text } from '#/components/Typography'; +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: [a.flex_row, a.align_center, a.pt_md], children: [_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __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 }), !serviceDescription || isProcessing ? (_jsx(ActivityIndicator, {})) : (_jsx(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Next"], ["Next"])))), variant: "solid", color: 'primary', size: "large", onPress: onPressNext, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Next" }) }) })), !serviceDescription || isProcessing ? (_jsx(Text, { style: [t.atoms.text_contrast_high, a.pl_md], children: _jsx(Trans, { children: "Processing..." }) })) : undefined] }), _jsx(View, { style: [ + t.atoms.border_contrast_medium, + a.border_t, + a.pt_2xl, + a.mt_md, + a.flex_row, + a.justify_center, + ], children: _jsx(Button, { testID: "skipSendEmailButton", onPress: onEmailSent, label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Go to next"], ["Go to next"])))), accessibilityHint: _(msg(templateObject_8 || (templateObject_8 = __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; diff --git a/src/screens/Login/FormContainer.js b/src/screens/Login/FormContainer.js new file mode 100644 index 0000000000..e1e91b242c --- /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, useTheme } 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 t = useTheme(); + return (_jsxs(View, { testID: testID, style: [a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.py_md], style], children: [titleText && !gtMobile && (_jsx(Text, { style: [a.text_xl, a.font_semi_bold, t.atoms.text_contrast_high], children: titleText })), children] })); +} diff --git a/src/screens/Login/LoginForm.js b/src/screens/Login/LoginForm.js new file mode 100644 index 0000000000..64101d07e3 --- /dev/null +++ b/src/screens/Login/LoginForm.js @@ -0,0 +1,205 @@ +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, { useRef, useState } from 'react'; +import { ActivityIndicator, Keyboard, LayoutAnimation, View, } from 'react-native'; +import { ComAtprotoServerCreateSession, } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useRequestNotificationsPermission } from '#/lib/notifications/notifications'; +import { isNetworkError } from '#/lib/strings/errors'; +import { cleanError } 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 } 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 } 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(false), isAuthFactorTokenNeeded = _c[0], setIsAuthFactorTokenNeeded = _c[1]; + var identifierValueRef = useRef(initialHandle || ''); + var passwordValueRef = useRef(''); + var _d = useState(''), authFactorToken = _d[0], setAuthFactorToken = _d[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 = React.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(); + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setError(''); + 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"]))))); + return [2 /*return*/]; + } + if (!password) { + setError(_(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please enter your password"], ["Please enter your 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(); + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + 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."]))))); + } + 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, { 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; + }, 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, { 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; + }, 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, { 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, onChangeText: setAuthFactorToken, value: authFactorToken, 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.flex_row, a.align_center, a.pt_md], children: [_jsx(Button, { label: _(msg(templateObject_14 || (templateObject_14 = __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 }), !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"])))), variant: "solid", color: "secondary", size: "large", onPress: onPressRetryConnect, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }) })) : !serviceDescription ? (_jsxs(_Fragment, { children: [_jsx(ActivityIndicator, {}), _jsx(Text, { style: [t.atoms.text_contrast_high, a.pl_md], children: _jsx(Trans, { children: "Connecting..." }) })] })) : (_jsxs(Button, { testID: "loginNextButton", label: _(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Next"], ["Next"])))), accessibilityHint: _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Navigates to the next screen"], ["Navigates to the next screen"])))), variant: "solid", color: "primary", size: "large", onPress: onPressNext, 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, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18; diff --git a/src/screens/Login/PasswordUpdatedForm.js b/src/screens/Login/PasswordUpdatedForm.js new file mode 100644 index 0000000000..9ca20f8d23 --- /dev/null +++ b/src/screens/Login/PasswordUpdatedForm.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { atoms as a, useBreakpoints } 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_semi_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: [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"])))), variant: "solid", 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..2aad9d8d31 --- /dev/null +++ b/src/screens/Login/SetNewPasswordForm.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 { useState } from 'react'; +import { ActivityIndicator, View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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, useTheme } from '#/alf'; +import { Button, 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 { Text } from '#/components/Typography'; +import { useAnalytics } from '#/analytics'; +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 t = useTheme(); + 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: [a.flex_row, a.align_center, a.pt_lg], 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 }), isProcessing ? (_jsx(ActivityIndicator, {})) : (_jsx(Button, { label: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Next"], ["Next"])))), variant: "solid", color: "primary", size: "large", onPress: onPressNext, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Next" }) }) })), isProcessing ? (_jsx(Text, { style: [t.atoms.text_contrast_high, a.pl_md], children: _jsx(Trans, { children: "Updating..." }) })) : undefined] })] })); +}; +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/index.js b/src/screens/Login/index.js new file mode 100644 index 0000000000..a28f82b43c --- /dev/null +++ b/src/screens/Login/index.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; +}; +import { jsx as _jsx } 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/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'; +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 = ''; + 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"])))); + content = (_jsx(LoginForm, { error: error, serviceUrl: serviceUrl, serviceDescription: serviceDescription, initialHandle: initialHandle, setError: setError, onAttemptFailed: onAttemptFailed, onAttemptSuccess: onAttemptSuccess, setServiceUrl: setServiceUrl, onPressBack: function () { + return accounts.length ? gotoForm(Forms.ChooseAccount) : handlePressBack(); + }, 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"])))); + content = (_jsx(ChooseAccountForm, { onSelectAccount: onSelectAccount, onPressBack: handlePressBack })); + 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!"])))); + content = (_jsx(ForgotPasswordForm, { error: error, serviceUrl: serviceUrl, serviceDescription: serviceDescription, setError: setError, setServiceUrl: setServiceUrl, onPressBack: function () { return gotoForm(Forms.Login); }, 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!"])))); + content = (_jsx(SetNewPasswordForm, { error: error, serviceUrl: serviceUrl, setError: setError, onPressBack: function () { return gotoForm(Forms.ForgotPassword); }, 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; + } + return (_jsx(Animated.View, { style: a.flex_1, entering: native(FadeIn.duration(90)), children: _jsx(KeyboardAvoidingView, { testID: "signIn", behavior: "padding", style: a.flex_1, children: _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..9213dafa97 --- /dev/null +++ b/src/screens/Messages/ChatList.js @@ -0,0 +1,305 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..a88d1690a0 --- /dev/null +++ b/src/screens/Messages/Conversation.js @@ -0,0 +1,158 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect, useNavigation, useRoute, } from '@react-navigation/native'; +import { useEnableKeyboardControllerScreen } from '#/lib/hooks/useEnableKeyboardController'; +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; + useEnableKeyboardControllerScreen(true); + 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..b4458d0228 --- /dev/null +++ b/src/screens/Messages/Inbox.js @@ -0,0 +1,236 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..df14b2842b --- /dev/null +++ b/src/screens/Messages/Settings.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, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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(templateObject_5 || (templateObject_5 = __makeTemplateObject(["No one"], ["No one"])))), style: [a.justify_between, a.py_sm], children: [_jsx(Toggle.LabelText, { children: _jsx(Trans, { 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_6 || (templateObject_6 = __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_7 || (templateObject_7 = __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_8 || (templateObject_8 = __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, templateObject_8; diff --git a/src/screens/Messages/components/ChatDisabled.js b/src/screens/Messages/components/ChatDisabled.js new file mode 100644 index 0000000000..0260f0e432 --- /dev/null +++ b/src/screens/Messages/components/ChatDisabled.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, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { ToolsOzoneReportDefs } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..ac50eefd12 --- /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/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..93e041fed1 --- /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/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..9f9eaa29aa --- /dev/null +++ b/src/screens/Messages/components/InboxPreview.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..ec00905361 --- /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/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..b426fc3b31 --- /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/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..1a587d8ff4 --- /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/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..5d05e8619b --- /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/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..b2de5fe993 --- /dev/null +++ b/src/screens/Messages/components/RequestButtons.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..cf6abc753b --- /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/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..57ff00a692 --- /dev/null +++ b/src/screens/Moderation/VerificationSettings.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..29bee74cc0 --- /dev/null +++ b/src/screens/Moderation/index.js @@ -0,0 +1,231 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect } from '@react-navigation/native'; +import { getLabelingServiceTitle } from '#/lib/moderation'; +import { logger } from '#/logger'; +import { useIsBirthdateUpdateAllowed } from '#/state/birthdate'; +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 { Admonition } from '#/components/Admonition'; +import { AgeAssuranceAdmonition } from '#/components/ageAssurance/AgeAssuranceAdmonition'; +import { useAgeAssuranceCopy } from '#/components/ageAssurance/useAgeAssuranceCopy'; +import { Button } 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 { 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 aa = useAgeAssurance(); + var isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed(); + var aaCopy = useAgeAssuranceCopy(); + useFocusEffect(useCallback(function () { + setMinimalShellMode(false); + }, [setMinimalShellMode])); + var _c = usePreferencesSetAdultContentMutation(), setAdultContentPref = _c.mutateAsync, optimisticAdultContent = _c.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_1; + 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_1 = _a.sent(); + logger.error("Failed to set adult content pref", { + message: e_1.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, { 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_2 || (templateObject_2 = __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_3 || (templateObject_3 = __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_4 || (templateObject_4 = __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_5 || (templateObject_5 = __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_6 || (templateObject_6 = __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_7 || (templateObject_7 = __makeTemplateObject(["View your moderation lists"], ["View your moderation lists"])))), testID: "moderationlistsBtn", to: "/moderation/modlists", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Moderation lists"], ["Moderation lists"])))), icon: Group, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Link, { label: _(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["View your muted accounts"], ["View your muted accounts"])))), testID: "mutedAccountsBtn", to: "/moderation/muted-accounts", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Muted accounts"], ["Muted accounts"])))), icon: Person, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Link, { label: _(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["View your blocked accounts"], ["View your blocked accounts"])))), testID: "blockedAccountsBtn", to: "/moderation/blocked-accounts", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Blocked accounts"], ["Blocked accounts"])))), icon: CircleBanSign, style: [ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ] })); } }), _jsx(Divider, {}), _jsx(Link, { label: _(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Manage verification settings"], ["Manage verification settings"])))), testID: "verificationSettingsBtn", to: "/moderation/verification-settings", children: function (state) { return (_jsx(SubItem, { title: _(msg(templateObject_14 || (templateObject_14 = __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_15 || (templateObject_15 = __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_16 || (templateObject_16 = __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" }) }), 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; diff --git a/src/screens/ModerationInteractionSettings/index.js b/src/screens/ModerationInteractionSettings/index.js new file mode 100644 index 0000000000..c614e123ce --- /dev/null +++ b/src/screens/ModerationInteractionSettings/index.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..aacdf7e3e8 --- /dev/null +++ b/src/screens/Notifications/ActivityList.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 { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..b28f4c7de0 --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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_WEB } from '#/env'; +import { IS_INTERNAL } 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..138896c21d --- /dev/null +++ b/src/screens/Onboarding/StepFindContactsIntro/index.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..a0b12b1d5d --- /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/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..49472e7f87 --- /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/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..3f465d878e --- /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/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..0078d26134 --- /dev/null +++ b/src/screens/Onboarding/StepFinished/index.js @@ -0,0 +1,330 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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)), + (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..11d502f257 --- /dev/null +++ b/src/screens/Onboarding/StepInterests/index.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..c7bf06b225 --- /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/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..46f17d2c5f --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..02d8833d8a --- /dev/null +++ b/src/screens/Onboarding/StepProfile/index.js @@ -0,0 +1,264 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..ad35b7678e --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.js @@ -0,0 +1,280 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { 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 { useSuggestedUsers } from '#/screens/Search/util/useSuggestedUsers'; +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 = useSuggestedUsers({ + 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, + }); + }, + 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 () { + 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: undefined, + position: position, + suggestedDid: did, + category: selectedInterest, + }); + } + }, [ax, selectedInterest]); + 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 }, 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 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; + 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: undefined, + 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..44e354ec57 --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.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 { useState } from 'react'; +import { View } from 'react-native'; +import { AppBskyGraphStarterpack } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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)]; + 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..73fd2a97d2 --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/index.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 { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..090d86be74 --- /dev/null +++ b/src/screens/Onboarding/index.js @@ -0,0 +1,46 @@ +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 { useEnableKeyboardControllerScreen } from '#/lib/hooks/useEnableKeyboardController'; +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]; + useEnableKeyboardControllerScreen(true); + 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..aba86da6fc --- /dev/null +++ b/src/screens/Onboarding/util.js @@ -0,0 +1,110 @@ +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) { + 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(), + }; + }); + 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..0757250893 --- /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/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..2edfee9fe9 --- /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/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..7b2fa97363 --- /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/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..e4d48fb348 --- /dev/null +++ b/src/screens/PostThread/components/HeaderDropdown.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..4ada4bca1f --- /dev/null +++ b/src/screens/PostThread/components/ThreadComposePrompt.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 { View } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..6a6b4a42ab --- /dev/null +++ b/src/screens/PostThread/components/ThreadError.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 { useMemo } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..20b02dffb8 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchor.js @@ -0,0 +1,359 @@ +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 { msg, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useActorStatus } from '#/lib/actor-status'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { useTranslate } from '#/lib/hooks/useTranslate'; +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 { 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 { colors } from '#/components/Admonition'; +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 { 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 { 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 * 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(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 _ = useLingui()._; + 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, + }); + 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, 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: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Reposts of this post"], ["Reposts of this post"])))), children: _jsxs(Text, { testID: "repostCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], 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: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Quotes of this post"], ["Quotes of this post"])))), children: _jsxs(Text, { testID: "quoteCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], 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: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Likes on this post"], ["Likes on this post"])))), children: _jsxs(Text, { testID: "likeCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], 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 ? (_jsxs(Text, { testID: "bookmarkCount-expanded", style: [a.text_md, t.atoms.text_contrast_medium], 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 ExpandedPostDetails(_a) { + var post = _a.post, isThreadAuthor = _a.isThreadAuthor; + var t = useTheme(); + var ax = useAnalytics(); + var _b = useLingui(), _ = _b._, i18n = _b.i18n; + var translate = useTranslate(); + var isRootPost = !('reply' in post.record); + var langPrefs = useLanguagePrefs(); + var needsTranslation = useMemo(function () { + return Boolean(langPrefs.primaryLanguage && + !isPostInLanguage(post, [langPrefs.primaryLanguage])); + }, [post, langPrefs.primaryLanguage]); + var onTranslatePress = useCallback(function (e) { + var _a; + e.preventDefault(); + translate(post.record.text || '', langPrefs.primaryLanguage); + 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, translate, langPrefs, post]); + 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 })), needsTranslation && (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium], children: "\u00B7" }), _jsx(InlineLinkText + // overridden to open an intent on android, but keep + // as anchor tag for accessibility + , { + // overridden to open an intent on android, but keep + // as anchor tag for accessibility + to: getTranslatorLink(post.record.text, langPrefs.primaryLanguage), label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Translate"], ["Translate"])))), style: [a.text_sm], onPress: onTranslatePress, children: _jsx(Trans, { children: "Translate" }) })] }))] })] })); +} +function BackdatedPostIndicator(_a) { + var post = _a.post; + var t = useTheme(); + var _b = useLingui(), _ = _b._, 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; + var orange = colors.warning; + return (_jsxs(_Fragment, { children: [_jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Archived post"], ["Archived post"])))), accessibilityHint: _(msg(templateObject_6 || (templateObject_6 = __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: orange, 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: [_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(Text, { style: [ + a.text_md, + a.leading_snug, + t.atoms.text_contrast_high, + a.pb_xl, + ], children: _jsx(Trans, { children: "Bluesky cannot confirm the authenticity of the claimed date." }) }), _jsx(Prompt.Actions, { children: _jsx(Prompt.Action, { cta: _(msg(templateObject_7 || (templateObject_7 = __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; diff --git a/src/screens/PostThread/components/ThreadItemAnchorFollowButton.js b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.js new file mode 100644 index 0000000000..828cde5671 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.js @@ -0,0 +1,164 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..692b78591a --- /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/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..9dab5fa393 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemPost.js @@ -0,0 +1,165 @@ +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/macro'; +import { useActorStatus } from '#/lib/actor-status'; +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'; +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, + }); + }, [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(_Fragment, { 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..4292bd0a34 --- /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/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..1108397b86 --- /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/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..7ceed30ae5 --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReadMore.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, Fragment as _Fragment } from "react/jsx-runtime"; +import { memo } from 'react'; +import { View } from 'react-native'; +import { msg, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..a1549d10cd --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemReadMoreUp.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 { memo } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..9339923919 --- /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/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..f5ce19a71a --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemTreePost.js @@ -0,0 +1,192 @@ +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/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, + }); + }, [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(_Fragment, { 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..7234d6903c --- /dev/null +++ b/src/screens/PostThread/index.js @@ -0,0 +1,506 @@ +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/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, + }); + 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..baee707872 --- /dev/null +++ b/src/screens/Profile/ErrorState.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 React from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..7ff843c98c --- /dev/null +++ b/src/screens/Profile/Header/EditProfileDialog.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 __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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..18192dfc05 --- /dev/null +++ b/src/screens/Profile/Header/Handle.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 { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..252f82eb09 --- /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/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..01fdc810f7 --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.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 __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, plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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: [_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..965e5531b6 --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderStandard.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useActorStatus } from '#/lib/actor-status'; +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 { EditProfileDialog } from './EditProfileDialog'; +import { ProfileHeaderHandle } from './Handle'; +import { ProfileHeaderMetrics } from './Metrics'; +import { ProfileHeaderShell } from './Shell'; +import { AnimatedProfileHeaderSuggestedFollows } from './SuggestedFollows'; +var ProfileHeaderStandard = function (_a) { + var _b, _c, _d, _e, _f; + var profileUnshadowed = _a.profile, descriptionRT = _a.descriptionRT, moderationOpts = _a.moderationOpts, _g = _a.hideBackButton, hideBackButton = _g === void 0 ? false : _g, 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 _h = useProfileBlockMutationQueue(profile), queueUnblock = _h[1]; + var unblockPromptControl = Prompt.usePromptControl(); + var _j = useState(false), showSuggestedFollows = _j[0], setShowSuggestedFollows = _j[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, value: descriptionRT, enableTags: true, authorHandle: profile.handle }) })) : undefined, !isMe && + !isBlockedUser && + shouldShowKnownFollowers((_e = profile.viewer) === null || _e === void 0 ? void 0 : _e.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: ((_f = profile.viewer) === null || _f === void 0 ? void 0 : _f.blocking) ? _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Unblock"], ["Unblock"])))) : _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Block"], ["Block"])))), confirmButtonColor: "negative" })] }), _jsx(AnimatedProfileHeaderSuggestedFollows, { 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: 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..8187d717e6 --- /dev/null +++ b/src/screens/Profile/Header/Shell.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; +}; +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/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { useActorStatus } from '#/lib/actor-status'; +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 { EditLiveDialog } from '#/components/live/EditLiveDialog'; +import { LiveIndicator } from '#/components/live/LiveIndicator'; +import { LiveStatusDialog } from '#/components/live/LiveStatusDialog'; +import { LabelsOnMe } from '#/components/moderation/LabelsOnMe'; +import { ProfileHeaderAlerts } from '#/components/moderation/ProfileHeaderAlerts'; +import { useAnalytics } from '#/analytics'; +import { IS_IOS } from '#/env'; +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' + ? { + // 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 () { + 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; + if (avatar_1 && !(modui.blur && modui.noOverride)) { + runOnUI(function () { + 'worklet'; + var rect = measure(aviRef); + runOnJS(_openLightbox)(avatar_1, rect); + })(); + } + } + }, [ + 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..aa3b7936aa --- /dev/null +++ b/src/screens/Profile/Header/SuggestedFollows.js @@ -0,0 +1,156 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import React 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'; +var DISMISS_ANIMATION_DURATION = 200; +export function ProfileHeaderSuggestedFollows(_a) { + var actorDid = _a.actorDid; + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var maxLength = gtMobile ? 4 : 12; + var _b = useSuggestedFollowsByActorQuery({ + did: actorDid, + }), isLoading = _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 = React.useState(new Set()), dismissedDids = _d[0], setDismissedDids = _d[1]; + var _e = React.useState(new Set()), dismissingDids = _e[0], setDismissingDids = _e[1]; + var onDismiss = React.useCallback(function (did) { + // Start the fade animation + setDismissingDids(function (prev) { return new Set(prev).add(did); }); + // After animation completes, actually remove from list + setTimeout(function () { + setDismissedDids(function (prev) { return new Set(prev).add(did); }); + setDismissingDids(function (prev) { + var next = new Set(prev); + next.delete(did); + return next; + }); + }, DISMISS_ANIMATION_DURATION); + }, []); + // Combine profiles from the actor-specific query with fallback suggestions + var allProfiles = React.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; })) !== 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(profile); + } + } + for (var _c = 0, fallbackProfiles_1 = fallbackProfiles; _c < fallbackProfiles_1.length; _c++) { + var profile = fallbackProfiles_1[_c]; + if (!seen.has(profile.did) && profile.did !== actorDid) { + seen.add(profile.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]); + var filteredProfiles = React.useMemo(function () { + return allProfiles.filter(function (p) { return !dismissedDids.has(p.did); }); + }, [allProfiles, dismissedDids]); + // Fetch more when running low + React.useEffect(function () { + if (moderationOpts && + filteredProfiles.length < maxLength && + hasNextPage && + !isFetchingNextPage) { + fetchNextPage(); + } + }, [ + filteredProfiles.length, + maxLength, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + moderationOpts, + ]); + return (_jsx(ProfileGrid, { isSuggestionsLoading: isLoading, profiles: filteredProfiles, totalProfileCount: allProfiles.length, recId: data === null || data === void 0 ? void 0 : data.recId, error: error, viewContext: "profileHeader", onDismiss: onDismiss, dismissingDids: dismissingDids })); +} +export function AnimatedProfileHeaderSuggestedFollows(_a) { + var isExpanded = _a.isExpanded, actorDid = _a.actorDid; + var gtMobile = useBreakpoints().gtMobile; + var moderationOpts = useModerationOpts(); + var maxLength = gtMobile ? 4 : 12; + var _b = useSuggestedFollowsByActorQuery({ + did: actorDid, + }), isLoading = _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 = React.useState(new Set()), dismissedDids = _d[0], setDismissedDids = _d[1]; + var _e = React.useState(new Set()), dismissingDids = _e[0], setDismissingDids = _e[1]; + var onDismiss = React.useCallback(function (did) { + // Start the fade animation + setDismissingDids(function (prev) { return new Set(prev).add(did); }); + // After animation completes, actually remove from list + setTimeout(function () { + setDismissedDids(function (prev) { return new Set(prev).add(did); }); + setDismissingDids(function (prev) { + var next = new Set(prev); + next.delete(did); + return next; + }); + }, DISMISS_ANIMATION_DURATION); + }, []); + // Combine profiles from the actor-specific query with fallback suggestions + var allProfiles = React.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; })) !== null && _b !== void 0 ? _b : []; + // Dedupe by did, preferring actor-specific profiles + var seen = new Set(); + var combined = []; + for (var _i = 0, actorProfiles_2 = actorProfiles; _i < actorProfiles_2.length; _i++) { + var profile = actorProfiles_2[_i]; + if (!seen.has(profile.did)) { + seen.add(profile.did); + combined.push(profile); + } + } + for (var _c = 0, fallbackProfiles_2 = fallbackProfiles; _c < fallbackProfiles_2.length; _c++) { + var profile = fallbackProfiles_2[_c]; + if (!seen.has(profile.did) && profile.did !== actorDid) { + seen.add(profile.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]); + var filteredProfiles = React.useMemo(function () { + return allProfiles.filter(function (p) { return !dismissedDids.has(p.did); }); + }, [allProfiles, dismissedDids]); + // Fetch more when running low + React.useEffect(function () { + if (moderationOpts && + filteredProfiles.length < maxLength && + hasNextPage && + !isFetchingNextPage) { + fetchNextPage(); + } + }, [ + filteredProfiles.length, + maxLength, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + moderationOpts, + ]); + 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, recId: data === null || data === void 0 ? void 0 : data.recId, error: error, viewContext: "profileHeader", onDismiss: onDismiss, dismissingDids: dismissingDids, isVisible: isExpanded }) })); +} 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..911e04d29a --- /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/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..ca6df9953d --- /dev/null +++ b/src/screens/Profile/ProfileFeed/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; +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import React, { useCallback, useMemo } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useAnimatedRef } from 'react-native-reanimated'; +import { AppBskyFeedDefs } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useIsFocused, useNavigation } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { VIDEO_FEED_URIS } from '#/lib/constants'; +import { useOpenComposer } from '#/lib/hooks/useOpenComposer'; +import { usePalette } from '#/lib/hooks/usePalette'; +import { useSetTitle } from '#/lib/hooks/useSetTitle'; +import { ComposeIcon2 } from '#/lib/icons'; +import { makeRecordUri } from '#/lib/strings/url-helpers'; +import { s } from '#/lib/styles'; +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 { FAB } from '#/view/com/util/fab/FAB'; +import { Button } from '#/view/com/util/forms/Button'; +import { LoadLatestBtn } from '#/view/com/util/load-latest/LoadLatestBtn'; +import { PostFeedLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder'; +import { Text } from '#/view/com/util/text/Text'; +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 pal = usePalette('default'); + var _ = useLingui()._; + var navigation = useNavigation(); + var uri = useMemo(function () { return makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey); }, [rkey, handleOrDid]); + var _b = useResolveUriQuery(uri), error = _b.error, resolvedUri = _b.data; + var onPressBack = React.useCallback(function () { + if (navigation.canGoBack()) { + navigation.goBack(); + } + else { + navigation.navigate('Home'); + } + }, [navigation]); + if (error) { + return (_jsx(Layout.Screen, { testID: "profileFeedScreenError", children: _jsx(Layout.Content, { children: _jsxs(View, { style: [pal.view, pal.border, styles.notFoundContainer], children: [_jsx(Text, { type: "title-lg", style: [pal.text, s.mb10], children: _jsx(Trans, { children: "Could not load feed" }) }), _jsx(Text, { type: "md", style: [pal.text, s.mb20], children: error.toString() }), _jsx(View, { style: { flexDirection: 'row' }, children: _jsx(Button, { type: "default", accessibilityLabel: _(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, style: { flexShrink: 1 }, children: _jsx(Text, { type: "button", style: pal.text, children: _jsx(Trans, { children: "Go Back" }) }) }) })] }) }) })); + } + 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 = React.useState(false), hasNew = _b[0], setHasNew = _b[1]; + var _c = React.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, + }); + truncateAndInvalidate(queryClient, FEED_RQKEY(feed)); + setHasNew(false); + }, [scrollElRef, queryClient, feed, setHasNew]); + React.useEffect(function () { + if (!isScreenFocused) { + return; + } + return listenSoftReset(onScrollToTop); + }, [onScrollToTop, isScreenFocused]); + var renderPostsEmpty = useCallback(function () { + return (_jsx(EmptyState, { icon: HashtagWideIcon, iconSize: "2xl", message: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["This feed is empty."], ["This feed is empty."])))) })); + }, [_]); + var isVideoFeed = React.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_4 || (templateObject_4 = __makeTemplateObject(["Load new posts"], ["Load new posts"])))), showIndicator: hasNew })), hasSession && (_jsx(FAB, { testID: "composeFAB", onPress: function () { return openComposer({}); }, 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: "" }))] })); +} +var styles = StyleSheet.create({ + btn: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingVertical: 7, + paddingHorizontal: 14, + borderRadius: 50, + marginLeft: 6, + }, + notFoundContainer: { + margin: 10, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 6, + }, + aboutSectionContainer: { + paddingVertical: 4, + paddingHorizontal: 16, + gap: 12, + }, +}); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5; diff --git a/src/screens/Profile/ProfileFollowers.js b/src/screens/Profile/ProfileFollowers.js new file mode 100644 index 0000000000..3088f51cc0 --- /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/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..ee20769551 --- /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/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..aa67ccef02 --- /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/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..7044deb2bd --- /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/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..22c2c0970c --- /dev/null +++ b/src/screens/Profile/Sections/Feed.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 { useCallback, useEffect, useImperativeHandle, useState } from 'react'; +import { findNodeHandle, View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..2c7aefc27a --- /dev/null +++ b/src/screens/Profile/Sections/Labels.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 { useCallback, useEffect, useImperativeHandle, useMemo } from 'react'; +import { findNodeHandle, View } from 'react-native'; +import { interpretLabelValueDefinitions, } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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/ProfileFeedHeader.js b/src/screens/Profile/components/ProfileFeedHeader.js new file mode 100644 index 0000000000..70ba878831 --- /dev/null +++ b/src/screens/Profile/components/ProfileFeedHeader.js @@ -0,0 +1,321 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { DotGrid_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.like + : 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", variant: "solid", color: "secondary", onPress: onToggleLiked, style: [a.flex_1], children: [isLiked ? (_jsx(HeartFilled, { size: "sm", fill: t.palette.like })) : (_jsx(ButtonIcon, { icon: Heart, position: "left" })), _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", variant: "solid", 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..b6b482489f --- /dev/null +++ b/src/screens/ProfileList/AboutSection.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 { useCallback, useImperativeHandle, useState } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..c61ffc3a27 --- /dev/null +++ b/src/screens/ProfileList/FeedSection.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 { useCallback, useEffect, useImperativeHandle, useState } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..6e30770bbd --- /dev/null +++ b/src/screens/ProfileList/components/ErrorScreen.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 { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..592486ddb7 --- /dev/null +++ b/src/screens/ProfileList/components/Header.js @@ -0,0 +1,186 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..4eeb880cc2 --- /dev/null +++ b/src/screens/ProfileList/components/MoreOptionsMenu.js @@ -0,0 +1,224 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { DotGrid_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..61bf280b84 --- /dev/null +++ b/src/screens/ProfileList/components/SubscribeMenu.js @@ -0,0 +1,120 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..2b1211816b --- /dev/null +++ b/src/screens/ProfileList/index.js @@ -0,0 +1,124 @@ +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 } from 'react'; +import { View } from 'react-native'; +import { useAnimatedRef } from 'react-native-reanimated'; +import { AppBskyGraphDefs, AtUri, moderateUserList, } from '@atproto/api'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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, platform } 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 { 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 _ = 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"]))))]; + 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({}); }, 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, { children: renderHeader() }), _jsx(AboutSection, { list: list, scrollElRef: scrollElRef, onPressAddUser: addUserDialogControl.open, headerHeight: 0 }), _jsx(FAB, { testID: "composeFAB", onPress: function () { return openComposer({}); }, 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..5d66d8324c --- /dev/null +++ b/src/screens/SavedFeeds.js @@ -0,0 +1,252 @@ +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 Animated, { LinearTransition } from 'react-native-reanimated'; +import { TID } from '@atproto/common-web'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect } from '@react-navigation/native'; +import { 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 { 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 { 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; + if (!preferences) { + return _jsx(View, {}); + } + 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(); + /* + * 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; + 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, { 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." }) }) })) : (pinnedFeeds.map(function (f) { return (_jsx(ListItem, { feed: f, isPinned: true, currentFeeds: currentFeeds, setCurrentFeeds: setCurrentFeeds, preferences: preferences }, f.id)); }))) : (_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(ListItem, { feed: f, isPinned: false, currentFeeds: currentFeeds, setCurrentFeeds: setCurrentFeeds, preferences: preferences }, 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 ListItem(_a) { + var _this = this; + var feed = _a.feed, isPinned = _a.isPinned, currentFeeds = _a.currentFeeds, setCurrentFeeds = _a.setCurrentFeeds; + var _ = useLingui()._; + var t = useTheme(); + var playHaptic = useHaptics(); + var feedUri = feed.value; + var onTogglePinned = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + playHaptic(); + setCurrentFeeds(currentFeeds.map(function (f) { + return f.id === feed.id ? __assign(__assign({}, feed), { pinned: !feed.pinned }) : f; + })); + return [2 /*return*/]; + }); + }); }; + var onPressUp = function () { return __awaiter(_this, void 0, void 0, function () { + var nextFeeds, ids, index, nextIndex; + var _a; + return __generator(this, function (_b) { + if (!isPinned) + return [2 /*return*/]; + nextFeeds = currentFeeds.slice(); + ids = currentFeeds.map(function (f) { return f.id; }); + index = ids.indexOf(feed.id); + nextIndex = index - 1; + if (index === -1 || index === 0) + return [2 /*return*/]; + _a = [ + nextFeeds[nextIndex], + nextFeeds[index], + ], nextFeeds[index] = _a[0], nextFeeds[nextIndex] = _a[1]; + setCurrentFeeds(nextFeeds); + return [2 /*return*/]; + }); + }); }; + var onPressDown = function () { return __awaiter(_this, void 0, void 0, function () { + var nextFeeds, ids, index, nextIndex; + var _a; + return __generator(this, function (_b) { + if (!isPinned) + return [2 /*return*/]; + nextFeeds = currentFeeds.slice(); + ids = currentFeeds.map(function (f) { return f.id; }); + index = ids.indexOf(feed.id); + nextIndex = index + 1; + if (index === -1 || index >= nextFeeds.filter(function (f) { return f.pinned; }).length - 1) + return [2 /*return*/]; + _a = [ + nextFeeds[nextIndex], + nextFeeds[index], + ], nextFeeds[index] = _a[0], nextFeeds[nextIndex] = _a[1]; + setCurrentFeeds(nextFeeds); + return [2 /*return*/]; + }); + }); }; + var onPressRemove = function () { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + playHaptic(); + setCurrentFeeds(currentFeeds.filter(function (f) { return f.id !== feed.id; })); + return [2 /*return*/]; + }); + }); }; + return (_jsxs(Animated.View, { style: [a.flex_row, a.border_b, t.atoms.border_contrast_low], layout: LinearTransition.duration(100), children: [feed.type === 'timeline' ? (_jsx(FollowingFeedCard, {})) : (_jsx(FeedSourceCard, { feedUri: feedUri, style: [isPinned && a.pr_sm], showMinimalPlaceholder: true, hideTopBorder: true }, feedUri)), _jsxs(View, { style: [a.pr_lg, a.flex_row, a.align_center, a.gap_sm], children: [isPinned ? (_jsxs(_Fragment, { children: [_jsx(Button, { testID: "feed-".concat(feed.type, "-moveUp"), label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Move feed up"], ["Move feed up"])))), onPress: onPressUp, size: "small", color: "secondary", shape: "square", children: _jsx(ButtonIcon, { icon: ArrowUpIcon }) }), _jsx(Button, { testID: "feed-".concat(feed.type, "-moveDown"), label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Move feed down"], ["Move feed down"])))), onPress: onPressDown, size: "small", color: "secondary", shape: "square", children: _jsx(ButtonIcon, { icon: ArrowDownIcon }) })] })) : (_jsx(Button, { testID: "feed-".concat(feedUri, "-toggleSave"), label: _(msg(templateObject_6 || (templateObject_6 = __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: isPinned ? _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Unpin feed"], ["Unpin feed"])))) : _(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Pin feed"], ["Pin feed"])))), onPress: onTogglePinned, size: "small", color: isPinned ? 'primary_subtle' : '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; diff --git a/src/screens/Search/Explore.js b/src/screens/Search/Explore.js new file mode 100644 index 0000000000..0341751a2f --- /dev/null +++ b/src/screens/Search/Explore.js @@ -0,0 +1,856 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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()._; + return (_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Load more"], ["Load more"])))), onPress: item.onLoadMore, 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: 'feedsError', + message: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Failed to load suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(feedsError), + }); + } + 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 suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(feedsError), + }); + } + else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg(templateObject_11 || (templateObject_11 = __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_12 || (templateObject_12 = __makeTemplateObject(["Failed to load suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(feedsError), + }); + } + else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg(templateObject_13 || (templateObject_13 = __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_14 || (templateObject_14 = __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_15 || (templateObject_15 = __makeTemplateObject(["Failed to load suggested feeds"], ["Failed to load suggested feeds"])))), + error: cleanError(feedsError), + }); + } + else if (preferencesError) { + i.push({ + type: 'error', + key: 'preferencesError', + message: _(msg(templateObject_16 || (templateObject_16 = __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_17 || (templateObject_17 = __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; + 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_18 || (templateObject_18 = __makeTemplateObject(["There was an issue fetching posts. Tap here to try again."], ["There was an issue fetching posts. Tap here to try again."])))), onPress: fetchNextPageFeedPreviews })); + } + 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]); + 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: onLoadMoreFeedPreviews, + /** + * 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: onPTR })); +} +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; diff --git a/src/screens/Search/SearchResults.js b/src/screens/Search/SearchResults.js new file mode 100644 index 0000000000..a6051eb349 --- /dev/null +++ b/src/screens/Search/SearchResults.js @@ -0,0 +1,276 @@ +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 { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 _ = useLingui()._; + var sections = useMemo(function () { + if (!queryWithParams) + return []; + var noParams = queryWithParams === query; + return [ + { + title: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Top"], ["Top"])))), + component: (_jsx(SearchScreenPostResults, { query: queryWithParams, sort: "top", active: activeTab === 0 })), + }, + { + title: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Latest"], ["Latest"])))), + component: (_jsx(SearchScreenPostResults, { query: queryWithParams, sort: "latest", active: activeTab === 1 })), + }, + noParams && { + title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["People"], ["People"])))), + component: (_jsx(SearchScreenUserResults, { query: query, active: activeTab === 2 })), + }, + noParams && { + title: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Feeds"], ["Feeds"])))), + component: (_jsx(SearchScreenFeedsResults, { query: query, active: activeTab === 3 })), + }, + ].filter(Boolean); + }, [_, query, queryWithParams, activeTab]); + 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: initialPage, 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 _ = useLingui()._; + return (_jsxs(_Fragment, { children: [_jsx(Text, { style: [a.text_lg, t.atoms.text_contrast_high], children: _jsxs(Trans, { children: ["No results found for \"", _jsx(Text, { style: [a.text_lg, t.atoms.text, a.font_medium], children: query }), "\"."] }) }), '\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: _(msg({ + 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 _ = useLingui()._; + 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; + 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: _(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 error ? (_jsx(EmptyState, { messageText: _(msg(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: 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 _ = useLingui()._; + 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; + 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: _(msg(templateObject_9 || (templateObject_9 = __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: 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: 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..a11d476072 --- /dev/null +++ b/src/screens/Search/Shell.js @@ -0,0 +1,404 @@ +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 } from "react/jsx-runtime"; +import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react'; +import { View, } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; +import { useQueryClient } from '@tanstack/react-query'; +import { HITSLOP_20 } from '#/lib/constants'; +import { HITSLOP_10 } 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'; +export function SearchScreenShell(_a) { + var _this = this; + var _b, _c; + var queryParam = _a.queryParam, testID = _a.testID, fixedParams = _a.fixedParams, _d = _a.navButton, navButton = _d === void 0 ? 'menu' : _d, inputPlaceholder = _a.inputPlaceholder, isExplore = _a.isExplore; + var t = useTheme(); + var gtMobile = useBreakpoints().gtMobile; + var navigation = useNavigation(); + var route = useRoute(); + var textInput = useRef(null); + var _ = useLingui()._; + var setMinimalShellMode = useSetMinimalShellMode(); + var currentAccount = useSession().currentAccount; + var queryClient = useQueryClient(); + // Query terms + var _e = useState(queryParam), searchText = _e[0], setSearchText = _e[1]; + var _f = useActorAutocompleteQuery(searchText, true), autocompleteData = _f.data, isAutocompleteFetching = _f.isFetching; + var _g = useState(false), showAutocomplete = _g[0], setShowAutocomplete = _g[1]; + var _h = useStorage(account, [ + (_b = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) !== null && _b !== void 0 ? _b : 'pwi', + 'searchTermHistory', + ]), _j = _h[0], termHistory = _j === void 0 ? [] : _j, setTermHistory = _h[1]; + var _k = useStorage(account, [ + (_c = currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) !== null && _c !== void 0 ? _c : 'pwi', + 'searchAccountHistory', + ]), _l = _k[0], accountHistory = _l === void 0 ? [] : _l, setAccountHistory = _k[1]; + var accountHistoryProfiles = useProfilesQuery({ + handles: accountHistory, + maintainData: true, + }).data; + var updateSearchHistory = useCallback(function (item) { return __awaiter(_this, void 0, void 0, function () { + var newSearchHistory; + return __generator(this, function (_a) { + if (!item) + return [2 /*return*/]; + newSearchHistory = __spreadArray([ + item + ], termHistory.filter(function (search) { return search !== item; }), true).slice(0, 6); + setTermHistory(newSearchHistory); + return [2 /*return*/]; + }); + }); }, [termHistory, setTermHistory]); + var updateProfileHistory = useCallback(function (item) { return __awaiter(_this, void 0, void 0, function () { + var newAccountHistory; + return __generator(this, function (_a) { + newAccountHistory = __spreadArray([ + item.did + ], accountHistory.filter(function (p) { return p !== item.did; }), true).slice(0, 10); + setAccountHistory(newAccountHistory); + return [2 /*return*/]; + }); + }); }, [accountHistory, setAccountHistory]); + var deleteSearchHistoryItem = useCallback(function (item) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + setTermHistory(termHistory.filter(function (search) { return search !== item; })); + return [2 /*return*/]; + }); + }); }, [termHistory, setTermHistory]); + var deleteProfileHistoryItem = useCallback(function (item) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + setAccountHistory(accountHistory.filter(function (p) { return p !== item.did; })); + return [2 /*return*/]; + }); + }); }, [accountHistory, setAccountHistory]); + var _m = useQueryManager({ + initialQuery: queryParam, + fixedParams: fixedParams, + }), params = _m.params, query = _m.query, queryWithParams = _m.queryWithParams; + var showFilters = Boolean(queryWithParams && !showAutocomplete); + // web only - measure header height for sticky positioning + var _o = useState(0), headerHeight = _o[0], setHeaderHeight = _o[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) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + scrollToTopWeb(); + setSearchText(text); + return [2 /*return*/]; + }); + }); }, []); + 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 : _(msg(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: _(msg(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, { query: query, queryWithParams: queryWithParams, headerHeight: headerHeight, focusSearchInput: focusSearchInput }) })] })); +} +var SearchScreenInner = function (_a) { + var _b; + var 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 route = useRoute(); + // Get tab parameter from route params + var tabParam = (_b = route.params) === null || _b === void 0 ? void 0 : _b.tab; + // Map tab parameter to tab index + var getInitialTabIndex = useCallback(function () { + if (!tabParam) + return 0; + switch (tabParam) { + case 'user': + case 'profile': + return 2; // People tab + case 'feed': + return 3; // Feeds tab + default: + return 0; + } + }, [tabParam]); + var _c = useState(getInitialTabIndex()), activeTab = _c[0], setActiveTab = _c[1]; + // Update activeTab when tabParam changes + useLayoutEffect(function () { + var newTabIndex = getInitialTabIndex(); + if (newTabIndex !== activeTab) { + setActiveTab(newTabIndex); + } + }, [tabParam, activeTab, getInitialTabIndex]); + var onPageSelected = useCallback(function (index) { + setMinimalShellMode(false); + setActiveTab(index); + }, [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..7374e4421a --- /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/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..e5557c2037 --- /dev/null +++ b/src/screens/Search/components/SearchHistory.js @@ -0,0 +1,62 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..82c11bb5f0 --- /dev/null +++ b/src/screens/Search/components/SearchLanguageDropdown.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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; + 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); + }); + }, [appLanguage, contentLanguages]); + 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..0467d5bab1 --- /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/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..ba40fd5e7b --- /dev/null +++ b/src/screens/Search/components/StarterPackCard.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..1f9abf074c --- /dev/null +++ b/src/screens/Search/modules/ExploreInterestsCard.js @@ -0,0 +1,62 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..341714f470 --- /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/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..feaffa244f --- /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/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..fefa77605e --- /dev/null +++ b/src/screens/Search/modules/ExploreTrendingTopics.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; +}; +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, plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { formatCount } from '#/view/com/util/numeric/format'; +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 _b = useLingui(), _ = _b._, i18n = _b.i18n; + 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 postCount = trend.postCount + ? _(plural(trend.postCount, { + other: "".concat(formatCount(i18n, trend.postCount), " posts"), + })) + : null; + 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 })), _jsxs(Text, { style: [ + a.text_sm, + t.atoms.text_contrast_medium, + web(a.leading_snug), + ], numberOfLines: 1, children: [postCount, postCount && category && _jsx(_Fragment, { children: " \u00B7 " }), 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..52a04915a9 --- /dev/null +++ b/src/screens/Search/modules/ExploreTrendingVideos.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; +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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/useSuggestedUsers.js b/src/screens/Search/util/useSuggestedUsers.js new file mode 100644 index 0000000000..6da78ca5c0 --- /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, overrideInterests = _a.overrideInterests; + var interestsDisplayNames = useInterestsDisplayNames(); + var curated = useGetSuggestedUsersQuery({ + 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 : [], + } + : 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..f42f268eb2 --- /dev/null +++ b/src/screens/Settings/AboutSettings.js @@ -0,0 +1,129 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..db48cb4963 --- /dev/null +++ b/src/screens/Settings/AccessibilitySettings.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, Fragment as _Fragment } from "react/jsx-runtime"; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..c017e3d0c9 --- /dev/null +++ b/src/screens/Settings/AccountSettings.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useModalControls } from '#/state/modals'; +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 { ExportCarDialog } from './components/ExportCarDialog'; +export function AccountSettingsScreen(_a) { + var t = useTheme(); + var _ = useLingui()._; + var currentAccount = useSession().currentAccount; + var openModal = useModalControls().openModal; + var emailDialogControl = useEmailDialogControl(); + var birthdayControl = useDialogControl(); + var changeHandleControl = useDialogControl(); + var changePasswordControl = useDialogControl(); + var exportCarControl = useDialogControl(); + var deactivateAccountControl = 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 openModal({ name: 'delete-account' }); }, 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 })] })); +} +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..87d269d6b2 --- /dev/null +++ b/src/screens/Settings/ActivityPrivacySettings.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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(templateObject_4 || (templateObject_4 = __makeTemplateObject(["No one"], ["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, { children: "No one" }) })] })] }) })); +} +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; 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..bf801e2f91 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/SettingsListItem.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..30cf2ca902 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/index.js @@ -0,0 +1,136 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..f0ae9693f8 --- /dev/null +++ b/src/screens/Settings/AppIconSettings/useAppIconSets.js @@ -0,0 +1,132 @@ +import { useMemo } from 'react'; +import { msg } from '@lingui/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..baecc12bdd --- /dev/null +++ b/src/screens/Settings/AppPasswords.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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, colors } 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: colors.warning }] }), _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..03551269af --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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_NATIVE } from '#/env'; +import { IS_INTERNAL } 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..8f17e64321 --- /dev/null +++ b/src/screens/Settings/ContentAndMediaSettings.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 { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..c51aeb43c8 --- /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/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..7caa9e1676 --- /dev/null +++ b/src/screens/Settings/FindContactsSettings.js @@ -0,0 +1,332 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..448ef7b38e --- /dev/null +++ b/src/screens/Settings/FollowingFeedPreferences.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 { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..8b71f671e0 --- /dev/null +++ b/src/screens/Settings/InterestsSettings.js @@ -0,0 +1,190 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..b99b65d9fa --- /dev/null +++ b/src/screens/Settings/LanguageSettings.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 { useCallback, useMemo } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { APP_LANGUAGES, LANGUAGES } from '#/lib/../locale/languages'; +import { languageName, sanitizeAppLanguageSetting } from '#/locale/helpers'; +import { useModalControls } from '#/state/modals'; +import { useLanguagePrefs, useLanguagePrefsApi } from '#/state/preferences'; +import { atoms as a, useTheme, web } 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 * 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 t = useTheme(); + var openModal = useModalControls().openModal; + var onPressContentLanguages = useCallback(function () { + openModal({ name: 'content-languages-settings' }); + }, [openModal]); + 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 myLanguages = useMemo(function () { + return (langPrefs.contentLanguages + .map(function (lang) { return LANGUAGES.find(function (l) { return l.code2 === lang; }); }) + .filter(Boolean) + // @ts-ignore + .map(function (l) { return languageName(l, langPrefs.appLanguage); }) + .join(', ')); + }, [langPrefs.appLanguage, langPrefs.contentLanguages]); + 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.map(function (l) { return ({ + label: languageName(l, langPrefs.appLanguage), + 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." }) }), _jsxs(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Select content languages"], ["Select content languages"])))), size: "small", color: "secondary", shape: "rectangular", onPress: onPressContentLanguages, style: [a.justify_start, web({ maxWidth: 400 })], children: [_jsx(ButtonIcon, { icon: myLanguages.length > 0 ? CheckIcon : PlusIcon }), _jsx(ButtonText, { style: [t.atoms.text, a.text_md, a.flex_1, a.text_left], numberOfLines: 1, children: myLanguages.length > 0 + ? myLanguages + : _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Select languages"], ["Select languages"])))) })] })] })] })] }) })] })); +} +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..e9e813359d --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 } from '#/components/icons/BellRinging'; +import { 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..1345f90683 --- /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/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..284ae3360f --- /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/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..156d522d3a --- /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/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..4a63cfc745 --- /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/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..a07d925375 --- /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/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..e3bcd17eb4 --- /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/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..4c001c6ce7 --- /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/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..b92ac82d03 --- /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/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..f61d2ab64c --- /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/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..1aed9906ef --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.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); +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..bf41d4e551 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/index.js @@ -0,0 +1,182 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..93b58e1a38 --- /dev/null +++ b/src/screens/Settings/PrivacyAndSecuritySettings.js @@ -0,0 +1,49 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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, { 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..7c08f402ff --- /dev/null +++ b/src/screens/Settings/Settings.js @@ -0,0 +1,297 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useNavigation } from '@react-navigation/native'; +import { useActorStatus } from '#/lib/actor-status'; +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 { DotGrid_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 { 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..dbec7058ca --- /dev/null +++ b/src/screens/Settings/ThreadPreferences.js @@ -0,0 +1,25 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..30e0f771ef --- /dev/null +++ b/src/screens/Settings/components/AddAppPasswordDialog.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..fe5dc499a6 --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { HITSLOP_10, urls } from '#/lib/constants'; +import { cleanError } from '#/lib/strings/errors'; +import { createFullHandle, validateServiceHandle } from '#/lib/strings/handles'; +import { sanitizeHandle } 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..a8f4a71c2b --- /dev/null +++ b/src/screens/Settings/components/ChangePasswordDialog.js @@ -0,0 +1,196 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..c2b9813c8a --- /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/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..1135620c01 --- /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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { logger } from '#/logger'; +import { useAgent, useSessionApi } from '#/state/session'; +import { atoms as a, useBreakpoints, 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 gtMobile = useBreakpoints().gtMobile; + 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, { variant: "solid", color: "negative", size: gtMobile ? 'small' : '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/DisableEmail2FADialog.js b/src/screens/Settings/components/DisableEmail2FADialog.js new file mode 100644 index 0000000000..3e71d4e827 --- /dev/null +++ b/src/screens/Settings/components/DisableEmail2FADialog.js @@ -0,0 +1,152 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..0d882df27e --- /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/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..d4bd0ad4e0 --- /dev/null +++ b/src/screens/Settings/components/ExportCarDialog.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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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(true); + 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]); + 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_3 || (templateObject_3 = __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 && _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_4 || (templateObject_4 = __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; diff --git a/src/screens/Settings/components/OTAInfo.js b/src/screens/Settings/components/OTAInfo.js new file mode 100644 index 0000000000..0bf4c6401a --- /dev/null +++ b/src/screens/Settings/components/OTAInfo.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 __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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..52f25d05a0 --- /dev/null +++ b/src/screens/Settings/components/PwiOptOut.js @@ -0,0 +1,69 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..1b07305b0b --- /dev/null +++ b/src/screens/Signup/BackNextButtons.js @@ -0,0 +1,18 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..33ed0b6c29 --- /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/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..32016dbf3e --- /dev/null +++ b/src/screens/Signup/StepHandle/HandleSuggestions.js @@ -0,0 +1,47 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import Animated, { Easing, FadeInDown, FadeOut } from 'react-native-reanimated'; +import { msg, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..d7f13ec4f9 --- /dev/null +++ b/src/screens/Signup/StepHandle/index.js @@ -0,0 +1,184 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..9a31a1f4a1 --- /dev/null +++ b/src/screens/Signup/StepInfo/Policies.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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..ff29f2ad48 --- /dev/null +++ b/src/screens/Signup/StepInfo/index.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); +}; +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 ? (_jsxs(Trans, { children: ["You must be ", MIN_ACCESS_AGE, " years of age or older to create an account."] })) : (_jsxs(Trans, { children: ["You must be ", aaRegionConfig.minAccessAge, " 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..3c8cf77881 --- /dev/null +++ b/src/screens/Signup/index.js @@ -0,0 +1,134 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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..5ba0cbe0c7 --- /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/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..c1cf8a876a --- /dev/null +++ b/src/screens/SignupQueued.js @@ -0,0 +1,175 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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_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 && _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..7164e504b5 --- /dev/null +++ b/src/screens/StarterPack/StarterPackLandingScreen.js @@ -0,0 +1,188 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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: [_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..a0cc2d3f48 --- /dev/null +++ b/src/screens/StarterPack/StarterPackScreen.js @@ -0,0 +1,454 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { 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 { DotGrid_Stroke2_Corner0_Rounded as Ellipsis } from '#/components/icons/DotGrid'; +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)]; + 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" }), ' ', "used 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 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(_Fragment, { children: [_jsx(Menu.Group, { children: _jsxs(Menu.Item, { label: IS_WEB + ? _(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Copy link to starter pack"], ["Copy link to starter pack"])))) + : _(msg(templateObject_19 || (templateObject_19 = __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_20 || (templateObject_20 = __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_21 || (templateObject_21 = __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, {})] })] })] })); +} +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_22 || (templateObject_22 = __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_23 || (templateObject_23 = __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_24 || (templateObject_24 = __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; diff --git a/src/screens/StarterPack/Wizard/State.js b/src/screens/StarterPack/Wizard/State.js new file mode 100644 index 0000000000..2c58dcd825 --- /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/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..09064dbe50 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepDetails.js @@ -0,0 +1,34 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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: 300, + }).data; + 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: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["", "'s starter pack"], ["", "'s starter pack"])), (currentProfile === null || currentProfile === void 0 ? void 0 : currentProfile.displayName) || (currentProfile === null || currentProfile === void 0 ? void 0 : currentProfile.handle))), 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: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["", "'s favorite feeds and people - join me!"], ["", "'s favorite feeds and people - join me!"])), (currentProfile === null || currentProfile === void 0 ? void 0 : currentProfile.displayName) || (currentProfile === null || currentProfile === void 0 ? void 0 : currentProfile.handle))), value: state.description, onChangeText: function (text) { + return dispatch({ type: 'SetDescription', description: text }); + }, multiline: true, style: { minHeight: 150 } }) })] })] }) })); +} +var templateObject_1, templateObject_2; diff --git a/src/screens/StarterPack/Wizard/StepFeeds.js b/src/screens/StarterPack/Wizard/StepFeeds.js new file mode 100644 index 0000000000..ff9b45158a --- /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/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..d4fc89de45 --- /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/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..e8ce1ee659 --- /dev/null +++ b/src/screens/StarterPack/Wizard/index.js @@ -0,0 +1,321 @@ +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, Plural, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; +import { STARTER_PACK_MAX_SIZE } from '#/lib/constants'; +import { useEnableKeyboardControllerScreen } from '#/lib/hooks/useEnableKeyboardController'; +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]); + useEnableKeyboardControllerScreen(true); + 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..4114dc5bb5 --- /dev/null +++ b/src/screens/Takendown.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, 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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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 { useEnableKeyboardController } from '#/lib/hooks/useEnableKeyboardController'; +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; + useEnableKeyboardController(true); + 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..203fd74e48 --- /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/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..c32e54acb0 --- /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/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..36bd56d85c --- /dev/null +++ b/src/screens/VideoFeed/index.js @@ -0,0 +1,639 @@ +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, Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +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, + }, + }); + }, [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: _("Video from ".concat(sanitizeHandle(post.author.handle, '@'), ". Tap to play or pause the video")), accessibilityHint: _(msg(templateObject_12 || (templateObject_12 = __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_13 || (templateObject_13 = __makeTemplateObject(["Go back"], ["Go back"])))), accessibilityHint: _(msg(templateObject_14 || (templateObject_14 = __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; 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/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..b9fddf9ff3 --- /dev/null +++ b/src/state/cache/profile-shadow.js @@ -0,0 +1,310 @@ +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 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(findAllProfilesInSuggestedUsersQueryData(queryClient, did))]; + case 10: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInSuggestedFollowsQueryData(queryClient, did))]; + case 11: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInActorSearchQueryData(queryClient, did))]; + case 12: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInListConvosQueryData(queryClient, did))]; + case 13: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInFeedsQueryData(queryClient, did))]; + case 14: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInPostThreadV2QueryData(queryClient, did))]; + case 15: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInKnownFollowersQueryData(queryClient, did))]; + case 16: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did))]; + case 17: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInActivitySubscriptionsQueryData(queryClient, did))]; + case 18: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInNotifsQueryData(queryClient, did))]; + case 19: + _a.sent(); + return [5 /*yield**/, __values(findAllProfilesInContactMatchesQueryData(queryClient, did))]; + case 20: + _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..e07ef0e802 --- /dev/null +++ b/src/state/gallery.js @@ -0,0 +1,314 @@ +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, 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 } 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*/, moveIfNecessary(raw.path)]; + case 1: return [2 /*return*/, (_a.source = (_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]; + } + }); + }); +} +/** 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..e5d589c5b1 --- /dev/null +++ b/src/state/persisted/schema.js @@ -0,0 +1,214 @@ +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(), + }) + .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..073dded0ba --- /dev/null +++ b/src/state/preferences/languages.js @@ -0,0 +1,130 @@ +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 (_) { }, + setContentLanguage: function (_) { }, + toggleContentLanguage: function (_) { }, + togglePostLanguage: function (_) { }, + savePostLanguageToHistory: function () { }, + setAppLanguage: function (_) { }, +}); +apiContext.displayName = 'LanguagePrefsApiContext'; +export function Provider(_a) { + var children = _a.children; + var _b = React.useState(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 })); }); + }, + setContentLanguage: function (code2) { + setStateWrapped(function (s) { return (__assign(__assign({}, s), { contentLanguages: [code2] })); }); + }, + toggleContentLanguage: function (code2) { + setStateWrapped(function (s) { + var exists = s.contentLanguages.includes(code2); + var next = exists + ? s.contentLanguages.filter(function (lang) { return lang !== code2; }) + : s.contentLanguages.concat(code2); + return __assign(__assign({}, s), { contentLanguages: next }); + }); + }, + togglePostLanguage: function (code2) { + setStateWrapped(function (s) { + var exists = hasPostLanguage(state.postLanguage, code2); + var next = s.postLanguage; + if (exists) { + next = toPostLanguages(s.postLanguage) + .filter(function (lang) { return lang !== code2; }) + .join(','); + } + else { + // sort alphabetically for deterministic comparison in context menu + next = toPostLanguages(s.postLanguage) + .concat([code2]) + .sort(function (a, b) { return a.localeCompare(b); }) + .join(','); + } + return __assign(__assign({}, s), { postLanguage: next }); + }); + }, + /** + * 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 })); }); + }, + }); }, [state, 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..9b1c4dd3c6 --- /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/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..94a4ea000b --- /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..4570f5a7c3 --- /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/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..da4eaefd97 --- /dev/null +++ b/src/state/queries/feed.js @@ -0,0 +1,609 @@ +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 { 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 { 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) + : "Feed by ".concat(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) + : "User List by ".concat(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 pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'; +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({ + staleTime: STALE.INFINITY, + enabled: !isLoadingPrefs, + queryKey: [ + pinnedFeedInfosQueryKeyRoot, + (hasSession ? 'authed:' : 'unauthed:') + + pinnedItems.map(function (f) { return f.value; }).join(','), + ], + 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({ + staleTime: STALE.INFINITY, + enabled: !isLoadingPrefs, + queryKey: __spreadArray([pinnedFeedInfosQueryKeyRoot], savedItems, true), + 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); +} 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..f790d7622b --- /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 { createFullHandle } from '#/lib/strings/handles'; +import { useDebouncedValue } from '#/components/live/utils'; +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..019367d3c2 --- /dev/null +++ b/src/state/queries/index.js @@ -0,0 +1,19 @@ +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, +}; 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..a8b5eb3820 --- /dev/null +++ b/src/state/queries/labeler.js @@ -0,0 +1,210 @@ +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 { labelersDetailedInfoQueryKeyRoot } from '#/lib/react-query'; +import { 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(), +]; }; +export var labelersDetailedInfoQueryKey = function (dids) { return [ + labelersDetailedInfoQueryKeyRoot, + 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: labelersDetailedInfoQueryKey(dids), + gcTime: 1000 * 60 * 60 * 6, // 6 hours + 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 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..2f33147662 --- /dev/null +++ b/src/state/queries/list-memberships.js @@ -0,0 +1,250 @@ +/** + * 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 }; + } +}; +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'; +// 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, 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); + 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); + 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..8ae7079bd0 --- /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/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..04fb36973e --- /dev/null +++ b/src/state/queries/nuxs/definitions.js @@ -0,0 +1,34 @@ +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"; + /* + * 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); 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..ea92bef995 --- /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/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..87e301026d --- /dev/null +++ b/src/state/queries/post.js @@ -0,0 +1,455 @@ +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 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: + 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..eae6566c0a --- /dev/null +++ b/src/state/queries/preferences/index.js @@ -0,0 +1,642 @@ +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 { 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'; +var preferencesQueryKeyRoot = 'getPreferences'; +export var preferencesQueryKey = [preferencesQueryKeyRoot]; +export function usePreferencesQuery() { + var _this = this; + var agent = useAgent(); + var aa = useAgeAssurance(); + return useQuery({ + staleTime: STALE.SECONDS.FIFTEEN, + structuralSharing: replaceEqualDeep, + refetchOnWindowFocus: true, + queryKey: preferencesQueryKey, + 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]), + }); +} +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() { + 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: 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*/]; + } + }); + }); }, + }); +} +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..c93b097c2a --- /dev/null +++ b/src/state/queries/preferences/useThreadPreferences.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 { useCallback, useMemo, useRef, useState } from 'react'; +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 _this = this; + 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 = useState(false), isSaving = _f[0], setIsSaving = _f[1]; + var mutateAsync = useSetThreadViewPreferencesMutation().mutateAsync; + var savePrefs = useMemo(function () { + return debounce(function (prefs) { 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]); + setIsSaving(true); + return [4 /*yield*/, mutateAsync(prefs)]; + case 1: + _a.sent(); + ax.metric('thread:preferences:update', { + sort: prefs.sort, + view: prefs.lab_treeViewEnabled ? 'tree' : 'linear', + }); + return [3 /*break*/, 4]; + case 2: + e_1 = _a.sent(); + ax.logger.error('useThreadPreferences failed to save', { + safeMessage: e_1, + }); + return [3 /*break*/, 4]; + case 3: + setIsSaving(false); + return [7 /*endfinally*/]; + case 4: return [2 /*return*/]; + } + }); + }); }, 4e3); + }, [mutateAsync]); + 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..498f821a9d --- /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 { STALE } from '#/state/queries/index'; +import { useAgent } from '../session'; +var RQKEY_LINK_ROOT = 'resolve-link'; +export var RQKEY_LINK = function (url) { return [RQKEY_LINK_ROOT, url]; }; +var RQKEY_GIF_ROOT = 'resolve-gif'; +export var RQKEY_GIF = function (url) { return [RQKEY_GIF_ROOT, url]; }; +import { resolveGif, resolveLink } from '#/lib/api/resolve'; +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..4a376d6cf4 --- /dev/null +++ b/src/state/queries/starter-packs.js @@ -0,0 +1,460 @@ +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) { + return __awaiter(this, void 0, void 0, function () { + var starterPackView, feeds, _i, _a, feed, listView; + return __generator(this, function (_b) { + if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) { + return [2 /*return*/]; + } + if (AppBskyGraphDefs.isStarterPackView(starterPack)) { + starterPackView = starterPack; + } + else if (AppBskyGraphDefs.isStarterPackViewBasic(starterPack) && + bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)) { + feeds = void 0; + if (starterPack.record.feeds) { + feeds = []; + for (_i = 0, _a = starterPack.record.feeds; _i < _a.length; _i++) { + 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); + } + } + } + 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); + } + return [2 /*return*/]; + }); + }); +} 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/useGetSuggestedUsersQuery.js b/src/state/queries/trending/useGetSuggestedUsersQuery.js new file mode 100644 index 0000000000..b6ec3e3745 --- /dev/null +++ b/src/state/queries/trending/useGetSuggestedUsersQuery.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); +}; +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) { + var _a; + return [ + getSuggestedUsersQueryKeyRoot, + props.category, + props.limit, + (_a = props.overrideInterests) === null || _a === void 0 ? void 0 : _a.join(','), + ]; +}; +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, interests, data, fallbackData; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + contentLangs = getContentLanguages().join(','); + userInterests = aggregateUserInterests(preferences); + interests = props.overrideInterests && props.overrideInterests.length > 0 + ? props.overrideInterests.join(',') + : userInterests; + 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(interests)), { '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(interests)); + 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..dac0d9f46d --- /dev/null +++ b/src/state/queries/trending/useTrendingTopics.js @@ -0,0 +1,92 @@ +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 { STALE } from '#/state/queries'; +import { usePreferencesQuery } from '#/state/queries/preferences'; +import { useAgent } from '#/state/session'; +export var DEFAULT_LIMIT = 14; +export var trendingTopicsQueryKey = ['trending-topics']; +export function useTrendingTopics() { + 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({ + 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.api.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: React.useCallback(function (data) { + return { + topics: data.topics.filter(function (t) { + return !hasMutedWord({ + mutedWords: mutedWords, + text: t.topic + ' ' + t.displayName + ' ' + t.description, + }); + }), + suggested: data.suggested.filter(function (t) { + return !hasMutedWord({ + mutedWords: mutedWords, + text: t.topic + ' ' + t.displayName + ' ' + t.description, + }); + }), + }; + }, [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..8abf1c99a9 --- /dev/null +++ b/src/state/service-config.js @@ -0,0 +1,84 @@ +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 { useSession } from '#/state/session'; +import { useAnalytics } from '#/analytics'; +import { IS_DEV } from '#/env'; +import { device } from '#/storage'; +var TrendingContext = createContext({ + enabled: false, +}); +TrendingContext.displayName = 'TrendingContext'; +var LiveNowContext = createContext([]); +LiveNowContext.displayName = 'LiveNowContext'; +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]); + var liveNow = useMemo(function () { var _a; return (_a = config === null || config === void 0 ? void 0 : config.liveNow) !== null && _a !== void 0 ? _a : []; }, [config]); + // 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(LiveNowContext.Provider, { value: liveNow, children: _jsx(CheckEmailConfirmedContext.Provider, { value: checkEmailConfirmed, children: children }) }) })); +} +export function useTrendingConfig() { + return useContext(TrendingContext); +} +var DEFAULT_LIVE_ALLOWED_DOMAINS = [ + 'twitch.tv', + 'www.twitch.tv', + 'stream.place', + 'bluecast.app', + 'www.bluecast.app', +]; +export function useLiveNowConfig() { + var ctx = useContext(LiveNowContext); + var canGoLive = useCanGoLive(); + var currentAccount = useSession().currentAccount; + if (!(currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) || !canGoLive) + return { allowedDomains: new Set() }; + var vip = ctx.find(function (live) { return live.did === currentAccount.did; }); + return { + allowedDomains: new Set(DEFAULT_LIVE_ALLOWED_DOMAINS.concat(vip ? vip.domains : [])), + }; +} +export function useCanGoLive() { + var ax = useAnalytics(); + var hasSession = useSession().hasSession; + if (!hasSession) + return false; + return IS_DEV ? true : !ax.features.enabled(ax.features.LiveNowBetaDisable); +} +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..f082213b27 --- /dev/null +++ b/src/state/session/index.js @@ -0,0 +1,413 @@ +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 React 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 { useOnboardingDispatch } from '#/state/shell/onboarding'; +import { clearAgeAssuranceData, clearAgeAssuranceDataForDid, } from '#/ageAssurance/data'; +var StateContext = React.createContext({ + accounts: [], + currentAccount: undefined, + hasSession: false, +}); +StateContext.displayName = 'SessionStateContext'; +var AgentContext = React.createContext(null); +AgentContext.displayName = 'SessionAgentContext'; +var ApiContext = React.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 () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); }, + logoutEveryAccount: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); }, + 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 = React.useState(function () { return new SessionStore(); })[0]; + var state = React.useSyncExternalStore(store.subscribe, store.getState); + var onboardingDispatch = useOnboardingDispatch(); + var onAgentSessionChange = React.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 = React.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 = React.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 = React.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 }); + } + // reset onboarding flow on logout + onboardingDispatch({ type: 'skip' }); + }, [ax, store, cancelPendingTask, onboardingDispatch]); + var logoutEveryAccount = React.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(); + // reset onboarding flow on logout + onboardingDispatch({ type: 'skip' }); + }, [store, cancelPendingTask, onboardingDispatch]); + var resumeSession = React.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 = React.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 = React.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]); + React.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 = React.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 = React.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 = React.useRef(agent); + React.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 = React.useRef(null); + var cancelPendingTask = React.useCallback(function () { + if (abortController.current) { + abortController.current.abort(); + } + abortController.current = new AbortController(); + return abortController.current.signal; + }, []); + return cancelPendingTask; +} +export function useSession() { + return React.useContext(StateContext); +} +export function useSessionApi() { + return React.useContext(ApiContext); +} +export function useRequireAuth() { + var hasSession = useSession().hasSession; + var closeAll = useCloseAllActiveElements(); + var signinDialogControl = useGlobalDialogsControlContext().signinDialogControl; + return React.useCallback(function (fn) { + if (hasSession) { + fn(); + } + else { + closeAll(); + signinDialogControl.open(); + } + }, [hasSession, signinDialogControl, closeAll]); +} +export function useAgent() { + var agent = React.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..5859b9ccc7 --- /dev/null +++ b/src/state/shell/color-mode.js @@ -0,0 +1,48 @@ +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(persisted.get('colorMode')), colorMode = _b[0], setColorMode = _b[1]; + var _c = React.useState(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..c9a3e8fa36 --- /dev/null +++ b/src/state/shell/composer/index.js @@ -0,0 +1,95 @@ +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/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 } 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(); + } + 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..196224e32e --- /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