Compare commits

...

1 Commits

Author SHA1 Message Date
Claude 2b2973a19e Fix double border on custom feed posts (web)
Add missing visibility check for ageAssuranceBanner on non-discover
feeds. The banner was being added to the feedItems array without
checking ageAssuranceBannerState.visible, causing the first post's
rowIndex to be 1 instead of 0, which prevented hideTopBorder from
being set correctly. This was particularly visible on web when
directly navigating to a custom feed URL.
2026-01-25 15:11:24 +00:00
1349 changed files with 129093 additions and 1 deletions
@@ -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 = {}));
@@ -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 <BottomSheetPortalProvider> 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 }) }) }) })));
}
@@ -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 }) }));
}
+45
View File
@@ -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 };
}
@@ -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 });
}
@@ -0,0 +1,2 @@
import { requireNativeModule } from 'expo-modules-core';
export var BackgroundNotificationHandler = requireNativeModule('ExpoBackgroundNotificationHandler');
@@ -0,0 +1 @@
export {};
+189
View File
@@ -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;
+174
View File
@@ -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;
+636
View File
@@ -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;
+13
View File
@@ -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;
}
}
+183
View File
@@ -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 }) }) }))] }))] }));
}
+14
View File
@@ -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" }) }) }));
}
+3
View File
@@ -0,0 +1,3 @@
export var prefetchAgeAssuranceData = function () { };
export var setBirthdateForDid = function () { };
export var setCreatedAtForDid = function () { };
@@ -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;
@@ -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;
+587
View File
@@ -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 }));
}
+106
View File
@@ -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];
}
});
});
}
+80
View File
@@ -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 }));
}
+2
View File
@@ -0,0 +1,2 @@
import { Logger } from '#/logger';
export var logger = Logger.create(Logger.Context.AgeAssurance);
+89
View File
@@ -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]);
}
+45
View File
@@ -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;
}
}
+115
View File
@@ -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,
});
}
},
});
}
@@ -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]);
}
+97
View File
@@ -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 })); };
+109
View File
@@ -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: [],
},
}) });
+38
View File
@@ -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,
};
}
+88
View File
@@ -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';
+79
View File
@@ -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]);
}
+37
View File
@@ -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;
+72
View File
@@ -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',
},
};
+74
View File
@@ -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);
}
+32
View File
@@ -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);
});
});
+6
View File
@@ -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;
+2
View File
@@ -0,0 +1,2 @@
import { StyleSheet } from 'react-native';
export var flatten = StyleSheet.flatten;
+1
View File
@@ -0,0 +1 @@
export { android, ios, native, platform, web } from '@bsky.app/alf';
+19
View File
@@ -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 });
}
}
}
+2
View File
@@ -0,0 +1,2 @@
import { utils } from '@bsky.app/alf';
export var select = utils.select;
+20
View File
@@ -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'),
});
}
+14
View File
@@ -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'),
});
}
+51
View File
@@ -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;
}
}
+42
View File
@@ -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]);
}
+25
View File
@@ -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;
}
+126
View File
@@ -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,
});
}
+11
View File
@@ -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 = {}));
+68
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
export * from '#/analytics/identifiers/device';
export * from '#/analytics/identifiers/session';
+34
View File
@@ -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;
}
+67
View File
@@ -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);
});
});
+37
View File
@@ -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;
}
+8
View File
@@ -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;
}
+140
View File
@@ -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.<platform>.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 `<Fragment key={did} />` breaker in `App.<platform>.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;
}
+22
View File
@@ -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',
};
}
+155
View File
@@ -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 };
+235
View File
@@ -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*/];
}
});
}); });
});
+3
View File
@@ -0,0 +1,3 @@
import { MetricsClient } from '#/analytics/metrics/client';
export * from '#/analytics/metrics/utils';
export var metrics = new MetricsClient();
+4
View File
@@ -0,0 +1,4 @@
/*
* Do not import runtime code into this file
*/
export {};
+8
View File
@@ -0,0 +1,8 @@
export function toClout(n) {
if (n == null) {
return undefined;
}
else {
return Math.max(0, Math.round(Math.log(n)));
}
}
+16
View File
@@ -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;
+25
View File
@@ -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),
};
}
}
+103
View File
@@ -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;
+99
View File
@@ -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 }) })] }) }));
}
+65
View File
@@ -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;
+60
View File
@@ -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 }));
}
+752
View File
@@ -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 })] }));
}
@@ -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;
+24
View File
@@ -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;
+28
View File
@@ -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;
}
+663
View File
@@ -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 '<failed capture>';
}),
])];
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;
+9
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export {};
+41
View File
@@ -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) })] }) })] }));
}
+57
View File
@@ -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]);
}
+281
View File
@@ -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;
+232
View File
@@ -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;
+29
View File
@@ -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 }));
}
+67
View File
@@ -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()];
}
});
}); }, []);
}
+1
View File
@@ -0,0 +1 @@
export {};
+16
View File
@@ -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]);
}
+8
View File
@@ -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] }));
}
+35
View File
@@ -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;
+266
View File
@@ -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;
+583
View File
@@ -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;
+7
View File
@@ -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 });
}
+103
View File
@@ -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;
+11
View File
@@ -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 }));
}
+1
View File
@@ -0,0 +1 @@
export { FullWindowOverlay } from 'react-native-screens';
+1
View File
@@ -0,0 +1 @@
export { Fragment as FullWindowOverlay } from 'react';
+10
View File
@@ -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] }));
}
+33
View File
@@ -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 }));
}
+18
View File
@@ -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] }) }));
}
+260
View File
@@ -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;
@@ -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;
+29
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
export function KeepAwake() {
return null;
}
+139
View File
@@ -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;
+104
View File
@@ -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;
+28
View File
@@ -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;
+146
View File
@@ -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;

Some files were not shown because too many files have changed in this diff Show More