Add pre-existing JS build artifacts
These compiled JS files were already present in the working directory but untracked by git. https://claude.ai/code/session_01AMCp4M56WtFPz1pZtwgcvx
This commit is contained in:
@@ -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,196 @@
|
|||||||
|
var __extends = (this && this.__extends) || (function () {
|
||||||
|
var extendStatics = function (d, b) {
|
||||||
|
extendStatics = Object.setPrototypeOf ||
|
||||||
|
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||||
|
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||||
|
return extendStatics(d, b);
|
||||||
|
};
|
||||||
|
return function (d, b) {
|
||||||
|
if (typeof b !== "function" && b !== null)
|
||||||
|
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
||||||
|
extendStatics(d, b);
|
||||||
|
function __() { this.constructor = d; }
|
||||||
|
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var __rest = (this && this.__rest) || function (s, e) {
|
||||||
|
var t = {};
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||||
|
t[p] = s[p];
|
||||||
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||||
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||||
|
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||||
|
t[p[i]] = s[p[i]];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx } from "react/jsx-runtime";
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Dimensions, Platform, useWindowDimensions, View, } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { requireNativeModule, requireNativeViewManager } from 'expo-modules-core';
|
||||||
|
import { IS_IOS } from '#/env';
|
||||||
|
import { BottomSheetPortalProvider, Context as PortalContext, } from './BottomSheetPortal';
|
||||||
|
var NativeView = requireNativeViewManager('BottomSheet');
|
||||||
|
var NativeModule = requireNativeModule('BottomSheet');
|
||||||
|
var IS_IOS15 = Platform.OS === 'ios' &&
|
||||||
|
// semvar - can be 3 segments, so can't use Number(Platform.Version)
|
||||||
|
Number(Platform.Version.split('.').at(0)) < 16;
|
||||||
|
var BottomSheetNativeComponent = /** @class */ (function (_super) {
|
||||||
|
__extends(BottomSheetNativeComponent, _super);
|
||||||
|
function BottomSheetNativeComponent(props) {
|
||||||
|
var _this = _super.call(this, props) || this;
|
||||||
|
_this.ref = React.createRef();
|
||||||
|
_this.onStateChange = function (event) {
|
||||||
|
var _b, _c;
|
||||||
|
var state = event.nativeEvent.state;
|
||||||
|
var isOpen = state !== 'closed';
|
||||||
|
_this.setState({ open: isOpen });
|
||||||
|
(_c = (_b = _this.props).onStateChange) === null || _c === void 0 ? void 0 : _c.call(_b, event);
|
||||||
|
};
|
||||||
|
_this.updateLayout = function () {
|
||||||
|
var _b;
|
||||||
|
(_b = _this.ref.current) === null || _b === void 0 ? void 0 : _b.updateLayout();
|
||||||
|
};
|
||||||
|
_this.state = {
|
||||||
|
open: false,
|
||||||
|
};
|
||||||
|
return _this;
|
||||||
|
}
|
||||||
|
BottomSheetNativeComponent.prototype.present = function () {
|
||||||
|
this.setState({ open: true });
|
||||||
|
};
|
||||||
|
BottomSheetNativeComponent.prototype.dismiss = function () {
|
||||||
|
var _b;
|
||||||
|
(_b = this.ref.current) === null || _b === void 0 ? void 0 : _b.dismiss();
|
||||||
|
};
|
||||||
|
BottomSheetNativeComponent.prototype.render = function () {
|
||||||
|
var _this = this;
|
||||||
|
var _b;
|
||||||
|
var Portal = this.context;
|
||||||
|
if (!Portal) {
|
||||||
|
throw new Error('BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.');
|
||||||
|
}
|
||||||
|
if (!this.state.open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var extraStyles;
|
||||||
|
if (IS_IOS15 && this.state.viewHeight) {
|
||||||
|
var screenHeight = Dimensions.get('screen').height;
|
||||||
|
var viewHeight = this.state.viewHeight;
|
||||||
|
var cornerRadius = (_b = this.props.cornerRadius) !== null && _b !== void 0 ? _b : 0;
|
||||||
|
if (viewHeight < screenHeight / 2) {
|
||||||
|
extraStyles = {
|
||||||
|
height: viewHeight,
|
||||||
|
marginTop: screenHeight / 2 - viewHeight,
|
||||||
|
borderTopLeftRadius: cornerRadius,
|
||||||
|
borderTopRightRadius: cornerRadius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (_jsx(Portal, { children: _jsx(BottomSheetNativeComponentInner, __assign({}, this.props, { nativeViewRef: this.ref, onStateChange: this.onStateChange, extraStyles: extraStyles, onLayout: function (e) {
|
||||||
|
if (IS_IOS15) {
|
||||||
|
var height = e.nativeEvent.layout.height;
|
||||||
|
_this.setState({ viewHeight: height });
|
||||||
|
}
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
// TEMP HACKFIX: I had to timebox this, but this is Bad.
|
||||||
|
// On Android, if you run updateLayout() immediately,
|
||||||
|
// it will take ages to actually run on the native side.
|
||||||
|
// However, adding literally any delay will fix this, including
|
||||||
|
// a console.log() - just sending the log to the CLI is enough.
|
||||||
|
// TODO: Get to the bottom of this and fix it properly! -sfn
|
||||||
|
setTimeout(function () { return _this.updateLayout(); });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_this.updateLayout();
|
||||||
|
}
|
||||||
|
} })) }));
|
||||||
|
};
|
||||||
|
var _a;
|
||||||
|
_a = BottomSheetNativeComponent;
|
||||||
|
BottomSheetNativeComponent.contextType = PortalContext;
|
||||||
|
BottomSheetNativeComponent.dismissAll = function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||||
|
return __generator(_a, function (_b) {
|
||||||
|
switch (_b.label) {
|
||||||
|
case 0: return [4 /*yield*/, NativeModule.dismissAll()];
|
||||||
|
case 1:
|
||||||
|
_b.sent();
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); };
|
||||||
|
return BottomSheetNativeComponent;
|
||||||
|
}(React.Component));
|
||||||
|
export { BottomSheetNativeComponent };
|
||||||
|
function BottomSheetNativeComponentInner(_b) {
|
||||||
|
var _c;
|
||||||
|
var children = _b.children, backgroundColor = _b.backgroundColor, onLayout = _b.onLayout, onStateChange = _b.onStateChange, nativeViewRef = _b.nativeViewRef, extraStyles = _b.extraStyles, rest = __rest(_b, ["children", "backgroundColor", "onLayout", "onStateChange", "nativeViewRef", "extraStyles"]);
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
var cornerRadius = (_c = rest.cornerRadius) !== null && _c !== void 0 ? _c : 0;
|
||||||
|
var screenHeight = useWindowDimensions().height;
|
||||||
|
var sheetHeight = IS_IOS ? screenHeight - insets.top : screenHeight;
|
||||||
|
return (_jsx(NativeView, __assign({}, rest, { onStateChange: onStateChange, ref: nativeViewRef, style: {
|
||||||
|
position: 'absolute',
|
||||||
|
height: sheetHeight,
|
||||||
|
width: '100%',
|
||||||
|
}, containerBackgroundColor: backgroundColor, children: _jsx(View, { style: [
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
},
|
||||||
|
Platform.OS === 'android' && {
|
||||||
|
borderTopLeftRadius: cornerRadius,
|
||||||
|
borderTopRightRadius: cornerRadius,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
extraStyles,
|
||||||
|
], children: _jsx(View, { onLayout: onLayout, children: _jsx(BottomSheetPortalProvider, { children: children }) }) }) })));
|
||||||
|
}
|
||||||
@@ -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 }) }));
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
+113
@@ -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 });
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
import { requireNativeModule } from 'expo-modules-core';
|
||||||
|
export var BackgroundNotificationHandler = requireNativeModule('ExpoBackgroundNotificationHandler');
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import '#/logger/sentry/setup';
|
||||||
|
import '#/view/icons';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
|
import { KeyboardProvider as KeyboardControllerProvider } from 'react-native-keyboard-controller';
|
||||||
|
import { initialWindowMetrics, SafeAreaProvider, } from 'react-native-safe-area-context';
|
||||||
|
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||||
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
|
import * as SystemUI from 'expo-system-ui';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import * as Sentry from '@sentry/react-native';
|
||||||
|
import { Provider as HideBottomBarBorderProvider } from '#/lib/hooks/useHideBottomBarBorder';
|
||||||
|
import { QueryProvider } from '#/lib/react-query';
|
||||||
|
import { s } from '#/lib/styles';
|
||||||
|
import { ThemeProvider } from '#/lib/ThemeContext';
|
||||||
|
import I18nProvider from '#/locale/i18nProvider';
|
||||||
|
import { logger } from '#/logger';
|
||||||
|
import { Provider as A11yProvider } from '#/state/a11y';
|
||||||
|
import { prefetchAppConfig, Provider as AppConfigProvider, } from '#/state/appConfig';
|
||||||
|
import { Provider as MutedThreadsProvider } from '#/state/cache/thread-mutes';
|
||||||
|
import { Provider as DialogStateProvider } from '#/state/dialogs';
|
||||||
|
import { Provider as EmailVerificationProvider } from '#/state/email-verification';
|
||||||
|
import { listenSessionDropped } from '#/state/events';
|
||||||
|
import { GlobalGestureEventsProvider } from '#/state/global-gesture-events';
|
||||||
|
import { Provider as HomeBadgeProvider } from '#/state/home-badge';
|
||||||
|
import { Provider as LightboxStateProvider } from '#/state/lightbox';
|
||||||
|
import { MessagesProvider } from '#/state/messages';
|
||||||
|
import { Provider as ModalStateProvider } from '#/state/modals';
|
||||||
|
import { init as initPersistedState } from '#/state/persisted';
|
||||||
|
import { Provider as PrefsStateProvider } from '#/state/preferences';
|
||||||
|
import { Provider as LabelDefsProvider } from '#/state/preferences/label-defs';
|
||||||
|
import { Provider as ModerationOptsProvider } from '#/state/preferences/moderation-opts';
|
||||||
|
import { Provider as UnreadNotifsProvider } from '#/state/queries/notifications/unread';
|
||||||
|
import { Provider as ServiceAccountManager } from '#/state/service-config';
|
||||||
|
import { Provider as SessionProvider, useSession, useSessionApi, } from '#/state/session';
|
||||||
|
import { readLastActiveAccount } from '#/state/session/util';
|
||||||
|
import { Provider as ShellStateProvider } from '#/state/shell';
|
||||||
|
import { Provider as ComposerProvider } from '#/state/shell/composer';
|
||||||
|
import { Provider as LoggedOutViewProvider } from '#/state/shell/logged-out';
|
||||||
|
import { Provider as OnboardingProvider } from '#/state/shell/onboarding';
|
||||||
|
import { Provider as ProgressGuideProvider } from '#/state/shell/progress-guide';
|
||||||
|
import { Provider as SelectedFeedProvider } from '#/state/shell/selected-feed';
|
||||||
|
import { Provider as StarterPackProvider } from '#/state/shell/starter-pack';
|
||||||
|
import { Provider as HiddenRepliesProvider } from '#/state/threadgate-hidden-replies';
|
||||||
|
import { TestCtrls } from '#/view/com/testing/TestCtrls';
|
||||||
|
import * as Toast from '#/view/com/util/Toast';
|
||||||
|
import { Shell } from '#/view/shell';
|
||||||
|
import { ThemeProvider as Alf } from '#/alf';
|
||||||
|
import { useColorModeTheme } from '#/alf/util/useColorModeTheme';
|
||||||
|
import { Provider as ContextMenuProvider } from '#/components/ContextMenu';
|
||||||
|
import { useStarterPackEntry } from '#/components/hooks/useStarterPackEntry';
|
||||||
|
import { Provider as IntentDialogProvider } from '#/components/intents/IntentDialogs';
|
||||||
|
import { Provider as PolicyUpdateOverlayProvider } from '#/components/PolicyUpdateOverlay';
|
||||||
|
import { Provider as PortalProvider } from '#/components/Portal';
|
||||||
|
import { Provider as VideoVolumeProvider } from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext';
|
||||||
|
import { ToastOutlet } from '#/components/Toast';
|
||||||
|
import { prefetchAgeAssuranceConfig, Provider as AgeAssuranceV2Provider, } from '#/ageAssurance';
|
||||||
|
import { AnalyticsContext, AnalyticsFeaturesContext, features, setupDeviceId, } from '#/analytics';
|
||||||
|
import { IS_ANDROID, IS_IOS } from '#/env';
|
||||||
|
import { prefetchLiveEvents, Provider as LiveEventsProvider, } from '#/features/liveEvents/context';
|
||||||
|
import * as Geo from '#/geolocation';
|
||||||
|
import { Splash } from '#/Splash';
|
||||||
|
import { BottomSheetProvider } from '../modules/bottom-sheet';
|
||||||
|
import { BackgroundNotificationPreferencesProvider } from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider';
|
||||||
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
if (IS_IOS) {
|
||||||
|
SystemUI.setBackgroundColorAsync('black');
|
||||||
|
}
|
||||||
|
if (IS_ANDROID) {
|
||||||
|
// iOS is handled by the config plugin -sfn
|
||||||
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(function (error) {
|
||||||
|
return logger.debug('Could not lock orientation', { safeMessage: error });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Begin geolocation ASAP
|
||||||
|
*/
|
||||||
|
Geo.resolve();
|
||||||
|
prefetchAgeAssuranceConfig();
|
||||||
|
prefetchLiveEvents();
|
||||||
|
prefetchAppConfig();
|
||||||
|
function InnerApp() {
|
||||||
|
var _a = React.useState(false), isReady = _a[0], setIsReady = _a[1];
|
||||||
|
var currentAccount = useSession().currentAccount;
|
||||||
|
var resumeSession = useSessionApi().resumeSession;
|
||||||
|
var theme = useColorModeTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var hasCheckedReferrer = useStarterPackEntry();
|
||||||
|
// init
|
||||||
|
useEffect(function () {
|
||||||
|
function onLaunch(account) {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var e_1;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
_a.trys.push([0, 5, 6, 7]);
|
||||||
|
if (!account) return [3 /*break*/, 2];
|
||||||
|
return [4 /*yield*/, resumeSession(account)];
|
||||||
|
case 1:
|
||||||
|
_a.sent();
|
||||||
|
return [3 /*break*/, 4];
|
||||||
|
case 2: return [4 /*yield*/, features.init];
|
||||||
|
case 3:
|
||||||
|
_a.sent();
|
||||||
|
_a.label = 4;
|
||||||
|
case 4: return [3 /*break*/, 7];
|
||||||
|
case 5:
|
||||||
|
e_1 = _a.sent();
|
||||||
|
logger.error("session: resume failed", { message: e_1 });
|
||||||
|
return [3 /*break*/, 7];
|
||||||
|
case 6:
|
||||||
|
setIsReady(true);
|
||||||
|
return [7 /*endfinally*/];
|
||||||
|
case 7: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var account = readLastActiveAccount();
|
||||||
|
onLaunch(account);
|
||||||
|
}, [resumeSession]);
|
||||||
|
useEffect(function () {
|
||||||
|
return listenSessionDropped(function () {
|
||||||
|
Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sorry! Your session expired. Please sign in again."], ["Sorry! Your session expired. Please sign in again."])))), 'info');
|
||||||
|
});
|
||||||
|
}, [_]);
|
||||||
|
return (_jsx(Alf, { theme: theme, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(ContextMenuProvider, { children: _jsx(Splash, { isReady: isReady && hasCheckedReferrer, children: _jsx(VideoVolumeProvider, { children: _jsx(React.Fragment
|
||||||
|
// Resets the entire tree below when it changes:
|
||||||
|
, { children: _jsx(AnalyticsFeaturesContext, { children: _jsx(QueryProvider, { currentDid: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, children: _jsx(PolicyUpdateOverlayProvider, { children: _jsx(LiveEventsProvider, { children: _jsx(AgeAssuranceV2Provider, { children: _jsx(ComposerProvider, { children: _jsx(MessagesProvider, { children: _jsx(LabelDefsProvider, { children: _jsx(ModerationOptsProvider, { children: _jsx(LoggedOutViewProvider, { children: _jsx(SelectedFeedProvider, { children: _jsx(HiddenRepliesProvider, { children: _jsx(HomeBadgeProvider, { children: _jsx(UnreadNotifsProvider, { children: _jsx(BackgroundNotificationPreferencesProvider, { children: _jsx(MutedThreadsProvider, { children: _jsx(ProgressGuideProvider, { children: _jsx(ServiceAccountManager, { children: _jsx(EmailVerificationProvider, { children: _jsx(HideBottomBarBorderProvider, { children: _jsx(GestureHandlerRootView, { style: s.h100pct, children: _jsx(GlobalGestureEventsProvider, { children: _jsxs(IntentDialogProvider, { children: [_jsx(TestCtrls, {}), _jsx(Shell, {}), _jsx(ToastOutlet, {})] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) }) }) }) }) }));
|
||||||
|
}
|
||||||
|
function App() {
|
||||||
|
var _a = useState(false), isReady = _a[0], setReady = _a[1];
|
||||||
|
React.useEffect(function () {
|
||||||
|
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(function () {
|
||||||
|
return setReady(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
if (!isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* NOTE: only nothing here can depend on other data or session state, since
|
||||||
|
* that is set up in the InnerApp component above.
|
||||||
|
*/
|
||||||
|
return (_jsx(Geo.Provider, { children: _jsx(AppConfigProvider, { children: _jsx(A11yProvider, { children: _jsx(KeyboardControllerProvider, { children: _jsx(OnboardingProvider, { children: _jsx(AnalyticsContext, { children: _jsx(SessionProvider, { children: _jsx(PrefsStateProvider, { children: _jsx(I18nProvider, { children: _jsx(ShellStateProvider, { children: _jsx(ModalStateProvider, { children: _jsx(DialogStateProvider, { children: _jsx(LightboxStateProvider, { children: _jsx(PortalProvider, { children: _jsx(BottomSheetProvider, { children: _jsx(StarterPackProvider, { children: _jsx(SafeAreaProvider, { initialMetrics: initialWindowMetrics, children: _jsx(InnerApp, {}) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
|
||||||
|
}
|
||||||
|
export default Sentry.wrap(App);
|
||||||
|
var templateObject_1;
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import '#/logger/sentry/setup'; // must be near top
|
||||||
|
import '#/view/icons';
|
||||||
|
import './style.css';
|
||||||
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import * as Sentry from '@sentry/react-native';
|
||||||
|
import { QueryProvider } from '#/lib/react-query';
|
||||||
|
import { ThemeProvider } from '#/lib/ThemeContext';
|
||||||
|
import I18nProvider from '#/locale/i18nProvider';
|
||||||
|
import { logger } from '#/logger';
|
||||||
|
import { Provider as A11yProvider } from '#/state/a11y';
|
||||||
|
import { prefetchAppConfig, Provider as AppConfigProvider, } from '#/state/appConfig';
|
||||||
|
import { Provider as MutedThreadsProvider } from '#/state/cache/thread-mutes';
|
||||||
|
import { Provider as DialogStateProvider } from '#/state/dialogs';
|
||||||
|
import { Provider as EmailVerificationProvider } from '#/state/email-verification';
|
||||||
|
import { listenSessionDropped } from '#/state/events';
|
||||||
|
import { Provider as HomeBadgeProvider } from '#/state/home-badge';
|
||||||
|
import { Provider as LightboxStateProvider } from '#/state/lightbox';
|
||||||
|
import { MessagesProvider } from '#/state/messages';
|
||||||
|
import { Provider as ModalStateProvider } from '#/state/modals';
|
||||||
|
import { init as initPersistedState } from '#/state/persisted';
|
||||||
|
import { Provider as PrefsStateProvider } from '#/state/preferences';
|
||||||
|
import { Provider as LabelDefsProvider } from '#/state/preferences/label-defs';
|
||||||
|
import { Provider as ModerationOptsProvider } from '#/state/preferences/moderation-opts';
|
||||||
|
import { Provider as UnreadNotifsProvider } from '#/state/queries/notifications/unread';
|
||||||
|
import { Provider as ServiceConfigProvider } from '#/state/service-config';
|
||||||
|
import { Provider as SessionProvider, useSession, useSessionApi, } from '#/state/session';
|
||||||
|
import { readLastActiveAccount } from '#/state/session/util';
|
||||||
|
import { Provider as ShellStateProvider } from '#/state/shell';
|
||||||
|
import { Provider as ComposerProvider } from '#/state/shell/composer';
|
||||||
|
import { Provider as LoggedOutViewProvider } from '#/state/shell/logged-out';
|
||||||
|
import { Provider as OnboardingProvider } from '#/state/shell/onboarding';
|
||||||
|
import { Provider as ProgressGuideProvider } from '#/state/shell/progress-guide';
|
||||||
|
import { Provider as SelectedFeedProvider } from '#/state/shell/selected-feed';
|
||||||
|
import { Provider as StarterPackProvider } from '#/state/shell/starter-pack';
|
||||||
|
import { Provider as HiddenRepliesProvider } from '#/state/threadgate-hidden-replies';
|
||||||
|
import * as Toast from '#/view/com/util/Toast';
|
||||||
|
import { Shell } from '#/view/shell/index';
|
||||||
|
import { ThemeProvider as Alf } from '#/alf';
|
||||||
|
import { useColorModeTheme } from '#/alf/util/useColorModeTheme';
|
||||||
|
import { Provider as ContextMenuProvider } from '#/components/ContextMenu';
|
||||||
|
import { useStarterPackEntry } from '#/components/hooks/useStarterPackEntry';
|
||||||
|
import { Provider as IntentDialogProvider } from '#/components/intents/IntentDialogs';
|
||||||
|
import { Provider as PolicyUpdateOverlayProvider } from '#/components/PolicyUpdateOverlay';
|
||||||
|
import { Provider as PortalProvider } from '#/components/Portal';
|
||||||
|
import { Provider as ActiveVideoProvider } from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext';
|
||||||
|
import { Provider as VideoVolumeProvider } from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext';
|
||||||
|
import { ToastOutlet } from '#/components/Toast';
|
||||||
|
import { prefetchAgeAssuranceConfig, Provider as AgeAssuranceV2Provider, } from '#/ageAssurance';
|
||||||
|
import { AnalyticsContext, AnalyticsFeaturesContext, features, setupDeviceId, } from '#/analytics';
|
||||||
|
import { prefetchLiveEvents, Provider as LiveEventsProvider, } from '#/features/liveEvents/context';
|
||||||
|
import * as Geo from '#/geolocation';
|
||||||
|
import { Splash } from '#/Splash';
|
||||||
|
import { BackgroundNotificationPreferencesProvider } from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider';
|
||||||
|
import { Provider as HideBottomBarBorderProvider } from './lib/hooks/useHideBottomBarBorder';
|
||||||
|
/**
|
||||||
|
* Begin geolocation ASAP
|
||||||
|
*/
|
||||||
|
Geo.resolve();
|
||||||
|
prefetchAgeAssuranceConfig();
|
||||||
|
prefetchLiveEvents();
|
||||||
|
prefetchAppConfig();
|
||||||
|
function InnerApp() {
|
||||||
|
var _a = useState(false), isReady = _a[0], setIsReady = _a[1];
|
||||||
|
var currentAccount = useSession().currentAccount;
|
||||||
|
var resumeSession = useSessionApi().resumeSession;
|
||||||
|
var theme = useColorModeTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var hasCheckedReferrer = useStarterPackEntry();
|
||||||
|
// init
|
||||||
|
useEffect(function () {
|
||||||
|
function onLaunch(account) {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var e_1;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
_a.trys.push([0, 5, 6, 7]);
|
||||||
|
if (!account) return [3 /*break*/, 2];
|
||||||
|
return [4 /*yield*/, resumeSession(account)];
|
||||||
|
case 1:
|
||||||
|
_a.sent();
|
||||||
|
return [3 /*break*/, 4];
|
||||||
|
case 2: return [4 /*yield*/, features.init];
|
||||||
|
case 3:
|
||||||
|
_a.sent();
|
||||||
|
_a.label = 4;
|
||||||
|
case 4: return [3 /*break*/, 7];
|
||||||
|
case 5:
|
||||||
|
e_1 = _a.sent();
|
||||||
|
logger.error("session: resumeSession failed", { message: e_1 });
|
||||||
|
return [3 /*break*/, 7];
|
||||||
|
case 6:
|
||||||
|
setIsReady(true);
|
||||||
|
return [7 /*endfinally*/];
|
||||||
|
case 7: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var account = readLastActiveAccount();
|
||||||
|
onLaunch(account);
|
||||||
|
}, [resumeSession]);
|
||||||
|
useEffect(function () {
|
||||||
|
return listenSessionDropped(function () {
|
||||||
|
Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sorry! Your session expired. Please sign in again."], ["Sorry! Your session expired. Please sign in again."])))), 'info');
|
||||||
|
});
|
||||||
|
}, [_]);
|
||||||
|
return (_jsx(Alf, { theme: theme, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(ContextMenuProvider, { children: _jsx(Splash, { isReady: isReady && hasCheckedReferrer, children: _jsx(VideoVolumeProvider, { children: _jsx(ActiveVideoProvider, { children: _jsx(Fragment
|
||||||
|
// Resets the entire tree below when it changes:
|
||||||
|
, { children: _jsx(AnalyticsFeaturesContext, { children: _jsx(QueryProvider, { currentDid: currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did, children: _jsx(PolicyUpdateOverlayProvider, { children: _jsx(LiveEventsProvider, { children: _jsx(AgeAssuranceV2Provider, { children: _jsx(ComposerProvider, { children: _jsx(MessagesProvider, { children: _jsx(LabelDefsProvider, { children: _jsx(ModerationOptsProvider, { children: _jsx(LoggedOutViewProvider, { children: _jsx(SelectedFeedProvider, { children: _jsx(HiddenRepliesProvider, { children: _jsx(HomeBadgeProvider, { children: _jsx(UnreadNotifsProvider, { children: _jsx(BackgroundNotificationPreferencesProvider, { children: _jsx(MutedThreadsProvider, { children: _jsx(SafeAreaProvider, { children: _jsx(ProgressGuideProvider, { children: _jsx(ServiceConfigProvider, { children: _jsx(EmailVerificationProvider, { children: _jsx(HideBottomBarBorderProvider, { children: _jsxs(IntentDialogProvider, { children: [_jsx(Shell, {}), _jsx(ToastOutlet, {})] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }, currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) }) }) }) }) }) }));
|
||||||
|
}
|
||||||
|
function App() {
|
||||||
|
var _a = useState(false), isReady = _a[0], setReady = _a[1];
|
||||||
|
useEffect(function () {
|
||||||
|
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(function () {
|
||||||
|
return setReady(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
if (!isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* NOTE: only nothing here can depend on other data or session state, since
|
||||||
|
* that is set up in the InnerApp component above.
|
||||||
|
*/
|
||||||
|
return (_jsx(Geo.Provider, { children: _jsx(AppConfigProvider, { children: _jsx(A11yProvider, { children: _jsx(OnboardingProvider, { children: _jsx(AnalyticsContext, { children: _jsx(SessionProvider, { children: _jsx(PrefsStateProvider, { children: _jsx(I18nProvider, { children: _jsx(ShellStateProvider, { children: _jsx(ModalStateProvider, { children: _jsx(DialogStateProvider, { children: _jsx(LightboxStateProvider, { children: _jsx(PortalProvider, { children: _jsx(StarterPackProvider, { children: _jsx(InnerApp, {}) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
|
||||||
|
}
|
||||||
|
export default Sentry.wrap(App);
|
||||||
|
var templateObject_1;
|
||||||
@@ -0,0 +1,599 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import * as Linking from 'expo-linking';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { createBottomTabNavigator, } from '@react-navigation/bottom-tabs';
|
||||||
|
import { CommonActions, createNavigationContainerRef, DarkTheme, DefaultTheme, NavigationContainer, StackActions, } from '@react-navigation/native';
|
||||||
|
import { timeout } from '#/lib/async/timeout';
|
||||||
|
import { useAccountSwitcher } from '#/lib/hooks/useAccountSwitcher';
|
||||||
|
import { useColorSchemeStyle } from '#/lib/hooks/useColorSchemeStyle';
|
||||||
|
import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback';
|
||||||
|
import { getNotificationPayload, notificationToURL, storePayloadForAccountSwitch, } from '#/lib/hooks/useNotificationHandler';
|
||||||
|
import { useWebScrollRestoration } from '#/lib/hooks/useWebScrollRestoration';
|
||||||
|
import { useCallOnce } from '#/lib/once';
|
||||||
|
import { buildStateObject } from '#/lib/routes/helpers';
|
||||||
|
import { bskyTitle } from '#/lib/strings/headings';
|
||||||
|
import { useUnreadNotifications } from '#/state/queries/notifications/unread';
|
||||||
|
import { useSession } from '#/state/session';
|
||||||
|
import { useLoggedOutViewControls } from '#/state/shell/logged-out';
|
||||||
|
import { shouldRequestEmailConfirmation, snoozeEmailConfirmationPrompt, } from '#/state/shell/reminders';
|
||||||
|
import { useCloseAllActiveElements } from '#/state/util';
|
||||||
|
import { CommunityGuidelinesScreen } from '#/view/screens/CommunityGuidelines';
|
||||||
|
import { CopyrightPolicyScreen } from '#/view/screens/CopyrightPolicy';
|
||||||
|
import { DebugModScreen } from '#/view/screens/DebugMod';
|
||||||
|
import { FeedsScreen } from '#/view/screens/Feeds';
|
||||||
|
import { HomeScreen } from '#/view/screens/Home';
|
||||||
|
import { ListsScreen } from '#/view/screens/Lists';
|
||||||
|
import { ModerationBlockedAccounts } from '#/view/screens/ModerationBlockedAccounts';
|
||||||
|
import { ModerationModlistsScreen } from '#/view/screens/ModerationModlists';
|
||||||
|
import { ModerationMutedAccounts } from '#/view/screens/ModerationMutedAccounts';
|
||||||
|
import { NotFoundScreen } from '#/view/screens/NotFound';
|
||||||
|
import { NotificationsScreen } from '#/view/screens/Notifications';
|
||||||
|
import { PostThreadScreen } from '#/view/screens/PostThread';
|
||||||
|
import { PrivacyPolicyScreen } from '#/view/screens/PrivacyPolicy';
|
||||||
|
import { ProfileScreen } from '#/view/screens/Profile';
|
||||||
|
import { ProfileFeedLikedByScreen } from '#/view/screens/ProfileFeedLikedBy';
|
||||||
|
import { StorybookScreen } from '#/view/screens/Storybook';
|
||||||
|
import { SupportScreen } from '#/view/screens/Support';
|
||||||
|
import { TermsOfServiceScreen } from '#/view/screens/TermsOfService';
|
||||||
|
import { BottomBar } from '#/view/shell/bottom-bar/BottomBar';
|
||||||
|
import { createNativeStackNavigatorWithAuth } from '#/view/shell/createNativeStackNavigatorWithAuth';
|
||||||
|
import { BookmarksScreen } from '#/screens/Bookmarks';
|
||||||
|
import { SharedPreferencesTesterScreen } from '#/screens/E2E/SharedPreferencesTesterScreen';
|
||||||
|
import { FindContactsFlowScreen } from '#/screens/FindContactsFlowScreen';
|
||||||
|
import HashtagScreen from '#/screens/Hashtag';
|
||||||
|
import { LogScreen } from '#/screens/Log';
|
||||||
|
import { MessagesScreen } from '#/screens/Messages/ChatList';
|
||||||
|
import { MessagesConversationScreen } from '#/screens/Messages/Conversation';
|
||||||
|
import { MessagesInboxScreen } from '#/screens/Messages/Inbox';
|
||||||
|
import { MessagesSettingsScreen } from '#/screens/Messages/Settings';
|
||||||
|
import { ModerationScreen } from '#/screens/Moderation';
|
||||||
|
import { Screen as ModerationVerificationSettings } from '#/screens/Moderation/VerificationSettings';
|
||||||
|
import { Screen as ModerationInteractionSettings } from '#/screens/ModerationInteractionSettings';
|
||||||
|
import { NotificationsActivityListScreen } from '#/screens/Notifications/ActivityList';
|
||||||
|
import { PostLikedByScreen } from '#/screens/Post/PostLikedBy';
|
||||||
|
import { PostQuotesScreen } from '#/screens/Post/PostQuotes';
|
||||||
|
import { PostRepostedByScreen } from '#/screens/Post/PostRepostedBy';
|
||||||
|
import { ProfileKnownFollowersScreen } from '#/screens/Profile/KnownFollowers';
|
||||||
|
import { ProfileFeedScreen } from '#/screens/Profile/ProfileFeed';
|
||||||
|
import { ProfileFollowersScreen } from '#/screens/Profile/ProfileFollowers';
|
||||||
|
import { ProfileFollowsScreen } from '#/screens/Profile/ProfileFollows';
|
||||||
|
import { ProfileLabelerLikedByScreen } from '#/screens/Profile/ProfileLabelerLikedBy';
|
||||||
|
import { ProfileSearchScreen } from '#/screens/Profile/ProfileSearch';
|
||||||
|
import { ProfileListScreen } from '#/screens/ProfileList';
|
||||||
|
import { SavedFeeds } from '#/screens/SavedFeeds';
|
||||||
|
import { SearchScreen } from '#/screens/Search';
|
||||||
|
import { AboutSettingsScreen } from '#/screens/Settings/AboutSettings';
|
||||||
|
import { AccessibilitySettingsScreen } from '#/screens/Settings/AccessibilitySettings';
|
||||||
|
import { AccountSettingsScreen } from '#/screens/Settings/AccountSettings';
|
||||||
|
import { ActivityPrivacySettingsScreen } from '#/screens/Settings/ActivityPrivacySettings';
|
||||||
|
import { AppearanceSettingsScreen } from '#/screens/Settings/AppearanceSettings';
|
||||||
|
import { AppIconSettingsScreen } from '#/screens/Settings/AppIconSettings';
|
||||||
|
import { AppPasswordsScreen } from '#/screens/Settings/AppPasswords';
|
||||||
|
import { ContentAndMediaSettingsScreen } from '#/screens/Settings/ContentAndMediaSettings';
|
||||||
|
import { ExternalMediaPreferencesScreen } from '#/screens/Settings/ExternalMediaPreferences';
|
||||||
|
import { FindContactsSettingsScreen } from '#/screens/Settings/FindContactsSettings';
|
||||||
|
import { FollowingFeedPreferencesScreen } from '#/screens/Settings/FollowingFeedPreferences';
|
||||||
|
import { InterestsSettingsScreen } from '#/screens/Settings/InterestsSettings';
|
||||||
|
import { LanguageSettingsScreen } from '#/screens/Settings/LanguageSettings';
|
||||||
|
import { LegacyNotificationSettingsScreen } from '#/screens/Settings/LegacyNotificationSettings';
|
||||||
|
import { NotificationSettingsScreen } from '#/screens/Settings/NotificationSettings';
|
||||||
|
import { ActivityNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings';
|
||||||
|
import { LikeNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/LikeNotificationSettings';
|
||||||
|
import { LikesOnRepostsNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings';
|
||||||
|
import { MentionNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/MentionNotificationSettings';
|
||||||
|
import { MiscellaneousNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings';
|
||||||
|
import { NewFollowerNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings';
|
||||||
|
import { QuoteNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings';
|
||||||
|
import { ReplyNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings';
|
||||||
|
import { RepostNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/RepostNotificationSettings';
|
||||||
|
import { RepostsOnRepostsNotificationSettingsScreen } from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings';
|
||||||
|
import { PrivacyAndSecuritySettingsScreen } from '#/screens/Settings/PrivacyAndSecuritySettings';
|
||||||
|
import { SettingsScreen } from '#/screens/Settings/Settings';
|
||||||
|
import { ThreadPreferencesScreen } from '#/screens/Settings/ThreadPreferences';
|
||||||
|
import { StarterPackScreen, StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen';
|
||||||
|
import { Wizard } from '#/screens/StarterPack/Wizard';
|
||||||
|
import TopicScreen from '#/screens/Topic';
|
||||||
|
import { VideoFeed } from '#/screens/VideoFeed';
|
||||||
|
import { useTheme } from '#/alf';
|
||||||
|
import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog';
|
||||||
|
import { useAnalytics } from '#/analytics';
|
||||||
|
import { setNavigationMetadata } from '#/analytics/metadata';
|
||||||
|
import { IS_LIQUID_GLASS, IS_NATIVE, IS_WEB } from '#/env';
|
||||||
|
import { router } from '#/routes';
|
||||||
|
import { Referrer } from '../modules/expo-bluesky-swiss-army';
|
||||||
|
var navigationRef = createNavigationContainerRef();
|
||||||
|
var HomeTab = createNativeStackNavigatorWithAuth();
|
||||||
|
var SearchTab = createNativeStackNavigatorWithAuth();
|
||||||
|
var NotificationsTab = createNativeStackNavigatorWithAuth();
|
||||||
|
var MyProfileTab = createNativeStackNavigatorWithAuth();
|
||||||
|
var MessagesTab = createNativeStackNavigatorWithAuth();
|
||||||
|
var Flat = createNativeStackNavigatorWithAuth();
|
||||||
|
var Tab = createBottomTabNavigator();
|
||||||
|
/**
|
||||||
|
* These "common screens" are reused across stacks.
|
||||||
|
*/
|
||||||
|
function commonScreens(Stack, unreadCountLabel) {
|
||||||
|
var title = function (page) {
|
||||||
|
return bskyTitle(i18n._(page), unreadCountLabel);
|
||||||
|
};
|
||||||
|
return (_jsxs(_Fragment, { children: [_jsx(Stack.Screen, { name: "NotFound", getComponent: function () { return NotFoundScreen; }, options: { title: title(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Not Found"], ["Not Found"])))) } }), _jsx(Stack.Screen, { name: "Lists", component: ListsScreen, options: { title: title(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Lists"], ["Lists"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Moderation", getComponent: function () { return ModerationScreen; }, options: { title: title(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Moderation"], ["Moderation"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationModlists", getComponent: function () { return ModerationModlistsScreen; }, options: { title: title(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Moderation Lists"], ["Moderation Lists"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationMutedAccounts", getComponent: function () { return ModerationMutedAccounts; }, options: { title: title(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Muted Accounts"], ["Muted Accounts"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationBlockedAccounts", getComponent: function () { return ModerationBlockedAccounts; }, options: { title: title(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Blocked Accounts"], ["Blocked Accounts"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ModerationInteractionSettings", getComponent: function () { return ModerationInteractionSettings; }, options: {
|
||||||
|
title: title(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Post Interaction Settings"], ["Post Interaction Settings"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ModerationVerificationSettings", getComponent: function () { return ModerationVerificationSettings; }, options: {
|
||||||
|
title: title(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Verification Settings"], ["Verification Settings"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "Settings", getComponent: function () { return SettingsScreen; }, options: { title: title(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Settings"], ["Settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "LanguageSettings", getComponent: function () { return LanguageSettingsScreen; }, options: { title: title(msg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Language Settings"], ["Language Settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Profile", getComponent: function () { return ProfileScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: bskyTitle("@".concat(route.params.name), unreadCountLabel),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ProfileFollowers", getComponent: function () { return ProfileFollowersScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_11 || (templateObject_11 = __makeTemplateObject(["People following @", ""], ["People following @", ""])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ProfileFollows", getComponent: function () { return ProfileFollowsScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_12 || (templateObject_12 = __makeTemplateObject(["People followed by @", ""], ["People followed by @", ""])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ProfileKnownFollowers", getComponent: function () { return ProfileKnownFollowersScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Followers of @", " that you know"], ["Followers of @", " that you know"])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ProfileList", getComponent: function () { return ProfileListScreen; }, options: { title: title(msg(templateObject_14 || (templateObject_14 = __makeTemplateObject(["List"], ["List"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ProfileSearch", getComponent: function () { return ProfileSearchScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Search @", "'s posts"], ["Search @", "'s posts"])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "PostThread", getComponent: function () { return PostThreadScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "PostLikedBy", getComponent: function () { return PostLikedByScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "PostRepostedBy", getComponent: function () { return PostRepostedByScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "PostQuotes", getComponent: function () { return PostQuotesScreen; }, options: function (_a) {
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
title: title(msg(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Post by @", ""], ["Post by @", ""])), route.params.name)),
|
||||||
|
});
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ProfileFeed", getComponent: function () { return ProfileFeedScreen; }, options: { title: title(msg(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Feed"], ["Feed"])))) } }), _jsx(Stack.Screen, { name: "ProfileFeedLikedBy", getComponent: function () { return ProfileFeedLikedByScreen; }, options: { title: title(msg(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Liked by"], ["Liked by"])))) } }), _jsx(Stack.Screen, { name: "ProfileLabelerLikedBy", getComponent: function () { return ProfileLabelerLikedByScreen; }, options: { title: title(msg(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Liked by"], ["Liked by"])))) } }), _jsx(Stack.Screen, { name: "Debug", getComponent: function () { return StorybookScreen; }, options: { title: title(msg(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Storybook"], ["Storybook"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "DebugMod", getComponent: function () { return DebugModScreen; }, options: { title: title(msg(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Moderation states"], ["Moderation states"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "SharedPreferencesTester", getComponent: function () { return SharedPreferencesTesterScreen; }, options: { title: title(msg(templateObject_25 || (templateObject_25 = __makeTemplateObject(["Shared Preferences Tester"], ["Shared Preferences Tester"])))) } }), _jsx(Stack.Screen, { name: "Log", getComponent: function () { return LogScreen; }, options: { title: title(msg(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Log"], ["Log"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Support", getComponent: function () { return SupportScreen; }, options: { title: title(msg(templateObject_27 || (templateObject_27 = __makeTemplateObject(["Support"], ["Support"])))) } }), _jsx(Stack.Screen, { name: "PrivacyPolicy", getComponent: function () { return PrivacyPolicyScreen; }, options: { title: title(msg(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Privacy Policy"], ["Privacy Policy"])))) } }), _jsx(Stack.Screen, { name: "TermsOfService", getComponent: function () { return TermsOfServiceScreen; }, options: { title: title(msg(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Terms of Service"], ["Terms of Service"])))) } }), _jsx(Stack.Screen, { name: "CommunityGuidelines", getComponent: function () { return CommunityGuidelinesScreen; }, options: { title: title(msg(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Community Guidelines"], ["Community Guidelines"])))) } }), _jsx(Stack.Screen, { name: "CopyrightPolicy", getComponent: function () { return CopyrightPolicyScreen; }, options: { title: title(msg(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Copyright Policy"], ["Copyright Policy"])))) } }), _jsx(Stack.Screen, { name: "AppPasswords", getComponent: function () { return AppPasswordsScreen; }, options: { title: title(msg(templateObject_32 || (templateObject_32 = __makeTemplateObject(["App Passwords"], ["App Passwords"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "SavedFeeds", getComponent: function () { return SavedFeeds; }, options: { title: title(msg(templateObject_33 || (templateObject_33 = __makeTemplateObject(["Edit My Feeds"], ["Edit My Feeds"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "PreferencesFollowingFeed", getComponent: function () { return FollowingFeedPreferencesScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_34 || (templateObject_34 = __makeTemplateObject(["Following Feed Preferences"], ["Following Feed Preferences"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "PreferencesThreads", getComponent: function () { return ThreadPreferencesScreen; }, options: { title: title(msg(templateObject_35 || (templateObject_35 = __makeTemplateObject(["Threads Preferences"], ["Threads Preferences"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "PreferencesExternalEmbeds", getComponent: function () { return ExternalMediaPreferencesScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_36 || (templateObject_36 = __makeTemplateObject(["External Media Preferences"], ["External Media Preferences"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "AccessibilitySettings", getComponent: function () { return AccessibilitySettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Accessibility Settings"], ["Accessibility Settings"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "AppearanceSettings", getComponent: function () { return AppearanceSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Appearance"], ["Appearance"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "AccountSettings", getComponent: function () { return AccountSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Account"], ["Account"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "PrivacyAndSecuritySettings", getComponent: function () { return PrivacyAndSecuritySettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_40 || (templateObject_40 = __makeTemplateObject(["Privacy and Security"], ["Privacy and Security"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ActivityPrivacySettings", getComponent: function () { return ActivityPrivacySettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Privacy and Security"], ["Privacy and Security"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "FindContactsSettings", getComponent: function () { return FindContactsSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_42 || (templateObject_42 = __makeTemplateObject(["Find Contacts"], ["Find Contacts"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "NotificationSettings", getComponent: function () { return NotificationSettingsScreen; }, options: { title: title(msg(templateObject_43 || (templateObject_43 = __makeTemplateObject(["Notification settings"], ["Notification settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "ReplyNotificationSettings", getComponent: function () { return ReplyNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_44 || (templateObject_44 = __makeTemplateObject(["Reply notifications"], ["Reply notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "MentionNotificationSettings", getComponent: function () { return MentionNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_45 || (templateObject_45 = __makeTemplateObject(["Mention notifications"], ["Mention notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "QuoteNotificationSettings", getComponent: function () { return QuoteNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_46 || (templateObject_46 = __makeTemplateObject(["Quote notifications"], ["Quote notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "LikeNotificationSettings", getComponent: function () { return LikeNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_47 || (templateObject_47 = __makeTemplateObject(["Like notifications"], ["Like notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "RepostNotificationSettings", getComponent: function () { return RepostNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_48 || (templateObject_48 = __makeTemplateObject(["Repost notifications"], ["Repost notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "NewFollowerNotificationSettings", getComponent: function () { return NewFollowerNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_49 || (templateObject_49 = __makeTemplateObject(["New follower notifications"], ["New follower notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "LikesOnRepostsNotificationSettings", getComponent: function () { return LikesOnRepostsNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_50 || (templateObject_50 = __makeTemplateObject(["Likes of your reposts notifications"], ["Likes of your reposts notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "RepostsOnRepostsNotificationSettings", getComponent: function () { return RepostsOnRepostsNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_51 || (templateObject_51 = __makeTemplateObject(["Reposts of your reposts notifications"], ["Reposts of your reposts notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ActivityNotificationSettings", getComponent: function () { return ActivityNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_52 || (templateObject_52 = __makeTemplateObject(["Activity notifications"], ["Activity notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "MiscellaneousNotificationSettings", getComponent: function () { return MiscellaneousNotificationSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_53 || (templateObject_53 = __makeTemplateObject(["Miscellaneous notifications"], ["Miscellaneous notifications"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "ContentAndMediaSettings", getComponent: function () { return ContentAndMediaSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_54 || (templateObject_54 = __makeTemplateObject(["Content and Media"], ["Content and Media"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "InterestsSettings", getComponent: function () { return InterestsSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_55 || (templateObject_55 = __makeTemplateObject(["Your interests"], ["Your interests"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "AboutSettings", getComponent: function () { return AboutSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_56 || (templateObject_56 = __makeTemplateObject(["About"], ["About"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "AppIconSettings", getComponent: function () { return AppIconSettingsScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_57 || (templateObject_57 = __makeTemplateObject(["App Icon"], ["App Icon"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "Hashtag", getComponent: function () { return HashtagScreen; }, options: { title: title(msg(templateObject_58 || (templateObject_58 = __makeTemplateObject(["Hashtag"], ["Hashtag"])))) } }), _jsx(Stack.Screen, { name: "Topic", getComponent: function () { return TopicScreen; }, options: { title: title(msg(templateObject_59 || (templateObject_59 = __makeTemplateObject(["Topic"], ["Topic"])))) } }), _jsx(Stack.Screen, { name: "MessagesConversation", getComponent: function () { return MessagesConversationScreen; }, options: { title: title(msg(templateObject_60 || (templateObject_60 = __makeTemplateObject(["Chat"], ["Chat"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "MessagesSettings", getComponent: function () { return MessagesSettingsScreen; }, options: { title: title(msg(templateObject_61 || (templateObject_61 = __makeTemplateObject(["Chat settings"], ["Chat settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "MessagesInbox", getComponent: function () { return MessagesInboxScreen; }, options: { title: title(msg(templateObject_62 || (templateObject_62 = __makeTemplateObject(["Chat request inbox"], ["Chat request inbox"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "NotificationsActivityList", getComponent: function () { return NotificationsActivityListScreen; }, options: { title: title(msg(templateObject_63 || (templateObject_63 = __makeTemplateObject(["Notifications"], ["Notifications"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "LegacyNotificationSettings", getComponent: function () { return LegacyNotificationSettingsScreen; }, options: { title: title(msg(templateObject_64 || (templateObject_64 = __makeTemplateObject(["Notification settings"], ["Notification settings"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "Feeds", getComponent: function () { return FeedsScreen; }, options: { title: title(msg(templateObject_65 || (templateObject_65 = __makeTemplateObject(["Feeds"], ["Feeds"])))) } }), _jsx(Stack.Screen, { name: "StarterPack", getComponent: function () { return StarterPackScreen; }, options: { title: title(msg(templateObject_66 || (templateObject_66 = __makeTemplateObject(["Starter Pack"], ["Starter Pack"])))) } }), _jsx(Stack.Screen, { name: "StarterPackShort", getComponent: function () { return StarterPackScreenShort; }, options: { title: title(msg(templateObject_67 || (templateObject_67 = __makeTemplateObject(["Starter Pack"], ["Starter Pack"])))) } }), _jsx(Stack.Screen, { name: "StarterPackWizard", getComponent: function () { return Wizard; }, options: { title: title(msg(templateObject_68 || (templateObject_68 = __makeTemplateObject(["Create a starter pack"], ["Create a starter pack"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "StarterPackEdit", getComponent: function () { return Wizard; }, options: { title: title(msg(templateObject_69 || (templateObject_69 = __makeTemplateObject(["Edit your starter pack"], ["Edit your starter pack"])))), requireAuth: true } }), _jsx(Stack.Screen, { name: "VideoFeed", getComponent: function () { return VideoFeed; }, options: {
|
||||||
|
title: title(msg(templateObject_70 || (templateObject_70 = __makeTemplateObject(["Video Feed"], ["Video Feed"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "Bookmarks", getComponent: function () { return BookmarksScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_71 || (templateObject_71 = __makeTemplateObject(["Saved Posts"], ["Saved Posts"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
} }), _jsx(Stack.Screen, { name: "FindContactsFlow", getComponent: function () { return FindContactsFlowScreen; }, options: {
|
||||||
|
title: title(msg(templateObject_72 || (templateObject_72 = __makeTemplateObject(["Find Contacts"], ["Find Contacts"])))),
|
||||||
|
requireAuth: true,
|
||||||
|
gestureEnabled: false,
|
||||||
|
} })] }));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The TabsNavigator is used by native mobile to represent the routes
|
||||||
|
* in 3 distinct tab-stacks with a different root screen on each.
|
||||||
|
*/
|
||||||
|
function TabsNavigator(_a) {
|
||||||
|
var layout = _a.layout;
|
||||||
|
var tabBar = useCallback(function (props) { return (_jsx(BottomBar, __assign({}, props))); }, []);
|
||||||
|
return (_jsxs(Tab.Navigator, { initialRouteName: "HomeTab", backBehavior: "initialRoute", screenOptions: { headerShown: false, lazy: true }, tabBar: tabBar, layout: layout, children: [_jsx(Tab.Screen, { name: "HomeTab", getComponent: function () { return HomeTabNavigator; } }), _jsx(Tab.Screen, { name: "SearchTab", getComponent: function () { return SearchTabNavigator; } }), _jsx(Tab.Screen, { name: "MessagesTab", getComponent: function () { return MessagesTabNavigator; } }), _jsx(Tab.Screen, { name: "NotificationsTab", getComponent: function () { return NotificationsTabNavigator; } }), _jsx(Tab.Screen, { name: "MyProfileTab", getComponent: function () { return MyProfileTabNavigator; } })] }));
|
||||||
|
}
|
||||||
|
function screenOptions(t) {
|
||||||
|
return {
|
||||||
|
fullScreenGestureEnabled: true,
|
||||||
|
headerShown: false,
|
||||||
|
contentStyle: t.atoms.bg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function HomeTabNavigator() {
|
||||||
|
var t = useTheme();
|
||||||
|
var BLURRED_SCROLL_EDGE_EFFECT = IS_LIQUID_GLASS
|
||||||
|
? {
|
||||||
|
headerShown: true,
|
||||||
|
headerTransparent: true,
|
||||||
|
headerTitle: '',
|
||||||
|
scrollEdgeEffects: {
|
||||||
|
top: 'soft',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
return (_jsxs(HomeTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Home", children: [_jsx(HomeTab.Screen, { name: "Home", getComponent: function () { return HomeScreen; }, options: BLURRED_SCROLL_EDGE_EFFECT }), _jsx(HomeTab.Screen, { name: "Start", getComponent: function () { return HomeScreen; }, options: BLURRED_SCROLL_EDGE_EFFECT }), commonScreens(HomeTab)] }));
|
||||||
|
}
|
||||||
|
function SearchTabNavigator() {
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(SearchTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Search", children: [_jsx(SearchTab.Screen, { name: "Search", getComponent: function () { return SearchScreen; } }), commonScreens(SearchTab)] }));
|
||||||
|
}
|
||||||
|
function NotificationsTabNavigator() {
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(NotificationsTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Notifications", children: [_jsx(NotificationsTab.Screen, { name: "Notifications", getComponent: function () { return NotificationsScreen; }, options: { requireAuth: true } }), commonScreens(NotificationsTab)] }));
|
||||||
|
}
|
||||||
|
function MyProfileTabNavigator() {
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(MyProfileTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "MyProfile", children: [_jsx(MyProfileTab.Screen
|
||||||
|
// MyProfile is not in AllNavigationParams - asserting as Profile at least
|
||||||
|
// gives us typechecking for initialParams -sfn
|
||||||
|
, {
|
||||||
|
// MyProfile is not in AllNavigationParams - asserting as Profile at least
|
||||||
|
// gives us typechecking for initialParams -sfn
|
||||||
|
name: 'MyProfile', getComponent: function () { return ProfileScreen; }, initialParams: { name: 'me', hideBackButton: true } }), commonScreens(MyProfileTab)] }));
|
||||||
|
}
|
||||||
|
function MessagesTabNavigator() {
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(MessagesTab.Navigator, { screenOptions: screenOptions(t), initialRouteName: "Messages", children: [_jsx(MessagesTab.Screen, { name: "Messages", getComponent: function () { return MessagesScreen; }, options: function (_a) {
|
||||||
|
var _b, _c;
|
||||||
|
var route = _a.route;
|
||||||
|
return ({
|
||||||
|
requireAuth: true,
|
||||||
|
animationTypeForReplace: (_c = (_b = route.params) === null || _b === void 0 ? void 0 : _b.animation) !== null && _c !== void 0 ? _c : 'push',
|
||||||
|
});
|
||||||
|
} }), commonScreens(MessagesTab)] }));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The FlatNavigator is used by Web to represent the routes
|
||||||
|
* in a single ("flat") stack.
|
||||||
|
*/
|
||||||
|
var FlatNavigator = function (_a) {
|
||||||
|
var layout = _a.layout;
|
||||||
|
var t = useTheme();
|
||||||
|
var numUnread = useUnreadNotifications();
|
||||||
|
var screenListeners = useWebScrollRestoration();
|
||||||
|
var title = function (page) { return bskyTitle(i18n._(page), numUnread); };
|
||||||
|
return (_jsxs(Flat.Navigator, { layout: layout, screenListeners: screenListeners, screenOptions: screenOptions(t), children: [_jsx(Flat.Screen, { name: "Home", getComponent: function () { return HomeScreen; }, options: { title: title(msg(templateObject_73 || (templateObject_73 = __makeTemplateObject(["Home"], ["Home"])))) } }), _jsx(Flat.Screen, { name: "Search", getComponent: function () { return SearchScreen; }, options: { title: title(msg(templateObject_74 || (templateObject_74 = __makeTemplateObject(["Explore"], ["Explore"])))) } }), _jsx(Flat.Screen, { name: "Notifications", getComponent: function () { return NotificationsScreen; }, options: { title: title(msg(templateObject_75 || (templateObject_75 = __makeTemplateObject(["Notifications"], ["Notifications"])))), requireAuth: true } }), _jsx(Flat.Screen, { name: "Messages", getComponent: function () { return MessagesScreen; }, options: { title: title(msg(templateObject_76 || (templateObject_76 = __makeTemplateObject(["Messages"], ["Messages"])))), requireAuth: true } }), _jsx(Flat.Screen, { name: "Start", getComponent: function () { return HomeScreen; }, options: { title: title(msg(templateObject_77 || (templateObject_77 = __makeTemplateObject(["Home"], ["Home"])))) } }), commonScreens(Flat, numUnread)] }));
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The RoutesContainer should wrap all components which need access
|
||||||
|
* to the navigation context.
|
||||||
|
*/
|
||||||
|
var LINKING = {
|
||||||
|
// TODO figure out what we are going to use
|
||||||
|
// note: `bluesky://` is what is used in app.config.js
|
||||||
|
prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
|
||||||
|
getPathFromState: function (state) {
|
||||||
|
var _a, _b, _c, _d;
|
||||||
|
// find the current node in the navigation tree
|
||||||
|
var node = state.routes[state.index || 0];
|
||||||
|
while (((_a = node.state) === null || _a === void 0 ? void 0 : _a.routes) && typeof ((_b = node.state) === null || _b === void 0 ? void 0 : _b.index) === 'number') {
|
||||||
|
node = (_c = node.state) === null || _c === void 0 ? void 0 : _c.routes[(_d = node.state) === null || _d === void 0 ? void 0 : _d.index];
|
||||||
|
}
|
||||||
|
// build the path
|
||||||
|
var route = router.matchName(node.name);
|
||||||
|
if (typeof route === 'undefined') {
|
||||||
|
return '/'; // default to home
|
||||||
|
}
|
||||||
|
return route.build((node.params || {}));
|
||||||
|
},
|
||||||
|
getStateFromPath: function (path) {
|
||||||
|
var _a = router.matchPath(path), name = _a[0], params = _a[1];
|
||||||
|
// Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the
|
||||||
|
// intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid
|
||||||
|
// intent
|
||||||
|
// On web, there is no route state that's created by default, so we should initialize it as the home route. On
|
||||||
|
// native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
|
||||||
|
// since it will be created by react-navigation.
|
||||||
|
if (path.includes('intent/')) {
|
||||||
|
if (IS_NATIVE)
|
||||||
|
return;
|
||||||
|
return buildStateObject('Flat', 'Home', params);
|
||||||
|
}
|
||||||
|
if (IS_NATIVE) {
|
||||||
|
if (name === 'Search') {
|
||||||
|
return buildStateObject('SearchTab', 'Search', params);
|
||||||
|
}
|
||||||
|
if (name === 'Notifications') {
|
||||||
|
return buildStateObject('NotificationsTab', 'Notifications', params);
|
||||||
|
}
|
||||||
|
if (name === 'Home') {
|
||||||
|
return buildStateObject('HomeTab', 'Home', params);
|
||||||
|
}
|
||||||
|
if (name === 'Messages') {
|
||||||
|
return buildStateObject('MessagesTab', 'Messages', params);
|
||||||
|
}
|
||||||
|
// if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work
|
||||||
|
return buildStateObject('HomeTab', name, params, [
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var res = buildStateObject('Flat', name, params);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
function RoutesContainer(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var ax = useAnalytics();
|
||||||
|
var notyLogger = ax.logger.useChild(ax.logger.Context.Notifications);
|
||||||
|
var theme = useColorSchemeStyle(DefaultTheme, DarkTheme);
|
||||||
|
var _b = useSession(), currentAccount = _b.currentAccount, accounts = _b.accounts;
|
||||||
|
var onPressSwitchAccount = useAccountSwitcher().onPressSwitchAccount;
|
||||||
|
var setShowLoggedOut = useLoggedOutViewControls().setShowLoggedOut;
|
||||||
|
var previousScreen = useRef(undefined);
|
||||||
|
var emailDialogControl = useEmailDialogControl();
|
||||||
|
var closeAllActiveElements = useCloseAllActiveElements();
|
||||||
|
var linkingUrl = Linking.useLinkingURL();
|
||||||
|
/**
|
||||||
|
* Handle navigation to a conversation, or prepares for account switch.
|
||||||
|
*
|
||||||
|
* Non-reactive because we need the latest data from some hooks
|
||||||
|
* after an async call - sfn
|
||||||
|
*/
|
||||||
|
var handleChatMessage = useNonReactiveCallback(function (payload) {
|
||||||
|
notyLogger.debug("handleChatMessage", { payload: payload });
|
||||||
|
if (payload.recipientDid !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did)) {
|
||||||
|
// handled in useNotificationHandler after account switch finishes
|
||||||
|
storePayloadForAccountSwitch(payload);
|
||||||
|
closeAllActiveElements();
|
||||||
|
var account = accounts.find(function (a) { return a.did === payload.recipientDid; });
|
||||||
|
if (account) {
|
||||||
|
onPressSwitchAccount(account, 'Notification');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setShowLoggedOut(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// @ts-expect-error nested navigators aren't typed -sfn
|
||||||
|
navigate('MessagesTab', {
|
||||||
|
screen: 'Messages',
|
||||||
|
params: {
|
||||||
|
pushToConversation: payload.convoId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function handlePushNotificationEntry() {
|
||||||
|
if (!IS_NATIVE)
|
||||||
|
return;
|
||||||
|
// intent urls are handled by `useIntentHandler`
|
||||||
|
if (linkingUrl)
|
||||||
|
return;
|
||||||
|
var notificationResponse = Notifications.getLastNotificationResponse();
|
||||||
|
if (notificationResponse) {
|
||||||
|
notyLogger.debug("handlePushNotificationEntry: response", {
|
||||||
|
response: notificationResponse,
|
||||||
|
});
|
||||||
|
// Clear the last notification response to ensure it's not used again
|
||||||
|
try {
|
||||||
|
Notifications.clearLastNotificationResponse();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notyLogger.error("handlePushNotificationEntry: error clearing notification response", { error: error });
|
||||||
|
}
|
||||||
|
var payload = getNotificationPayload(notificationResponse.notification);
|
||||||
|
if (payload) {
|
||||||
|
ax.metric('notifications:openApp', {
|
||||||
|
reason: payload.reason,
|
||||||
|
causedBoot: true,
|
||||||
|
});
|
||||||
|
if (payload.reason === 'chat-message') {
|
||||||
|
handleChatMessage(payload);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var path = notificationToURL(payload);
|
||||||
|
if (path === '/notifications') {
|
||||||
|
resetToTab('NotificationsTab');
|
||||||
|
notyLogger.debug("handlePushNotificationEntry: default navigate");
|
||||||
|
}
|
||||||
|
else if (path) {
|
||||||
|
var _a = router.matchPath(path), screen_1 = _a[0], params = _a[1];
|
||||||
|
// @ts-expect-error nested navigators aren't typed -sfn
|
||||||
|
navigate('HomeTab', { screen: screen_1, params: params });
|
||||||
|
notyLogger.debug("handlePushNotificationEntry: navigate", {
|
||||||
|
screen: screen_1,
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var onNavigationReady = useCallOnce(function () {
|
||||||
|
var currentScreen = getCurrentRouteName();
|
||||||
|
setNavigationMetadata({
|
||||||
|
previousScreen: currentScreen,
|
||||||
|
currentScreen: currentScreen,
|
||||||
|
});
|
||||||
|
previousScreen.current = currentScreen;
|
||||||
|
handlePushNotificationEntry();
|
||||||
|
ax.metric('router:navigate', {});
|
||||||
|
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
|
||||||
|
emailDialogControl.open({
|
||||||
|
id: EmailDialogScreenID.VerificationReminder,
|
||||||
|
});
|
||||||
|
snoozeEmailConfirmationPrompt();
|
||||||
|
}
|
||||||
|
ax.metric('init', {
|
||||||
|
initMs: Math.round(
|
||||||
|
// @ts-ignore Emitted by Metro in the bundle prelude
|
||||||
|
performance.now() - global.__BUNDLE_START_TIME__),
|
||||||
|
});
|
||||||
|
if (IS_WEB) {
|
||||||
|
var referrerInfo = Referrer.getReferrerInfo();
|
||||||
|
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||||
|
ax.metric('deepLink:referrerReceived', {
|
||||||
|
to: window.location.href,
|
||||||
|
referrer: referrerInfo === null || referrerInfo === void 0 ? void 0 : referrerInfo.referrer,
|
||||||
|
hostname: referrerInfo === null || referrerInfo === void 0 ? void 0 : referrerInfo.hostname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// temp, just testing
|
||||||
|
void ax.features.enabled(ax.features.AATest);
|
||||||
|
});
|
||||||
|
return (_jsx(NavigationContainer, { ref: navigationRef, linking: LINKING, theme: theme, onStateChange: function () {
|
||||||
|
var currentScreen = getCurrentRouteName();
|
||||||
|
// do this before metric
|
||||||
|
setNavigationMetadata({
|
||||||
|
previousScreen: previousScreen.current,
|
||||||
|
currentScreen: currentScreen,
|
||||||
|
});
|
||||||
|
ax.metric('router:navigate', { from: previousScreen.current });
|
||||||
|
previousScreen.current = currentScreen;
|
||||||
|
}, onReady: onNavigationReady,
|
||||||
|
// WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
|
||||||
|
// However, there's a fair amount of places we do that, especially in when popping to the top of stacks.
|
||||||
|
// See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly.
|
||||||
|
// I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now.
|
||||||
|
// We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x
|
||||||
|
// -sfn
|
||||||
|
navigationInChildEnabled: true, children: children }));
|
||||||
|
}
|
||||||
|
function getCurrentRouteName() {
|
||||||
|
var _a;
|
||||||
|
if (navigationRef.isReady()) {
|
||||||
|
return (_a = navigationRef.getCurrentRoute()) === null || _a === void 0 ? void 0 : _a.name;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* These helpers can be used from outside of the RoutesContainer
|
||||||
|
* (eg in the state models).
|
||||||
|
*/
|
||||||
|
function navigate(name, params) {
|
||||||
|
if (navigationRef.isReady()) {
|
||||||
|
return Promise.race([
|
||||||
|
new Promise(function (resolve) {
|
||||||
|
var handler = function () {
|
||||||
|
resolve();
|
||||||
|
navigationRef.removeListener('state', handler);
|
||||||
|
};
|
||||||
|
navigationRef.addListener('state', handler);
|
||||||
|
// @ts-ignore I dont know what would make typescript happy but I have a life -prf
|
||||||
|
navigationRef.navigate(name, params);
|
||||||
|
}),
|
||||||
|
timeout(1e3),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
function resetToTab(tabName) {
|
||||||
|
if (navigationRef.isReady()) {
|
||||||
|
navigate(tabName);
|
||||||
|
if (navigationRef.canGoBack()) {
|
||||||
|
navigationRef.dispatch(StackActions.popToTop()); //we need to check .canGoBack() before calling it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// returns a promise that resolves after the state reset is complete
|
||||||
|
function reset() {
|
||||||
|
if (navigationRef.isReady()) {
|
||||||
|
navigationRef.dispatch(CommonActions.reset({
|
||||||
|
index: 0,
|
||||||
|
routes: [{ name: IS_NATIVE ? 'HomeTab' : 'Home' }],
|
||||||
|
}));
|
||||||
|
return Promise.race([
|
||||||
|
timeout(1e3),
|
||||||
|
new Promise(function (resolve) {
|
||||||
|
var handler = function () {
|
||||||
|
resolve();
|
||||||
|
navigationRef.removeListener('state', handler);
|
||||||
|
};
|
||||||
|
navigationRef.addListener('state', handler);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { FlatNavigator, navigate, reset, resetToTab, RoutesContainer, TabsNavigator, };
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26, templateObject_27, templateObject_28, templateObject_29, templateObject_30, templateObject_31, templateObject_32, templateObject_33, templateObject_34, templateObject_35, templateObject_36, templateObject_37, templateObject_38, templateObject_39, templateObject_40, templateObject_41, templateObject_42, templateObject_43, templateObject_44, templateObject_45, templateObject_46, templateObject_47, templateObject_48, templateObject_49, templateObject_50, templateObject_51, templateObject_52, templateObject_53, templateObject_54, templateObject_55, templateObject_56, templateObject_57, templateObject_58, templateObject_59, templateObject_60, templateObject_61, templateObject_62, templateObject_63, templateObject_64, templateObject_65, templateObject_66, templateObject_67, templateObject_68, templateObject_69, templateObject_70, templateObject_71, templateObject_72, templateObject_73, templateObject_74, templateObject_75, templateObject_76, templateObject_77;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { AccessibilityInfo, Image as RNImage, StyleSheet, useColorScheme, View, } from 'react-native';
|
||||||
|
import Animated, { Easing, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import Svg, { Path } from 'react-native-svg';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
|
import { Logotype } from '#/view/icons/Logotype';
|
||||||
|
// @ts-ignore
|
||||||
|
import splashImagePointer from '../assets/splash/splash.png';
|
||||||
|
// @ts-ignore
|
||||||
|
import darkSplashImagePointer from '../assets/splash/splash-dark.png';
|
||||||
|
var splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri;
|
||||||
|
var darkSplashImageUri = RNImage.resolveAssetSource(darkSplashImagePointer).uri;
|
||||||
|
export var Logo = React.forwardRef(function LogoImpl(props, ref) {
|
||||||
|
var width = 1000;
|
||||||
|
var height = width * (67 / 64);
|
||||||
|
return (_jsx(Svg, { fill: "none",
|
||||||
|
// @ts-ignore it's fiiiiine
|
||||||
|
ref: ref, viewBox: "0 0 64 66", style: [{ width: width, height: height }, props.style], children: _jsx(Path, { fill: props.fill || '#fff', d: "M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z" }) }));
|
||||||
|
});
|
||||||
|
export function Splash(props) {
|
||||||
|
'use no memo';
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
var intro = useSharedValue(0);
|
||||||
|
var outroLogo = useSharedValue(0);
|
||||||
|
var outroApp = useSharedValue(0);
|
||||||
|
var outroAppOpacity = useSharedValue(0);
|
||||||
|
var _a = React.useState(false), isAnimationComplete = _a[0], setIsAnimationComplete = _a[1];
|
||||||
|
var _b = React.useState(false), isImageLoaded = _b[0], setIsImageLoaded = _b[1];
|
||||||
|
var _c = React.useState(false), isLayoutReady = _c[0], setIsLayoutReady = _c[1];
|
||||||
|
var _d = React.useState(false), reduceMotion = _d[0], setReduceMotion = _d[1];
|
||||||
|
var isReady = props.isReady &&
|
||||||
|
isImageLoaded &&
|
||||||
|
isLayoutReady &&
|
||||||
|
reduceMotion !== undefined;
|
||||||
|
var colorScheme = useColorScheme();
|
||||||
|
var isDarkMode = colorScheme === 'dark';
|
||||||
|
var logoAnimation = useAnimatedStyle(function () {
|
||||||
|
return {
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: interpolate(outroLogo.get(), [0, 0.08, 1], [1, 0.8, 500], 'clamp'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var bottomLogoAnimation = useAnimatedStyle(function () {
|
||||||
|
return {
|
||||||
|
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var reducedLogoAnimation = useAnimatedStyle(function () {
|
||||||
|
return {
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var logoWrapperAnimation = useAnimatedStyle(function () {
|
||||||
|
return {
|
||||||
|
opacity: interpolate(outroAppOpacity.get(), [0, 0.1, 0.2, 1], [1, 1, 0, 0], 'clamp'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var appAnimation = useAnimatedStyle(function () {
|
||||||
|
return {
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
opacity: interpolate(outroAppOpacity.get(), [0, 0.1, 0.2, 1], [0, 0, 1, 1], 'clamp'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var onFinish = useCallback(function () { return setIsAnimationComplete(true); }, []);
|
||||||
|
var onLayout = useCallback(function () { return setIsLayoutReady(true); }, []);
|
||||||
|
var onLoadEnd = useCallback(function () { return setIsImageLoaded(true); }, []);
|
||||||
|
useEffect(function () {
|
||||||
|
if (isReady) {
|
||||||
|
SplashScreen.hideAsync()
|
||||||
|
.then(function () {
|
||||||
|
intro.set(function () {
|
||||||
|
return withTiming(1, { duration: 400, easing: Easing.out(Easing.cubic) }, function () {
|
||||||
|
'worklet';
|
||||||
|
// set these values to check animation at specific point
|
||||||
|
outroLogo.set(function () {
|
||||||
|
return withTiming(1, { duration: 1200, easing: Easing.in(Easing.cubic) }, function () {
|
||||||
|
runOnJS(onFinish)();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
outroApp.set(function () {
|
||||||
|
return withTiming(1, {
|
||||||
|
duration: 1200,
|
||||||
|
easing: Easing.inOut(Easing.cubic),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
outroAppOpacity.set(function () {
|
||||||
|
return withTiming(1, {
|
||||||
|
duration: 1200,
|
||||||
|
easing: Easing.in(Easing.cubic),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function () { });
|
||||||
|
}
|
||||||
|
}, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady]);
|
||||||
|
useEffect(function () {
|
||||||
|
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
|
||||||
|
}, []);
|
||||||
|
var logoAnimations = reduceMotion === true ? reducedLogoAnimation : logoAnimation;
|
||||||
|
// special off-spec color for dark mode
|
||||||
|
var logoBg = isDarkMode ? '#0F1824' : '#fff';
|
||||||
|
return (_jsxs(View, { style: { flex: 1 }, onLayout: onLayout, children: [!isAnimationComplete && (_jsxs(View, { style: StyleSheet.absoluteFillObject, children: [_jsx(Image, { accessibilityIgnoresInvertColors: true, onLoadEnd: onLoadEnd, source: { uri: isDarkMode ? darkSplashImageUri : splashImageUri }, style: StyleSheet.absoluteFillObject }), _jsx(Animated.View, { style: [
|
||||||
|
bottomLogoAnimation,
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: insets.bottom + 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
], children: _jsx(Logotype, { fill: "#fff", width: 90 }) })] })), isReady && (_jsxs(_Fragment, { children: [_jsx(Animated.View, { style: [{ flex: 1 }, appAnimation], children: props.children }), !isAnimationComplete && (_jsx(Animated.View, { style: [
|
||||||
|
StyleSheet.absoluteFillObject,
|
||||||
|
logoWrapperAnimation,
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
transform: [{ translateY: -(insets.top / 2) }, { scale: 0.1 }], // scale from 1000px to 100px
|
||||||
|
},
|
||||||
|
], children: _jsx(Animated.View, { style: [logoAnimations], children: _jsx(Logo, { fill: logoBg }) }) }))] }))] }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
/*
|
||||||
|
* This is a reimplementation of what exists in our HTML template files
|
||||||
|
* already. Once the React tree mounts, this is what gets rendered first, until
|
||||||
|
* the app is ready to go.
|
||||||
|
*/
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import Svg, { Path } from 'react-native-svg';
|
||||||
|
import { atoms as a, flatten } from '#/alf';
|
||||||
|
var size = 100;
|
||||||
|
var ratio = 57 / 64;
|
||||||
|
export function Splash(_a) {
|
||||||
|
var isReady = _a.isReady, children = _a.children;
|
||||||
|
var _b = useState(false), isAnimationComplete = _b[0], setIsAnimationComplete = _b[1];
|
||||||
|
var splashRef = useRef(null);
|
||||||
|
// hide the static one that's baked into the HTML - gets replaced by our React version below
|
||||||
|
useEffect(function () {
|
||||||
|
// double rAF ensures that the React version gets painted first
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
var splash = document.getElementById('splash');
|
||||||
|
if (splash) {
|
||||||
|
splash.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
// when ready, we fade/scale out
|
||||||
|
useEffect(function () {
|
||||||
|
if (!isReady)
|
||||||
|
return;
|
||||||
|
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
var node = splashRef.current;
|
||||||
|
if (!node || reduceMotion) {
|
||||||
|
setIsAnimationComplete(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var animation = node.animate([
|
||||||
|
{ opacity: 1, transform: 'scale(1)' },
|
||||||
|
{ opacity: 0, transform: 'scale(1.5)' },
|
||||||
|
], {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
fill: 'forwards',
|
||||||
|
});
|
||||||
|
animation.onfinish = function () { return setIsAnimationComplete(true); };
|
||||||
|
return function () {
|
||||||
|
animation.cancel();
|
||||||
|
};
|
||||||
|
}, [isReady]);
|
||||||
|
return (_jsxs(_Fragment, { children: [isReady && children, !isAnimationComplete && (_jsx("div", { ref: splashRef, style: flatten([
|
||||||
|
a.fixed,
|
||||||
|
a.inset_0,
|
||||||
|
a.flex,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
// to compensate for the `top: -50px` below
|
||||||
|
{ transformOrigin: 'center calc(50% - 50px)' },
|
||||||
|
]), children: _jsx(Svg, { fill: "none", viewBox: "0 0 64 57", style: [a.relative, { width: size, height: size * ratio, top: -50 }], children: _jsx(Path, { fill: "#006AFF", d: "M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z" }) }) }))] }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export var prefetchAgeAssuranceData = function () { };
|
||||||
|
export var setBirthdateForDid = function () { };
|
||||||
|
export var setCreatedAtForDid = function () { };
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { ScrollView, View } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { SupportCode, useCreateSupportLink, } from '#/lib/hooks/useCreateSupportLink';
|
||||||
|
import { dateDiff, useGetTimeAgo } from '#/lib/hooks/useTimeAgo';
|
||||||
|
import { useIsBirthdateUpdateAllowed } from '#/state/birthdate';
|
||||||
|
import { useSessionApi } from '#/state/session';
|
||||||
|
import { atoms as a, useBreakpoints, useTheme, web } from '#/alf';
|
||||||
|
import { Admonition } from '#/components/Admonition';
|
||||||
|
import { AgeAssuranceAppealDialog } from '#/components/ageAssurance/AgeAssuranceAppealDialog';
|
||||||
|
import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge';
|
||||||
|
import { AgeAssuranceInitDialog } from '#/components/ageAssurance/AgeAssuranceInitDialog';
|
||||||
|
import { Button, ButtonIcon, ButtonText } from '#/components/Button';
|
||||||
|
import { useDialogControl } from '#/components/Dialog';
|
||||||
|
import * as Dialog from '#/components/Dialog';
|
||||||
|
import { BirthDateSettingsDialog } from '#/components/dialogs/BirthDateSettings';
|
||||||
|
import { DeviceLocationRequestDialog } from '#/components/dialogs/DeviceLocationRequestDialog';
|
||||||
|
import { Full as Logo } from '#/components/icons/Logo';
|
||||||
|
import { ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon } from '#/components/icons/Shield';
|
||||||
|
import { createStaticClick, SimpleInlineLinkText } from '#/components/Link';
|
||||||
|
import { Outlet as PortalOutlet } from '#/components/Portal';
|
||||||
|
import * as Toast from '#/components/Toast';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { BottomSheetOutlet } from '#/../modules/bottom-sheet';
|
||||||
|
import { useAgeAssurance } from '#/ageAssurance';
|
||||||
|
import { useAgeAssuranceDataContext } from '#/ageAssurance/data';
|
||||||
|
import { useComputeAgeAssuranceRegionAccess } from '#/ageAssurance/useComputeAgeAssuranceRegionAccess';
|
||||||
|
import { isLegacyBirthdateBug, useAgeAssuranceRegionConfig, } from '#/ageAssurance/util';
|
||||||
|
import { useAnalytics } from '#/analytics';
|
||||||
|
import { IS_NATIVE, IS_WEB } from '#/env';
|
||||||
|
import { useDeviceGeolocationApi } from '#/geolocation';
|
||||||
|
var textStyles = [a.text_md, a.leading_snug];
|
||||||
|
export function NoAccessScreen() {
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var ax = useAnalytics();
|
||||||
|
var gtPhone = useBreakpoints().gtPhone;
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
var birthdateControl = useDialogControl();
|
||||||
|
var data = useAgeAssuranceDataContext().data;
|
||||||
|
var region = useAgeAssuranceRegionConfig();
|
||||||
|
var isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed();
|
||||||
|
var logoutCurrentAccount = useSessionApi().logoutCurrentAccount;
|
||||||
|
var createSupportLink = useCreateSupportLink();
|
||||||
|
var aa = useAgeAssurance();
|
||||||
|
var isBlocked = aa.state.status === aa.Status.Blocked;
|
||||||
|
var isAARegion = !!region;
|
||||||
|
var hasDeclaredAge = (data === null || data === void 0 ? void 0 : data.declaredAge) !== undefined;
|
||||||
|
var canUpdateBirthday = isBirthdateUpdateAllowed || isLegacyBirthdateBug((data === null || data === void 0 ? void 0 : data.birthdate) || '');
|
||||||
|
useEffect(function () {
|
||||||
|
// just counting overall hits here
|
||||||
|
ax.metric("blockedGeoOverlay:shown", {});
|
||||||
|
ax.metric("ageAssurance:noAccessScreen:shown", {
|
||||||
|
accountCreatedAt: (data === null || data === void 0 ? void 0 : data.accountCreatedAt) || 'unknown',
|
||||||
|
isAARegion: isAARegion,
|
||||||
|
hasDeclaredAge: hasDeclaredAge,
|
||||||
|
canUpdateBirthday: canUpdateBirthday,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
var onPressLogout = useCallback(function () {
|
||||||
|
if (IS_WEB) {
|
||||||
|
// We're switching accounts, which remounts the entire app.
|
||||||
|
// On mobile, this gets us Home, but on the web we also need reset the URL.
|
||||||
|
// We can't change the URL via a navigate() call because the navigator
|
||||||
|
// itself is about to unmount, and it calls pushState() too late.
|
||||||
|
// So we change the URL ourselves. The navigator will pick it up on remount.
|
||||||
|
history.pushState(null, '', '/');
|
||||||
|
}
|
||||||
|
logoutCurrentAccount('AgeAssuranceNoAccessScreen');
|
||||||
|
}, [logoutCurrentAccount]);
|
||||||
|
var orgAdmonition = (_jsx(Admonition, { type: "tip", children: _jsx(Trans, { children: "For organizational accounts, use the birthdate of the person who is responsible for the account." }) }));
|
||||||
|
var birthdateUpdateText = canUpdateBirthday ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: [textStyles], children: _jsxs(Trans, { children: ["If you believe your birthdate is incorrect, you can update it by", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Click here to update your birthdate"], ["Click here to update your birthdate"])))), style: [textStyles] }, createStaticClick(function () {
|
||||||
|
ax.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {});
|
||||||
|
birthdateControl.open();
|
||||||
|
}), { children: "clicking here" })), "."] }) }), orgAdmonition] })) : (_jsx(Text, { style: [textStyles], children: _jsxs(Trans, { children: ["If you believe your birthdate is incorrect, please", ' ', _jsx(SimpleInlineLinkText, { to: createSupportLink({ code: SupportCode.AA_BIRTHDATE }), label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Click here to contact our support team"], ["Click here to contact our support team"])))), style: [textStyles], children: "contact our support team" }), "."] }) }));
|
||||||
|
return (_jsxs(_Fragment, { children: [_jsx(View, { style: [a.util_screen_outer, a.flex_1], children: _jsx(ScrollView, { contentContainerStyle: [
|
||||||
|
a.px_2xl,
|
||||||
|
{
|
||||||
|
paddingTop: IS_WEB
|
||||||
|
? a.p_5xl.padding
|
||||||
|
: insets.top + a.p_2xl.padding,
|
||||||
|
paddingBottom: 100,
|
||||||
|
},
|
||||||
|
], children: _jsxs(View, { style: [
|
||||||
|
a.mx_auto,
|
||||||
|
a.w_full,
|
||||||
|
web({
|
||||||
|
maxWidth: 380,
|
||||||
|
paddingTop: gtPhone ? '8vh' : undefined,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
gap: 32,
|
||||||
|
},
|
||||||
|
], children: [_jsx(View, { style: [a.align_start], children: _jsx(AgeAssuranceBadge, {}) }), hasDeclaredAge ? (_jsx(_Fragment, { children: isAARegion ? (_jsxs(_Fragment, { children: [_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Hey there!" }) }), _jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "You are accessing Bluesky from a region that legally requires us to verify your age before allowing you to access the app." }) }), !aa.flags.isOverRegionMinAccessAge && (_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Unfortunately, your declared age indicates that you are not old enough to access Bluesky in your region." }) })), !isBlocked && birthdateUpdateText] }), aa.flags.isOverRegionMinAccessAge && _jsx(AccessSection, {})] })) : (_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Unfortunately, the birthdate you have saved to your profile makes you too young to access Bluesky." }) }), birthdateUpdateText] })) })) : (_jsxs(View, { style: [a.gap_lg], children: [_jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Hi there!" }) }), _jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "In order to provide an age-appropriate experience, we need to know your birthdate. This is a one-time thing, and your data will be kept private." }) }), _jsx(Text, { style: [textStyles], children: _jsx(Trans, { children: "Set your birthdate below and we'll get you back to posting and exploring in no time!" }) }), _jsx(Button, { color: "primary", size: "large", label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Click here to update your birthdate"], ["Click here to update your birthdate"])))), onPress: function () { return birthdateControl.open(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Add your birthdate" }) }) }), orgAdmonition] })), _jsxs(View, { style: [a.pt_lg, a.gap_xl], children: [_jsx(Logo, { width: 120, textFill: t.atoms.text.color }), _jsx(Text, { style: [a.text_sm, a.italic, t.atoms.text_contrast_medium], children: _jsxs(Trans, { children: ["To log out,", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Click here to log out"], ["Click here to log out"])))) }, createStaticClick(function () {
|
||||||
|
onPressLogout();
|
||||||
|
}), { children: "click here" })), "."] }) })] })] }) }) }), _jsx(BirthDateSettingsDialog, { control: birthdateControl }), _jsx(BottomSheetOutlet, {}), _jsx(PortalOutlet, {})] }));
|
||||||
|
}
|
||||||
|
function AccessSection() {
|
||||||
|
var t = useTheme();
|
||||||
|
var _a = useLingui(), _ = _a._, i18n = _a.i18n;
|
||||||
|
var ax = useAnalytics();
|
||||||
|
var control = useDialogControl();
|
||||||
|
var appealControl = Dialog.useDialogControl();
|
||||||
|
var locationControl = Dialog.useDialogControl();
|
||||||
|
var getTimeAgo = useGetTimeAgo();
|
||||||
|
var setDeviceGeolocation = useDeviceGeolocationApi().setDeviceGeolocation;
|
||||||
|
var computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess();
|
||||||
|
var aa = useAgeAssurance();
|
||||||
|
var _b = aa.state, status = _b.status, lastInitiatedAt = _b.lastInitiatedAt;
|
||||||
|
var isBlocked = status === aa.Status.Blocked;
|
||||||
|
var hasInitiated = !!lastInitiatedAt;
|
||||||
|
var timeAgo = lastInitiatedAt
|
||||||
|
? getTimeAgo(lastInitiatedAt, new Date())
|
||||||
|
: null;
|
||||||
|
var diff = lastInitiatedAt
|
||||||
|
? dateDiff(lastInitiatedAt, new Date(), 'down')
|
||||||
|
: null;
|
||||||
|
return (_jsxs(_Fragment, { children: [_jsx(AgeAssuranceInitDialog, { control: control }), _jsx(AgeAssuranceAppealDialog, { control: appealControl }), _jsxs(View, { style: [a.gap_xl], children: [isBlocked ? (_jsx(Admonition, { type: "warning", children: _jsxs(Trans, { children: ["You are currently unable to access Bluesky's Age Assurance flow. Please", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Contact our moderation team"], ["Contact our moderation team"])))) }, createStaticClick(function () {
|
||||||
|
appealControl.open();
|
||||||
|
ax.metric('ageAssurance:appealDialogOpen', {});
|
||||||
|
}), { children: "contact our moderation team" })), ' ', "if you believe this is an error."] }) })) : (_jsx(_Fragment, { children: _jsxs(View, { style: [a.gap_md], children: [_jsxs(Button, { label: _(msg(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Verify now"], ["Verify now"])))), size: "large", color: hasInitiated ? 'secondary' : 'primary', onPress: function () {
|
||||||
|
control.open();
|
||||||
|
ax.metric('ageAssurance:initDialogOpen', {
|
||||||
|
hasInitiatedPreviously: hasInitiated,
|
||||||
|
});
|
||||||
|
}, children: [_jsx(ButtonIcon, { icon: ShieldIcon }), _jsx(ButtonText, { children: hasInitiated ? (_jsx(Trans, { children: "Verify again" })) : (_jsx(Trans, { children: "Verify now" })) })] }), lastInitiatedAt && timeAgo && diff ? (_jsx(Text, { style: [a.text_sm, a.italic, t.atoms.text_contrast_medium], title: i18n.date(lastInitiatedAt, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'medium',
|
||||||
|
}), children: diff.value === 0 ? (_jsx(Trans, { children: "Last initiated just now" })) : (_jsxs(Trans, { children: ["Last initiated ", timeAgo, " ago"] })) })) : (_jsx(Text, { style: [a.text_sm, a.italic, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Age assurance only takes a few minutes" }) }))] }) })), _jsx(View, { style: [a.gap_xs], children: IS_NATIVE && (_jsxs(_Fragment, { children: [_jsx(Admonition, { children: _jsxs(Trans, { children: ["Is your location not accurate?", ' ', _jsx(SimpleInlineLinkText, __assign({ label: _(msg(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Confirm your location"], ["Confirm your location"])))) }, createStaticClick(function () {
|
||||||
|
locationControl.open();
|
||||||
|
}), { children: "Tap here to confirm your location." })), ' '] }) }), _jsx(DeviceLocationRequestDialog, { control: locationControl, onLocationAcquired: function (props) {
|
||||||
|
var access = computeAgeAssuranceRegionAccess(props.geolocation);
|
||||||
|
if (access !== aa.Access.Full) {
|
||||||
|
props.disableDialogAction();
|
||||||
|
props.setDialogError(_(msg(templateObject_8 || (templateObject_8 = __makeTemplateObject(["We're sorry, but based on your device's location, you are currently located in a region that requires age assurance."], ["We're sorry, but based on your device's location, you are currently located in a region that requires age assurance."])))));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
props.closeDialog(function () {
|
||||||
|
// set this after close!
|
||||||
|
setDeviceGeolocation(props.geolocation);
|
||||||
|
Toast.show(_(msg(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Thanks! You're all set."], ["Thanks! You're all set."])))), {
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} })] })) })] })] }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9;
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
||||||
|
import { Dimensions, View } from 'react-native';
|
||||||
|
import * as Linking from 'expo-linking';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { retry } from '#/lib/async/retry';
|
||||||
|
import { wait } from '#/lib/async/wait';
|
||||||
|
import { parseLinkingUrl } from '#/lib/parseLinkingUrl';
|
||||||
|
import { useAgent, useSession } from '#/state/session';
|
||||||
|
import { atoms as a, platform, useBreakpoints, useTheme } from '#/alf';
|
||||||
|
import { AgeAssuranceBadge } from '#/components/ageAssurance/AgeAssuranceBadge';
|
||||||
|
import { Button, ButtonText } from '#/components/Button';
|
||||||
|
import { FullWindowOverlay } from '#/components/FullWindowOverlay';
|
||||||
|
import { CheckThick_Stroke2_Corner0_Rounded as SuccessIcon } from '#/components/icons/Check';
|
||||||
|
import { CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon } from '#/components/icons/CircleInfo';
|
||||||
|
import { Loader } from '#/components/Loader';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { refetchAgeAssuranceServerState } from '#/ageAssurance';
|
||||||
|
import { useAnalytics } from '#/analytics';
|
||||||
|
import { IS_IOS, IS_WEB } from '#/env';
|
||||||
|
/**
|
||||||
|
* Validate and parse the query parameters returned from the age assurance
|
||||||
|
* redirect. If not valid, returns `undefined` and the dialog will not open.
|
||||||
|
*/
|
||||||
|
export function parseRedirectOverlayState(state) {
|
||||||
|
if (state === void 0) { state = {}; }
|
||||||
|
var result = 'unknown';
|
||||||
|
var actorDid = state.actorDid;
|
||||||
|
switch (state.result) {
|
||||||
|
case 'success':
|
||||||
|
result = 'success';
|
||||||
|
break;
|
||||||
|
case 'unknown':
|
||||||
|
default:
|
||||||
|
result = 'unknown';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (actorDid) {
|
||||||
|
return {
|
||||||
|
result: result,
|
||||||
|
actorDid: actorDid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var Context = createContext({
|
||||||
|
isOpen: false,
|
||||||
|
open: function () { },
|
||||||
|
close: function () { },
|
||||||
|
});
|
||||||
|
export function useRedirectOverlayContext() {
|
||||||
|
return useContext(Context);
|
||||||
|
}
|
||||||
|
export function Provider(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var currentAccount = useSession().currentAccount;
|
||||||
|
var incomingUrl = Linking.useLinkingURL();
|
||||||
|
var _b = useState(function () {
|
||||||
|
var _a, _b;
|
||||||
|
if (!incomingUrl)
|
||||||
|
return null;
|
||||||
|
var url = parseLinkingUrl(incomingUrl);
|
||||||
|
if (url.pathname !== '/intent/age-assurance')
|
||||||
|
return null;
|
||||||
|
var params = url.searchParams;
|
||||||
|
var state = parseRedirectOverlayState({
|
||||||
|
result: (_a = params.get('result')) !== null && _a !== void 0 ? _a : undefined,
|
||||||
|
actorDid: (_b = params.get('actorDid')) !== null && _b !== void 0 ? _b : undefined,
|
||||||
|
});
|
||||||
|
if (IS_WEB) {
|
||||||
|
// Clear the URL parameters so they don't re-trigger
|
||||||
|
history.pushState(null, '', '/');
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* If we don't have an account or the account doesn't match, do
|
||||||
|
* nothing. By the time the user switches to their other account, AA
|
||||||
|
* state should be ready for them.
|
||||||
|
*/
|
||||||
|
if (state && currentAccount && state.actorDid === currentAccount.did) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}), state = _b[0], setState = _b[1];
|
||||||
|
var open = useCallback(function (state) {
|
||||||
|
setState(state);
|
||||||
|
}, []);
|
||||||
|
var close = useCallback(function () {
|
||||||
|
setState(null);
|
||||||
|
}, []);
|
||||||
|
return (_jsx(Context.Provider, { value: useMemo(function () { return ({
|
||||||
|
isOpen: state !== null,
|
||||||
|
open: open,
|
||||||
|
close: close,
|
||||||
|
}); }, [state, open, close]), children: children }));
|
||||||
|
}
|
||||||
|
export function RedirectOverlay() {
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var isOpen = useRedirectOverlayContext().isOpen;
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
return isOpen ? (_jsx(FullWindowOverlay, { children: _jsx(View, { style: [
|
||||||
|
a.fixed,
|
||||||
|
a.inset_0,
|
||||||
|
// setting a zIndex when using FullWindowOverlay on iOS
|
||||||
|
// means the taps pass straight through to the underlying content (???)
|
||||||
|
// so don't set it on iOS. FullWindowOverlay already does the job.
|
||||||
|
!IS_IOS && { zIndex: 9999 },
|
||||||
|
t.atoms.bg,
|
||||||
|
gtMobile ? a.p_2xl : a.p_xl,
|
||||||
|
a.align_center,
|
||||||
|
// @ts-ignore
|
||||||
|
platform({
|
||||||
|
web: {
|
||||||
|
paddingTop: '35vh',
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
paddingTop: Dimensions.get('window').height * 0.35,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
], children: _jsx(View, { role: "dialog", "aria-role": "dialog", "aria-label": _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Verifying your age assurance status"], ["Verifying your age assurance status"])))), children: _jsx(View, { style: [a.pb_3xl, { width: 300 }], children: _jsx(Inner, {}) }) }) }) })) : null;
|
||||||
|
}
|
||||||
|
function Inner() {
|
||||||
|
var _this = this;
|
||||||
|
var t = useTheme();
|
||||||
|
var ax = useAnalytics();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var agent = useAgent();
|
||||||
|
var polling = useRef(false);
|
||||||
|
var unmounted = useRef(false);
|
||||||
|
var _a = useState(false), error = _a[0], setError = _a[1];
|
||||||
|
var _b = useState(false), success = _b[0], setSuccess = _b[1];
|
||||||
|
var close = useRedirectOverlayContext().close;
|
||||||
|
useEffect(function () {
|
||||||
|
if (polling.current)
|
||||||
|
return;
|
||||||
|
polling.current = true;
|
||||||
|
ax.metric('ageAssurance:redirectDialogOpen', {});
|
||||||
|
wait(3e3, retry(5, function () { return true; }, function () { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var data;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
if (!agent.session)
|
||||||
|
return [2 /*return*/];
|
||||||
|
if (unmounted.current)
|
||||||
|
return [2 /*return*/];
|
||||||
|
return [4 /*yield*/, refetchAgeAssuranceServerState({ agent: agent })];
|
||||||
|
case 1:
|
||||||
|
data = _a.sent();
|
||||||
|
if ((data === null || data === void 0 ? void 0 : data.state.status) !== 'assured') {
|
||||||
|
throw new Error("Polling for age assurance state did not receive assured status");
|
||||||
|
}
|
||||||
|
return [2 /*return*/, data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); }, 1e3))
|
||||||
|
.then(function (data) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
if (!data)
|
||||||
|
return [2 /*return*/];
|
||||||
|
if (!agent.session)
|
||||||
|
return [2 /*return*/];
|
||||||
|
if (unmounted.current)
|
||||||
|
return [2 /*return*/];
|
||||||
|
setSuccess(true);
|
||||||
|
ax.metric('ageAssurance:redirectDialogSuccess', {});
|
||||||
|
return [2 /*return*/];
|
||||||
|
});
|
||||||
|
}); })
|
||||||
|
.catch(function () {
|
||||||
|
if (unmounted.current)
|
||||||
|
return;
|
||||||
|
setError(true);
|
||||||
|
ax.metric('ageAssurance:redirectDialogFail', {});
|
||||||
|
});
|
||||||
|
return function () {
|
||||||
|
unmounted.current = true;
|
||||||
|
};
|
||||||
|
}, [ax, agent]);
|
||||||
|
if (success) {
|
||||||
|
return (_jsx(_Fragment, { children: _jsxs(View, { style: [a.align_start, a.w_full], children: [_jsx(AgeAssuranceBadge, {}), _jsxs(View, { style: [
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.pt_lg,
|
||||||
|
a.pb_md,
|
||||||
|
], children: [_jsx(SuccessIcon, { size: "sm", fill: t.palette.positive_500 }), _jsx(Text, { style: [a.text_3xl, a.font_bold], children: _jsx(Trans, { children: "Success" }) })] }), _jsx(Text, { style: [a.text_md, a.leading_snug], children: _jsx(Trans, { children: "We've confirmed your age assurance status. You can now close this dialog." }) }), _jsx(View, { style: [a.w_full, a.pt_lg], children: _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", variant: "solid", color: "secondary", onPress: function () { return close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }) })] }) }));
|
||||||
|
}
|
||||||
|
return (_jsx(_Fragment, { children: _jsxs(View, { style: [a.align_start, a.w_full], children: [_jsx(AgeAssuranceBadge, {}), _jsxs(View, { style: [
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.pt_lg,
|
||||||
|
a.pb_md,
|
||||||
|
], children: [error && _jsx(ErrorIcon, { size: "lg", fill: t.palette.negative_500 }), _jsx(Text, { style: [a.text_3xl, a.font_bold], children: error ? _jsx(Trans, { children: "Connection issue" }) : _jsx(Trans, { children: "Verifying" }) }), !error && _jsx(Loader, { size: "lg" })] }), _jsx(Text, { style: [a.text_md, t.atoms.text_contrast_medium, a.leading_snug], children: error ? (_jsx(Trans, { children: "We were unable to receive the verification due to a connection issue. It may arrive later. If it does, your account will update automatically." })) : (_jsx(Trans, { children: "We're confirming your age assurance status with our servers. This should only take a few seconds." })) }), error && (_jsx(View, { style: [a.w_full, a.pt_lg], children: _jsx(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Close"], ["Close"])))), size: "large", variant: "solid", color: "secondary", onPress: function () { return close(); }, children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Close" }) }) }) }))] }) }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3;
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
import { jsx as _jsx } from "react/jsx-runtime";
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
|
import { AtpAgent, getAgeAssuranceRegionConfig, } from '@atproto/api';
|
||||||
|
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
|
||||||
|
import { focusManager, QueryClient, useQuery } from '@tanstack/react-query';
|
||||||
|
import { persistQueryClient } from '@tanstack/react-query-persist-client';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { networkRetry } from '#/lib/async/retry';
|
||||||
|
import { PUBLIC_BSKY_SERVICE } from '#/lib/constants';
|
||||||
|
import { createPersistedQueryStorage } from '#/lib/persisted-query-storage';
|
||||||
|
import { getAge } from '#/lib/strings/time';
|
||||||
|
import { hasSnoozedBirthdateUpdateForDid, snoozeBirthdateUpdateAllowedForDid, } from '#/state/birthdate';
|
||||||
|
import { useAgent, useSession } from '#/state/session';
|
||||||
|
import * as debug from '#/ageAssurance/debug';
|
||||||
|
import { logger } from '#/ageAssurance/logger';
|
||||||
|
import { getBirthdateStringFromAge, isLegacyBirthdateBug, } from '#/ageAssurance/util';
|
||||||
|
import { IS_DEV } from '#/env';
|
||||||
|
import { device } from '#/storage';
|
||||||
|
/**
|
||||||
|
* Special query client for age assurance data so we can prefetch on app
|
||||||
|
* load without interfering with other queries.
|
||||||
|
*/
|
||||||
|
var qc = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
/**
|
||||||
|
* We clear this manually, so disable automatic garbage collection.
|
||||||
|
* @see https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#how-it-works
|
||||||
|
*/
|
||||||
|
gcTime: Infinity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var persister = createAsyncStoragePersister({
|
||||||
|
storage: createPersistedQueryStorage('age-assurance'),
|
||||||
|
key: 'age-assurance-query-client',
|
||||||
|
});
|
||||||
|
var _a = persistQueryClient({
|
||||||
|
queryClient: qc,
|
||||||
|
persister: persister,
|
||||||
|
}), cacheHydrationPromise = _a[1];
|
||||||
|
function getDidFromAgentSession(agent) {
|
||||||
|
var sessionManager = agent.sessionManager;
|
||||||
|
if (!sessionManager || !sessionManager.did)
|
||||||
|
return;
|
||||||
|
return sessionManager.did;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Optimistic data
|
||||||
|
*/
|
||||||
|
var createdAtCache = new Map();
|
||||||
|
export function setCreatedAtForDid(_a) {
|
||||||
|
var did = _a.did, createdAt = _a.createdAt;
|
||||||
|
createdAtCache.set(did, createdAt);
|
||||||
|
}
|
||||||
|
var birthdateCache = new Map();
|
||||||
|
export function setBirthdateForDid(_a) {
|
||||||
|
var did = _a.did, birthdate = _a.birthdate;
|
||||||
|
birthdateCache.set(did, birthdate);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Config
|
||||||
|
*/
|
||||||
|
export var configQueryKey = ['config'];
|
||||||
|
export function getConfig() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var agent, res;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
if (debug.enabled)
|
||||||
|
return [2 /*return*/, debug.resolve(debug.config)];
|
||||||
|
agent = new AtpAgent({
|
||||||
|
service: PUBLIC_BSKY_SERVICE,
|
||||||
|
});
|
||||||
|
return [4 /*yield*/, agent.app.bsky.ageassurance.getConfig()];
|
||||||
|
case 1:
|
||||||
|
res = _a.sent();
|
||||||
|
return [2 /*return*/, res.data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function getConfigFromCache() {
|
||||||
|
return qc.getQueryData(configQueryKey);
|
||||||
|
}
|
||||||
|
var configPrefetchPromise;
|
||||||
|
export function prefetchConfig() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var _this = this;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
if (configPrefetchPromise) {
|
||||||
|
logger.debug("prefetchAgeAssuranceConfig: already in progress");
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
configPrefetchPromise = new Promise(function (resolve) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var cached, res, e_1;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0: return [4 /*yield*/, cacheHydrationPromise];
|
||||||
|
case 1:
|
||||||
|
_a.sent();
|
||||||
|
cached = getConfigFromCache();
|
||||||
|
if (!cached) return [3 /*break*/, 2];
|
||||||
|
logger.debug("prefetchAgeAssuranceConfig: using cache");
|
||||||
|
resolve();
|
||||||
|
return [3 /*break*/, 6];
|
||||||
|
case 2:
|
||||||
|
_a.trys.push([2, 4, 5, 6]);
|
||||||
|
logger.debug("prefetchAgeAssuranceConfig: resolving...");
|
||||||
|
return [4 /*yield*/, networkRetry(3, function () { return getConfig(); })];
|
||||||
|
case 3:
|
||||||
|
res = _a.sent();
|
||||||
|
qc.setQueryData(configQueryKey, res);
|
||||||
|
return [3 /*break*/, 6];
|
||||||
|
case 4:
|
||||||
|
e_1 = _a.sent();
|
||||||
|
logger.warn("prefetchAgeAssuranceConfig: failed", {
|
||||||
|
safeMessage: e_1.message,
|
||||||
|
});
|
||||||
|
return [3 /*break*/, 6];
|
||||||
|
case 5:
|
||||||
|
resolve();
|
||||||
|
return [7 /*endfinally*/];
|
||||||
|
case 6: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); });
|
||||||
|
return [2 /*return*/];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function refetchConfig() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var res;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
logger.debug("refetchConfig: fetching...");
|
||||||
|
return [4 /*yield*/, getConfig()];
|
||||||
|
case 1:
|
||||||
|
res = _a.sent();
|
||||||
|
qc.setQueryData(configQueryKey, res);
|
||||||
|
return [2 /*return*/, res];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function useConfigQuery() {
|
||||||
|
return useQuery({
|
||||||
|
/**
|
||||||
|
* Will re-fetch when stale, at most every hour (or 5s in dev for easier
|
||||||
|
* testing).
|
||||||
|
*
|
||||||
|
* @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat
|
||||||
|
*/
|
||||||
|
staleTime: IS_DEV ? 5e3 : 1000 * 60 * 60,
|
||||||
|
/**
|
||||||
|
* N.B. if prefetch failed above, we'll have no `initialData`, and this
|
||||||
|
* query will run on startup.
|
||||||
|
*/
|
||||||
|
initialData: getConfigFromCache(),
|
||||||
|
initialDataUpdatedAt: function () { var _a; return (_a = qc.getQueryState(configQueryKey)) === null || _a === void 0 ? void 0 : _a.dataUpdatedAt; },
|
||||||
|
queryKey: configQueryKey,
|
||||||
|
queryFn: function () {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
logger.debug("useConfigQuery: fetching config");
|
||||||
|
return [2 /*return*/, getConfig()];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, qc);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Server state
|
||||||
|
*/
|
||||||
|
export function createServerStateQueryKey(_a) {
|
||||||
|
var did = _a.did;
|
||||||
|
return ['serverState', did];
|
||||||
|
}
|
||||||
|
export function getServerState(_a) {
|
||||||
|
return __awaiter(this, arguments, void 0, function (_b) {
|
||||||
|
var geolocation, data, did;
|
||||||
|
var agent = _b.agent;
|
||||||
|
return __generator(this, function (_c) {
|
||||||
|
switch (_c.label) {
|
||||||
|
case 0:
|
||||||
|
if (debug.enabled && debug.serverState)
|
||||||
|
return [2 /*return*/, debug.resolve(debug.serverState)];
|
||||||
|
geolocation = device.get(['mergedGeolocation']);
|
||||||
|
if (!geolocation || !geolocation.countryCode) {
|
||||||
|
logger.error("getServerState: missing geolocation countryCode");
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
return [4 /*yield*/, agent.app.bsky.ageassurance.getState({
|
||||||
|
countryCode: geolocation.countryCode,
|
||||||
|
regionCode: geolocation.regionCode,
|
||||||
|
})];
|
||||||
|
case 1:
|
||||||
|
data = (_c.sent()).data;
|
||||||
|
did = getDidFromAgentSession(agent);
|
||||||
|
if (data && did && createdAtCache.has(did)) {
|
||||||
|
/*
|
||||||
|
* If account was just created, just use the local cache if available. On
|
||||||
|
* subsequent reloads, the server should have the correct value.
|
||||||
|
*/
|
||||||
|
data.metadata.accountCreatedAt = createdAtCache.get(did);
|
||||||
|
}
|
||||||
|
return [2 /*return*/, data !== null && data !== void 0 ? data : null];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function getServerStateFromCache(_a) {
|
||||||
|
var did = _a.did;
|
||||||
|
return qc.getQueryData(createServerStateQueryKey({ did: did }));
|
||||||
|
}
|
||||||
|
export function prefetchServerState(_a) {
|
||||||
|
return __awaiter(this, arguments, void 0, function (_b) {
|
||||||
|
var did, qk, cached, res, e_2;
|
||||||
|
var agent = _b.agent;
|
||||||
|
return __generator(this, function (_c) {
|
||||||
|
switch (_c.label) {
|
||||||
|
case 0:
|
||||||
|
did = getDidFromAgentSession(agent);
|
||||||
|
if (!did)
|
||||||
|
return [2 /*return*/];
|
||||||
|
return [4 /*yield*/, cacheHydrationPromise];
|
||||||
|
case 1:
|
||||||
|
_c.sent();
|
||||||
|
qk = createServerStateQueryKey({ did: did });
|
||||||
|
cached = getServerStateFromCache({ did: did });
|
||||||
|
if (cached) {
|
||||||
|
logger.debug("prefetchServerState: using cache");
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
_c.label = 2;
|
||||||
|
case 2:
|
||||||
|
_c.trys.push([2, 4, , 5]);
|
||||||
|
logger.debug("prefetchServerState: resolving...");
|
||||||
|
return [4 /*yield*/, networkRetry(3, function () { return getServerState({ agent: agent }); })];
|
||||||
|
case 3:
|
||||||
|
res = _c.sent();
|
||||||
|
qc.setQueryData(qk, res);
|
||||||
|
return [3 /*break*/, 5];
|
||||||
|
case 4:
|
||||||
|
e_2 = _c.sent();
|
||||||
|
logger.warn("prefetchServerState: failed", {
|
||||||
|
safeMessage: e_2.message,
|
||||||
|
});
|
||||||
|
return [3 /*break*/, 5];
|
||||||
|
case 5: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function refetchServerState(_a) {
|
||||||
|
return __awaiter(this, arguments, void 0, function (_b) {
|
||||||
|
var did, res;
|
||||||
|
var agent = _b.agent;
|
||||||
|
return __generator(this, function (_c) {
|
||||||
|
switch (_c.label) {
|
||||||
|
case 0:
|
||||||
|
did = getDidFromAgentSession(agent);
|
||||||
|
if (!did)
|
||||||
|
return [2 /*return*/];
|
||||||
|
logger.debug("refetchServerState: fetching...");
|
||||||
|
return [4 /*yield*/, networkRetry(3, function () { return getServerState({ agent: agent }); })];
|
||||||
|
case 1:
|
||||||
|
res = _c.sent();
|
||||||
|
qc.setQueryData(createServerStateQueryKey({ did: did }), res);
|
||||||
|
return [2 /*return*/, res];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function usePatchServerState() {
|
||||||
|
var _this = this;
|
||||||
|
var currentAccount = useSession().currentAccount;
|
||||||
|
return useCallback(function (next) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var did, prev, merged;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
if (!currentAccount)
|
||||||
|
return [2 /*return*/];
|
||||||
|
did = currentAccount.did;
|
||||||
|
prev = getServerStateFromCache({ did: did });
|
||||||
|
merged = __assign(__assign({ metadata: {} }, (prev || {})), { state: next });
|
||||||
|
qc.setQueryData(createServerStateQueryKey({ did: did }), merged);
|
||||||
|
return [2 /*return*/];
|
||||||
|
});
|
||||||
|
}); }, [currentAccount]);
|
||||||
|
}
|
||||||
|
export function useServerStateQuery() {
|
||||||
|
var _a, _b;
|
||||||
|
var agent = useAgent();
|
||||||
|
var did = getDidFromAgentSession(agent);
|
||||||
|
var query = useQuery({
|
||||||
|
enabled: !!did,
|
||||||
|
initialData: function () {
|
||||||
|
if (!did)
|
||||||
|
return;
|
||||||
|
return getServerStateFromCache({ did: did });
|
||||||
|
},
|
||||||
|
queryKey: createServerStateQueryKey({ did: did }),
|
||||||
|
queryFn: function () {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
return [2 /*return*/, getServerState({ agent: agent })];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, qc);
|
||||||
|
var refetch = useMemo(function () { return debounce(query.refetch, 100); }, [query.refetch]);
|
||||||
|
var isAssured = ((_b = (_a = query.data) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.status) === 'assured';
|
||||||
|
/**
|
||||||
|
* `refetchOnWindowFocus` doesn't seem to want to work for this custom query
|
||||||
|
* client, so we manually subscribe to focus changes.
|
||||||
|
*/
|
||||||
|
useEffect(function () {
|
||||||
|
return focusManager.subscribe(function () {
|
||||||
|
var _a;
|
||||||
|
// logged out
|
||||||
|
if (!did)
|
||||||
|
return;
|
||||||
|
var isFocused = focusManager.isFocused();
|
||||||
|
if (!isFocused)
|
||||||
|
return;
|
||||||
|
var config = getConfigFromCache();
|
||||||
|
var geolocation = device.get(['mergedGeolocation']);
|
||||||
|
var isAArequired = Boolean(config &&
|
||||||
|
geolocation &&
|
||||||
|
!!getAgeAssuranceRegionConfig(config, {
|
||||||
|
countryCode: (_a = geolocation === null || geolocation === void 0 ? void 0 : geolocation.countryCode) !== null && _a !== void 0 ? _a : '',
|
||||||
|
regionCode: geolocation === null || geolocation === void 0 ? void 0 : geolocation.regionCode,
|
||||||
|
}));
|
||||||
|
// only refetch when needed
|
||||||
|
if (isAssured || !isAArequired)
|
||||||
|
return;
|
||||||
|
refetch();
|
||||||
|
});
|
||||||
|
}, [did, refetch, isAssured]);
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
export function createOtherRequiredDataQueryKey(_a) {
|
||||||
|
var did = _a.did;
|
||||||
|
return ['otherRequiredData', did];
|
||||||
|
}
|
||||||
|
export function getOtherRequiredData(_a) {
|
||||||
|
return __awaiter(this, arguments, void 0, function (_b) {
|
||||||
|
var prefs, data, did;
|
||||||
|
var _c, _d, _e;
|
||||||
|
var agent = _b.agent;
|
||||||
|
return __generator(this, function (_f) {
|
||||||
|
switch (_f.label) {
|
||||||
|
case 0:
|
||||||
|
if (debug.enabled)
|
||||||
|
return [2 /*return*/, debug.resolve(debug.otherRequiredData)];
|
||||||
|
return [4 /*yield*/, Promise.all([agent.getPreferences()])];
|
||||||
|
case 1:
|
||||||
|
prefs = (_f.sent())[0];
|
||||||
|
data = {
|
||||||
|
birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* If we can't read a birthdate, it may be due to the user accessing the
|
||||||
|
* account via an app password. In that case, fall-back to declared age
|
||||||
|
* flags.
|
||||||
|
*/
|
||||||
|
if (!data.birthdate) {
|
||||||
|
if ((_c = prefs.declaredAge) === null || _c === void 0 ? void 0 : _c.isOverAge18) {
|
||||||
|
data.birthdate = getBirthdateStringFromAge(18);
|
||||||
|
}
|
||||||
|
else if ((_d = prefs.declaredAge) === null || _d === void 0 ? void 0 : _d.isOverAge16) {
|
||||||
|
data.birthdate = getBirthdateStringFromAge(16);
|
||||||
|
}
|
||||||
|
else if ((_e = prefs.declaredAge) === null || _e === void 0 ? void 0 : _e.isOverAge13) {
|
||||||
|
data.birthdate = getBirthdateStringFromAge(13);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
did = getDidFromAgentSession(agent);
|
||||||
|
if (data && did && birthdateCache.has(did)) {
|
||||||
|
/*
|
||||||
|
* If birthdate was just set, use the local cache value. On subsequent
|
||||||
|
* reloads, the server should have the correct value.
|
||||||
|
*/
|
||||||
|
data.birthdate = birthdateCache.get(did);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* If the user is under the minimum age, and the birthdate is not due to the
|
||||||
|
* legacy bug, AND we've not already snoozed their birthdate update, snooze
|
||||||
|
* further birthdate updates for this user.
|
||||||
|
*
|
||||||
|
* This is basically a migration step for this initial rollout.
|
||||||
|
*/
|
||||||
|
if (data.birthdate &&
|
||||||
|
!isLegacyBirthdateBug(data.birthdate) &&
|
||||||
|
!hasSnoozedBirthdateUpdateForDid(did)) {
|
||||||
|
snoozeBirthdateUpdateAllowedForDid(did);
|
||||||
|
}
|
||||||
|
return [2 /*return*/, data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function getOtherRequiredDataFromCache(_a) {
|
||||||
|
var did = _a.did;
|
||||||
|
return qc.getQueryData(createOtherRequiredDataQueryKey({ did: did }));
|
||||||
|
}
|
||||||
|
export function prefetchOtherRequiredData(_a) {
|
||||||
|
return __awaiter(this, arguments, void 0, function (_b) {
|
||||||
|
var did, qk, cached, res, e_3;
|
||||||
|
var agent = _b.agent;
|
||||||
|
return __generator(this, function (_c) {
|
||||||
|
switch (_c.label) {
|
||||||
|
case 0:
|
||||||
|
did = getDidFromAgentSession(agent);
|
||||||
|
if (!did)
|
||||||
|
return [2 /*return*/];
|
||||||
|
return [4 /*yield*/, cacheHydrationPromise];
|
||||||
|
case 1:
|
||||||
|
_c.sent();
|
||||||
|
qk = createOtherRequiredDataQueryKey({ did: did });
|
||||||
|
cached = getOtherRequiredDataFromCache({ did: did });
|
||||||
|
if (cached) {
|
||||||
|
logger.debug("prefetchOtherRequiredData: using cache");
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
_c.label = 2;
|
||||||
|
case 2:
|
||||||
|
_c.trys.push([2, 4, , 5]);
|
||||||
|
logger.debug("prefetchOtherRequiredData: resolving...");
|
||||||
|
return [4 /*yield*/, networkRetry(3, function () { return getOtherRequiredData({ agent: agent }); })];
|
||||||
|
case 3:
|
||||||
|
res = _c.sent();
|
||||||
|
qc.setQueryData(qk, res);
|
||||||
|
return [3 /*break*/, 5];
|
||||||
|
case 4:
|
||||||
|
e_3 = _c.sent();
|
||||||
|
logger.warn("prefetchOtherRequiredData: failed", {
|
||||||
|
safeMessage: e_3.message,
|
||||||
|
});
|
||||||
|
return [3 /*break*/, 5];
|
||||||
|
case 5: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function usePatchOtherRequiredData() {
|
||||||
|
var _this = this;
|
||||||
|
var currentAccount = useSession().currentAccount;
|
||||||
|
return useCallback(function (next) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var did, prev, merged;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
if (!currentAccount)
|
||||||
|
return [2 /*return*/];
|
||||||
|
did = currentAccount.did;
|
||||||
|
prev = getOtherRequiredDataFromCache({ did: did });
|
||||||
|
merged = __assign(__assign({}, (prev || {})), next);
|
||||||
|
qc.setQueryData(createOtherRequiredDataQueryKey({ did: did }), merged);
|
||||||
|
return [2 /*return*/];
|
||||||
|
});
|
||||||
|
}); }, [currentAccount]);
|
||||||
|
}
|
||||||
|
export function useOtherRequiredDataQuery() {
|
||||||
|
var agent = useAgent();
|
||||||
|
var did = getDidFromAgentSession(agent);
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!did,
|
||||||
|
initialData: function () {
|
||||||
|
if (!did)
|
||||||
|
return;
|
||||||
|
return getOtherRequiredDataFromCache({ did: did });
|
||||||
|
},
|
||||||
|
queryKey: createOtherRequiredDataQueryKey({ did: did }),
|
||||||
|
queryFn: function () {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
return [2 /*return*/, getOtherRequiredData({ agent: agent })];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, qc);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper to prefetch all age assurance data.
|
||||||
|
*/
|
||||||
|
export function prefetchAgeAssuranceData(_a) {
|
||||||
|
var agent = _a.agent;
|
||||||
|
return Promise.allSettled([
|
||||||
|
// config fetch initiated at the top of the App.platform.tsx files, awaited here
|
||||||
|
configPrefetchPromise,
|
||||||
|
prefetchServerState({ agent: agent }),
|
||||||
|
prefetchOtherRequiredData({ agent: agent }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
export function clearAgeAssuranceDataForDid(_a) {
|
||||||
|
var did = _a.did;
|
||||||
|
logger.debug("clearAgeAssuranceDataForDid: ".concat(did));
|
||||||
|
qc.removeQueries({ queryKey: createServerStateQueryKey({ did: did }), exact: true });
|
||||||
|
qc.removeQueries({
|
||||||
|
queryKey: createOtherRequiredDataQueryKey({ did: did }),
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function clearAgeAssuranceData() {
|
||||||
|
logger.debug("clearAgeAssuranceData");
|
||||||
|
qc.clear();
|
||||||
|
}
|
||||||
|
export var AgeAssuranceDataContext = createContext({
|
||||||
|
config: undefined,
|
||||||
|
state: undefined,
|
||||||
|
data: {
|
||||||
|
accountCreatedAt: undefined,
|
||||||
|
declaredAge: undefined,
|
||||||
|
birthdate: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export function useAgeAssuranceDataContext() {
|
||||||
|
return useContext(AgeAssuranceDataContext);
|
||||||
|
}
|
||||||
|
export function AgeAssuranceDataProvider(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var config = useConfigQuery().data;
|
||||||
|
var serverState = useServerStateQuery();
|
||||||
|
var _b = serverState.data || {}, state = _b.state, metadata = _b.metadata;
|
||||||
|
var data = useOtherRequiredDataQuery().data;
|
||||||
|
var ctx = useMemo(function () { return ({
|
||||||
|
config: config,
|
||||||
|
state: state,
|
||||||
|
data: {
|
||||||
|
accountCreatedAt: metadata === null || metadata === void 0 ? void 0 : metadata.accountCreatedAt,
|
||||||
|
declaredAge: (data === null || data === void 0 ? void 0 : data.birthdate)
|
||||||
|
? getAge(new Date(data.birthdate))
|
||||||
|
: undefined,
|
||||||
|
birthdate: data === null || data === void 0 ? void 0 : data.birthdate,
|
||||||
|
},
|
||||||
|
}); }, [config, state, data, metadata]);
|
||||||
|
return (_jsx(AgeAssuranceDataContext.Provider, { value: ctx, children: children }));
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { Logger } from '#/logger';
|
||||||
|
export var logger = Logger.create(Logger.Context.AgeAssurance);
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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 })); };
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
import { atoms as baseAtoms } from '@bsky.app/alf';
|
||||||
|
import { CARD_ASPECT_RATIO } from '#/lib/constants';
|
||||||
|
import { native, platform, web } from '#/alf/util/platform';
|
||||||
|
import * as Layout from '#/components/Layout';
|
||||||
|
var EXP_CURVE = 'cubic-bezier(0.16, 1, 0.3, 1)';
|
||||||
|
export var atoms = __assign(__assign({}, baseAtoms), { h_full_vh: web({
|
||||||
|
height: '100vh',
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Used for the outermost components on screens, to ensure that they can fill
|
||||||
|
* the screen and extend beyond.
|
||||||
|
*/
|
||||||
|
util_screen_outer: [
|
||||||
|
web({
|
||||||
|
minHeight: '100vh',
|
||||||
|
}),
|
||||||
|
native({
|
||||||
|
height: '100%',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
* Theme-independent bg colors
|
||||||
|
*/
|
||||||
|
bg_transparent: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Aspect ratios
|
||||||
|
*/
|
||||||
|
aspect_square: {
|
||||||
|
aspectRatio: 1,
|
||||||
|
}, aspect_card: {
|
||||||
|
aspectRatio: CARD_ASPECT_RATIO,
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
* Transition
|
||||||
|
*/
|
||||||
|
transition_none: web({
|
||||||
|
transitionProperty: 'none',
|
||||||
|
}), transition_timing_default: web({
|
||||||
|
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||||
|
transitionDuration: '100ms',
|
||||||
|
}), transition_all: web({
|
||||||
|
transitionProperty: 'all',
|
||||||
|
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||||
|
transitionDuration: '100ms',
|
||||||
|
}), transition_color: web({
|
||||||
|
transitionProperty: 'color, background-color, border-color, text-decoration-color, fill, stroke',
|
||||||
|
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||||
|
transitionDuration: '100ms',
|
||||||
|
}), transition_opacity: web({
|
||||||
|
transitionProperty: 'opacity',
|
||||||
|
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||||
|
transitionDuration: '100ms',
|
||||||
|
}), transition_transform: web({
|
||||||
|
transitionProperty: 'transform',
|
||||||
|
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||||
|
transitionDuration: '100ms',
|
||||||
|
}), transition_delay_50ms: web({
|
||||||
|
transitionDelay: '50ms',
|
||||||
|
}),
|
||||||
|
/*
|
||||||
|
* Animations
|
||||||
|
*/
|
||||||
|
fade_in: web({
|
||||||
|
animation: 'fadeIn ease-out 0.15s',
|
||||||
|
}), fade_out: web({
|
||||||
|
animation: 'fadeOut ease-out 0.15s',
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
}), zoom_in: web({
|
||||||
|
animation: 'zoomIn ease-out 0.1s',
|
||||||
|
}), zoom_out: web({
|
||||||
|
animation: 'zoomOut ease-out 0.1s',
|
||||||
|
}), slide_in_left: web({
|
||||||
|
// exponential easing function
|
||||||
|
animation: 'slideInLeft cubic-bezier(0.16, 1, 0.3, 1) 0.5s',
|
||||||
|
}), slide_out_left: web({
|
||||||
|
animation: 'slideOutLeft ease-in 0.15s',
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
}),
|
||||||
|
// special composite animation for dialogs
|
||||||
|
zoom_fade_in: web({
|
||||||
|
animation: "zoomIn ".concat(EXP_CURVE, " 0.3s, fadeIn ").concat(EXP_CURVE, " 0.3s"),
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Visually hidden but available to screen readers (web).
|
||||||
|
* Use for live regions or off-screen labels (e.g. "Image 1 of 3").
|
||||||
|
*/
|
||||||
|
sr_only: web({
|
||||||
|
position: 'absolute',
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
padding: 0,
|
||||||
|
margin: -1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
clip: 'rect(0,0,0,0)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
borderWidth: 0,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* {@link Layout.SCROLLBAR_OFFSET}
|
||||||
|
*/
|
||||||
|
scrollbar_offset: platform({
|
||||||
|
web: {
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateX: Layout.SCROLLBAR_OFFSET,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
native: {
|
||||||
|
transform: [],
|
||||||
|
},
|
||||||
|
}) });
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
export var flatten = StyleSheet.flatten;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { android, ios, native, platform, web } from '@bsky.app/alf';
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { utils } from '@bsky.app/alf';
|
||||||
|
export var select = utils.select;
|
||||||
@@ -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'),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export var Features;
|
||||||
|
(function (Features) {
|
||||||
|
// core flags
|
||||||
|
Features["IsBskyTeam"] = "is_bsky_team";
|
||||||
|
// debug flags
|
||||||
|
Features["DebugFeedContext"] = "debug_feed_context";
|
||||||
|
// feature flags
|
||||||
|
Features["ImportContactsOnboardingDisable"] = "import_contacts:onboarding:disable";
|
||||||
|
Features["ImportContactsSettingsDisable"] = "import_contacts:settings:disable";
|
||||||
|
Features["LiveNowBetaDisable"] = "live_now_beta:disable";
|
||||||
|
Features["AATest"] = "aa-test";
|
||||||
|
})(Features || (Features = {}));
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from '#/analytics/identifiers/device';
|
||||||
|
export * from '#/analytics/identifiers/session';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import uuid from 'react-native-uuid';
|
||||||
|
import { onAppStateChange } from '#/lib/appState';
|
||||||
|
import { isSessionIdExpired } from '#/analytics/identifiers/util';
|
||||||
|
import { device } from '#/storage';
|
||||||
|
var sessionId = (function () {
|
||||||
|
var existing = device.get(['nativeSessionId']);
|
||||||
|
var lastEvent = device.get(['nativeSessionIdLastEventAt']);
|
||||||
|
var id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4();
|
||||||
|
device.set(['nativeSessionId'], id);
|
||||||
|
device.set(['nativeSessionIdLastEventAt'], Date.now());
|
||||||
|
return id;
|
||||||
|
})();
|
||||||
|
export function getInitialSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the current session ID. Freshness depends on `useSessionId` being
|
||||||
|
* mounted, which handles refreshing this value between foreground/background
|
||||||
|
* transitions. Since that's mounted in `analytics/index.tsx`, this value can
|
||||||
|
* generally be trusted to be up to date.
|
||||||
|
*/
|
||||||
|
export function getSessionId() {
|
||||||
|
return device.get(['nativeSessionId']);
|
||||||
|
}
|
||||||
|
export function useSessionId() {
|
||||||
|
var _a = useState(function () { return sessionId; }), id = _a[0], setId = _a[1];
|
||||||
|
useEffect(function () {
|
||||||
|
var sub = onAppStateChange(function (state) {
|
||||||
|
if (state === 'active') {
|
||||||
|
var lastEvent = device.get(['nativeSessionIdLastEventAt']);
|
||||||
|
if (isSessionIdExpired(lastEvent)) {
|
||||||
|
sessionId = uuid.v4();
|
||||||
|
device.set(['nativeSessionId'], sessionId);
|
||||||
|
setId(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
device.set(['nativeSessionIdLastEventAt'], Date.now());
|
||||||
|
});
|
||||||
|
return function () { return sub.remove(); };
|
||||||
|
}, []);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import uuid from 'react-native-uuid';
|
||||||
|
import { onAppStateChange } from '#/lib/appState';
|
||||||
|
import { isSessionIdExpired } from '#/analytics/identifiers/util';
|
||||||
|
var SESSION_ID_KEY = 'bsky_session_id';
|
||||||
|
var LAST_EVENT_KEY = 'bsky_session_id_last_event_at';
|
||||||
|
var sessionId = (function () {
|
||||||
|
var existing = window.sessionStorage.getItem(SESSION_ID_KEY);
|
||||||
|
var lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY);
|
||||||
|
var lastEvent = lastEventStr ? Number(lastEventStr) : undefined;
|
||||||
|
var id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4();
|
||||||
|
window.sessionStorage.setItem(SESSION_ID_KEY, id);
|
||||||
|
window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now()));
|
||||||
|
return id;
|
||||||
|
})();
|
||||||
|
export function getInitialSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the current session ID. Freshness depends on `useSessionId` being
|
||||||
|
* mounted, which handles refreshing this value between foreground/background
|
||||||
|
* transitions. Since that's mounted in `analytics/index.tsx`, this value can
|
||||||
|
* generally be trusted to be up to date.
|
||||||
|
*/
|
||||||
|
export function getSessionId() {
|
||||||
|
return window.sessionStorage.getItem(SESSION_ID_KEY);
|
||||||
|
}
|
||||||
|
export function useSessionId() {
|
||||||
|
var _a = useState(function () { return sessionId; }), id = _a[0], setId = _a[1];
|
||||||
|
useEffect(function () {
|
||||||
|
var sub = onAppStateChange(function (state) {
|
||||||
|
if (state === 'active') {
|
||||||
|
var lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY);
|
||||||
|
var lastEvent = lastEventStr ? Number(lastEventStr) : undefined;
|
||||||
|
if (isSessionIdExpired(lastEvent)) {
|
||||||
|
sessionId = uuid.v4();
|
||||||
|
window.sessionStorage.setItem(SESSION_ID_KEY, sessionId);
|
||||||
|
setId(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now()));
|
||||||
|
});
|
||||||
|
return function () { return sub.remove(); };
|
||||||
|
}, []);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var _a;
|
||||||
|
import { jsx as _jsx } from "react/jsx-runtime";
|
||||||
|
import { createContext, useContext, useMemo } from 'react';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import { Logger } from '#/logger';
|
||||||
|
import { Features, features as feats, init, refresh, setAttributes, } from '#/analytics/features';
|
||||||
|
import { getAndMigrateDeviceId, getDeviceId, getInitialSessionId, useSessionId, } from '#/analytics/identifiers';
|
||||||
|
import { getMetadataForLogger, getNavigationMetadata, } from '#/analytics/metadata';
|
||||||
|
import { metrics } from '#/analytics/metrics';
|
||||||
|
import * as refParams from '#/analytics/misc/refParams';
|
||||||
|
import * as env from '#/env';
|
||||||
|
import { useGeolocationServiceResponse } from '#/geolocation/service';
|
||||||
|
import { device } from '#/storage';
|
||||||
|
export * as utils from '#/analytics/utils';
|
||||||
|
export var features = { init: init, refresh: refresh };
|
||||||
|
export { Features } from '#/analytics/features';
|
||||||
|
function createLogger(context, metadata) {
|
||||||
|
var logger = Logger.create(context, metadata);
|
||||||
|
return {
|
||||||
|
debug: logger.debug.bind(logger),
|
||||||
|
info: logger.info.bind(logger),
|
||||||
|
log: logger.log.bind(logger),
|
||||||
|
warn: logger.warn.bind(logger),
|
||||||
|
error: logger.error.bind(logger),
|
||||||
|
useChild: function (context) {
|
||||||
|
return useMemo(function () { return createLogger(context, metadata); }, [context, metadata]);
|
||||||
|
},
|
||||||
|
Context: Logger.Context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var Context = createContext({
|
||||||
|
logger: createLogger(Logger.Context.Default, {}),
|
||||||
|
metric: function (event, payload, metadata) {
|
||||||
|
if (metadata && '__meta' in metadata) {
|
||||||
|
delete metadata.__meta;
|
||||||
|
}
|
||||||
|
metrics.track(event, payload, __assign(__assign({}, metadata), { navigation: getNavigationMetadata() }));
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
base: {
|
||||||
|
deviceId: (_a = getDeviceId()) !== null && _a !== void 0 ? _a : 'unknown',
|
||||||
|
sessionId: getInitialSessionId(),
|
||||||
|
platform: Platform.OS,
|
||||||
|
appVersion: env.APP_VERSION,
|
||||||
|
bundleIdentifier: env.BUNDLE_IDENTIFIER,
|
||||||
|
bundleDate: env.BUNDLE_DATE,
|
||||||
|
referrerSrc: refParams.src,
|
||||||
|
referrerUrl: refParams.url,
|
||||||
|
},
|
||||||
|
geolocation: device.get(['geolocationServiceResponse']) || {
|
||||||
|
countryCode: '',
|
||||||
|
regionCode: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Ensures that deviceId is set and migrated from legacy storage. Handled on
|
||||||
|
* startup in `App.<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 = useGeolocationServiceResponse();
|
||||||
|
var parentContext = useContext(Context);
|
||||||
|
var childContext = useMemo(function () {
|
||||||
|
var combinedMetadata = __assign(__assign(__assign({}, parentContext.metadata), metadata), { base: __assign(__assign({}, parentContext.metadata.base), { sessionId: sessionId }), geolocation: geolocation });
|
||||||
|
var context = __assign(__assign({}, parentContext), { logger: createLogger(Logger.Context.Default, getMetadataForLogger(combinedMetadata)), metadata: combinedMetadata, metric: function (event, payload, extraMetadata) {
|
||||||
|
parentContext.metric(event, payload, __assign(__assign({}, combinedMetadata), extraMetadata));
|
||||||
|
} });
|
||||||
|
return context;
|
||||||
|
}, [sessionId, geolocation, parentContext, metadata]);
|
||||||
|
return _jsx(Context.Provider, { value: childContext, children: children });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Feature gates provider. Decorates the parent analytics context with
|
||||||
|
* feature gate capabilities. Should be mounted within `AnalyticsContext`,
|
||||||
|
* and below the `<Fragment key={did} />` breaker in `App.<platform>.tsx`.
|
||||||
|
*/
|
||||||
|
export function AnalyticsFeaturesContext(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var parentContext = useContext(Context);
|
||||||
|
/**
|
||||||
|
* Side-effects: we need to synchronously set these during the same render
|
||||||
|
* cycle. These calls do not trigger re-renders, they just set properties on
|
||||||
|
* the singleton GrowthBook instance.
|
||||||
|
*/
|
||||||
|
setAttributes(parentContext.metadata);
|
||||||
|
feats.setTrackingCallback(function (experiment, result) {
|
||||||
|
parentContext.metric('experiment:viewed', {
|
||||||
|
experimentId: experiment.key,
|
||||||
|
variationId: result.key,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
feats.setFeatureUsageCallback(function (feature, result) {
|
||||||
|
var _a, _b;
|
||||||
|
parentContext.metric('feature:viewed', {
|
||||||
|
featureId: feature,
|
||||||
|
featureResultValue: result.value,
|
||||||
|
experimentId: (_a = result.experiment) === null || _a === void 0 ? void 0 : _a.key,
|
||||||
|
variationId: (_b = result.experimentResult) === null || _b === void 0 ? void 0 : _b.key,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var childContext = useMemo(function () {
|
||||||
|
return __assign(__assign({}, parentContext), { features: __assign({ enabled: feats.isOn.bind(feats) }, Features) });
|
||||||
|
}, [parentContext]);
|
||||||
|
return _jsx(Context.Provider, { value: childContext, children: children });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Basic analytics context without feature gates. Should really only be used
|
||||||
|
* above the `AnalyticsFeaturesContext` provider.
|
||||||
|
*/
|
||||||
|
export function useAnalyticsBase() {
|
||||||
|
return useContext(Context);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The main analytics context, including feature gates. Use this everywhere you
|
||||||
|
* need metrics, features, or logging within the React tree.
|
||||||
|
*/
|
||||||
|
export function useAnalytics() {
|
||||||
|
var ctx = useContext(Context);
|
||||||
|
if (!('features' in ctx)) {
|
||||||
|
throw new Error('useAnalytics must be used within an AnalyticsFeaturesContext');
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
import { onAppStateChange } from '#/lib/appState';
|
||||||
|
import { isNetworkError } from '#/lib/strings/errors';
|
||||||
|
import { Logger } from '#/logger';
|
||||||
|
import * as env from '#/env';
|
||||||
|
var TRACKING_ENDPOINT = env.METRICS_API_HOST + '/t';
|
||||||
|
var logger = Logger.create(Logger.Context.Metric, {});
|
||||||
|
var MetricsClient = /** @class */ (function () {
|
||||||
|
function MetricsClient() {
|
||||||
|
this.maxBatchSize = 100;
|
||||||
|
this.started = false;
|
||||||
|
this.queue = [];
|
||||||
|
this.failedQueue = [];
|
||||||
|
this.flushInterval = null;
|
||||||
|
}
|
||||||
|
MetricsClient.prototype.start = function () {
|
||||||
|
var _this = this;
|
||||||
|
if (this.started)
|
||||||
|
return;
|
||||||
|
this.started = true;
|
||||||
|
this.flushInterval = setInterval(function () {
|
||||||
|
_this.flush();
|
||||||
|
}, 10000);
|
||||||
|
onAppStateChange(function (state) {
|
||||||
|
if (state === 'active') {
|
||||||
|
_this.retryFailedLogs();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_this.flush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
MetricsClient.prototype.track = function (event, payload, metadata) {
|
||||||
|
if (metadata === void 0) { metadata = {}; }
|
||||||
|
this.start();
|
||||||
|
var e = {
|
||||||
|
source: 'app',
|
||||||
|
time: Date.now(),
|
||||||
|
event: event,
|
||||||
|
payload: payload,
|
||||||
|
metadata: metadata,
|
||||||
|
};
|
||||||
|
this.queue.push(e);
|
||||||
|
logger.debug("event: ".concat(e.event), e);
|
||||||
|
if (this.queue.length > this.maxBatchSize) {
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MetricsClient.prototype.flush = function () {
|
||||||
|
if (!this.queue.length)
|
||||||
|
return;
|
||||||
|
var events = this.queue.splice(0, this.queue.length);
|
||||||
|
this.sendBatch(events);
|
||||||
|
};
|
||||||
|
MetricsClient.prototype.sendBatch = function (events_1) {
|
||||||
|
return __awaiter(this, arguments, void 0, function (events, isRetry) {
|
||||||
|
var body, success, res, error, e_1;
|
||||||
|
var _a;
|
||||||
|
if (isRetry === void 0) { isRetry = false; }
|
||||||
|
return __generator(this, function (_b) {
|
||||||
|
switch (_b.label) {
|
||||||
|
case 0:
|
||||||
|
logger.debug("sendBatch: ".concat(events.length), {
|
||||||
|
isRetry: isRetry,
|
||||||
|
});
|
||||||
|
_b.label = 1;
|
||||||
|
case 1:
|
||||||
|
_b.trys.push([1, 6, , 7]);
|
||||||
|
body = JSON.stringify({ events: events });
|
||||||
|
if (!(env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon)) return [3 /*break*/, 2];
|
||||||
|
success = navigator.sendBeacon(TRACKING_ENDPOINT, new Blob([body], { type: 'application/json' }));
|
||||||
|
if (!success) {
|
||||||
|
// construct a "network error" for `isNetworkError` to work
|
||||||
|
throw new Error("Failed to fetch: sendBeacon returned false");
|
||||||
|
}
|
||||||
|
return [3 /*break*/, 5];
|
||||||
|
case 2: return [4 /*yield*/, fetch(TRACKING_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ events: events }),
|
||||||
|
keepalive: true,
|
||||||
|
})];
|
||||||
|
case 3:
|
||||||
|
res = _b.sent();
|
||||||
|
if (!!res.ok) return [3 /*break*/, 5];
|
||||||
|
return [4 /*yield*/, res.text().catch(function () { return 'Unknown error'; })
|
||||||
|
// construct a "network error" for `isNetworkError` to work
|
||||||
|
];
|
||||||
|
case 4:
|
||||||
|
error = _b.sent();
|
||||||
|
// construct a "network error" for `isNetworkError` to work
|
||||||
|
throw new Error("".concat(res.status, " Failed to fetch \u2014 ").concat(error));
|
||||||
|
case 5: return [3 /*break*/, 7];
|
||||||
|
case 6:
|
||||||
|
e_1 = _b.sent();
|
||||||
|
if (isNetworkError(e_1)) {
|
||||||
|
if (isRetry)
|
||||||
|
return [2 /*return*/]; // retry once
|
||||||
|
(_a = this.failedQueue).push.apply(_a, events);
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
logger.error("Failed to send metrics", {
|
||||||
|
safeMessage: e_1.toString(),
|
||||||
|
});
|
||||||
|
return [3 /*break*/, 7];
|
||||||
|
case 7: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
MetricsClient.prototype.retryFailedLogs = function () {
|
||||||
|
if (!this.failedQueue.length)
|
||||||
|
return;
|
||||||
|
var events = this.failedQueue.splice(0, this.failedQueue.length);
|
||||||
|
this.sendBatch(events, true);
|
||||||
|
};
|
||||||
|
return MetricsClient;
|
||||||
|
}());
|
||||||
|
export { MetricsClient };
|
||||||
@@ -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*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); });
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { MetricsClient } from '#/analytics/metrics/client';
|
||||||
|
export * from '#/analytics/metrics/utils';
|
||||||
|
export var metrics = new MetricsClient();
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
* Do not import runtime code into this file
|
||||||
|
*/
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function toClout(n) {
|
||||||
|
if (n == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Math.max(0, Math.round(Math.log(n)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { isJwtExpired } from '#/lib/jwt';
|
||||||
|
import { sanitizeDisplayName } from '#/lib/strings/display-names';
|
||||||
|
import { sanitizeHandle } from '#/lib/strings/handles';
|
||||||
|
import { useProfilesQuery } from '#/state/queries/profile';
|
||||||
|
import { useSession } from '#/state/session';
|
||||||
|
import { UserAvatar } from '#/view/com/util/UserAvatar';
|
||||||
|
import { atoms as a, useTheme } from '#/alf';
|
||||||
|
import { Button } from '#/components/Button';
|
||||||
|
import { CheckThick_Stroke2_Corner0_Rounded as CheckIcon } from '#/components/icons/Check';
|
||||||
|
import { ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon } from '#/components/icons/Chevron';
|
||||||
|
import { PlusLarge_Stroke2_Corner0_Rounded as PlusIcon } from '#/components/icons/Plus';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { useSimpleVerificationState } from '#/components/verification';
|
||||||
|
import { VerificationCheck } from '#/components/verification/VerificationCheck';
|
||||||
|
import { useActorStatus } from '#/features/liveNow';
|
||||||
|
export function AccountList(_a) {
|
||||||
|
var onSelectAccount = _a.onSelectAccount, onSelectOther = _a.onSelectOther, otherLabel = _a.otherLabel, pendingDid = _a.pendingDid;
|
||||||
|
var _b = useSession(), currentAccount = _b.currentAccount, accounts = _b.accounts;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var profiles = useProfilesQuery({
|
||||||
|
handles: accounts.map(function (acc) { return acc.did; }),
|
||||||
|
}).data;
|
||||||
|
var onPressAddAccount = useCallback(function () {
|
||||||
|
onSelectOther();
|
||||||
|
}, [onSelectOther]);
|
||||||
|
return (_jsxs(View, { pointerEvents: pendingDid ? 'none' : 'auto', style: [
|
||||||
|
a.rounded_lg,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
], children: [accounts.map(function (account) { return (_jsxs(React.Fragment, { children: [_jsx(AccountItem, { profile: profiles === null || profiles === void 0 ? void 0 : profiles.profiles.find(function (p) { return p.did === account.did; }), account: account, onSelect: onSelectAccount, isCurrentAccount: account.did === (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did), isPendingAccount: account.did === pendingDid }), _jsx(View, { style: [a.border_b, t.atoms.border_contrast_low] })] }, account.did)); }), _jsx(Button, { testID: "chooseAddAccountBtn", style: [a.flex_1], onPress: pendingDid ? undefined : onPressAddAccount, label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Sign in to account that is not listed"], ["Sign in to account that is not listed"])))), children: function (_a) {
|
||||||
|
var hovered = _a.hovered, pressed = _a.pressed;
|
||||||
|
return (_jsxs(View, { style: [
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.p_lg,
|
||||||
|
a.gap_sm,
|
||||||
|
(hovered || pressed) && t.atoms.bg_contrast_25,
|
||||||
|
], children: [_jsx(View, { style: [
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.rounded_full,
|
||||||
|
{ width: 48, height: 48 },
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
(hovered || pressed) && t.atoms.bg_contrast_50,
|
||||||
|
], children: _jsx(PlusIcon, { style: [t.atoms.text_contrast_low], size: "md" }) }), _jsx(Text, { style: [a.flex_1, a.leading_tight, a.text_md, a.font_medium], children: otherLabel !== null && otherLabel !== void 0 ? otherLabel : _jsx(Trans, { children: "Other account" }) }), _jsx(ChevronIcon, { size: "md", style: [t.atoms.text_contrast_low] })] }));
|
||||||
|
} })] }));
|
||||||
|
}
|
||||||
|
function AccountItem(_a) {
|
||||||
|
var profile = _a.profile, account = _a.account, onSelect = _a.onSelect, isCurrentAccount = _a.isCurrentAccount, isPendingAccount = _a.isPendingAccount;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var verification = useSimpleVerificationState({ profile: profile });
|
||||||
|
var live = useActorStatus(profile).isActive;
|
||||||
|
var onPress = useCallback(function () {
|
||||||
|
onSelect(account);
|
||||||
|
}, [account, onSelect]);
|
||||||
|
var isLoggedOut = !account.refreshJwt || isJwtExpired(account.refreshJwt);
|
||||||
|
return (_jsx(Button, { testID: "chooseAccountBtn-".concat(account.handle), style: [a.w_full], onPress: onPress, label: isCurrentAccount
|
||||||
|
? _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Continue as ", " (currently signed in)"], ["Continue as ", " (currently signed in)"])), account.handle))
|
||||||
|
: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Sign in as ", ""], ["Sign in as ", ""])), account.handle)), children: function (_a) {
|
||||||
|
var _b;
|
||||||
|
var hovered = _a.hovered, pressed = _a.pressed;
|
||||||
|
return (_jsxs(View, { style: [
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.p_lg,
|
||||||
|
a.gap_sm,
|
||||||
|
(hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25,
|
||||||
|
], children: [_jsx(UserAvatar, { avatar: profile === null || profile === void 0 ? void 0 : profile.avatar, size: 48, type: ((_b = profile === null || profile === void 0 ? void 0 : profile.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', live: live, hideLiveBadge: true }), _jsxs(View, { style: [a.flex_1, a.gap_2xs, a.pr_2xl], children: [_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_xs], children: [_jsx(Text, { emoji: true, style: [a.font_medium, a.leading_tight, a.text_md], numberOfLines: 1, children: sanitizeDisplayName((profile === null || profile === void 0 ? void 0 : profile.displayName) || (profile === null || profile === void 0 ? void 0 : profile.handle) || account.handle) }), verification.showBadge && (_jsx(View, { children: _jsx(VerificationCheck, { width: 12, verifier: verification.role === 'verifier' }) }))] }), _jsx(Text, { style: [
|
||||||
|
a.leading_tight,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
a.text_sm,
|
||||||
|
], children: sanitizeHandle(account.handle, '@') }), isLoggedOut && (_jsx(Text, { style: [
|
||||||
|
a.leading_tight,
|
||||||
|
a.text_xs,
|
||||||
|
a.italic,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
], children: _jsx(Trans, { children: "Logged out" }) }))] }), isCurrentAccount ? (_jsx(View, { style: [
|
||||||
|
{
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
backgroundColor: t.palette.positive_500,
|
||||||
|
},
|
||||||
|
a.rounded_full,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
], children: _jsx(CheckIcon, { size: "xs", style: [{ color: t.palette.white }] }) })) : (_jsx(ChevronIcon, { size: "md", style: [t.atoms.text_contrast_low] }))] }));
|
||||||
|
} }, account.did));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __rest = (this && this.__rest) || function (s, e) {
|
||||||
|
var t = {};
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||||
|
t[p] = s[p];
|
||||||
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||||
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||||
|
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||||
|
t[p[i]] = s[p[i]];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { atoms as a, useBreakpoints, useTheme } from '#/alf';
|
||||||
|
import { Button as BaseButton } from '#/components/Button';
|
||||||
|
import { CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon } from '#/components/icons/CircleInfo';
|
||||||
|
import { CircleX_Stroke2_Corner0_Rounded as CircleXIcon } from '#/components/icons/CircleX';
|
||||||
|
import { Warning_Stroke2_Corner0_Rounded as WarningIcon } from '#/components/icons/Warning';
|
||||||
|
import { Text as BaseText } from '#/components/Typography';
|
||||||
|
import { EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon } from './icons/Emoji';
|
||||||
|
var Context = createContext({
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
Context.displayName = 'AdmonitionContext';
|
||||||
|
export function Icon() {
|
||||||
|
var t = useTheme();
|
||||||
|
var type = useContext(Context).type;
|
||||||
|
var Icon = {
|
||||||
|
info: CircleInfoIcon,
|
||||||
|
tip: CircleInfoIcon,
|
||||||
|
warning: WarningIcon,
|
||||||
|
error: CircleXIcon,
|
||||||
|
apology: EmojiSadIcon,
|
||||||
|
}[type];
|
||||||
|
var fill = {
|
||||||
|
info: t.atoms.text_contrast_medium.color,
|
||||||
|
tip: t.palette.primary_500,
|
||||||
|
warning: t.palette.yellow,
|
||||||
|
error: t.palette.negative_500,
|
||||||
|
apology: t.atoms.text_contrast_medium.color,
|
||||||
|
}[type];
|
||||||
|
return _jsx(Icon, { fill: fill, size: "md" });
|
||||||
|
}
|
||||||
|
export function Content(_a) {
|
||||||
|
var children = _a.children, style = _a.style, rest = __rest(_a, ["children", "style"]);
|
||||||
|
return (_jsx(View, __assign({ style: [a.gap_sm, a.flex_1, { minHeight: 20 }, a.justify_center, style] }, rest, { children: children })));
|
||||||
|
}
|
||||||
|
export function Text(_a) {
|
||||||
|
var children = _a.children, style = _a.style, rest = __rest(_a, ["children", "style"]);
|
||||||
|
return (_jsx(BaseText, __assign({}, rest, { style: [a.text_sm, a.leading_snug, a.pr_md, style], children: children })));
|
||||||
|
}
|
||||||
|
export function Button(_a) {
|
||||||
|
var children = _a.children, props = __rest(_a, ["children"]);
|
||||||
|
return (_jsx(BaseButton, __assign({ size: "tiny" }, props, { children: children })));
|
||||||
|
}
|
||||||
|
export function Row(_a) {
|
||||||
|
var children = _a.children, style = _a.style;
|
||||||
|
return (_jsx(View, { style: [a.w_full, a.flex_row, a.align_start, a.gap_sm, style], children: children }));
|
||||||
|
}
|
||||||
|
export function Outer(_a) {
|
||||||
|
var children = _a.children, _b = _a.type, type = _b === void 0 ? 'info' : _b, style = _a.style;
|
||||||
|
var t = useTheme();
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var borderColor = {
|
||||||
|
info: t.atoms.border_contrast_high.borderColor,
|
||||||
|
tip: t.palette.primary_500,
|
||||||
|
warning: t.palette.yellow,
|
||||||
|
error: t.palette.negative_500,
|
||||||
|
apology: t.atoms.border_contrast_high.borderColor,
|
||||||
|
}[type];
|
||||||
|
return (_jsx(Context.Provider, { value: { type: type }, children: _jsx(View, { style: [
|
||||||
|
gtMobile ? a.p_md : a.p_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.border,
|
||||||
|
t.atoms.bg,
|
||||||
|
{ borderColor: borderColor },
|
||||||
|
style,
|
||||||
|
], children: children }) }));
|
||||||
|
}
|
||||||
|
export function Admonition(_a) {
|
||||||
|
var children = _a.children, type = _a.type, style = _a.style;
|
||||||
|
return (_jsx(Outer, { type: type, style: style, children: _jsxs(Row, { children: [_jsx(Icon, {}), _jsx(Content, { children: _jsx(Text, { children: children }) })] }) }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import React from 'react';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { sanitizeAppLanguageSetting } from '#/locale/helpers';
|
||||||
|
import { APP_LANGUAGES } from '#/locale/languages';
|
||||||
|
import { useLanguagePrefs, useLanguagePrefsApi } from '#/state/preferences';
|
||||||
|
import { resetPostsFeedQueries } from '#/state/queries/post-feed';
|
||||||
|
import { atoms as a, platform, useTheme } from '#/alf';
|
||||||
|
import * as Select from '#/components/Select';
|
||||||
|
import { Button } from './Button';
|
||||||
|
export function AppLanguageDropdown() {
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var queryClient = useQueryClient();
|
||||||
|
var langPrefs = useLanguagePrefs();
|
||||||
|
var setLangPrefs = useLanguagePrefsApi();
|
||||||
|
var sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage);
|
||||||
|
var onChangeAppLanguage = React.useCallback(function (value) {
|
||||||
|
if (!value)
|
||||||
|
return;
|
||||||
|
if (sanitizedLang !== value) {
|
||||||
|
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value));
|
||||||
|
}
|
||||||
|
// reset feeds to refetch content
|
||||||
|
resetPostsFeedQueries(queryClient);
|
||||||
|
}, [sanitizedLang, setLangPrefs, queryClient]);
|
||||||
|
return (_jsxs(Select.Root, { value: sanitizeAppLanguageSetting(langPrefs.appLanguage), onValueChange: onChangeAppLanguage, children: [_jsx(Select.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Change app language"], ["Change app language"])))), children: function (_a) {
|
||||||
|
var props = _a.props;
|
||||||
|
return (_jsxs(Button, __assign({}, props, { label: props.accessibilityLabel, size: platform({
|
||||||
|
web: 'tiny',
|
||||||
|
native: 'small',
|
||||||
|
}), variant: "ghost", color: "secondary", shape: "rectangular", style: [
|
||||||
|
a.pr_xs,
|
||||||
|
a.pl_sm,
|
||||||
|
platform({
|
||||||
|
web: [{ alignSelf: 'flex-start' }, a.gap_sm],
|
||||||
|
native: [a.gap_xs],
|
||||||
|
}),
|
||||||
|
], children: [_jsx(Select.ValueText, { placeholder: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Select an app language"], ["Select an app language"])))), style: [t.atoms.text_contrast_medium] }), _jsx(Select.Icon, { style: [t.atoms.text_contrast_medium] })] })));
|
||||||
|
} }), _jsx(Select.Content, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Select language"], ["Select language"])))), renderItem: function (_a) {
|
||||||
|
var label = _a.label, value = _a.value;
|
||||||
|
return (_jsxs(Select.Item, { value: value, label: label, children: [_jsx(Select.ItemIndicator, {}), _jsx(Select.ItemText, { children: label })] }));
|
||||||
|
}, items: APP_LANGUAGES.map(function (l) { return ({
|
||||||
|
label: l.name,
|
||||||
|
value: l.code2,
|
||||||
|
}); }) })] }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3;
|
||||||
@@ -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 }));
|
||||||
|
}
|
||||||
@@ -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/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { atoms as a, useTheme } from '#/alf';
|
||||||
|
import { useContextMenuContext } from './context';
|
||||||
|
var AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
|
||||||
|
export function Backdrop(props) {
|
||||||
|
var mode = useContextMenuContext().mode;
|
||||||
|
switch (mode) {
|
||||||
|
case 'full':
|
||||||
|
return _jsx(BlurredBackdrop, __assign({}, props));
|
||||||
|
case 'auxiliary-only':
|
||||||
|
return _jsx(OpacityBackdrop, __assign({}, props));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function BlurredBackdrop(_a) {
|
||||||
|
var animation = _a.animation, _b = _a.intensity, intensity = _b === void 0 ? 50 : _b, onPress = _a.onPress;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var animatedProps = useAnimatedProps(function () { return ({
|
||||||
|
intensity: interpolate(animation.get(), [0, 1], [0, intensity], Extrapolation.CLAMP),
|
||||||
|
}); });
|
||||||
|
return (_jsx(AnimatedBlurView, { animatedProps: animatedProps, style: [a.absolute, a.inset_0], tint: "systemMaterialDark", children: _jsx(Pressable, { style: a.flex_1, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close menu"], ["Close menu"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Tap to close context menu"], ["Tap to close context menu"])))), onPress: onPress }) }));
|
||||||
|
}
|
||||||
|
function OpacityBackdrop(_a) {
|
||||||
|
var animation = _a.animation, onPress = _a.onPress;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var animatedStyle = useAnimatedStyle(function () { return ({
|
||||||
|
opacity: interpolate(animation.get(), [0, 1], [0, 0.05], Extrapolation.CLAMP),
|
||||||
|
}); });
|
||||||
|
return (_jsx(Animated.View, { style: [a.absolute, a.inset_0, t.atoms.bg_contrast_975, animatedStyle], children: _jsx(Pressable, { style: a.flex_1, accessibilityLabel: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Close menu"], ["Close menu"])))), accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Tap to close context menu"], ["Tap to close context menu"])))), onPress: onPress }) }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3, templateObject_4;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx } from "react/jsx-runtime";
|
||||||
|
import { Pressable } from 'react-native';
|
||||||
|
import Animated, { Extrapolation, interpolate, useAnimatedStyle, } from 'react-native-reanimated';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { atoms as a, useTheme } from '#/alf';
|
||||||
|
import { useContextMenuContext } from './context';
|
||||||
|
export function Backdrop(_a) {
|
||||||
|
var animation = _a.animation, _b = _a.intensity, intensity = _b === void 0 ? 50 : _b, onPress = _a.onPress;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var mode = useContextMenuContext().mode;
|
||||||
|
var reduced = mode === 'auxiliary-only';
|
||||||
|
var target = reduced ? 0.05 : intensity / 100;
|
||||||
|
var animatedStyle = useAnimatedStyle(function () { return ({
|
||||||
|
opacity: interpolate(animation.get(), [0, 1], [0, target], Extrapolation.CLAMP),
|
||||||
|
}); });
|
||||||
|
return (_jsx(Animated.View, { style: [a.absolute, a.inset_0, t.atoms.bg_contrast_975, animatedStyle], children: _jsx(Pressable, { style: a.flex_1, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close menu"], ["Close menu"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Tap to close context menu"], ["Tap to close context menu"])))), onPress: onPress }) }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
export var Context = createContext(null);
|
||||||
|
Context.displayName = 'ContextMenuContext';
|
||||||
|
export var MenuContext = createContext(null);
|
||||||
|
MenuContext.displayName = 'ContextMenuMenuContext';
|
||||||
|
export var ItemContext = createContext(null);
|
||||||
|
ItemContext.displayName = 'ContextMenuItemContext';
|
||||||
|
export function useContextMenuContext() {
|
||||||
|
var context = useContext(Context);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useContextMenuContext must be used within a Context.Provider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
export function useContextMenuMenuContext() {
|
||||||
|
var context = useContext(MenuContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useContextMenuMenuContext must be used within a Context.Provider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
export function useContextMenuItemContext() {
|
||||||
|
var context = useContext(ItemContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useContextMenuItemContext must be used within a Context.Provider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,714 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var __rest = (this && this.__rest) || function (s, e) {
|
||||||
|
var t = {};
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||||
|
t[p] = s[p];
|
||||||
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||||
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||||
|
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||||
|
t[p[i]] = s[p[i]];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
||||||
|
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
||||||
|
if (ar || !(i in from)) {
|
||||||
|
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
||||||
|
ar[i] = from[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to.concat(ar || Array.prototype.slice.call(from));
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import React, { useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
||||||
|
import { BackHandler, Keyboard, Pressable, useWindowDimensions, View, } from 'react-native';
|
||||||
|
import { Gesture, GestureDetector, } from 'react-native-gesture-handler';
|
||||||
|
import { KeyboardEvents } from 'react-native-keyboard-controller';
|
||||||
|
import Animated, { clamp, interpolate, runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated';
|
||||||
|
import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context';
|
||||||
|
import { captureRef } from 'react-native-view-shot';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { useIsFocused } from '@react-navigation/native';
|
||||||
|
import flattenReactChildren from 'react-keyed-flatten-children';
|
||||||
|
import { HITSLOP_10 } from '#/lib/constants';
|
||||||
|
import { useHaptics } from '#/lib/haptics';
|
||||||
|
import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback';
|
||||||
|
import { logger } from '#/logger';
|
||||||
|
import { atoms as a, platform, tokens, useTheme } from '#/alf';
|
||||||
|
import { Context, ItemContext, MenuContext, useContextMenuContext, useContextMenuItemContext, useContextMenuMenuContext, } from '#/components/ContextMenu/context';
|
||||||
|
import { useInteractionState } from '#/components/hooks/useInteractionState';
|
||||||
|
import { createPortalGroup } from '#/components/Portal';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { IS_ANDROID, IS_IOS } from '#/env';
|
||||||
|
import { Backdrop } from './Backdrop';
|
||||||
|
export { useDialogControl as useContextMenuControl, } from '#/components/Dialog';
|
||||||
|
var _a = createPortalGroup(), PortalProvider = _a.Provider, Outlet = _a.Outlet, Portal = _a.Portal;
|
||||||
|
var SPRING_IN = {
|
||||||
|
mass: 0.75,
|
||||||
|
damping: 300,
|
||||||
|
stiffness: 1200,
|
||||||
|
restDisplacementThreshold: 0.01,
|
||||||
|
};
|
||||||
|
var SPRING_OUT = {
|
||||||
|
mass: IS_IOS ? 1.25 : 0.75,
|
||||||
|
damping: 150,
|
||||||
|
stiffness: 1000,
|
||||||
|
restDisplacementThreshold: 0.01,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Needs placing near the top of the provider stack, but BELOW the theme provider.
|
||||||
|
*/
|
||||||
|
export function Provider(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
return (_jsxs(PortalProvider, { children: [children, _jsx(Outlet, {})] }));
|
||||||
|
}
|
||||||
|
export function Root(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var playHaptic = useHaptics();
|
||||||
|
var _b = useState('full'), mode = _b[0], setMode = _b[1];
|
||||||
|
var _c = useState(null), measurement = _c[0], setMeasurement = _c[1];
|
||||||
|
var returnLocationSV = useSharedValue(null);
|
||||||
|
var animationSV = useSharedValue(0);
|
||||||
|
var translationSV = useSharedValue(0);
|
||||||
|
var isFocused = useIsFocused();
|
||||||
|
var hoverables = useRef(new Map());
|
||||||
|
var hoverablesSV = useSharedValue({});
|
||||||
|
var syncHoverablesThrottleRef = useRef(undefined);
|
||||||
|
var _d = useState(null), hoveredMenuItem = _d[0], setHoveredMenuItem = _d[1];
|
||||||
|
var onHoverableTouchUp = useCallback(function (id) {
|
||||||
|
var hoverable = hoverables.current.get(id);
|
||||||
|
if (!hoverable) {
|
||||||
|
logger.warn("No such hoverable with id ".concat(id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hoverable.onTouchUp();
|
||||||
|
}, []);
|
||||||
|
var onCompletedClose = useCallback(function () {
|
||||||
|
hoverables.current.clear();
|
||||||
|
setMeasurement(null);
|
||||||
|
}, []);
|
||||||
|
var context = useMemo(function () {
|
||||||
|
return ({
|
||||||
|
isOpen: !!measurement && isFocused,
|
||||||
|
measurement: measurement,
|
||||||
|
returnLocationSV: returnLocationSV,
|
||||||
|
animationSV: animationSV,
|
||||||
|
translationSV: translationSV,
|
||||||
|
mode: mode,
|
||||||
|
open: function (evt, mode) {
|
||||||
|
setMeasurement(evt);
|
||||||
|
setMode(mode);
|
||||||
|
animationSV.set(withSpring(1, SPRING_IN));
|
||||||
|
// reset return location
|
||||||
|
returnLocationSV.set(null);
|
||||||
|
},
|
||||||
|
close: function () {
|
||||||
|
animationSV.set(withSpring(0, SPRING_OUT, function (finished) {
|
||||||
|
if (finished) {
|
||||||
|
hoverablesSV.set({});
|
||||||
|
translationSV.set(0);
|
||||||
|
// note: return location has to be reset on open,
|
||||||
|
// rather than on close, otherwise there's a flicker
|
||||||
|
// where the reanimated update is faster than the react render
|
||||||
|
runOnJS(onCompletedClose)();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
registerHoverable: function (id, rect, onTouchUp) {
|
||||||
|
hoverables.current.set(id, { id: id, rect: rect, onTouchUp: onTouchUp });
|
||||||
|
// we need this data on the UI thread, but we want to limit cross-thread communication
|
||||||
|
// and this function will be called in quick succession, so we need to throttle it
|
||||||
|
if (syncHoverablesThrottleRef.current)
|
||||||
|
clearTimeout(syncHoverablesThrottleRef.current);
|
||||||
|
syncHoverablesThrottleRef.current = setTimeout(function () {
|
||||||
|
syncHoverablesThrottleRef.current = undefined;
|
||||||
|
hoverablesSV.set(Object.fromEntries(
|
||||||
|
// eslint-ignore
|
||||||
|
__spreadArray([], hoverables.current.entries(), true).map(function (_a) {
|
||||||
|
var id = _a[0], rect = _a[1].rect;
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
{ id: id, rect: rect },
|
||||||
|
];
|
||||||
|
})));
|
||||||
|
}, 1);
|
||||||
|
},
|
||||||
|
hoverablesSV: hoverablesSV,
|
||||||
|
onTouchUpMenuItem: onHoverableTouchUp,
|
||||||
|
hoveredMenuItem: hoveredMenuItem,
|
||||||
|
setHoveredMenuItem: function (item) {
|
||||||
|
if (item)
|
||||||
|
playHaptic('Light');
|
||||||
|
setHoveredMenuItem(item);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
measurement,
|
||||||
|
returnLocationSV,
|
||||||
|
setMeasurement,
|
||||||
|
onCompletedClose,
|
||||||
|
isFocused,
|
||||||
|
animationSV,
|
||||||
|
translationSV,
|
||||||
|
hoverablesSV,
|
||||||
|
onHoverableTouchUp,
|
||||||
|
hoveredMenuItem,
|
||||||
|
setHoveredMenuItem,
|
||||||
|
playHaptic,
|
||||||
|
mode,
|
||||||
|
]);
|
||||||
|
useEffect(function () {
|
||||||
|
if (IS_ANDROID && context.isOpen) {
|
||||||
|
var listener_1 = BackHandler.addEventListener('hardwareBackPress', function () {
|
||||||
|
context.close();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return function () { return listener_1.remove(); };
|
||||||
|
}
|
||||||
|
}, [context]);
|
||||||
|
return _jsx(Context.Provider, { value: context, children: children });
|
||||||
|
}
|
||||||
|
export function Trigger(_a) {
|
||||||
|
var _this = this;
|
||||||
|
var children = _a.children, label = _a.label, contentLabel = _a.contentLabel, style = _a.style;
|
||||||
|
var context = useContextMenuContext();
|
||||||
|
var playHaptic = useHaptics();
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
var ref = useRef(null);
|
||||||
|
var isFocused = useIsFocused();
|
||||||
|
var _b = useState(null), image = _b[0], setImage = _b[1];
|
||||||
|
var _c = useState(null), pendingMeasurement = _c[0], setPendingMeasurement = _c[1];
|
||||||
|
var open = useNonReactiveCallback(function (mode) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var _a, measurement, capture;
|
||||||
|
return __generator(this, function (_b) {
|
||||||
|
switch (_b.label) {
|
||||||
|
case 0:
|
||||||
|
playHaptic();
|
||||||
|
return [4 /*yield*/, Promise.all([
|
||||||
|
measureView(ref.current, insets),
|
||||||
|
captureRef(ref, { result: 'data-uri' }).catch(function (err) {
|
||||||
|
logger.error(err instanceof Error ? err : String(err), {
|
||||||
|
message: 'Failed to capture image of context menu trigger',
|
||||||
|
});
|
||||||
|
// will cause the image to fail to load, but it will get handled gracefully
|
||||||
|
return '<failed capture>';
|
||||||
|
}),
|
||||||
|
])];
|
||||||
|
case 1:
|
||||||
|
_a = _b.sent(), measurement = _a[0], capture = _a[1];
|
||||||
|
Keyboard.dismiss();
|
||||||
|
setImage(capture);
|
||||||
|
if (measurement) {
|
||||||
|
setPendingMeasurement({ measurement: measurement, mode: mode });
|
||||||
|
}
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); });
|
||||||
|
// after keyboard hides, the position might change - set a return location
|
||||||
|
useEffect(function () {
|
||||||
|
if (context.isOpen && context.measurement) {
|
||||||
|
var hide_1 = KeyboardEvents.addListener('keyboardDidHide', function () {
|
||||||
|
measureView(ref.current, insets)
|
||||||
|
.then(function (newMeasurement) {
|
||||||
|
if (!newMeasurement || !context.measurement)
|
||||||
|
return;
|
||||||
|
if (newMeasurement.x !== context.measurement.x ||
|
||||||
|
newMeasurement.y !== context.measurement.y) {
|
||||||
|
context.returnLocationSV.set({
|
||||||
|
x: newMeasurement.x,
|
||||||
|
y: newMeasurement.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () { });
|
||||||
|
});
|
||||||
|
return function () {
|
||||||
|
hide_1.remove();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [context, insets]);
|
||||||
|
var doubleTapGesture = useMemo(function () {
|
||||||
|
return Gesture.Tap()
|
||||||
|
.numberOfTaps(2)
|
||||||
|
.hitSlop(HITSLOP_10)
|
||||||
|
.onEnd(function () { return void open('auxiliary-only'); })
|
||||||
|
.runOnJS(true);
|
||||||
|
}, [open]);
|
||||||
|
var hoverablesSV = context.hoverablesSV, setHoveredMenuItem = context.setHoveredMenuItem, onTouchUpMenuItem = context.onTouchUpMenuItem, translationSV = context.translationSV, animationSV = context.animationSV;
|
||||||
|
var hoveredItemSV = useSharedValue(null);
|
||||||
|
useAnimatedReaction(function () { return hoveredItemSV.get(); }, function (hovered, prev) {
|
||||||
|
if (hovered !== prev) {
|
||||||
|
runOnJS(setHoveredMenuItem)(hovered);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var pressAndHoldGesture = useMemo(function () {
|
||||||
|
return Gesture.Pan()
|
||||||
|
.activateAfterLongPress(500)
|
||||||
|
.cancelsTouchesInView(false)
|
||||||
|
.averageTouches(true)
|
||||||
|
.onStart(function () {
|
||||||
|
'worklet';
|
||||||
|
runOnJS(open)('full');
|
||||||
|
})
|
||||||
|
.onUpdate(function (evt) {
|
||||||
|
'worklet';
|
||||||
|
var item = getHoveredHoverable(evt, hoverablesSV, translationSV);
|
||||||
|
hoveredItemSV.set(item);
|
||||||
|
})
|
||||||
|
.onEnd(function () {
|
||||||
|
'worklet';
|
||||||
|
// don't recalculate hovered item - if they haven't moved their finger from
|
||||||
|
// the initial press, it's jarring to then select the item underneath
|
||||||
|
// as the menu may have slid into place beneath their finger
|
||||||
|
var item = hoveredItemSV.get();
|
||||||
|
if (item) {
|
||||||
|
runOnJS(onTouchUpMenuItem)(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV]);
|
||||||
|
var composedGestures = Gesture.Exclusive(doubleTapGesture, pressAndHoldGesture);
|
||||||
|
var measurement = context.measurement || (pendingMeasurement === null || pendingMeasurement === void 0 ? void 0 : pendingMeasurement.measurement);
|
||||||
|
return (_jsxs(_Fragment, { children: [_jsx(GestureDetector, { gesture: composedGestures, children: _jsx(View, { ref: ref, style: [{ opacity: context.isOpen ? 0 : 1 }, style], children: children({
|
||||||
|
IS_NATIVE: true,
|
||||||
|
control: { isOpen: context.isOpen, open: open },
|
||||||
|
state: {
|
||||||
|
pressed: false,
|
||||||
|
hovered: false,
|
||||||
|
focused: false,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
ref: null,
|
||||||
|
onPress: null,
|
||||||
|
onFocus: null,
|
||||||
|
onBlur: null,
|
||||||
|
onPressIn: null,
|
||||||
|
onPressOut: null,
|
||||||
|
accessibilityHint: null,
|
||||||
|
accessibilityLabel: label,
|
||||||
|
accessibilityRole: null,
|
||||||
|
},
|
||||||
|
}) }) }), isFocused && image && measurement && (_jsx(Portal, { children: _jsx(TriggerClone, { label: contentLabel, translation: translationSV, animation: animationSV, image: image, measurement: measurement, returnLocation: context.returnLocationSV, onDisplay: function () {
|
||||||
|
if (pendingMeasurement) {
|
||||||
|
context.open(pendingMeasurement.measurement, pendingMeasurement.mode);
|
||||||
|
setPendingMeasurement(null);
|
||||||
|
}
|
||||||
|
} }) }))] }));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* an image of the underlying trigger with a grow animation
|
||||||
|
*/
|
||||||
|
function TriggerClone(_a) {
|
||||||
|
var translation = _a.translation, animation = _a.animation, image = _a.image, measurement = _a.measurement, returnLocation = _a.returnLocation, onDisplay = _a.onDisplay, label = _a.label;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var animatedStyles = useAnimatedStyle(function () {
|
||||||
|
var anim = animation.get();
|
||||||
|
var ret = returnLocation.get();
|
||||||
|
var returnOffsetX = ret
|
||||||
|
? interpolate(anim, [0, 1], [ret.x - measurement.x, 0])
|
||||||
|
: 0;
|
||||||
|
var returnOffsetY = ret
|
||||||
|
? interpolate(anim, [0, 1], [ret.y - measurement.y, 0])
|
||||||
|
: 0;
|
||||||
|
return {
|
||||||
|
transform: [
|
||||||
|
{ translateX: returnOffsetX },
|
||||||
|
{ translateY: translation.get() * anim + returnOffsetY },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var handleError = useCallback(function (evt) {
|
||||||
|
logger.error('Context menu image load error', { message: evt.error });
|
||||||
|
onDisplay();
|
||||||
|
}, [onDisplay]);
|
||||||
|
return (_jsx(Animated.View, { style: [
|
||||||
|
a.absolute,
|
||||||
|
{
|
||||||
|
top: measurement.y,
|
||||||
|
left: measurement.x,
|
||||||
|
width: measurement.width,
|
||||||
|
height: measurement.height,
|
||||||
|
},
|
||||||
|
a.z_10,
|
||||||
|
a.pointer_events_none,
|
||||||
|
animatedStyles,
|
||||||
|
], children: _jsx(Image, { onDisplay: onDisplay, onError: handleError, source: image, style: {
|
||||||
|
width: measurement.width,
|
||||||
|
height: measurement.height,
|
||||||
|
}, accessibilityLabel: label, accessibilityHint: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["The subject of the context menu"], ["The subject of the context menu"])))), accessibilityIgnoresInvertColors: false }) }));
|
||||||
|
}
|
||||||
|
export function AuxiliaryView(_a) {
|
||||||
|
var children = _a.children, _b = _a.align, align = _b === void 0 ? 'left' : _b;
|
||||||
|
var context = useContextMenuContext();
|
||||||
|
var screenWidth = useWindowDimensions().width;
|
||||||
|
var topInset = useSafeAreaInsets().top;
|
||||||
|
var ensureOnScreenTranslationSV = useSharedValue(0);
|
||||||
|
var isOpen = context.isOpen, mode = context.mode, measurement = context.measurement, translationSV = context.translationSV, animationSV = context.animationSV;
|
||||||
|
var animatedStyle = useAnimatedStyle(function () {
|
||||||
|
return {
|
||||||
|
opacity: clamp(animationSV.get(), 0, 1),
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateY: (ensureOnScreenTranslationSV.get() || translationSV.get()) *
|
||||||
|
animationSV.get(),
|
||||||
|
},
|
||||||
|
{ scale: interpolate(animationSV.get(), [0, 1], [0.2, 1]) },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var menuContext = useMemo(function () { return ({ align: align }); }, [align]);
|
||||||
|
var onLayout = useCallback(function () {
|
||||||
|
if (!measurement)
|
||||||
|
return;
|
||||||
|
var translation = 0;
|
||||||
|
// vibes based, just assuming it'll fit within this space. revisit if we use
|
||||||
|
// AuxiliaryView for something tall
|
||||||
|
var TOP_INSET = topInset + 80;
|
||||||
|
var distanceMessageFromTop = measurement.y - TOP_INSET;
|
||||||
|
if (distanceMessageFromTop < 0) {
|
||||||
|
translation = -distanceMessageFromTop;
|
||||||
|
}
|
||||||
|
// normally, the context menu is responsible for measuring itself and moving everything into the right place
|
||||||
|
// however, in auxiliary-only mode, that doesn't happen, so we need to do it ourselves here
|
||||||
|
if (mode === 'auxiliary-only') {
|
||||||
|
translationSV.set(translation);
|
||||||
|
ensureOnScreenTranslationSV.set(0);
|
||||||
|
}
|
||||||
|
// however, we also need to make sure that for super tall triggers, we don't go off the screen
|
||||||
|
// so we have an additional cap on the standard transform every other element has
|
||||||
|
// note: this breaks the press-and-hold gesture for the reaction items. unfortunately I think
|
||||||
|
// we'll just have to live with it for now, fixing it would be possible but be a large complexity
|
||||||
|
// increase for an edge case
|
||||||
|
else {
|
||||||
|
ensureOnScreenTranslationSV.set(translation);
|
||||||
|
}
|
||||||
|
}, [mode, measurement, translationSV, topInset, ensureOnScreenTranslationSV]);
|
||||||
|
if (!isOpen || !measurement)
|
||||||
|
return null;
|
||||||
|
return (_jsx(Portal, { children: _jsx(Context.Provider, { value: context, children: _jsx(MenuContext.Provider, { value: menuContext, children: _jsx(Animated.View, { onLayout: onLayout, style: [
|
||||||
|
a.absolute,
|
||||||
|
{
|
||||||
|
top: measurement.y,
|
||||||
|
transformOrigin: align === 'left' ? 'bottom left' : 'bottom right',
|
||||||
|
},
|
||||||
|
align === 'left'
|
||||||
|
? { left: measurement.x }
|
||||||
|
: { right: screenWidth - measurement.x - measurement.width },
|
||||||
|
animatedStyle,
|
||||||
|
a.z_20,
|
||||||
|
], children: children }) }) }) }));
|
||||||
|
}
|
||||||
|
var MENU_WIDTH = 240;
|
||||||
|
export function Outer(_a) {
|
||||||
|
var children = _a.children, style = _a.style, _b = _a.align, align = _b === void 0 ? 'left' : _b;
|
||||||
|
var t = useTheme();
|
||||||
|
var context = useContextMenuContext();
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
var frame = useSafeAreaFrame();
|
||||||
|
var screenWidth = useWindowDimensions().width;
|
||||||
|
var animationSV = context.animationSV, translationSV = context.translationSV;
|
||||||
|
var animatedContainerStyle = useAnimatedStyle(function () { return ({
|
||||||
|
transform: [{ translateY: translationSV.get() * animationSV.get() }],
|
||||||
|
}); });
|
||||||
|
var animatedStyle = useAnimatedStyle(function () { return ({
|
||||||
|
opacity: clamp(animationSV.get(), 0, 1),
|
||||||
|
transform: [{ scale: interpolate(animationSV.get(), [0, 1], [0.2, 1]) }],
|
||||||
|
}); });
|
||||||
|
var onLayout = useCallback(function (evt) {
|
||||||
|
if (!context.measurement)
|
||||||
|
return; // should not happen
|
||||||
|
var translation = 0;
|
||||||
|
// pure vibes based
|
||||||
|
var TOP_INSET = insets.top + 80;
|
||||||
|
var BOTTOM_INSET_IOS = insets.bottom + 20;
|
||||||
|
var BOTTOM_INSET_ANDROID = insets.bottom + 12;
|
||||||
|
var height = evt.nativeEvent.layout.height;
|
||||||
|
var topPosition = context.measurement.y + context.measurement.height + tokens.space.xs;
|
||||||
|
var bottomPosition = topPosition + height;
|
||||||
|
var safeAreaBottomLimit = frame.height -
|
||||||
|
platform({
|
||||||
|
ios: BOTTOM_INSET_IOS,
|
||||||
|
android: BOTTOM_INSET_ANDROID,
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
|
var diff = bottomPosition - safeAreaBottomLimit;
|
||||||
|
if (diff > 0) {
|
||||||
|
translation = -diff;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var distanceMessageFromTop = context.measurement.y - TOP_INSET;
|
||||||
|
if (distanceMessageFromTop < 0) {
|
||||||
|
translation = -Math.max(distanceMessageFromTop, diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (translation !== 0) {
|
||||||
|
translationSV.set(translation);
|
||||||
|
}
|
||||||
|
}, [context.measurement, frame.height, insets, translationSV]);
|
||||||
|
var menuContext = useMemo(function () { return ({ align: align }); }, [align]);
|
||||||
|
if (!context.isOpen || !context.measurement)
|
||||||
|
return null;
|
||||||
|
return (_jsx(Portal, { children: _jsx(Context.Provider, { value: context, children: _jsxs(MenuContext.Provider, { value: menuContext, children: [_jsx(Backdrop, { animation: animationSV, onPress: context.close }), context.mode === 'full' && (
|
||||||
|
/* containing element - stays the same size, so we measure it
|
||||||
|
to determine if a translation is necessary. also has the positioning */
|
||||||
|
_jsx(Animated.View, { onLayout: onLayout, style: [
|
||||||
|
a.absolute,
|
||||||
|
a.z_10,
|
||||||
|
a.mt_xs,
|
||||||
|
{
|
||||||
|
width: MENU_WIDTH,
|
||||||
|
top: context.measurement.y + context.measurement.height,
|
||||||
|
},
|
||||||
|
align === 'left'
|
||||||
|
? { left: context.measurement.x }
|
||||||
|
: {
|
||||||
|
right: screenWidth -
|
||||||
|
context.measurement.x -
|
||||||
|
context.measurement.width,
|
||||||
|
},
|
||||||
|
animatedContainerStyle,
|
||||||
|
], children: _jsx(Animated.View, { style: [
|
||||||
|
a.rounded_md,
|
||||||
|
a.shadow_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.w_full,
|
||||||
|
// @ts-ignore react-native-web expects string, and this file is platform-split -sfn
|
||||||
|
// note: above @ts-ignore cannot be a @ts-expect-error because this does not cause an error
|
||||||
|
// in the typecheck CI - presumably because of RNW overriding the types
|
||||||
|
{
|
||||||
|
transformOrigin:
|
||||||
|
// "top right" doesn't seem to work on android, so set explicitly in pixels
|
||||||
|
align === 'left' ? [0, 0, 0] : [MENU_WIDTH, 0, 0],
|
||||||
|
},
|
||||||
|
animatedStyle,
|
||||||
|
style,
|
||||||
|
], children: _jsx(View, { style: [
|
||||||
|
a.flex_1,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
], children: flattenReactChildren(children).map(function (child, i) {
|
||||||
|
return React.isValidElement(child) &&
|
||||||
|
(child.type === Item || child.type === Divider) ? (_jsxs(React.Fragment, { children: [i > 0 ? (_jsx(View, { style: [a.border_b, t.atoms.border_contrast_low] })) : null, React.cloneElement(child, {
|
||||||
|
// @ts-expect-error not typed
|
||||||
|
style: {
|
||||||
|
borderRadius: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
},
|
||||||
|
})] }, i)) : null;
|
||||||
|
}) }) }) }))] }) }) }));
|
||||||
|
}
|
||||||
|
export function Item(_a) {
|
||||||
|
var children = _a.children, label = _a.label, unstyled = _a.unstyled, style = _a.style, onPress = _a.onPress, position = _a.position, rest = __rest(_a, ["children", "label", "unstyled", "style", "onPress", "position"]);
|
||||||
|
var t = useTheme();
|
||||||
|
var context = useContextMenuContext();
|
||||||
|
var playHaptic = useHaptics();
|
||||||
|
var _b = useInteractionState(), focused = _b.state, onFocus = _b.onIn, onBlur = _b.onOut;
|
||||||
|
var _c = useInteractionState(), pressed = _c.state, onPressIn = _c.onIn, onPressOut = _c.onOut;
|
||||||
|
var id = useId();
|
||||||
|
var align = useContextMenuMenuContext().align;
|
||||||
|
var close = context.close, measurement = context.measurement, registerHoverable = context.registerHoverable;
|
||||||
|
var handleLayout = useCallback(function (evt) {
|
||||||
|
if (!measurement)
|
||||||
|
return; // should be impossible
|
||||||
|
var layout = evt.nativeEvent.layout;
|
||||||
|
var yOffset = position
|
||||||
|
? position.y
|
||||||
|
: measurement.y + measurement.height + tokens.space.xs;
|
||||||
|
var xOffset = position
|
||||||
|
? position.x
|
||||||
|
: align === 'left'
|
||||||
|
? measurement.x
|
||||||
|
: measurement.x + measurement.width - layout.width;
|
||||||
|
registerHoverable(id, {
|
||||||
|
width: layout.width,
|
||||||
|
height: layout.height,
|
||||||
|
y: yOffset + layout.y,
|
||||||
|
x: xOffset + layout.x,
|
||||||
|
}, function () {
|
||||||
|
close();
|
||||||
|
onPress();
|
||||||
|
});
|
||||||
|
}, [id, measurement, registerHoverable, close, onPress, align, position]);
|
||||||
|
var itemContext = useMemo(function () { return ({ disabled: Boolean(rest.disabled) }); }, [rest.disabled]);
|
||||||
|
return (_jsx(Pressable, __assign({}, rest, { onLayout: handleLayout, accessibilityHint: "", accessibilityLabel: label, onFocus: onFocus, onBlur: onBlur, onPress: function (e) {
|
||||||
|
close();
|
||||||
|
onPress === null || onPress === void 0 ? void 0 : onPress(e);
|
||||||
|
}, onPressIn: function (e) {
|
||||||
|
var _a;
|
||||||
|
onPressIn();
|
||||||
|
(_a = rest.onPressIn) === null || _a === void 0 ? void 0 : _a.call(rest, e);
|
||||||
|
playHaptic('Light');
|
||||||
|
}, onPressOut: function (e) {
|
||||||
|
var _a;
|
||||||
|
onPressOut();
|
||||||
|
(_a = rest.onPressOut) === null || _a === void 0 ? void 0 : _a.call(rest, e);
|
||||||
|
}, style: [
|
||||||
|
!unstyled && [
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.px_md,
|
||||||
|
a.rounded_md,
|
||||||
|
a.border,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{ minHeight: 44, paddingVertical: 10 },
|
||||||
|
(focused || pressed || context.hoveredMenuItem === id) &&
|
||||||
|
!rest.disabled &&
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
],
|
||||||
|
style,
|
||||||
|
], children: _jsx(ItemContext.Provider, { value: itemContext, children: typeof children === 'function'
|
||||||
|
? children((focused || pressed || context.hoveredMenuItem === id) &&
|
||||||
|
!rest.disabled)
|
||||||
|
: children }) })));
|
||||||
|
}
|
||||||
|
export function ItemText(_a) {
|
||||||
|
var children = _a.children, style = _a.style;
|
||||||
|
var t = useTheme();
|
||||||
|
var disabled = useContextMenuItemContext().disabled;
|
||||||
|
return (_jsx(Text, { numberOfLines: 2, ellipsizeMode: "middle", style: [
|
||||||
|
a.flex_1,
|
||||||
|
a.text_md,
|
||||||
|
a.font_semi_bold,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
{ paddingTop: 3 },
|
||||||
|
style,
|
||||||
|
disabled && t.atoms.text_contrast_low,
|
||||||
|
], children: children }));
|
||||||
|
}
|
||||||
|
export function ItemIcon(_a) {
|
||||||
|
var Comp = _a.icon;
|
||||||
|
var t = useTheme();
|
||||||
|
var disabled = useContextMenuItemContext().disabled;
|
||||||
|
return (_jsx(Comp, { size: "lg", fill: disabled
|
||||||
|
? t.atoms.text_contrast_low.color
|
||||||
|
: t.atoms.text_contrast_medium.color }));
|
||||||
|
}
|
||||||
|
export function ItemRadio(_a) {
|
||||||
|
var selected = _a.selected;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.rounded_full,
|
||||||
|
t.atoms.border_contrast_high,
|
||||||
|
{
|
||||||
|
borderWidth: 1,
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
], children: selected ? (_jsx(View, { style: [
|
||||||
|
a.absolute,
|
||||||
|
a.rounded_full,
|
||||||
|
{ height: 14, width: 14 },
|
||||||
|
selected ? { backgroundColor: t.palette.primary_500 } : {},
|
||||||
|
] })) : null }));
|
||||||
|
}
|
||||||
|
export function LabelText(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsx(Text, { style: [
|
||||||
|
a.font_semi_bold,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
{ marginBottom: -8 },
|
||||||
|
], children: children }));
|
||||||
|
}
|
||||||
|
export function Divider() {
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsx(View, { style: [t.atoms.border_contrast_low, a.flex_1, { borderTopWidth: 3 }] }));
|
||||||
|
}
|
||||||
|
function measureView(view, insets) {
|
||||||
|
if (!view)
|
||||||
|
return Promise.resolve(null);
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
view === null || view === void 0 ? void 0 : view.measureInWindow(function (x, y, width, height) {
|
||||||
|
return resolve({
|
||||||
|
x: x,
|
||||||
|
y: y +
|
||||||
|
platform({
|
||||||
|
default: 0,
|
||||||
|
android: insets.top, // not included in measurement
|
||||||
|
}),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getHoveredHoverable(evt, hoverables, translation) {
|
||||||
|
'worklet';
|
||||||
|
var x = evt.absoluteX;
|
||||||
|
var y = evt.absoluteY;
|
||||||
|
var yOffset = translation.get();
|
||||||
|
var rects = Object.values(hoverables.get());
|
||||||
|
for (var _i = 0, rects_1 = rects; _i < rects_1.length; _i++) {
|
||||||
|
var _a = rects_1[_i], id = _a.id, rect = _a.rect;
|
||||||
|
var isWithinLeftBound = x >= rect.x;
|
||||||
|
var isWithinRightBound = x <= rect.x + rect.width;
|
||||||
|
var isWithinTopBound = y >= rect.y + yOffset;
|
||||||
|
var isWithinBottomBound = y <= rect.y + rect.height + yOffset;
|
||||||
|
if (isWithinLeftBound &&
|
||||||
|
isWithinRightBound &&
|
||||||
|
isWithinTopBound &&
|
||||||
|
isWithinBottomBound) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var templateObject_1;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -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) })] }) })] }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { createContext, useContext, useEffect, useId, useMemo, useRef, } from 'react';
|
||||||
|
import { useDialogStateContext } from '#/state/dialogs';
|
||||||
|
import { IS_DEV } from '#/env';
|
||||||
|
import { BottomSheetSnapPoint } from '../../../modules/bottom-sheet/src/BottomSheet.types';
|
||||||
|
export var Context = createContext({
|
||||||
|
close: function () { },
|
||||||
|
isNativeDialog: false,
|
||||||
|
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
|
||||||
|
disableDrag: false,
|
||||||
|
setDisableDrag: function () { },
|
||||||
|
isWithinDialog: false,
|
||||||
|
});
|
||||||
|
Context.displayName = 'DialogContext';
|
||||||
|
export function useDialogContext() {
|
||||||
|
return useContext(Context);
|
||||||
|
}
|
||||||
|
export function useDialogControl() {
|
||||||
|
var id = useId();
|
||||||
|
var control = useRef({
|
||||||
|
open: function () { },
|
||||||
|
close: function () { },
|
||||||
|
});
|
||||||
|
var activeDialogs = useDialogStateContext().activeDialogs;
|
||||||
|
useEffect(function () {
|
||||||
|
activeDialogs.current.set(id, control);
|
||||||
|
return function () {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
activeDialogs.current.delete(id);
|
||||||
|
};
|
||||||
|
}, [id, activeDialogs]);
|
||||||
|
return useMemo(function () { return ({
|
||||||
|
id: id,
|
||||||
|
ref: control,
|
||||||
|
open: function () {
|
||||||
|
if (control.current) {
|
||||||
|
control.current.open();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (IS_DEV) {
|
||||||
|
console.warn('Attemped to open a dialog control that was not attached to a dialog!\n' +
|
||||||
|
'Please ensure that the Dialog is mounted when calling open/close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: function (cb) {
|
||||||
|
if (control.current) {
|
||||||
|
control.current.close(cb);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (IS_DEV) {
|
||||||
|
console.warn('Attemped to close a dialog control that was not attached to a dialog!\n' +
|
||||||
|
'Please ensure that the Dialog is mounted when calling open/close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}); }, [id, control]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __rest = (this && this.__rest) || function (s, e) {
|
||||||
|
var t = {};
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||||
|
t[p] = s[p];
|
||||||
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||||
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||||
|
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||||
|
t[p[i]] = s[p[i]];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import React, { useImperativeHandle } from 'react';
|
||||||
|
import { Pressable, ScrollView, TextInput, View, } from 'react-native';
|
||||||
|
import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller';
|
||||||
|
import Animated, { runOnJS, useAnimatedStyle, } from 'react-native-reanimated';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { ScrollProvider } from '#/lib/ScrollContext';
|
||||||
|
import { logger } from '#/logger';
|
||||||
|
import { useA11y } from '#/state/a11y';
|
||||||
|
import { useDialogStateControlContext } from '#/state/dialogs';
|
||||||
|
import { List } from '#/view/com/util/List';
|
||||||
|
import { android, atoms as a, ios, platform, tokens, useTheme } from '#/alf';
|
||||||
|
import { useThemeName } from '#/alf/util/useColorModeTheme';
|
||||||
|
import { Context, useDialogContext } from '#/components/Dialog/context';
|
||||||
|
import { createInput } from '#/components/forms/TextField';
|
||||||
|
import { IS_ANDROID, IS_IOS, IS_LIQUID_GLASS } from '#/env';
|
||||||
|
import { BottomSheet, BottomSheetSnapPoint } from '../../../modules/bottom-sheet';
|
||||||
|
export { useDialogContext, useDialogControl } from '#/components/Dialog/context';
|
||||||
|
export * from '#/components/Dialog/shared';
|
||||||
|
export * from '#/components/Dialog/types';
|
||||||
|
export * from '#/components/Dialog/utils';
|
||||||
|
export var Input = createInput(TextInput);
|
||||||
|
export function Outer(_a) {
|
||||||
|
var children = _a.children, control = _a.control, onClose = _a.onClose, nativeOptions = _a.nativeOptions, testID = _a.testID;
|
||||||
|
var themeName = useThemeName();
|
||||||
|
var t = useTheme(themeName);
|
||||||
|
var ref = React.useRef(null);
|
||||||
|
var closeCallbacks = React.useRef([]);
|
||||||
|
var _b = useDialogStateControlContext(), setDialogIsOpen = _b.setDialogIsOpen, setFullyExpandedCount = _b.setFullyExpandedCount;
|
||||||
|
var prevSnapPoint = React.useRef(BottomSheetSnapPoint.Hidden);
|
||||||
|
var _c = React.useState(false), disableDrag = _c[0], setDisableDrag = _c[1];
|
||||||
|
var _d = React.useState(BottomSheetSnapPoint.Partial), snapPoint = _d[0], setSnapPoint = _d[1];
|
||||||
|
var callQueuedCallbacks = React.useCallback(function () {
|
||||||
|
for (var _i = 0, _a = closeCallbacks.current; _i < _a.length; _i++) {
|
||||||
|
var cb = _a[_i];
|
||||||
|
try {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logger.error(e || 'Error running close callback');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeCallbacks.current = [];
|
||||||
|
}, []);
|
||||||
|
var open = React.useCallback(function () {
|
||||||
|
var _a;
|
||||||
|
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||||
|
callQueuedCallbacks();
|
||||||
|
setDialogIsOpen(control.id, true);
|
||||||
|
(_a = ref.current) === null || _a === void 0 ? void 0 : _a.present();
|
||||||
|
}, [setDialogIsOpen, control.id, callQueuedCallbacks]);
|
||||||
|
// This is the function that we call when we want to dismiss the dialog.
|
||||||
|
var close = React.useCallback(function (cb) {
|
||||||
|
var _a;
|
||||||
|
if (typeof cb === 'function') {
|
||||||
|
closeCallbacks.current.push(cb);
|
||||||
|
}
|
||||||
|
(_a = ref.current) === null || _a === void 0 ? void 0 : _a.dismiss();
|
||||||
|
}, []);
|
||||||
|
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
|
||||||
|
// happen before we run this. It is passed to the `BottomSheet` component.
|
||||||
|
var onCloseAnimationComplete = React.useCallback(function () {
|
||||||
|
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
|
||||||
|
// tells us that we need to toggle the accessibility overlay setting
|
||||||
|
setDialogIsOpen(control.id, false);
|
||||||
|
callQueuedCallbacks();
|
||||||
|
onClose === null || onClose === void 0 ? void 0 : onClose();
|
||||||
|
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]);
|
||||||
|
var onSnapPointChange = function (e) {
|
||||||
|
var snapPoint = e.nativeEvent.snapPoint;
|
||||||
|
setSnapPoint(snapPoint);
|
||||||
|
if (snapPoint === BottomSheetSnapPoint.Full &&
|
||||||
|
prevSnapPoint.current !== BottomSheetSnapPoint.Full) {
|
||||||
|
setFullyExpandedCount(function (c) { return c + 1; });
|
||||||
|
}
|
||||||
|
else if (snapPoint !== BottomSheetSnapPoint.Full &&
|
||||||
|
prevSnapPoint.current === BottomSheetSnapPoint.Full) {
|
||||||
|
setFullyExpandedCount(function (c) { return c - 1; });
|
||||||
|
}
|
||||||
|
prevSnapPoint.current = snapPoint;
|
||||||
|
};
|
||||||
|
var onStateChange = function (e) {
|
||||||
|
if (e.nativeEvent.state === 'closed') {
|
||||||
|
onCloseAnimationComplete();
|
||||||
|
if (prevSnapPoint.current === BottomSheetSnapPoint.Full) {
|
||||||
|
setFullyExpandedCount(function (c) { return c - 1; });
|
||||||
|
}
|
||||||
|
prevSnapPoint.current = BottomSheetSnapPoint.Hidden;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useImperativeHandle(control.ref, function () { return ({
|
||||||
|
open: open,
|
||||||
|
close: close,
|
||||||
|
}); }, [open, close]);
|
||||||
|
var context = React.useMemo(function () { return ({
|
||||||
|
close: close,
|
||||||
|
isNativeDialog: true,
|
||||||
|
nativeSnapPoint: snapPoint,
|
||||||
|
disableDrag: disableDrag,
|
||||||
|
setDisableDrag: setDisableDrag,
|
||||||
|
isWithinDialog: true,
|
||||||
|
}); }, [close, snapPoint, disableDrag, setDisableDrag]);
|
||||||
|
return (_jsx(BottomSheet, __assign({ ref: ref,
|
||||||
|
// device-bezel radius when undefined
|
||||||
|
cornerRadius: IS_LIQUID_GLASS ? undefined : 20, backgroundColor: t.atoms.bg.backgroundColor }, nativeOptions, { onSnapPointChange: onSnapPointChange, onStateChange: onStateChange, disableDrag: disableDrag, children: _jsx(Context.Provider, { value: context, children: _jsx(View, { testID: testID, style: [a.relative], children: children }) }) })));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @deprecated use `Dialog.ScrollableInner` instead
|
||||||
|
*/
|
||||||
|
export function Inner(_a) {
|
||||||
|
var children = _a.children, style = _a.style, header = _a.header;
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
return (_jsxs(_Fragment, { children: [header, _jsx(View, { style: [
|
||||||
|
a.pt_2xl,
|
||||||
|
a.px_xl,
|
||||||
|
IS_LIQUID_GLASS
|
||||||
|
? a.pb_2xl
|
||||||
|
: { paddingBottom: insets.bottom + insets.top },
|
||||||
|
style,
|
||||||
|
], children: children })] }));
|
||||||
|
}
|
||||||
|
export var ScrollableInner = React.forwardRef(function ScrollableInner(_a, ref) {
|
||||||
|
var children = _a.children, contentContainerStyle = _a.contentContainerStyle, header = _a.header, props = __rest(_a, ["children", "contentContainerStyle", "header"]);
|
||||||
|
var _b = useDialogContext(), nativeSnapPoint = _b.nativeSnapPoint, disableDrag = _b.disableDrag, setDisableDrag = _b.setDisableDrag;
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
var isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full;
|
||||||
|
var paddingBottom = 0;
|
||||||
|
if (IS_IOS) {
|
||||||
|
paddingBottom = tokens.space._2xl;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
paddingBottom =
|
||||||
|
Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl;
|
||||||
|
if (isAtMaxSnapPoint) {
|
||||||
|
paddingBottom += insets.top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var onScroll = function (e) {
|
||||||
|
if (!IS_ANDROID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var contentOffset = e.nativeEvent.contentOffset;
|
||||||
|
if (contentOffset.y > 0 && !disableDrag) {
|
||||||
|
setDisableDrag(true);
|
||||||
|
}
|
||||||
|
else if (contentOffset.y <= 1 && disableDrag) {
|
||||||
|
setDisableDrag(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (_jsxs(ScrollView, __assign({ contentContainerStyle: [
|
||||||
|
a.pt_2xl,
|
||||||
|
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
|
||||||
|
{ paddingBottom: paddingBottom },
|
||||||
|
contentContainerStyle,
|
||||||
|
], ref: ref, showsVerticalScrollIndicator: IS_ANDROID ? false : undefined, contentInsetAdjustmentBehavior: isAtMaxSnapPoint ? 'automatic' : 'never', automaticallyAdjustKeyboardInsets: isAtMaxSnapPoint }, props, { bounces: isAtMaxSnapPoint, scrollEventThrottle: 50, onScroll: IS_ANDROID ? onScroll : undefined, keyboardShouldPersistTaps: "handled",
|
||||||
|
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
||||||
|
// on Android. fine to disable for now, because we don't have any
|
||||||
|
// dialogs that use this that actually scroll -sfn
|
||||||
|
stickyHeaderIndices: ios(header ? [0] : undefined), children: [header, children] })));
|
||||||
|
});
|
||||||
|
export var InnerFlatList = React.forwardRef(function InnerFlatList(_a, ref) {
|
||||||
|
var headerOffset = _a.headerOffset, footer = _a.footer, style = _a.style, contentContainerStyle = _a.contentContainerStyle, props = __rest(_a, ["headerOffset", "footer", "style", "contentContainerStyle"]);
|
||||||
|
var insets = useSafeAreaInsets();
|
||||||
|
var _b = useDialogContext(), nativeSnapPoint = _b.nativeSnapPoint, disableDrag = _b.disableDrag, setDisableDrag = _b.setDisableDrag;
|
||||||
|
var isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full;
|
||||||
|
var onScroll = function (e) {
|
||||||
|
'worklet';
|
||||||
|
if (!IS_ANDROID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var contentOffset = e.contentOffset;
|
||||||
|
if (contentOffset.y > 0 && !disableDrag) {
|
||||||
|
runOnJS(setDisableDrag)(true);
|
||||||
|
}
|
||||||
|
else if (contentOffset.y <= 1 && disableDrag) {
|
||||||
|
runOnJS(setDisableDrag)(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (_jsxs(ScrollProvider, { onScroll: onScroll, children: [_jsx(List, __assign({ keyboardShouldPersistTaps: "handled", contentInsetAdjustmentBehavior: isAtMaxSnapPoint ? 'automatic' : 'never', automaticallyAdjustKeyboardInsets: isAtMaxSnapPoint, scrollIndicatorInsets: { top: headerOffset }, bounces: isAtMaxSnapPoint, ref: ref, showsVerticalScrollIndicator: IS_ANDROID ? false : undefined }, props, { style: [a.h_full, style], contentContainerStyle: [
|
||||||
|
{ paddingTop: headerOffset },
|
||||||
|
android({
|
||||||
|
paddingBottom: insets.top + insets.bottom + tokens.space.xl,
|
||||||
|
}),
|
||||||
|
contentContainerStyle,
|
||||||
|
] })), footer] }));
|
||||||
|
});
|
||||||
|
export function FlatListFooter(_a) {
|
||||||
|
var children = _a.children, onLayout = _a.onLayout;
|
||||||
|
var t = useTheme();
|
||||||
|
var _b = useSafeAreaInsets(), top = _b.top, bottom = _b.bottom;
|
||||||
|
var height = useReanimatedKeyboardAnimation().height;
|
||||||
|
var animatedStyle = useAnimatedStyle(function () {
|
||||||
|
if (!IS_IOS)
|
||||||
|
return {};
|
||||||
|
return {
|
||||||
|
transform: [{ translateY: Math.min(0, height.get() + bottom - 10) }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (_jsx(Animated.View, { onLayout: onLayout, style: [
|
||||||
|
a.absolute,
|
||||||
|
a.bottom_0,
|
||||||
|
a.w_full,
|
||||||
|
a.z_10,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.bg,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.px_lg,
|
||||||
|
a.pt_md,
|
||||||
|
{
|
||||||
|
paddingBottom: platform({
|
||||||
|
ios: tokens.space.md + bottom + (IS_LIQUID_GLASS ? top : 0),
|
||||||
|
android: tokens.space.md + bottom + top,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// TODO: had to admit defeat here, but we should
|
||||||
|
// try and get this to work for Android as well -sfn
|
||||||
|
ios(animatedStyle),
|
||||||
|
], children: children }));
|
||||||
|
}
|
||||||
|
export function Handle(_a) {
|
||||||
|
var _b = _a.difference, difference = _b === void 0 ? false : _b, fill = _a.fill;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var screenReaderEnabled = useA11y().screenReaderEnabled;
|
||||||
|
var close = useDialogContext().close;
|
||||||
|
return (_jsx(View, { style: [a.absolute, a.w_full, a.align_center, a.z_10, { height: 20 }], children: _jsx(Pressable, { accessible: screenReaderEnabled, onPress: function () { return close(); }, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Dismiss"], ["Dismiss"])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Double tap to close the dialog"], ["Double tap to close the dialog"])))), children: _jsx(View, { style: [
|
||||||
|
a.rounded_sm,
|
||||||
|
{
|
||||||
|
top: tokens.space._2xl / 2 - 2.5,
|
||||||
|
width: 35,
|
||||||
|
height: 5,
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
difference
|
||||||
|
? {
|
||||||
|
// TODO: mixBlendMode is only available on the new architecture -sfn
|
||||||
|
// backgroundColor: t.palette.white,
|
||||||
|
// mixBlendMode: 'difference',
|
||||||
|
backgroundColor: t.palette.white,
|
||||||
|
opacity: 0.75,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
backgroundColor: fill || t.palette.contrast_975,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
] }) }) }));
|
||||||
|
}
|
||||||
|
export function Close() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export function Backdrop() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2;
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var __rest = (this && this.__rest) || function (s, e) {
|
||||||
|
var t = {};
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||||
|
t[p] = s[p];
|
||||||
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||||
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||||
|
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||||
|
t[p[i]] = s[p[i]];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import React, { useImperativeHandle } from 'react';
|
||||||
|
import { FlatList, Pressable, View, } from 'react-native';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { DismissableLayer, FocusGuards, FocusScope } from 'radix-ui/internal';
|
||||||
|
import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
||||||
|
import { logger } from '#/logger';
|
||||||
|
import { useA11y } from '#/state/a11y';
|
||||||
|
import { useDialogStateControlContext } from '#/state/dialogs';
|
||||||
|
import { atoms as a, flatten, useBreakpoints, useTheme, web } from '#/alf';
|
||||||
|
import { Button, ButtonIcon } from '#/components/Button';
|
||||||
|
import { Context } from '#/components/Dialog/context';
|
||||||
|
import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times';
|
||||||
|
import { Portal } from '#/components/Portal';
|
||||||
|
export { useDialogContext, useDialogControl } from '#/components/Dialog/context';
|
||||||
|
export * from '#/components/Dialog/shared';
|
||||||
|
export * from '#/components/Dialog/types';
|
||||||
|
export * from '#/components/Dialog/utils';
|
||||||
|
export { Input } from '#/components/forms/TextField';
|
||||||
|
// 100 minus 10vh of paddingVertical
|
||||||
|
export var WEB_DIALOG_HEIGHT = '80vh';
|
||||||
|
var stopPropagation = function (e) { return e.stopPropagation(); };
|
||||||
|
var preventDefault = function (e) { return e.preventDefault(); };
|
||||||
|
export function Outer(_a) {
|
||||||
|
var _this = this;
|
||||||
|
var children = _a.children, control = _a.control, onClose = _a.onClose, webOptions = _a.webOptions;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var _b = React.useState(false), isOpen = _b[0], setIsOpen = _b[1];
|
||||||
|
var setDialogIsOpen = useDialogStateControlContext().setDialogIsOpen;
|
||||||
|
var open = React.useCallback(function () {
|
||||||
|
setDialogIsOpen(control.id, true);
|
||||||
|
setIsOpen(true);
|
||||||
|
}, [setIsOpen, setDialogIsOpen, control.id]);
|
||||||
|
var close = React.useCallback(function (cb) {
|
||||||
|
setDialogIsOpen(control.id, false);
|
||||||
|
setIsOpen(false);
|
||||||
|
try {
|
||||||
|
if (cb && typeof cb === 'function') {
|
||||||
|
// This timeout ensures that the callback runs at the same time as it would on native. I.e.
|
||||||
|
// console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2')
|
||||||
|
// This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output
|
||||||
|
// 'Step 1', 'Step 3', 'Step 2'.
|
||||||
|
setTimeout(cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logger.error("Dialog closeCallback failed", {
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose === null || onClose === void 0 ? void 0 : onClose();
|
||||||
|
}, [control.id, onClose, setDialogIsOpen]);
|
||||||
|
var handleBackgroundPress = React.useCallback(function (e) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
(webOptions === null || webOptions === void 0 ? void 0 : webOptions.onBackgroundPress) ? webOptions.onBackgroundPress(e) : close();
|
||||||
|
return [2 /*return*/];
|
||||||
|
});
|
||||||
|
}); }, [webOptions, close]);
|
||||||
|
useImperativeHandle(control.ref, function () { return ({
|
||||||
|
open: open,
|
||||||
|
close: close,
|
||||||
|
}); }, [close, open]);
|
||||||
|
var context = React.useMemo(function () { return ({
|
||||||
|
close: close,
|
||||||
|
isNativeDialog: false,
|
||||||
|
nativeSnapPoint: 0,
|
||||||
|
disableDrag: false,
|
||||||
|
setDisableDrag: function () { },
|
||||||
|
isWithinDialog: true,
|
||||||
|
}); }, [close]);
|
||||||
|
return (_jsx(_Fragment, { children: isOpen && (_jsx(Portal, { children: _jsxs(Context.Provider, { value: context, children: [_jsx(RemoveScrollBar, {}), _jsx(Pressable, { accessibilityHint: undefined, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Close active dialog"], ["Close active dialog"])))), onPress: handleBackgroundPress, children: _jsxs(View, { style: [
|
||||||
|
web(a.fixed),
|
||||||
|
a.inset_0,
|
||||||
|
a.z_10,
|
||||||
|
a.px_xl,
|
||||||
|
(webOptions === null || webOptions === void 0 ? void 0 : webOptions.alignCenter) ? a.justify_center : undefined,
|
||||||
|
a.align_center,
|
||||||
|
{
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop,
|
||||||
|
},
|
||||||
|
], children: [_jsx(Backdrop, {}), _jsx(View, { style: [
|
||||||
|
a.w_full,
|
||||||
|
a.z_20,
|
||||||
|
a.align_center,
|
||||||
|
web({ minHeight: '60vh', position: 'static' }),
|
||||||
|
], children: children })] }) })] }) })) }));
|
||||||
|
}
|
||||||
|
export function Inner(_a) {
|
||||||
|
var children = _a.children, style = _a.style, label = _a.label, accessibilityLabelledBy = _a.accessibilityLabelledBy, accessibilityDescribedBy = _a.accessibilityDescribedBy, header = _a.header, contentContainerStyle = _a.contentContainerStyle;
|
||||||
|
var t = useTheme();
|
||||||
|
var close = React.useContext(Context).close;
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var reduceMotionEnabled = useA11y().reduceMotionEnabled;
|
||||||
|
FocusGuards.useFocusGuards();
|
||||||
|
return (_jsx(FocusScope.FocusScope, { loop: true, asChild: true, trapped: true, children: _jsx(View, { role: "dialog", "aria-role": "dialog", "aria-label": label, "aria-labelledby": accessibilityLabelledBy, "aria-describedby": accessibilityDescribedBy,
|
||||||
|
// @ts-expect-error web only -prf
|
||||||
|
onClick: stopPropagation, onStartShouldSetResponder: function (_) { return true; }, onTouchEnd: stopPropagation,
|
||||||
|
// note: flatten is required for some reason -sfn
|
||||||
|
style: flatten([
|
||||||
|
a.relative,
|
||||||
|
a.rounded_md,
|
||||||
|
a.w_full,
|
||||||
|
a.border,
|
||||||
|
t.atoms.bg,
|
||||||
|
{
|
||||||
|
maxWidth: 600,
|
||||||
|
borderColor: t.palette.contrast_200,
|
||||||
|
shadowColor: t.palette.black,
|
||||||
|
shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
|
||||||
|
shadowRadius: 30,
|
||||||
|
},
|
||||||
|
!reduceMotionEnabled && a.zoom_fade_in,
|
||||||
|
style,
|
||||||
|
]), children: _jsxs(DismissableLayer.DismissableLayer, { onInteractOutside: preventDefault, onFocusOutside: preventDefault, onDismiss: close, style: { height: '100%', display: 'flex', flexDirection: 'column' }, children: [header, _jsx(View, { style: [gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle], children: children })] }) }) }));
|
||||||
|
}
|
||||||
|
export var ScrollableInner = Inner;
|
||||||
|
export var InnerFlatList = React.forwardRef(function InnerFlatList(_a, ref) {
|
||||||
|
var label = _a.label, style = _a.style, webInnerStyle = _a.webInnerStyle, webInnerContentContainerStyle = _a.webInnerContentContainerStyle, footer = _a.footer, props = __rest(_a, ["label", "style", "webInnerStyle", "webInnerContentContainerStyle", "footer"]);
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
return (_jsxs(Inner, { label: label, style: [
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.px_0,
|
||||||
|
web({ maxHeight: WEB_DIALOG_HEIGHT }),
|
||||||
|
webInnerStyle,
|
||||||
|
], contentContainerStyle: [a.h_full, a.px_0, webInnerContentContainerStyle], children: [_jsx(FlatList, __assign({ ref: ref, style: [a.h_full, gtMobile ? a.px_2xl : a.px_xl, style] }, props)), footer] }));
|
||||||
|
});
|
||||||
|
export function FlatListFooter(_a) {
|
||||||
|
var children = _a.children, onLayout = _a.onLayout;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsx(View, { onLayout: onLayout, style: [
|
||||||
|
a.absolute,
|
||||||
|
a.bottom_0,
|
||||||
|
a.w_full,
|
||||||
|
a.z_10,
|
||||||
|
t.atoms.bg,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_md,
|
||||||
|
], children: children }));
|
||||||
|
}
|
||||||
|
export function Close() {
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var close = React.useContext(Context).close;
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
a.absolute,
|
||||||
|
a.z_10,
|
||||||
|
{
|
||||||
|
top: a.pt_md.paddingTop,
|
||||||
|
right: a.pr_md.paddingRight,
|
||||||
|
},
|
||||||
|
], children: _jsx(Button, { size: "small", variant: "ghost", color: "secondary", shape: "round", onPress: function () { return close(); }, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Close active dialog"], ["Close active dialog"])))), children: _jsx(ButtonIcon, { icon: X, size: "md" }) }) }));
|
||||||
|
}
|
||||||
|
export function Handle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export function Backdrop() {
|
||||||
|
var t = useTheme();
|
||||||
|
var reduceMotionEnabled = useA11y().reduceMotionEnabled;
|
||||||
|
return (_jsx(View, { style: { opacity: 0.8 }, children: _jsx(View, { style: [
|
||||||
|
a.fixed,
|
||||||
|
a.inset_0,
|
||||||
|
{ backgroundColor: t.palette.black },
|
||||||
|
!reduceMotionEnabled && a.fade_in,
|
||||||
|
] }) }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { View, } from 'react-native';
|
||||||
|
import { atoms as a, useTheme } from '#/alf';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { IS_LIQUID_GLASS } from '#/env';
|
||||||
|
export function Header(_a) {
|
||||||
|
var renderLeft = _a.renderLeft, renderRight = _a.renderRight, children = _a.children, style = _a.style, onLayout = _a.onLayout;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(View, { onLayout: onLayout, style: [
|
||||||
|
a.sticky,
|
||||||
|
a.top_0,
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
a.py_sm,
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
{ minHeight: IS_LIQUID_GLASS ? 64 : 50 },
|
||||||
|
a.border_b,
|
||||||
|
t.atoms.border_contrast_medium,
|
||||||
|
t.atoms.bg,
|
||||||
|
{ borderTopLeftRadius: a.rounded_md.borderRadius },
|
||||||
|
{ borderTopRightRadius: a.rounded_md.borderRadius },
|
||||||
|
style,
|
||||||
|
], children: [renderLeft && (_jsx(View, { style: [a.absolute, { left: IS_LIQUID_GLASS ? 12 : 6 }], children: renderLeft() })), children, renderRight && (_jsx(View, { style: [a.absolute, { right: IS_LIQUID_GLASS ? 12 : 6 }], children: renderRight() }))] }));
|
||||||
|
}
|
||||||
|
export function HeaderText(_a) {
|
||||||
|
var children = _a.children, style = _a.style;
|
||||||
|
return (_jsx(Text, { style: [a.text_lg, a.text_center, a.font_semi_bold, style], children: children }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { SystemBars } from 'react-native-edge-to-edge';
|
||||||
|
import { IS_IOS, IS_LIQUID_GLASS } from '#/env';
|
||||||
|
/**
|
||||||
|
* If we're calling a system API like the image picker that opens a sheet
|
||||||
|
* wrap it in this function to make sure the status bar is the correct color.
|
||||||
|
*/
|
||||||
|
export function useSheetWrapper() {
|
||||||
|
var _this = this;
|
||||||
|
return useCallback(function (promise) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var entry, res;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
if (!(IS_IOS && !IS_LIQUID_GLASS)) return [3 /*break*/, 2];
|
||||||
|
entry = SystemBars.pushStackEntry({
|
||||||
|
style: {
|
||||||
|
statusBar: 'light',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return [4 /*yield*/, promise];
|
||||||
|
case 1:
|
||||||
|
res = _a.sent();
|
||||||
|
SystemBars.popStackEntry(entry);
|
||||||
|
return [2 /*return*/, res];
|
||||||
|
case 2: return [4 /*yield*/, promise];
|
||||||
|
case 3: return [2 /*return*/, _a.sent()];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); }, []);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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] }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
||||||
|
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
||||||
|
if (ar || !(i in from)) {
|
||||||
|
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
||||||
|
ar[i] = from[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to.concat(ar || Array.prototype.slice.call(from));
|
||||||
|
};
|
||||||
|
import { jsx as _jsx } from "react/jsx-runtime";
|
||||||
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||||
|
import Animated, { measure, runOnJS, scrollTo, useAnimatedRef, useAnimatedStyle, useFrameCallback, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated';
|
||||||
|
import { useHaptics } from '#/lib/haptics';
|
||||||
|
import { atoms as a, useTheme, web } from '#/alf';
|
||||||
|
import { DotGrid2x3_Stroke2_Corner0_Rounded as GripIcon } from '#/components/icons/DotGrid';
|
||||||
|
import { IS_IOS } from '#/env';
|
||||||
|
var AUTO_SCROLL_THRESHOLD = 50;
|
||||||
|
var AUTO_SCROLL_SPEED = 4;
|
||||||
|
export function SortableList(_a) {
|
||||||
|
var data = _a.data, keyExtractor = _a.keyExtractor, renderItem = _a.renderItem, onReorder = _a.onReorder, onDragStart = _a.onDragStart, onDragEnd = _a.onDragEnd, itemHeight = _a.itemHeight, scrollRef = _a.scrollRef, scrollOffset = _a.scrollOffset;
|
||||||
|
var t = useTheme();
|
||||||
|
var state = useSharedValue({
|
||||||
|
slots: Object.fromEntries(data.map(function (item, i) { return [keyExtractor(item), i]; })),
|
||||||
|
activeKey: '',
|
||||||
|
dragStartSlot: -1,
|
||||||
|
});
|
||||||
|
var dragY = useSharedValue(0);
|
||||||
|
// Auto-scroll shared values
|
||||||
|
var scrollCompensation = useSharedValue(0);
|
||||||
|
var isGestureActive = useSharedValue(false);
|
||||||
|
// We track scroll position ourselves because scrollOffset.get() lags
|
||||||
|
// by one frame after scrollTo(), causing a feedback loop where the
|
||||||
|
// frame callback keeps thinking the item is at the edge.
|
||||||
|
var trackedScrollY = useSharedValue(0);
|
||||||
|
// For measuring list position within scroll content
|
||||||
|
var listRef = useAnimatedRef();
|
||||||
|
var listContentOffset = useSharedValue(0);
|
||||||
|
var viewportHeight = useSharedValue(0);
|
||||||
|
var measureDone = useSharedValue(false);
|
||||||
|
// Sync slots when data changes externally (e.g. pin/unpin).
|
||||||
|
// Skip after our own reorder — the worklet already set correct slots
|
||||||
|
// on the UI thread, and a redundant JS-side set() would be wasteful.
|
||||||
|
var skipNextSync = useRef(false);
|
||||||
|
var currentKeys = data.map(function (item) { return keyExtractor(item); }).join(',');
|
||||||
|
useLayoutEffect(function () {
|
||||||
|
if (skipNextSync.current) {
|
||||||
|
skipNextSync.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var nextSlots = {};
|
||||||
|
data.forEach(function (item, i) {
|
||||||
|
nextSlots[keyExtractor(item)] = i;
|
||||||
|
});
|
||||||
|
state.set({ slots: nextSlots, activeKey: '', dragStartSlot: -1 });
|
||||||
|
dragY.set(0);
|
||||||
|
}, [currentKeys, data, keyExtractor, state, dragY]);
|
||||||
|
var handleReorder = function (sortedKeys) {
|
||||||
|
skipNextSync.current = true;
|
||||||
|
var byKey = new Map(data.map(function (item) { return [keyExtractor(item), item]; }));
|
||||||
|
onReorder(sortedKeys.map(function (key) { return byKey.get(key); }));
|
||||||
|
onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd();
|
||||||
|
};
|
||||||
|
// Auto-scroll: runs every frame while a gesture is active.
|
||||||
|
useFrameCallback(function () {
|
||||||
|
if (!isGestureActive.get())
|
||||||
|
return;
|
||||||
|
if (!scrollRef || !scrollOffset)
|
||||||
|
return;
|
||||||
|
var s = state.get();
|
||||||
|
if (s.activeKey === '')
|
||||||
|
return;
|
||||||
|
// Measure list and scroll view on first frame of drag.
|
||||||
|
// Use scrollOffset here (only once) since no lag has occurred yet.
|
||||||
|
if (!measureDone.get()) {
|
||||||
|
var scrollM = measure(scrollRef);
|
||||||
|
var listM = measure(listRef);
|
||||||
|
if (!scrollM || !listM)
|
||||||
|
return;
|
||||||
|
trackedScrollY.set(scrollOffset.get());
|
||||||
|
listContentOffset.set(listM.pageY - scrollM.pageY + trackedScrollY.get());
|
||||||
|
viewportHeight.set(scrollM.height);
|
||||||
|
measureDone.set(true);
|
||||||
|
}
|
||||||
|
var startSlot = s.dragStartSlot;
|
||||||
|
var currentDragY = dragY.get();
|
||||||
|
// Use trackedScrollY (not scrollOffset) to avoid the one-frame lag
|
||||||
|
// after scrollTo() that causes a feedback loop.
|
||||||
|
var scrollY = trackedScrollY.get();
|
||||||
|
// Item position relative to scroll viewport top.
|
||||||
|
var itemContentY = listContentOffset.get() + startSlot * itemHeight + currentDragY;
|
||||||
|
var itemViewportY = itemContentY - scrollY;
|
||||||
|
var itemBottomViewportY = itemViewportY + itemHeight;
|
||||||
|
var scrollDelta = 0;
|
||||||
|
if (itemViewportY < AUTO_SCROLL_THRESHOLD) {
|
||||||
|
scrollDelta = -AUTO_SCROLL_SPEED;
|
||||||
|
}
|
||||||
|
else if (itemBottomViewportY >
|
||||||
|
viewportHeight.get() - AUTO_SCROLL_THRESHOLD) {
|
||||||
|
scrollDelta = AUTO_SCROLL_SPEED;
|
||||||
|
}
|
||||||
|
if (scrollDelta === 0)
|
||||||
|
return;
|
||||||
|
// Don't scroll if the item is already at a list boundary.
|
||||||
|
var effectiveSlotPos = (startSlot * itemHeight + currentDragY) / itemHeight;
|
||||||
|
if (scrollDelta < 0 && effectiveSlotPos <= 0)
|
||||||
|
return;
|
||||||
|
if (scrollDelta > 0 && effectiveSlotPos >= data.length - 1)
|
||||||
|
return;
|
||||||
|
// Don't scroll past the top.
|
||||||
|
if (scrollDelta < 0 && scrollY <= 0)
|
||||||
|
return;
|
||||||
|
var newScrollY = Math.max(0, scrollY + scrollDelta);
|
||||||
|
scrollTo(scrollRef, 0, newScrollY, false);
|
||||||
|
trackedScrollY.set(newScrollY);
|
||||||
|
scrollCompensation.set(scrollCompensation.get() + (newScrollY - scrollY));
|
||||||
|
});
|
||||||
|
// Render in stable key order so React never reorders native views.
|
||||||
|
// On Android, native ViewGroup child reordering causes a visual flash.
|
||||||
|
var sortedData = __spreadArray([], data, true).sort(function (a, b) {
|
||||||
|
var ka = keyExtractor(a);
|
||||||
|
var kb = keyExtractor(b);
|
||||||
|
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
||||||
|
});
|
||||||
|
return (_jsx(Animated.View, { ref: listRef, style: [{ height: data.length * itemHeight }, t.atoms.bg_contrast_25], children: sortedData.map(function (item) {
|
||||||
|
var key = keyExtractor(item);
|
||||||
|
return (_jsx(SortableItem, { item: item, itemKey: key, itemCount: data.length, itemHeight: itemHeight, state: state, dragY: dragY, scrollCompensation: scrollCompensation, isGestureActive: isGestureActive, measureDone: measureDone, renderItem: renderItem, onCommitReorder: handleReorder, onDragStart: onDragStart, onDragEnd: onDragEnd }, key));
|
||||||
|
}) }));
|
||||||
|
}
|
||||||
|
function SortableItem(_a) {
|
||||||
|
var item = _a.item, itemKey = _a.itemKey, itemCount = _a.itemCount, itemHeight = _a.itemHeight, state = _a.state, dragY = _a.dragY, scrollCompensation = _a.scrollCompensation, isGestureActive = _a.isGestureActive, measureDone = _a.measureDone, renderItem = _a.renderItem, onCommitReorder = _a.onCommitReorder, onDragStart = _a.onDragStart, onDragEnd = _a.onDragEnd;
|
||||||
|
var t = useTheme();
|
||||||
|
var playHaptic = useHaptics();
|
||||||
|
var lastHapticSlot = useSharedValue(-1);
|
||||||
|
var gesture = Gesture.Pan()
|
||||||
|
.onStart(function () {
|
||||||
|
'worklet';
|
||||||
|
var s = state.get();
|
||||||
|
var mySlot = s.slots[itemKey];
|
||||||
|
state.set(__assign(__assign({}, s), { activeKey: itemKey, dragStartSlot: mySlot }));
|
||||||
|
dragY.set(0);
|
||||||
|
scrollCompensation.set(0);
|
||||||
|
isGestureActive.set(true);
|
||||||
|
measureDone.set(false);
|
||||||
|
lastHapticSlot.set(mySlot);
|
||||||
|
if (onDragStart) {
|
||||||
|
runOnJS(onDragStart)();
|
||||||
|
}
|
||||||
|
runOnJS(playHaptic)();
|
||||||
|
})
|
||||||
|
.onChange(function (e) {
|
||||||
|
'worklet';
|
||||||
|
var startSlot = state.get().dragStartSlot;
|
||||||
|
var minY = -startSlot * itemHeight;
|
||||||
|
var maxY = (itemCount - 1 - startSlot) * itemHeight;
|
||||||
|
// Include scroll compensation so the item tracks with auto-scroll.
|
||||||
|
var effectiveY = e.translationY + scrollCompensation.get();
|
||||||
|
var clampedY = Math.max(minY, Math.min(effectiveY, maxY));
|
||||||
|
dragY.set(clampedY);
|
||||||
|
var currentSlot = Math.round((startSlot * itemHeight + clampedY) / itemHeight);
|
||||||
|
var clampedSlot = Math.max(0, Math.min(currentSlot, itemCount - 1));
|
||||||
|
if (IS_IOS && clampedSlot !== lastHapticSlot.get()) {
|
||||||
|
lastHapticSlot.set(clampedSlot);
|
||||||
|
runOnJS(playHaptic)('Light');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onEnd(function () {
|
||||||
|
'worklet';
|
||||||
|
// Stop auto-scroll BEFORE the snap animation.
|
||||||
|
isGestureActive.set(false);
|
||||||
|
var startSlot = state.get().dragStartSlot;
|
||||||
|
var rawNewSlot = Math.round((startSlot * itemHeight + dragY.get()) / itemHeight);
|
||||||
|
var newSlot = Math.max(0, Math.min(rawNewSlot, itemCount - 1));
|
||||||
|
var snapOffset = (newSlot - startSlot) * itemHeight;
|
||||||
|
// Animate to the target slot, then commit.
|
||||||
|
dragY.set(withTiming(snapOffset, { duration: 200 }, function (finished) {
|
||||||
|
if (finished) {
|
||||||
|
if (newSlot !== startSlot) {
|
||||||
|
// Compute new slots on the UI thread so animated styles
|
||||||
|
// reflect final positions before React re-renders.
|
||||||
|
var cur = state.get();
|
||||||
|
var sorted = new Array(itemCount);
|
||||||
|
for (var key in cur.slots) {
|
||||||
|
sorted[cur.slots[key]] = key;
|
||||||
|
}
|
||||||
|
var movedKey = sorted[startSlot];
|
||||||
|
sorted.splice(startSlot, 1);
|
||||||
|
sorted.splice(newSlot, 0, movedKey);
|
||||||
|
var nextSlots = {};
|
||||||
|
for (var i = 0; i < sorted.length; i++) {
|
||||||
|
nextSlots[sorted[i]] = i;
|
||||||
|
}
|
||||||
|
state.set({
|
||||||
|
slots: nextSlots,
|
||||||
|
activeKey: '',
|
||||||
|
dragStartSlot: -1,
|
||||||
|
});
|
||||||
|
dragY.set(0);
|
||||||
|
runOnJS(onCommitReorder)(sorted);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var s = state.get();
|
||||||
|
state.set(__assign(__assign({}, s), { activeKey: '', dragStartSlot: -1 }));
|
||||||
|
dragY.set(0);
|
||||||
|
if (onDragEnd) {
|
||||||
|
runOnJS(onDragEnd)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
// Reset if the gesture is cancelled without onEnd firing.
|
||||||
|
.onFinalize(function () {
|
||||||
|
'worklet';
|
||||||
|
isGestureActive.set(false);
|
||||||
|
if (state.get().activeKey === itemKey && dragY.get() === 0) {
|
||||||
|
var s = state.get();
|
||||||
|
state.set(__assign(__assign({}, s), { activeKey: '', dragStartSlot: -1 }));
|
||||||
|
if (onDragEnd) {
|
||||||
|
runOnJS(onDragEnd)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// All vertical positioning is via translateY (no `top`). This avoids
|
||||||
|
// discrete jumps when slots change — Reanimated smoothly animates from
|
||||||
|
// the current translateY to the new target on every state transition.
|
||||||
|
// On first mount we skip the animation so items appear instantly.
|
||||||
|
var isFirstRender = useSharedValue(true);
|
||||||
|
var animatedStyle = useAnimatedStyle(function () {
|
||||||
|
var s = state.get();
|
||||||
|
var mySlot = s.slots[itemKey];
|
||||||
|
if (mySlot === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
var baseY = mySlot * itemHeight;
|
||||||
|
// Active item: follow the finger with a slight scale-up and shadow.
|
||||||
|
if (s.activeKey === itemKey) {
|
||||||
|
return __assign({ transform: [
|
||||||
|
{ translateY: s.dragStartSlot * itemHeight + dragY.get() },
|
||||||
|
{ scale: withSpring(1.03) },
|
||||||
|
], zIndex: 999 }, (IS_IOS
|
||||||
|
? {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: withSpring(0.08),
|
||||||
|
shadowRadius: withSpring(4),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
elevation: withSpring(3),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Reset for non-active states. Without this, shadow props
|
||||||
|
// set during dragging linger on the native view.
|
||||||
|
var inactive = __assign({}, (IS_IOS
|
||||||
|
? {
|
||||||
|
shadowOpacity: withSpring(0),
|
||||||
|
shadowRadius: withSpring(0),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
elevation: withSpring(0),
|
||||||
|
}));
|
||||||
|
// Another item is being dragged — shift to make room.
|
||||||
|
if (s.activeKey !== '') {
|
||||||
|
isFirstRender.set(false);
|
||||||
|
var currentDragPos = Math.round((s.dragStartSlot * itemHeight + dragY.get()) / itemHeight);
|
||||||
|
var clampedPos = Math.max(0, Math.min(currentDragPos, itemCount - 1));
|
||||||
|
var offset = 0;
|
||||||
|
if (s.dragStartSlot < clampedPos &&
|
||||||
|
mySlot > s.dragStartSlot &&
|
||||||
|
mySlot <= clampedPos) {
|
||||||
|
offset = -itemHeight;
|
||||||
|
}
|
||||||
|
else if (s.dragStartSlot > clampedPos &&
|
||||||
|
mySlot < s.dragStartSlot &&
|
||||||
|
mySlot >= clampedPos) {
|
||||||
|
offset = itemHeight;
|
||||||
|
}
|
||||||
|
return __assign({ transform: [
|
||||||
|
{ translateY: withTiming(baseY + offset, { duration: 200 }) },
|
||||||
|
{ scale: withSpring(1) },
|
||||||
|
], zIndex: 0 }, inactive);
|
||||||
|
}
|
||||||
|
// Idle: sit at our slot. On first render use a direct value so items
|
||||||
|
// don't animate from y=0. After any drag, use withTiming so the
|
||||||
|
// shift→idle transition is smooth (no discrete jump).
|
||||||
|
if (isFirstRender.get()) {
|
||||||
|
isFirstRender.set(false);
|
||||||
|
return __assign({ transform: [{ translateY: baseY }, { scale: 1 }], zIndex: 0 }, inactive);
|
||||||
|
}
|
||||||
|
return __assign({ transform: [{ translateY: withTiming(baseY, { duration: 200 }) }, { scale: 1 }], zIndex: 0 }, inactive);
|
||||||
|
});
|
||||||
|
var dragHandle = (_jsx(GestureDetector, { gesture: gesture, children: _jsx(Animated.View, { style: [
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.px_sm,
|
||||||
|
a.py_md,
|
||||||
|
web({ cursor: 'grab' }),
|
||||||
|
], hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, children: _jsx(GripIcon, { size: "lg", fill: t.atoms.text_contrast_medium.color, style: web({ pointerEvents: 'none' }) }) }) }));
|
||||||
|
return (_jsx(Animated.View, { style: [
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: itemHeight,
|
||||||
|
},
|
||||||
|
animatedStyle,
|
||||||
|
], children: renderItem(item, dragHandle) }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
||||||
|
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
||||||
|
if (ar || !(i in from)) {
|
||||||
|
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
||||||
|
ar[i] = from[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to.concat(ar || Array.prototype.slice.call(from));
|
||||||
|
};
|
||||||
|
import { jsx as _jsx } from "react/jsx-runtime";
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { useTheme } from '#/alf';
|
||||||
|
import { DotGrid2x3_Stroke2_Corner0_Rounded as GripIcon } from '#/components/icons/DotGrid';
|
||||||
|
export function SortableList(_a) {
|
||||||
|
var data = _a.data, keyExtractor = _a.keyExtractor, renderItem = _a.renderItem, onReorder = _a.onReorder, onDragStart = _a.onDragStart, onDragEnd = _a.onDragEnd, itemHeight = _a.itemHeight;
|
||||||
|
var t = useTheme();
|
||||||
|
var _b = useState(null), dragState = _b[0], setDragState = _b[1];
|
||||||
|
var getNewPosition = function (state) {
|
||||||
|
var translationY = state.currentY - state.startY;
|
||||||
|
var rawNewPos = Math.round((state.activeIndex * itemHeight + translationY) / itemHeight);
|
||||||
|
return Math.max(0, Math.min(rawNewPos, data.length - 1));
|
||||||
|
};
|
||||||
|
var handlePointerMove = function (e) {
|
||||||
|
if (!dragState)
|
||||||
|
return;
|
||||||
|
e.preventDefault();
|
||||||
|
setDragState(function (prev) { return (prev ? __assign(__assign({}, prev), { currentY: e.clientY }) : null); });
|
||||||
|
};
|
||||||
|
var handlePointerUp = function () {
|
||||||
|
if (!dragState)
|
||||||
|
return;
|
||||||
|
var newPos = getNewPosition(dragState);
|
||||||
|
if (newPos !== dragState.activeIndex) {
|
||||||
|
var next = __spreadArray([], data, true);
|
||||||
|
var moved = next.splice(dragState.activeIndex, 1)[0];
|
||||||
|
next.splice(newPos, 0, moved);
|
||||||
|
onReorder(next);
|
||||||
|
}
|
||||||
|
setDragState(null);
|
||||||
|
onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd();
|
||||||
|
};
|
||||||
|
var handlePointerDown = function (e, index) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.setPointerCapture(e.pointerId);
|
||||||
|
setDragState({ activeIndex: index, currentY: e.clientY, startY: e.clientY });
|
||||||
|
onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart();
|
||||||
|
};
|
||||||
|
var newPos = dragState ? getNewPosition(dragState) : -1;
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
{ height: data.length * itemHeight, position: 'relative' },
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
],
|
||||||
|
// @ts-expect-error web-only pointer events
|
||||||
|
onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerUp, children: data.map(function (item, index) {
|
||||||
|
var isActive = (dragState === null || dragState === void 0 ? void 0 : dragState.activeIndex) === index;
|
||||||
|
// Clamp translation so the item stays within list bounds.
|
||||||
|
var rawTranslationY = isActive
|
||||||
|
? dragState.currentY - dragState.startY
|
||||||
|
: 0;
|
||||||
|
var translationY = isActive
|
||||||
|
? Math.max(-index * itemHeight, Math.min(rawTranslationY, (data.length - 1 - index) * itemHeight))
|
||||||
|
: 0;
|
||||||
|
// Non-dragged items shift to make room for the dragged item.
|
||||||
|
var offset = 0;
|
||||||
|
if (dragState && !isActive) {
|
||||||
|
var orig = dragState.activeIndex;
|
||||||
|
if (orig < newPos && index > orig && index <= newPos) {
|
||||||
|
offset = -itemHeight;
|
||||||
|
}
|
||||||
|
else if (orig > newPos && index < orig && index >= newPos) {
|
||||||
|
offset = itemHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dragHandle = (_jsx("div", { onPointerDown: function (e) {
|
||||||
|
return handlePointerDown(e, index);
|
||||||
|
}, style: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 8,
|
||||||
|
paddingRight: 8,
|
||||||
|
paddingTop: 12,
|
||||||
|
paddingBottom: 12,
|
||||||
|
cursor: isActive ? 'grabbing' : 'grab',
|
||||||
|
touchAction: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
}, children: _jsx(GripIcon, { size: "lg", fill: t.atoms.text_contrast_medium.color, style: { pointerEvents: 'none' } }) }));
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
top: index * itemHeight,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: itemHeight,
|
||||||
|
transform: [{ translateY: isActive ? translationY : offset }],
|
||||||
|
scale: isActive ? 1.03 : 1,
|
||||||
|
zIndex: isActive ? 999 : 0,
|
||||||
|
boxShadow: isActive ? '0 2px 12px rgba(0,0,0,0.06)' : 'none',
|
||||||
|
// Animate scale/shadow on pickup, and transform for
|
||||||
|
// non-dragged items shifting into place.
|
||||||
|
transition: isActive
|
||||||
|
? 'box-shadow 200ms ease, scale 200ms ease'
|
||||||
|
: dragState
|
||||||
|
? 'transform 200ms ease'
|
||||||
|
: 'none',
|
||||||
|
},
|
||||||
|
], children: renderItem(item, dragHandle) }, keyExtractor(item)));
|
||||||
|
}) }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useGoBack } from '#/lib/hooks/useGoBack';
|
||||||
|
import { CenteredView } from '#/view/com/util/Views';
|
||||||
|
import { atoms as a, useBreakpoints, useTheme } from '#/alf';
|
||||||
|
import { Button, ButtonText } from '#/components/Button';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
export function Error(_a) {
|
||||||
|
var title = _a.title, message = _a.message, onRetry = _a.onRetry, onGoBack = _a.onGoBack, hideBackButton = _a.hideBackButton, _b = _a.sideBorders, sideBorders = _b === void 0 ? true : _b;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var t = useTheme();
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var goBack = useGoBack(onGoBack);
|
||||||
|
return (_jsxs(CenteredView, { style: [
|
||||||
|
a.h_full_vh,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_5xl,
|
||||||
|
!gtMobile && a.justify_between,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{ paddingTop: 175, paddingBottom: 110 },
|
||||||
|
], sideBorders: sideBorders, children: [_jsxs(View, { style: [a.w_full, a.align_center, a.gap_lg], children: [_jsx(Text, { style: [a.font_semi_bold, a.text_3xl], children: title }), _jsx(Text, { style: [
|
||||||
|
a.text_md,
|
||||||
|
a.text_center,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
{ lineHeight: 1.4 },
|
||||||
|
gtMobile ? { width: 450 } : [a.w_full, a.px_lg],
|
||||||
|
], children: message })] }), _jsxs(View, { style: [a.gap_md, gtMobile ? { width: 350 } : [a.w_full, a.px_lg]], children: [onRetry && (_jsx(Button, { variant: "solid", color: "primary", label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Press to retry"], ["Press to retry"])))), onPress: onRetry, size: "large", style: [a.rounded_sm, a.overflow_hidden, { paddingVertical: 10 }], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Retry" }) }) })), !hideBackButton && (_jsx(Button, { variant: "solid", color: onRetry ? 'secondary' : 'primary', label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Return to previous page"], ["Return to previous page"])))), onPress: goBack, size: "large", style: [a.rounded_sm, a.overflow_hidden, { paddingVertical: 10 }], children: _jsx(ButtonText, { children: _jsx(Trans, { children: "Go Back" }) }) }))] })] }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2;
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var __rest = (this && this.__rest) || function (s, e) {
|
||||||
|
var t = {};
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||||
|
t[p] = s[p];
|
||||||
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||||
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||||
|
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||||
|
t[p[i]] = s[p[i]];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { AtUri, RichText as RichTextApi, } from '@atproto/api';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { sanitizeHandle } from '#/lib/strings/handles';
|
||||||
|
import { logger } from '#/logger';
|
||||||
|
import { precacheFeedFromGeneratorView } from '#/state/queries/feed';
|
||||||
|
import { useAddSavedFeedsMutation, usePreferencesQuery, useRemoveFeedMutation, } from '#/state/queries/preferences';
|
||||||
|
import { useSession } from '#/state/session';
|
||||||
|
import * as Toast from '#/view/com/util/Toast';
|
||||||
|
import { UserAvatar } from '#/view/com/util/UserAvatar';
|
||||||
|
import { atoms as a, select, useTheme } from '#/alf';
|
||||||
|
import { Button, ButtonIcon, ButtonText, } from '#/components/Button';
|
||||||
|
import { Live_Stroke2_Corner0_Rounded as LiveIcon } from '#/components/icons/Live';
|
||||||
|
import { Pin_Stroke2_Corner0_Rounded as PinIcon } from '#/components/icons/Pin';
|
||||||
|
import { Link as InternalLink } from '#/components/Link';
|
||||||
|
import { Loader } from '#/components/Loader';
|
||||||
|
import * as Prompt from '#/components/Prompt';
|
||||||
|
import { RichText } from '#/components/RichText';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { useActiveLiveEventFeedUris } from '#/features/liveEvents/context';
|
||||||
|
import { Trash_Stroke2_Corner0_Rounded as TrashIcon } from './icons/Trash';
|
||||||
|
export function Default(props) {
|
||||||
|
var view = props.view;
|
||||||
|
return (_jsx(Link, __assign({}, props, { children: _jsxs(Outer, { children: [_jsxs(Header, { children: [_jsx(Avatar, { src: view.avatar }), _jsx(TitleAndByline, { title: view.displayName, creator: view.creator, uri: view.uri }), _jsx(SaveButton, { view: view, pin: true })] }), _jsx(Description, { description: view.description }), _jsx(Likes, { count: view.likeCount || 0 })] }) })));
|
||||||
|
}
|
||||||
|
export function Link(_a) {
|
||||||
|
var view = _a.view, children = _a.children, props = __rest(_a, ["view", "children"]);
|
||||||
|
var queryClient = useQueryClient();
|
||||||
|
var href = React.useMemo(function () {
|
||||||
|
return createProfileFeedHref({ feed: view });
|
||||||
|
}, [view]);
|
||||||
|
React.useEffect(function () {
|
||||||
|
precacheFeedFromGeneratorView(queryClient, view);
|
||||||
|
}, [view, queryClient]);
|
||||||
|
return (_jsx(InternalLink, __assign({ label: view.displayName, to: href, style: [a.flex_col] }, props, { children: children })));
|
||||||
|
}
|
||||||
|
export function Outer(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
return _jsx(View, { style: [a.w_full, a.gap_sm], children: children });
|
||||||
|
}
|
||||||
|
export function Header(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
return _jsx(View, { style: [a.flex_row, a.align_center, a.gap_sm], children: children });
|
||||||
|
}
|
||||||
|
export function Avatar(_a) {
|
||||||
|
var src = _a.src, _b = _a.size, size = _b === void 0 ? 40 : _b;
|
||||||
|
return _jsx(UserAvatar, { type: "algo", size: size, avatar: src });
|
||||||
|
}
|
||||||
|
export function AvatarPlaceholder(_a) {
|
||||||
|
var _b = _a.size, size = _b === void 0 ? 40 : _b;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
] }));
|
||||||
|
}
|
||||||
|
export function TitleAndByline(_a) {
|
||||||
|
var title = _a.title, creator = _a.creator, uri = _a.uri;
|
||||||
|
var t = useTheme();
|
||||||
|
var activeLiveEvents = useActiveLiveEventFeedUris();
|
||||||
|
var liveColor = useMemo(function () {
|
||||||
|
return select(t.name, {
|
||||||
|
dark: t.palette.negative_600,
|
||||||
|
dim: t.palette.negative_600,
|
||||||
|
light: t.palette.negative_500,
|
||||||
|
});
|
||||||
|
}, [t]);
|
||||||
|
return (_jsxs(View, { style: [a.flex_1], children: [uri && activeLiveEvents.has(uri) && (_jsxs(View, { style: [a.flex_row, a.align_center, a.gap_2xs], children: [_jsx(LiveIcon, { size: "xs", fill: liveColor }), _jsx(Text, { style: [
|
||||||
|
a.text_2xs,
|
||||||
|
a.font_medium,
|
||||||
|
a.leading_snug,
|
||||||
|
{ color: liveColor },
|
||||||
|
], children: _jsx(Trans, { children: "Happening now" }) })] })), _jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_snug], numberOfLines: 1, children: title }), creator && (_jsx(Text, { style: [a.leading_snug, t.atoms.text_contrast_medium], numberOfLines: 1, children: _jsxs(Trans, { children: ["Feed by ", sanitizeHandle(creator.handle, '@')] }) }))] }));
|
||||||
|
}
|
||||||
|
export function TitleAndBylinePlaceholder(_a) {
|
||||||
|
var creator = _a.creator;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(View, { style: [a.flex_1, a.gap_xs], children: [_jsx(View, { style: [
|
||||||
|
a.rounded_xs,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
{
|
||||||
|
width: '60%',
|
||||||
|
height: 14,
|
||||||
|
},
|
||||||
|
] }), creator && (_jsx(View, { style: [
|
||||||
|
a.rounded_xs,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{
|
||||||
|
width: '40%',
|
||||||
|
height: 10,
|
||||||
|
},
|
||||||
|
] }))] }));
|
||||||
|
}
|
||||||
|
export function Description(_a) {
|
||||||
|
var description = _a.description, rest = __rest(_a, ["description"]);
|
||||||
|
var rt = React.useMemo(function () {
|
||||||
|
if (!description)
|
||||||
|
return;
|
||||||
|
var rt = new RichTextApi({ text: description || '' });
|
||||||
|
rt.detectFacetsWithoutResolution();
|
||||||
|
return rt;
|
||||||
|
}, [description]);
|
||||||
|
if (!rt)
|
||||||
|
return null;
|
||||||
|
return _jsx(RichText, __assign({ value: rt, disableLinks: true }, rest));
|
||||||
|
}
|
||||||
|
export function DescriptionPlaceholder() {
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(View, { style: [a.gap_xs], children: [_jsx(View, { style: [a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, { height: 12 }] }), _jsx(View, { style: [a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, { height: 12 }] }), _jsx(View, { style: [
|
||||||
|
a.rounded_xs,
|
||||||
|
a.w_full,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
{ height: 12, width: 100 },
|
||||||
|
] })] }));
|
||||||
|
}
|
||||||
|
export function Likes(_a) {
|
||||||
|
var count = _a.count;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsx(Text, { style: [a.text_sm, t.atoms.text_contrast_medium, a.font_semi_bold], children: _jsxs(Trans, { children: ["Liked by ", _jsx(Plural, { value: count || 0, one: "# user", other: "# users" })] }) }));
|
||||||
|
}
|
||||||
|
export function SaveButton(_a) {
|
||||||
|
var view = _a.view, pin = _a.pin, props = __rest(_a, ["view", "pin"]);
|
||||||
|
var hasSession = useSession().hasSession;
|
||||||
|
if (!hasSession)
|
||||||
|
return null;
|
||||||
|
return _jsx(SaveButtonInner, __assign({ view: view, pin: pin }, props));
|
||||||
|
}
|
||||||
|
function SaveButtonInner(_a) {
|
||||||
|
var _this = this;
|
||||||
|
var view = _a.view, pin = _a.pin, _b = _a.text, text = _b === void 0 ? true : _b, buttonProps = __rest(_a, ["view", "pin", "text"]);
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var preferences = usePreferencesQuery().data;
|
||||||
|
var _c = useAddSavedFeedsMutation(), isAddSavedFeedPending = _c.isPending, saveFeeds = _c.mutateAsync;
|
||||||
|
var _d = useRemoveFeedMutation(), isRemovePending = _d.isPending, removeFeed = _d.mutateAsync;
|
||||||
|
var uri = view.uri;
|
||||||
|
var type = view.uri.includes('app.bsky.feed.generator') ? 'feed' : 'list';
|
||||||
|
var savedFeedConfig = React.useMemo(function () {
|
||||||
|
var _a;
|
||||||
|
return (_a = preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds) === null || _a === void 0 ? void 0 : _a.find(function (feed) { return feed.value === uri; });
|
||||||
|
}, [preferences === null || preferences === void 0 ? void 0 : preferences.savedFeeds, uri]);
|
||||||
|
var removePromptControl = Prompt.usePromptControl();
|
||||||
|
var isPending = isAddSavedFeedPending || isRemovePending;
|
||||||
|
var toggleSave = React.useCallback(function (e) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var err_1;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
_a.label = 1;
|
||||||
|
case 1:
|
||||||
|
_a.trys.push([1, 6, , 7]);
|
||||||
|
if (!savedFeedConfig) return [3 /*break*/, 3];
|
||||||
|
return [4 /*yield*/, removeFeed(savedFeedConfig)];
|
||||||
|
case 2:
|
||||||
|
_a.sent();
|
||||||
|
return [3 /*break*/, 5];
|
||||||
|
case 3: return [4 /*yield*/, saveFeeds([
|
||||||
|
{
|
||||||
|
type: type,
|
||||||
|
value: uri,
|
||||||
|
pinned: pin || false,
|
||||||
|
},
|
||||||
|
])];
|
||||||
|
case 4:
|
||||||
|
_a.sent();
|
||||||
|
_a.label = 5;
|
||||||
|
case 5:
|
||||||
|
Toast.show(_(msg({ message: 'Feeds updated!', context: 'toast' })));
|
||||||
|
return [3 /*break*/, 7];
|
||||||
|
case 6:
|
||||||
|
err_1 = _a.sent();
|
||||||
|
logger.error(err_1, { message: "FeedCard: failed to update feeds", pin: pin });
|
||||||
|
Toast.show(_(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Failed to update feeds"], ["Failed to update feeds"])))), 'xmark');
|
||||||
|
return [3 /*break*/, 7];
|
||||||
|
case 7: return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); }, [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type]);
|
||||||
|
var onPrompRemoveFeed = React.useCallback(function (e) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
removePromptControl.open();
|
||||||
|
return [2 /*return*/];
|
||||||
|
});
|
||||||
|
}); }, [removePromptControl]);
|
||||||
|
return (_jsxs(_Fragment, { children: [_jsx(Button, __assign({ disabled: isPending, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Add this feed to your feeds"], ["Add this feed to your feeds"])))), size: "small", variant: "solid", color: savedFeedConfig ? 'secondary' : 'primary', onPress: savedFeedConfig ? onPrompRemoveFeed : toggleSave }, buttonProps, { children: savedFeedConfig ? (_jsxs(_Fragment, { children: [isPending ? (_jsx(ButtonIcon, { size: "md", icon: Loader })) : (!text && _jsx(ButtonIcon, { size: "md", icon: TrashIcon })), text && (_jsx(ButtonText, { children: _jsx(Trans, { children: "Unpin Feed" }) }))] })) : (_jsxs(_Fragment, { children: [_jsx(ButtonIcon, { size: "md", icon: isPending ? Loader : PinIcon }), text && (_jsx(ButtonText, { children: _jsx(Trans, { children: "Pin Feed" }) }))] })) })), _jsx(Prompt.Basic, { control: removePromptControl, title: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Remove from your feeds?"], ["Remove from your feeds?"])))), description: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Are you sure you want to remove this from your feeds?"], ["Are you sure you want to remove this from your feeds?"])))), onConfirm: toggleSave, confirmButtonCta: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Remove"], ["Remove"])))), confirmButtonColor: "negative" })] }));
|
||||||
|
}
|
||||||
|
export function createProfileFeedHref(_a) {
|
||||||
|
var feed = _a.feed;
|
||||||
|
var urip = new AtUri(feed.uri);
|
||||||
|
var handleOrDid = feed.creator.handle || feed.creator.did;
|
||||||
|
return "/profile/".concat(handleOrDid, "/feed/").concat(urip.rkey);
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5;
|
||||||
@@ -0,0 +1,563 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
||||||
|
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
||||||
|
if (ar || !(i in from)) {
|
||||||
|
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
||||||
|
ar[i] = from[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to.concat(ar || Array.prototype.slice.call(from));
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { ScrollView, View } from 'react-native';
|
||||||
|
import Animated, { Easing, FadeIn, FadeOut, LayoutAnimationConfig, LinearTransition, } from 'react-native-reanimated';
|
||||||
|
import { AtUri } from '@atproto/api';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useModerationOpts } from '#/state/preferences/moderation-opts';
|
||||||
|
import { useGetPopularFeedsQuery } from '#/state/queries/feed';
|
||||||
|
import { useProfilesQuery } from '#/state/queries/profile';
|
||||||
|
import { useSuggestedFollowsByActorQuery, useSuggestedFollowsQuery, } from '#/state/queries/suggested-follows';
|
||||||
|
import { useSession } from '#/state/session';
|
||||||
|
import * as userActionHistory from '#/state/userActionHistory';
|
||||||
|
import { BlockDrawerGesture } from '#/view/shell/BlockDrawerGesture';
|
||||||
|
import { atoms as a, native, useBreakpoints, useTheme, web, } from '#/alf';
|
||||||
|
import { Button, ButtonIcon, ButtonText } from '#/components/Button';
|
||||||
|
import { useDialogControl } from '#/components/Dialog';
|
||||||
|
import * as FeedCard from '#/components/FeedCard';
|
||||||
|
import { ArrowRight_Stroke2_Corner0_Rounded as ArrowRight } from '#/components/icons/Arrow';
|
||||||
|
import { Hashtag_Stroke2_Corner0_Rounded as Hashtag } from '#/components/icons/Hashtag';
|
||||||
|
import { TimesLarge_Stroke2_Corner0_Rounded as X } from '#/components/icons/Times';
|
||||||
|
import { InlineLinkText } from '#/components/Link';
|
||||||
|
import * as ProfileCard from '#/components/ProfileCard';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { useAnalytics } from '#/analytics';
|
||||||
|
import { IS_IOS } from '#/env';
|
||||||
|
import { FollowDialogWithoutGuide } from './ProgressGuide/FollowDialog';
|
||||||
|
import { ProgressGuideList } from './ProgressGuide/List';
|
||||||
|
var DISMISS_ANIMATION_DURATION = 200;
|
||||||
|
var MOBILE_CARD_WIDTH = 165;
|
||||||
|
var FINAL_CARD_WIDTH = 120;
|
||||||
|
function CardOuter(_a) {
|
||||||
|
var children = _a.children, style = _a.style;
|
||||||
|
var t = useTheme();
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
return (_jsx(View, { testID: "CardOuter", style: [
|
||||||
|
a.flex_1,
|
||||||
|
a.w_full,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_lg,
|
||||||
|
a.border,
|
||||||
|
t.atoms.bg,
|
||||||
|
t.atoms.shadow_sm,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
!gtMobile && {
|
||||||
|
width: MOBILE_CARD_WIDTH,
|
||||||
|
},
|
||||||
|
style,
|
||||||
|
], children: children }));
|
||||||
|
}
|
||||||
|
export function SuggestedFollowPlaceholder() {
|
||||||
|
return (_jsx(CardOuter, { children: _jsxs(ProfileCard.Outer, { children: [_jsxs(View, { style: [a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto], children: [_jsx(ProfileCard.AvatarPlaceholder, { size: 88 }), _jsx(ProfileCard.NamePlaceholder, {}), _jsx(View, { style: [a.w_full], children: _jsx(ProfileCard.DescriptionPlaceholder, { numberOfLines: 2 }) })] }), _jsx(ProfileCard.FollowButtonPlaceholder, {})] }) }));
|
||||||
|
}
|
||||||
|
export function SuggestedFeedsCardPlaceholder() {
|
||||||
|
return (_jsxs(CardOuter, { style: [a.gap_sm], children: [_jsxs(FeedCard.Header, { children: [_jsx(FeedCard.AvatarPlaceholder, {}), _jsx(FeedCard.TitleAndBylinePlaceholder, { creator: true })] }), _jsx(FeedCard.DescriptionPlaceholder, {})] }));
|
||||||
|
}
|
||||||
|
function getRank(seenPost) {
|
||||||
|
var _a, _b, _c;
|
||||||
|
var tier;
|
||||||
|
if (seenPost.feedContext === 'popfriends') {
|
||||||
|
tier = 'a';
|
||||||
|
}
|
||||||
|
else if ((_a = seenPost.feedContext) === null || _a === void 0 ? void 0 : _a.startsWith('cluster')) {
|
||||||
|
tier = 'b';
|
||||||
|
}
|
||||||
|
else if (seenPost.feedContext === 'popcluster') {
|
||||||
|
tier = 'c';
|
||||||
|
}
|
||||||
|
else if ((_b = seenPost.feedContext) === null || _b === void 0 ? void 0 : _b.startsWith('ntpc')) {
|
||||||
|
tier = 'd';
|
||||||
|
}
|
||||||
|
else if ((_c = seenPost.feedContext) === null || _c === void 0 ? void 0 : _c.startsWith('t-')) {
|
||||||
|
tier = 'e';
|
||||||
|
}
|
||||||
|
else if (seenPost.feedContext === 'nettop') {
|
||||||
|
tier = 'f';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tier = 'g';
|
||||||
|
}
|
||||||
|
var score = Math.round(Math.log(1 + seenPost.likeCount + seenPost.repostCount + seenPost.replyCount));
|
||||||
|
if (seenPost.isFollowedBy || Math.random() > 0.9) {
|
||||||
|
score *= 2;
|
||||||
|
}
|
||||||
|
var rank = 100 - score;
|
||||||
|
return "".concat(tier, "-").concat(rank);
|
||||||
|
}
|
||||||
|
function sortSeenPosts(postA, postB) {
|
||||||
|
var rankA = getRank(postA);
|
||||||
|
var rankB = getRank(postB);
|
||||||
|
// Yes, we're comparing strings here.
|
||||||
|
// The "larger" string means a worse rank.
|
||||||
|
if (rankA > rankB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else if (rankA < rankB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function useExperimentalSuggestedUsersQuery() {
|
||||||
|
var currentAccount = useSession().currentAccount;
|
||||||
|
var userActionSnapshot = userActionHistory.useActionHistorySnapshot();
|
||||||
|
var dids = useMemo(function () {
|
||||||
|
var likes = userActionSnapshot.likes, follows = userActionSnapshot.follows, followSuggestions = userActionSnapshot.followSuggestions, seen = userActionSnapshot.seen;
|
||||||
|
var likeDids = likes
|
||||||
|
.map(function (l) { return new AtUri(l); })
|
||||||
|
.map(function (uri) { return uri.host; })
|
||||||
|
.filter(function (did) { return !follows.includes(did); });
|
||||||
|
var suggestedDids = [];
|
||||||
|
if (followSuggestions.length > 0) {
|
||||||
|
suggestedDids = [
|
||||||
|
// It's ok if these will pick the same item (weighed by its frequency)
|
||||||
|
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||||
|
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||||
|
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||||
|
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
var seenDids = seen
|
||||||
|
.sort(sortSeenPosts)
|
||||||
|
.map(function (l) { return new AtUri(l.uri); })
|
||||||
|
.map(function (uri) { return uri.host; });
|
||||||
|
return __spreadArray([], new Set(__spreadArray(__spreadArray(__spreadArray([], suggestedDids, true), likeDids, true), seenDids, true)), true).filter(function (did) { return did !== (currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did); });
|
||||||
|
}, [userActionSnapshot, currentAccount]);
|
||||||
|
var _a = useProfilesQuery({
|
||||||
|
handles: dids.slice(0, 16),
|
||||||
|
}), data = _a.data, isLoading = _a.isLoading, error = _a.error;
|
||||||
|
var profiles = data
|
||||||
|
? data.profiles.filter(function (profile) {
|
||||||
|
var _a;
|
||||||
|
return !((_a = profile.viewer) === null || _a === void 0 ? void 0 : _a.following);
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
return {
|
||||||
|
isLoading: isLoading,
|
||||||
|
error: error,
|
||||||
|
profiles: profiles.slice(0, 6),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function SuggestedFollows(_a) {
|
||||||
|
var feed = _a.feed;
|
||||||
|
var currentAccount = useSession().currentAccount;
|
||||||
|
var _b = feed.split('|'), feedType = _b[0], feedUriOrDid = _b[1];
|
||||||
|
if (feedType === 'author') {
|
||||||
|
if ((currentAccount === null || currentAccount === void 0 ? void 0 : currentAccount.did) === feedUriOrDid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _jsx(SuggestedFollowsProfile, { did: feedUriOrDid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _jsx(SuggestedFollowsHome, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function SuggestedFollowsProfile(_a) {
|
||||||
|
var did = _a.did;
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var moderationOpts = useModerationOpts();
|
||||||
|
var maxLength = gtMobile ? 4 : 6;
|
||||||
|
var _b = useSuggestedFollowsByActorQuery({
|
||||||
|
did: did,
|
||||||
|
}), isSuggestionsLoading = _b.isLoading, data = _b.data, error = _b.error;
|
||||||
|
var _c = useSuggestedFollowsQuery({ limit: 25 }), moreSuggestions = _c.data, fetchNextPage = _c.fetchNextPage, hasNextPage = _c.hasNextPage, isFetchingNextPage = _c.isFetchingNextPage;
|
||||||
|
var _d = useState(new Set()), dismissedDids = _d[0], setDismissedDids = _d[1];
|
||||||
|
var onDismiss = useCallback(function (dismissedDid) {
|
||||||
|
setDismissedDids(function (prev) { return new Set(prev).add(dismissedDid); });
|
||||||
|
}, []);
|
||||||
|
// Combine profiles from the actor-specific query with fallback suggestions
|
||||||
|
var allProfiles = useMemo(function () {
|
||||||
|
var _a, _b;
|
||||||
|
var actorProfiles = (_a = data === null || data === void 0 ? void 0 : data.suggestions) !== null && _a !== void 0 ? _a : [];
|
||||||
|
var fallbackProfiles = (_b = moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages.flatMap(function (page) {
|
||||||
|
return page.actors.map(function (actor) { return ({ actor: actor, recId: page.recId }); });
|
||||||
|
})) !== null && _b !== void 0 ? _b : [];
|
||||||
|
// Dedupe by did, preferring actor-specific profiles
|
||||||
|
var seen = new Set();
|
||||||
|
var combined = [];
|
||||||
|
for (var _i = 0, actorProfiles_1 = actorProfiles; _i < actorProfiles_1.length; _i++) {
|
||||||
|
var profile = actorProfiles_1[_i];
|
||||||
|
if (!seen.has(profile.did)) {
|
||||||
|
seen.add(profile.did);
|
||||||
|
combined.push({ actor: profile, recId: data === null || data === void 0 ? void 0 : data.recId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var _c = 0, fallbackProfiles_1 = fallbackProfiles; _c < fallbackProfiles_1.length; _c++) {
|
||||||
|
var profile = fallbackProfiles_1[_c];
|
||||||
|
if (!seen.has(profile.actor.did) && profile.actor.did !== did) {
|
||||||
|
seen.add(profile.actor.did);
|
||||||
|
combined.push(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return combined;
|
||||||
|
}, [data === null || data === void 0 ? void 0 : data.suggestions, moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages, did, data === null || data === void 0 ? void 0 : data.recId]);
|
||||||
|
var filteredProfiles = useMemo(function () {
|
||||||
|
return allProfiles.filter(function (p) { return !dismissedDids.has(p.actor.did); });
|
||||||
|
}, [allProfiles, dismissedDids]);
|
||||||
|
// Fetch more when running low
|
||||||
|
useEffect(function () {
|
||||||
|
if (moderationOpts &&
|
||||||
|
filteredProfiles.length < maxLength &&
|
||||||
|
hasNextPage &&
|
||||||
|
!isFetchingNextPage) {
|
||||||
|
void fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
filteredProfiles.length,
|
||||||
|
maxLength,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
moderationOpts,
|
||||||
|
]);
|
||||||
|
return (_jsx(ProfileGrid, { isSuggestionsLoading: isSuggestionsLoading, profiles: filteredProfiles, totalProfileCount: allProfiles.length, error: error, viewContext: "profile", onDismiss: onDismiss }));
|
||||||
|
}
|
||||||
|
export function SuggestedFollowsHome() {
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var moderationOpts = useModerationOpts();
|
||||||
|
var maxLength = gtMobile ? 4 : 6;
|
||||||
|
var _a = useExperimentalSuggestedUsersQuery(), isSuggestionsLoading = _a.isLoading, experimentalProfiles = _a.profiles, experimentalError = _a.error;
|
||||||
|
var _b = useSuggestedFollowsQuery({ limit: 25 }), moreSuggestions = _b.data, fetchNextPage = _b.fetchNextPage, hasNextPage = _b.hasNextPage, isFetchingNextPage = _b.isFetchingNextPage, suggestionsError = _b.error;
|
||||||
|
var _c = useState(new Set()), dismissedDids = _c[0], setDismissedDids = _c[1];
|
||||||
|
var onDismiss = useCallback(function (did) {
|
||||||
|
setDismissedDids(function (prev) { return new Set(prev).add(did); });
|
||||||
|
}, []);
|
||||||
|
// Combine profiles from experimental query with paginated suggestions
|
||||||
|
var allProfiles = useMemo(function () {
|
||||||
|
var _a;
|
||||||
|
var fallbackProfiles = (_a = moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages.flatMap(function (page) {
|
||||||
|
return page.actors.map(function (actor) { return ({ actor: actor, recId: page.recId }); });
|
||||||
|
})) !== null && _a !== void 0 ? _a : [];
|
||||||
|
// Dedupe by did, preferring experimental profiles
|
||||||
|
var seen = new Set();
|
||||||
|
var combined = [];
|
||||||
|
for (var _i = 0, experimentalProfiles_1 = experimentalProfiles; _i < experimentalProfiles_1.length; _i++) {
|
||||||
|
var profile = experimentalProfiles_1[_i];
|
||||||
|
if (!seen.has(profile.did)) {
|
||||||
|
seen.add(profile.did);
|
||||||
|
combined.push({ actor: profile, recId: undefined });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var _b = 0, fallbackProfiles_2 = fallbackProfiles; _b < fallbackProfiles_2.length; _b++) {
|
||||||
|
var profile = fallbackProfiles_2[_b];
|
||||||
|
if (!seen.has(profile.actor.did)) {
|
||||||
|
seen.add(profile.actor.did);
|
||||||
|
combined.push(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return combined;
|
||||||
|
}, [experimentalProfiles, moreSuggestions === null || moreSuggestions === void 0 ? void 0 : moreSuggestions.pages]);
|
||||||
|
var filteredProfiles = useMemo(function () {
|
||||||
|
return allProfiles.filter(function (p) { return !dismissedDids.has(p.actor.did); });
|
||||||
|
}, [allProfiles, dismissedDids]);
|
||||||
|
// Fetch more when running low
|
||||||
|
useEffect(function () {
|
||||||
|
if (moderationOpts &&
|
||||||
|
filteredProfiles.length < maxLength &&
|
||||||
|
hasNextPage &&
|
||||||
|
!isFetchingNextPage) {
|
||||||
|
void fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
filteredProfiles.length,
|
||||||
|
maxLength,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
moderationOpts,
|
||||||
|
]);
|
||||||
|
return (_jsx(ProfileGrid, { isSuggestionsLoading: isSuggestionsLoading, profiles: filteredProfiles, totalProfileCount: allProfiles.length, error: experimentalError || suggestionsError, viewContext: "feed", onDismiss: onDismiss }));
|
||||||
|
}
|
||||||
|
export function ProfileGrid(_a) {
|
||||||
|
var isSuggestionsLoading = _a.isSuggestionsLoading, error = _a.error, profiles = _a.profiles, totalProfileCount = _a.totalProfileCount, _b = _a.viewContext, viewContext = _b === void 0 ? 'feed' : _b, onDismiss = _a.onDismiss, _c = _a.isVisible, isVisible = _c === void 0 ? true : _c;
|
||||||
|
var t = useTheme();
|
||||||
|
var ax = useAnalytics();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var moderationOpts = useModerationOpts();
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var followDialogControl = useDialogControl();
|
||||||
|
var isLoading = isSuggestionsLoading || !moderationOpts;
|
||||||
|
var isProfileHeaderContext = viewContext === 'profileHeader';
|
||||||
|
var isFeedContext = viewContext === 'feed';
|
||||||
|
var maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6;
|
||||||
|
var minLength = gtMobile ? 3 : 4;
|
||||||
|
// Track seen profiles
|
||||||
|
var seenProfilesRef = useRef(new Set());
|
||||||
|
var containerRef = useRef(null);
|
||||||
|
var hasTrackedRef = useRef(false);
|
||||||
|
var logContext = isFeedContext
|
||||||
|
? 'InterstitialDiscover'
|
||||||
|
: isProfileHeaderContext
|
||||||
|
? 'Profile'
|
||||||
|
: 'InterstitialProfile';
|
||||||
|
// Callback to fire seen events
|
||||||
|
var fireSeen = useCallback(function () {
|
||||||
|
if (isLoading || error || !profiles.length)
|
||||||
|
return;
|
||||||
|
if (hasTrackedRef.current)
|
||||||
|
return;
|
||||||
|
hasTrackedRef.current = true;
|
||||||
|
var profilesToShow = profiles.slice(0, maxLength);
|
||||||
|
profilesToShow.forEach(function (profile, index) {
|
||||||
|
if (!seenProfilesRef.current.has(profile.actor.did)) {
|
||||||
|
seenProfilesRef.current.add(profile.actor.did);
|
||||||
|
ax.metric('suggestedUser:seen', {
|
||||||
|
logContext: logContext,
|
||||||
|
recId: profile.recId,
|
||||||
|
position: index,
|
||||||
|
suggestedDid: profile.actor.did,
|
||||||
|
category: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [ax, isLoading, error, profiles, maxLength, logContext]);
|
||||||
|
// For profile header, fire when isVisible becomes true
|
||||||
|
useEffect(function () {
|
||||||
|
if (isProfileHeaderContext) {
|
||||||
|
if (!isVisible) {
|
||||||
|
hasTrackedRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fireSeen();
|
||||||
|
}
|
||||||
|
}, [isVisible, isProfileHeaderContext, fireSeen]);
|
||||||
|
// For feed interstitials, use IntersectionObserver to detect actual visibility
|
||||||
|
useEffect(function () {
|
||||||
|
if (isProfileHeaderContext)
|
||||||
|
return; // handled above
|
||||||
|
if (isLoading || error || !profiles.length)
|
||||||
|
return;
|
||||||
|
var node = containerRef.current;
|
||||||
|
if (!node)
|
||||||
|
return;
|
||||||
|
// Use IntersectionObserver on web to detect when actually visible
|
||||||
|
if (typeof IntersectionObserver !== 'undefined') {
|
||||||
|
var observer_1 = new IntersectionObserver(function (entries) {
|
||||||
|
var _a;
|
||||||
|
if ((_a = entries[0]) === null || _a === void 0 ? void 0 : _a.isIntersecting) {
|
||||||
|
fireSeen();
|
||||||
|
observer_1.disconnect();
|
||||||
|
}
|
||||||
|
}, { threshold: 0.5 });
|
||||||
|
// @ts-ignore - web only
|
||||||
|
observer_1.observe(node);
|
||||||
|
return function () { return observer_1.disconnect(); };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// On native, delay slightly to account for layout shifts during hydration
|
||||||
|
var timeout_1 = setTimeout(function () {
|
||||||
|
fireSeen();
|
||||||
|
}, 500);
|
||||||
|
return function () { return clearTimeout(timeout_1); };
|
||||||
|
}
|
||||||
|
}, [isProfileHeaderContext, isLoading, error, profiles.length, fireSeen]);
|
||||||
|
var content = isLoading
|
||||||
|
? Array(maxLength)
|
||||||
|
.fill(0)
|
||||||
|
.map(function (_, i) { return (_jsx(View, { style: [
|
||||||
|
a.flex_1,
|
||||||
|
gtMobile &&
|
||||||
|
web([
|
||||||
|
a.flex_0,
|
||||||
|
a.flex_grow,
|
||||||
|
{ width: "calc(30% - ".concat(a.gap_md.gap / 2, "px)") },
|
||||||
|
]),
|
||||||
|
], children: _jsx(SuggestedFollowPlaceholder, {}) }, i)); })
|
||||||
|
: error || !profiles.length
|
||||||
|
? null
|
||||||
|
: profiles.slice(0, maxLength).map(function (profile, index) { return (_jsx(Animated.View, { layout: native(LinearTransition.delay(DISMISS_ANIMATION_DURATION).easing(Easing.out(Easing.exp))), exiting: FadeOut.duration(DISMISS_ANIMATION_DURATION),
|
||||||
|
// for web, as the cards are static, not in a list
|
||||||
|
entering: web(FadeIn.delay(DISMISS_ANIMATION_DURATION * 2)), style: [
|
||||||
|
a.flex_1,
|
||||||
|
gtMobile &&
|
||||||
|
web([
|
||||||
|
a.flex_0,
|
||||||
|
a.flex_grow,
|
||||||
|
{ width: "calc(30% - ".concat(a.gap_md.gap / 2, "px)") },
|
||||||
|
]),
|
||||||
|
], children: _jsx(ProfileCard.Link, { profile: profile.actor, onPress: function () {
|
||||||
|
ax.metric('suggestedUser:press', {
|
||||||
|
logContext: isFeedContext
|
||||||
|
? 'InterstitialDiscover'
|
||||||
|
: 'InterstitialProfile',
|
||||||
|
recId: profile.recId,
|
||||||
|
position: index,
|
||||||
|
suggestedDid: profile.actor.did,
|
||||||
|
category: null,
|
||||||
|
});
|
||||||
|
}, style: [a.flex_1], children: function (_a) {
|
||||||
|
var hovered = _a.hovered, pressed = _a.pressed;
|
||||||
|
return (_jsx(CardOuter, { style: [
|
||||||
|
(hovered || pressed) && t.atoms.border_contrast_high,
|
||||||
|
], children: _jsxs(ProfileCard.Outer, { children: [onDismiss && (_jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Dismiss this suggestion"], ["Dismiss this suggestion"])))), onPress: function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
onDismiss(profile.actor.did);
|
||||||
|
ax.metric('suggestedUser:dismiss', {
|
||||||
|
logContext: isFeedContext
|
||||||
|
? 'InterstitialDiscover'
|
||||||
|
: 'InterstitialProfile',
|
||||||
|
position: index,
|
||||||
|
suggestedDid: profile.actor.did,
|
||||||
|
recId: profile.recId,
|
||||||
|
});
|
||||||
|
}, style: [
|
||||||
|
a.absolute,
|
||||||
|
a.z_10,
|
||||||
|
a.p_xs,
|
||||||
|
{ top: -4, right: -4 },
|
||||||
|
], children: function (_a) {
|
||||||
|
var dismissHovered = _a.hovered, dismissPressed = _a.pressed;
|
||||||
|
return (_jsx(X, { size: "xs", fill: dismissHovered || dismissPressed
|
||||||
|
? t.atoms.text.color
|
||||||
|
: t.atoms.text_contrast_medium.color }));
|
||||||
|
} })), _jsxs(View, { style: [
|
||||||
|
a.flex_col,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.pb_sm,
|
||||||
|
a.mb_auto,
|
||||||
|
], children: [_jsx(ProfileCard.Avatar, { profile: profile.actor, moderationOpts: moderationOpts, disabledPreview: true, size: 88 }), _jsxs(View, { style: [a.flex_col, a.align_center, a.max_w_full], children: [_jsx(ProfileCard.Name, { profile: profile.actor, moderationOpts: moderationOpts }), _jsx(ProfileCard.Description, { profile: profile.actor, numberOfLines: 2, style: [
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
a.text_center,
|
||||||
|
a.text_xs,
|
||||||
|
] })] })] }), _jsx(ProfileCard.FollowButton, { profile: profile.actor, moderationOpts: moderationOpts, logContext: "FeedInterstitial", withIcon: false, style: [a.rounded_sm], onFollow: function () {
|
||||||
|
ax.metric('suggestedUser:follow', {
|
||||||
|
logContext: isFeedContext
|
||||||
|
? 'InterstitialDiscover'
|
||||||
|
: 'InterstitialProfile',
|
||||||
|
location: 'Card',
|
||||||
|
recId: profile.recId,
|
||||||
|
position: index,
|
||||||
|
suggestedDid: profile.actor.did,
|
||||||
|
category: null,
|
||||||
|
});
|
||||||
|
} })] }) }));
|
||||||
|
} }) }, profile.actor.did)); });
|
||||||
|
// Use totalProfileCount (before dismissals) for minLength check on initial render.
|
||||||
|
var profileCountForMinCheck = totalProfileCount !== null && totalProfileCount !== void 0 ? totalProfileCount : profiles.length;
|
||||||
|
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
|
||||||
|
ax.logger.debug("Not enough profiles to show suggested follows");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (_jsxs(View, { ref: containerRef, style: [
|
||||||
|
!isProfileHeaderContext && a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: [_jsxs(View, { style: [
|
||||||
|
a.px_lg,
|
||||||
|
a.pt_md,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
], pointerEvents: IS_IOS ? 'auto' : 'box-none', children: [_jsx(Text, { style: [a.text_sm, a.font_semi_bold, t.atoms.text], children: _jsx(Trans, { children: "Suggested for you" }) }), !isProfileHeaderContext && (_jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["See more suggested profiles"], ["See more suggested profiles"])))), onPress: function () {
|
||||||
|
followDialogControl.open();
|
||||||
|
ax.metric('suggestedUser:seeMore', {
|
||||||
|
logContext: isFeedContext ? 'Explore' : 'Profile',
|
||||||
|
});
|
||||||
|
}, children: function (_a) {
|
||||||
|
var hovered = _a.hovered;
|
||||||
|
return (_jsx(Text, { style: [
|
||||||
|
a.text_sm,
|
||||||
|
{ color: t.palette.primary_500 },
|
||||||
|
hovered &&
|
||||||
|
web({
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
textDecorationColor: t.palette.primary_500,
|
||||||
|
}),
|
||||||
|
], children: _jsx(Trans, { children: "See more" }) }));
|
||||||
|
} }))] }), _jsx(FollowDialogWithoutGuide, { control: followDialogControl }), _jsx(LayoutAnimationConfig, { skipExiting: true, skipEntering: true, children: gtMobile ? (_jsx(View, { style: [a.p_lg, a.pt_md], children: _jsx(View, { style: [a.flex_1, a.flex_row, a.flex_wrap, a.gap_md], children: content }) })) : (_jsx(BlockDrawerGesture, { children: _jsxs(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: [a.p_lg, a.pt_md, a.flex_row, a.gap_md], snapToInterval: MOBILE_CARD_WIDTH + a.gap_md.gap, decelerationRate: "fast", children: [content, !isProfileHeaderContext && (_jsx(SeeMoreSuggestedProfilesCard, { onPress: function () {
|
||||||
|
followDialogControl.open();
|
||||||
|
ax.metric('suggestedUser:seeMore', {
|
||||||
|
logContext: 'Explore',
|
||||||
|
});
|
||||||
|
} }))] }) })) })] }));
|
||||||
|
}
|
||||||
|
function SeeMoreSuggestedProfilesCard(_a) {
|
||||||
|
var onPress = _a.onPress;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
return (_jsxs(Button, { label: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Browse more accounts"], ["Browse more accounts"])))), onPress: onPress, style: [
|
||||||
|
a.flex_col,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_lg,
|
||||||
|
{ width: FINAL_CARD_WIDTH },
|
||||||
|
], children: [_jsx(ButtonIcon, { icon: ArrowRight, size: "lg" }), _jsx(ButtonText, { style: [a.text_md, a.font_medium, a.leading_snug, a.text_center], children: _jsx(Trans, { children: "See more" }) })] }));
|
||||||
|
}
|
||||||
|
var numFeedsToDisplay = 3;
|
||||||
|
export function SuggestedFeeds() {
|
||||||
|
var t = useTheme();
|
||||||
|
var ax = useAnalytics();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var _a = useGetPopularFeedsQuery({
|
||||||
|
limit: numFeedsToDisplay,
|
||||||
|
}), data = _a.data, isLoading = _a.isLoading, error = _a.error;
|
||||||
|
var navigation = useNavigation();
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
var feeds = useMemo(function () {
|
||||||
|
var items = [];
|
||||||
|
if (!data)
|
||||||
|
return items;
|
||||||
|
for (var _i = 0, _a = data.pages; _i < _a.length; _i++) {
|
||||||
|
var page = _a[_i];
|
||||||
|
for (var _b = 0, _c = page.feeds; _b < _c.length; _b++) {
|
||||||
|
var feed = _c[_b];
|
||||||
|
items.push(feed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}, [data]);
|
||||||
|
var content = isLoading ? (Array(numFeedsToDisplay)
|
||||||
|
.fill(0)
|
||||||
|
.map(function (_, i) { return _jsx(SuggestedFeedsCardPlaceholder, {}, i); })) : error || !feeds ? null : (_jsx(_Fragment, { children: feeds.slice(0, numFeedsToDisplay).map(function (feed) { return (_jsx(FeedCard.Link, { view: feed, onPress: function () {
|
||||||
|
ax.metric('feed:interstitial:feedCard:press', {});
|
||||||
|
}, children: function (_a) {
|
||||||
|
var hovered = _a.hovered, pressed = _a.pressed;
|
||||||
|
return (_jsx(CardOuter, { style: [(hovered || pressed) && t.atoms.border_contrast_high], children: _jsxs(FeedCard.Outer, { children: [_jsxs(FeedCard.Header, { children: [_jsx(FeedCard.Avatar, { src: feed.avatar }), _jsx(FeedCard.TitleAndByline, { title: feed.displayName, creator: feed.creator, uri: feed.uri })] }), _jsx(FeedCard.Description, { description: feed.description, numberOfLines: 3 })] }) }));
|
||||||
|
} }, feed.uri)); }) }));
|
||||||
|
return error ? null : (_jsxs(View, { style: [a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25], children: [_jsxs(View, { style: [a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs], children: [_jsx(Text, { style: [
|
||||||
|
a.flex_1,
|
||||||
|
a.text_lg,
|
||||||
|
a.font_semi_bold,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
], children: _jsx(Trans, { children: "Some other feeds you might like" }) }), _jsx(Hashtag, { fill: t.atoms.text_contrast_low.color })] }), gtMobile ? (_jsxs(View, { style: [a.flex_1, a.px_lg, a.pt_md, a.pb_xl, a.gap_md], children: [content, _jsxs(View, { style: [
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_end,
|
||||||
|
a.align_center,
|
||||||
|
a.pt_xs,
|
||||||
|
a.gap_md,
|
||||||
|
], children: [_jsx(InlineLinkText, { label: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Browse more suggestions"], ["Browse more suggestions"])))), to: "/search", style: [t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Browse more suggestions" }) }), _jsx(ArrowRight, { size: "sm", fill: t.atoms.text_contrast_medium.color })] })] })) : (_jsx(BlockDrawerGesture, { children: _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, snapToInterval: MOBILE_CARD_WIDTH + a.gap_md.gap, decelerationRate: "fast", children: _jsxs(View, { style: [a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md], children: [content, _jsx(Button, { label: _(msg(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Browse more feeds on the Explore page"], ["Browse more feeds on the Explore page"])))), onPress: function () {
|
||||||
|
navigation.navigate('SearchTab');
|
||||||
|
}, style: [a.flex_col], children: _jsx(CardOuter, { children: _jsx(View, { style: [a.flex_1, a.justify_center], children: _jsxs(View, { style: [a.flex_row, a.px_lg], children: [_jsx(Text, { style: [a.pr_xl, a.flex_1, a.leading_snug], children: _jsx(Trans, { children: "Browse more suggestions on the Explore page" }) }), _jsx(ArrowRight, { size: "xl" })] }) }) }) })] }) }) }))] }));
|
||||||
|
}
|
||||||
|
export function ProgressGuide() {
|
||||||
|
var t = useTheme();
|
||||||
|
var gtMobile = useBreakpoints().gtMobile;
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_lg,
|
||||||
|
!gtMobile && { marginTop: 4 },
|
||||||
|
], children: _jsx(ProgressGuideList, {}) }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5;
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { Children, cloneElement, isValidElement, useCallback, useEffect, useMemo, useRef, } from 'react';
|
||||||
|
import { AccessibilityInfo, findNodeHandle, Pressable, Text, View, } from 'react-native';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { useA11y } from '#/state/a11y';
|
||||||
|
/**
|
||||||
|
* Conditionally wraps children in a `FocusTrap` component based on whether
|
||||||
|
* screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
|
||||||
|
* no better option is available.
|
||||||
|
*/
|
||||||
|
export function FocusScope(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var screenReaderEnabled = useA11y().screenReaderEnabled;
|
||||||
|
return screenReaderEnabled ? _jsx(FocusTrap, { children: children }) : children;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* `FocusTrap` is intended as a last-ditch effort to ensure that users keep
|
||||||
|
* focus within a certain section of the app, like an overlay.
|
||||||
|
*
|
||||||
|
* It works by placing "guards" at the start and end of the active content.
|
||||||
|
* Then when the user reaches either of those guards, it will announce that
|
||||||
|
* they have reached the start or end of the content and tell them how to
|
||||||
|
* remain within the active content section.
|
||||||
|
*/
|
||||||
|
function FocusTrap(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var child = useRef(null);
|
||||||
|
/*
|
||||||
|
* Here we add a ref to the first child of this component. This currently
|
||||||
|
* overrides any ref already on that first child, so we throw an error here
|
||||||
|
* to prevent us from ever accidentally doing this.
|
||||||
|
*/
|
||||||
|
var decoratedChildren = useMemo(function () {
|
||||||
|
return Children.toArray(children).map(function (node, i) {
|
||||||
|
if (i === 0 && isValidElement(node)) {
|
||||||
|
var n = node;
|
||||||
|
if (n.props.ref !== undefined) {
|
||||||
|
throw new Error('FocusScope needs to override the ref on its first child.');
|
||||||
|
}
|
||||||
|
return cloneElement(n, __assign(__assign({}, n.props), { ref: child }));
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}, [children]);
|
||||||
|
var focusNode = useCallback(function (ref) {
|
||||||
|
if (!ref)
|
||||||
|
return;
|
||||||
|
var node = findNodeHandle(ref);
|
||||||
|
if (node) {
|
||||||
|
AccessibilityInfo.setAccessibilityFocus(node);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEffect(function () {
|
||||||
|
setTimeout(function () {
|
||||||
|
focusNode(child.current);
|
||||||
|
}, 1e3);
|
||||||
|
}, [focusNode]);
|
||||||
|
return (_jsxs(_Fragment, { children: [_jsx(Pressable, { accessible: true, accessibilityLabel: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["You've reached the start of the active content."], ["You've reached the start of the active content."])))), accessibilityHint: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Please go back, or activate this element to return to the start of the active content."], ["Please go back, or activate this element to return to the start of the active content."])))), accessibilityActions: [{ name: 'activate', label: 'activate' }], onAccessibilityAction: function (event) {
|
||||||
|
switch (event.nativeEvent.actionName) {
|
||||||
|
case 'activate': {
|
||||||
|
focusNode(child.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, children: _jsx(Noop, {}) }), _jsx(View
|
||||||
|
/**
|
||||||
|
* This property traps focus effectively on iOS, but not on Android.
|
||||||
|
*/
|
||||||
|
, {
|
||||||
|
/**
|
||||||
|
* This property traps focus effectively on iOS, but not on Android.
|
||||||
|
*/
|
||||||
|
accessibilityViewIsModal: true, children: decoratedChildren }), _jsx(Pressable, { accessibilityLabel: _(msg(templateObject_3 || (templateObject_3 = __makeTemplateObject(["You've reached the end of the active content."], ["You've reached the end of the active content."])))), accessibilityHint: _(msg(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Please go back, or activate this element to return to the start of the active content."], ["Please go back, or activate this element to return to the start of the active content."])))), accessibilityActions: [{ name: 'activate', label: 'activate' }], onAccessibilityAction: function (event) {
|
||||||
|
switch (event.nativeEvent.actionName) {
|
||||||
|
case 'activate': {
|
||||||
|
focusNode(child.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, children: _jsx(Noop, {}) })] }));
|
||||||
|
}
|
||||||
|
function Noop() {
|
||||||
|
return (_jsx(Text, { accessible: false, style: {
|
||||||
|
height: 1,
|
||||||
|
opacity: 0,
|
||||||
|
}, children: ' ' }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2, templateObject_3, templateObject_4;
|
||||||
@@ -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 }));
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { FullWindowOverlay } from 'react-native-screens';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Fragment as FullWindowOverlay } from 'react';
|
||||||
@@ -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] }));
|
||||||
|
}
|
||||||
@@ -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 }));
|
||||||
|
}
|
||||||
@@ -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] }) }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { View, } from 'react-native';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback';
|
||||||
|
import { DraggableScrollView } from '#/view/com/pager/DraggableScrollView';
|
||||||
|
import { atoms as a, tokens, useTheme, web } from '#/alf';
|
||||||
|
import { transparentifyColor } from '#/alf/util/colorGeneration';
|
||||||
|
import { Button, ButtonIcon } from '#/components/Button';
|
||||||
|
import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft, ArrowRight_Stroke2_Corner0_Rounded as ArrowRight, } from '#/components/icons/Arrow';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { IS_WEB } from '#/env';
|
||||||
|
/**
|
||||||
|
* Tab component that automatically scrolls the selected tab into view - used for interests
|
||||||
|
* in the Find Follows dialog, Explore screen, etc.
|
||||||
|
*/
|
||||||
|
export function InterestTabs(_c) {
|
||||||
|
var onSelectTab = _c.onSelectTab, interests = _c.interests, selectedInterest = _c.selectedInterest, disabled = _c.disabled, interestsDisplayNames = _c.interestsDisplayNames, _d = _c.TabComponent, TabComponent = _d === void 0 ? Tab : _d, contentContainerStyle = _c.contentContainerStyle, _e = _c.gutterWidth, gutterWidth = _e === void 0 ? tokens.space.lg : _e;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var listRef = useRef(null);
|
||||||
|
var _f = useState(0), totalWidth = _f[0], setTotalWidth = _f[1];
|
||||||
|
var _g = useState(0), scrollX = _g[0], setScrollX = _g[1];
|
||||||
|
var _h = useState(0), contentWidth = _h[0], setContentWidth = _h[1];
|
||||||
|
var pendingTabOffsets = useRef([]);
|
||||||
|
var _j = useState([]), tabOffsets = _j[0], setTabOffsets = _j[1];
|
||||||
|
var onInitialLayout = useNonReactiveCallback(function () {
|
||||||
|
var index = interests.indexOf(selectedInterest);
|
||||||
|
scrollIntoViewIfNeeded(index);
|
||||||
|
});
|
||||||
|
useEffect(function () {
|
||||||
|
if (tabOffsets) {
|
||||||
|
onInitialLayout();
|
||||||
|
}
|
||||||
|
}, [tabOffsets, onInitialLayout]);
|
||||||
|
function scrollIntoViewIfNeeded(index) {
|
||||||
|
var _c;
|
||||||
|
var btnLayout = tabOffsets[index];
|
||||||
|
if (!btnLayout)
|
||||||
|
return;
|
||||||
|
(_c = listRef.current) === null || _c === void 0 ? void 0 : _c.scrollTo({
|
||||||
|
// centered
|
||||||
|
x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2),
|
||||||
|
animated: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function handleSelectTab(index) {
|
||||||
|
var tab = interests[index];
|
||||||
|
onSelectTab(tab);
|
||||||
|
scrollIntoViewIfNeeded(index);
|
||||||
|
}
|
||||||
|
function handleTabLayout(index, x, width) {
|
||||||
|
if (!tabOffsets.length) {
|
||||||
|
pendingTabOffsets.current[index] = { x: x, width: width };
|
||||||
|
// not only do we check if the length is equal to the number of interests,
|
||||||
|
// but we also need to ensure that the array isn't sparse. `.filter()`
|
||||||
|
// removes any empty slots from the array
|
||||||
|
if (pendingTabOffsets.current.filter(function (o) { return !!o; }).length === interests.length) {
|
||||||
|
setTabOffsets(pendingTabOffsets.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var canScrollLeft = scrollX > 0;
|
||||||
|
var canScrollRight = Math.ceil(scrollX) < contentWidth - totalWidth;
|
||||||
|
var cleanupRef = useRef(null);
|
||||||
|
function scrollLeft() {
|
||||||
|
if (isContinuouslyScrollingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (listRef.current && canScrollLeft) {
|
||||||
|
var newScrollX = Math.max(0, scrollX - 200);
|
||||||
|
listRef.current.scrollTo({ x: newScrollX, animated: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function scrollRight() {
|
||||||
|
if (isContinuouslyScrollingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (listRef.current && canScrollRight) {
|
||||||
|
var maxScroll = contentWidth - totalWidth;
|
||||||
|
var newScrollX = Math.min(maxScroll, scrollX + 200);
|
||||||
|
listRef.current.scrollTo({ x: newScrollX, animated: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var isContinuouslyScrollingRef = useRef(false);
|
||||||
|
function startContinuousScroll(direction) {
|
||||||
|
// Clear any existing continuous scroll
|
||||||
|
if (cleanupRef.current) {
|
||||||
|
cleanupRef.current();
|
||||||
|
}
|
||||||
|
var holdTimeout = null;
|
||||||
|
var animationFrame = null;
|
||||||
|
var isActive = true;
|
||||||
|
isContinuouslyScrollingRef.current = false;
|
||||||
|
var cleanup = function () {
|
||||||
|
isActive = false;
|
||||||
|
if (holdTimeout)
|
||||||
|
clearTimeout(holdTimeout);
|
||||||
|
if (animationFrame)
|
||||||
|
cancelAnimationFrame(animationFrame);
|
||||||
|
cleanupRef.current = null;
|
||||||
|
// Reset flag after a delay to prevent onPress from firing
|
||||||
|
setTimeout(function () {
|
||||||
|
isContinuouslyScrollingRef.current = false;
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
cleanupRef.current = cleanup;
|
||||||
|
// Start continuous scrolling after hold delay
|
||||||
|
holdTimeout = setTimeout(function () {
|
||||||
|
if (!isActive)
|
||||||
|
return;
|
||||||
|
isContinuouslyScrollingRef.current = true;
|
||||||
|
var currentScrollPosition = scrollX;
|
||||||
|
var scroll = function () {
|
||||||
|
if (!isActive || !listRef.current)
|
||||||
|
return;
|
||||||
|
var scrollAmount = 3;
|
||||||
|
var maxScroll = contentWidth - totalWidth;
|
||||||
|
var newScrollX;
|
||||||
|
var canContinue = false;
|
||||||
|
if (direction === 'left' && currentScrollPosition > 0) {
|
||||||
|
newScrollX = Math.max(0, currentScrollPosition - scrollAmount);
|
||||||
|
canContinue = newScrollX > 0;
|
||||||
|
}
|
||||||
|
else if (direction === 'right' && currentScrollPosition < maxScroll) {
|
||||||
|
newScrollX = Math.min(maxScroll, currentScrollPosition + scrollAmount);
|
||||||
|
canContinue = newScrollX < maxScroll;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentScrollPosition = newScrollX;
|
||||||
|
listRef.current.scrollTo({ x: newScrollX, animated: false });
|
||||||
|
if (canContinue && isActive) {
|
||||||
|
animationFrame = requestAnimationFrame(scroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
scroll();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
function stopContinuousScroll() {
|
||||||
|
if (cleanupRef.current) {
|
||||||
|
cleanupRef.current();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(function () {
|
||||||
|
return function () {
|
||||||
|
if (cleanupRef.current) {
|
||||||
|
cleanupRef.current();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (_jsxs(View, { style: [a.relative, a.flex_row], children: [_jsx(DraggableScrollView, { ref: listRef, contentContainerStyle: [
|
||||||
|
a.gap_sm,
|
||||||
|
{ paddingHorizontal: gutterWidth },
|
||||||
|
contentContainerStyle,
|
||||||
|
], showsHorizontalScrollIndicator: false, decelerationRate: "fast", snapToOffsets: tabOffsets.filter(function (o) { return !!o; }).length === interests.length
|
||||||
|
? tabOffsets.map(function (o) { return o.x - tokens.space.xl; })
|
||||||
|
: undefined, onLayout: function (evt) { return setTotalWidth(evt.nativeEvent.layout.width); }, onContentSizeChange: function (width) { return setContentWidth(width); }, onScroll: function (evt) {
|
||||||
|
var newScrollX = evt.nativeEvent.contentOffset.x;
|
||||||
|
setScrollX(newScrollX);
|
||||||
|
}, scrollEventThrottle: 16, children: interests.map(function (interest, i) {
|
||||||
|
var active = interest === selectedInterest && !disabled;
|
||||||
|
return (_jsx(TabComponent, { onSelectTab: handleSelectTab, active: active, index: i, interest: interest, interestsDisplayName: interestsDisplayNames[interest], onLayout: handleTabLayout }, interest));
|
||||||
|
}) }), IS_WEB && canScrollLeft && (_jsx(View, { style: [
|
||||||
|
a.absolute,
|
||||||
|
a.top_0,
|
||||||
|
a.left_0,
|
||||||
|
a.bottom_0,
|
||||||
|
a.justify_center,
|
||||||
|
{ paddingLeft: gutterWidth },
|
||||||
|
a.pr_md,
|
||||||
|
a.z_10,
|
||||||
|
web({
|
||||||
|
background: "linear-gradient(to right, ".concat(t.atoms.bg.backgroundColor, " 0%, ").concat(t.atoms.bg.backgroundColor, " 70%, ").concat(transparentifyColor(t.atoms.bg.backgroundColor, 0), " 100%)"),
|
||||||
|
}),
|
||||||
|
], children: _jsx(Button, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Scroll left"], ["Scroll left"])))), onPress: scrollLeft, onPressIn: function () { return startContinuousScroll('left'); }, onPressOut: stopContinuousScroll, color: "secondary", size: "small", style: [
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg,
|
||||||
|
a.h_full,
|
||||||
|
a.aspect_square,
|
||||||
|
a.rounded_full,
|
||||||
|
], children: _jsx(ButtonIcon, { icon: ArrowLeft }) }) })), IS_WEB && canScrollRight && (_jsx(View, { style: [
|
||||||
|
a.absolute,
|
||||||
|
a.top_0,
|
||||||
|
a.right_0,
|
||||||
|
a.bottom_0,
|
||||||
|
a.justify_center,
|
||||||
|
{ paddingRight: gutterWidth },
|
||||||
|
a.pl_md,
|
||||||
|
a.z_10,
|
||||||
|
web({
|
||||||
|
background: "linear-gradient(to left, ".concat(t.atoms.bg.backgroundColor, " 0%, ").concat(t.atoms.bg.backgroundColor, " 70%, ").concat(transparentifyColor(t.atoms.bg.backgroundColor, 0), " 100%)"),
|
||||||
|
}),
|
||||||
|
], children: _jsx(Button, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Scroll right"], ["Scroll right"])))), onPress: scrollRight, onPressIn: function () { return startContinuousScroll('right'); }, onPressOut: stopContinuousScroll, color: "secondary", size: "small", style: [
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg,
|
||||||
|
a.h_full,
|
||||||
|
a.aspect_square,
|
||||||
|
a.rounded_full,
|
||||||
|
], children: _jsx(ButtonIcon, { icon: ArrowRight }) }) }))] }));
|
||||||
|
}
|
||||||
|
function Tab(_c) {
|
||||||
|
var onSelectTab = _c.onSelectTab, interest = _c.interest, active = _c.active, index = _c.index, interestsDisplayName = _c.interestsDisplayName, onLayout = _c.onLayout;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var label = active
|
||||||
|
? _(msg({
|
||||||
|
message: "\"".concat(interestsDisplayName, "\" category (active)"),
|
||||||
|
comment: 'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is currently selected.',
|
||||||
|
}))
|
||||||
|
: _(msg({
|
||||||
|
message: "Select \"".concat(interestsDisplayName, "\" category"),
|
||||||
|
comment: 'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is not currently active and can be selected.',
|
||||||
|
}));
|
||||||
|
return (_jsx(View, { onLayout: function (e) {
|
||||||
|
return onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width);
|
||||||
|
}, children: _jsx(Button, { label: label, onPress: function () { return onSelectTab(index); },
|
||||||
|
// disable focus ring, we handle it
|
||||||
|
style: web({ outline: 'none' }), children: function (_c) {
|
||||||
|
var hovered = _c.hovered, pressed = _c.pressed, focused = _c.focused;
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
a.rounded_full,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_sm,
|
||||||
|
a.border,
|
||||||
|
active || hovered || pressed
|
||||||
|
? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
|
||||||
|
: focused
|
||||||
|
? {
|
||||||
|
borderColor: t.palette.primary_300,
|
||||||
|
backgroundColor: t.palette.primary_25,
|
||||||
|
}
|
||||||
|
: [t.atoms.bg, t.atoms.border_contrast_low],
|
||||||
|
], children: _jsx(Text, { style: [
|
||||||
|
a.font_medium,
|
||||||
|
active || hovered || pressed
|
||||||
|
? t.atoms.text
|
||||||
|
: t.atoms.text_contrast_medium,
|
||||||
|
], children: interestsDisplayName }) }));
|
||||||
|
} }) }, interest));
|
||||||
|
}
|
||||||
|
export function boostInterests(boosts) {
|
||||||
|
return function (_a, _b) {
|
||||||
|
var _c, _d;
|
||||||
|
var indexA = (_c = boosts === null || boosts === void 0 ? void 0 : boosts.indexOf(_a)) !== null && _c !== void 0 ? _c : -1;
|
||||||
|
var indexB = (_d = boosts === null || boosts === void 0 ? void 0 : boosts.indexOf(_b)) !== null && _d !== void 0 ? _d : -1;
|
||||||
|
var rankA = indexA === -1 ? Infinity : indexA;
|
||||||
|
var rankB = indexB === -1 ? Infinity : indexB;
|
||||||
|
return rankA - rankB;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { Fragment, useMemo } from 'react';
|
||||||
|
import { Text as RNText } from 'react-native';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { getDefaultCountry, INTERNATIONAL_TELEPHONE_CODES, } from '#/lib/international-telephone-codes';
|
||||||
|
import { regionName } from '#/locale/helpers';
|
||||||
|
import { atoms as a, web } from '#/alf';
|
||||||
|
import * as Select from '#/components/Select';
|
||||||
|
import { IS_WEB } from '#/env';
|
||||||
|
import { useGeolocation } from '#/geolocation';
|
||||||
|
/**
|
||||||
|
* Country picker for a phone number input
|
||||||
|
*
|
||||||
|
* Pro tip: you can use `location?.countryCode` from `useGeolocationStatus()`
|
||||||
|
* to set a default value.
|
||||||
|
*/
|
||||||
|
export function InternationalPhoneCodeSelect(_a) {
|
||||||
|
var value = _a.value, onChange = _a.onChange;
|
||||||
|
var _b = useLingui(), _ = _b._, i18n = _b.i18n;
|
||||||
|
var location = useGeolocation();
|
||||||
|
var defaultCountry = useMemo(function () {
|
||||||
|
return getDefaultCountry(location);
|
||||||
|
}, [location]);
|
||||||
|
var items = useMemo(function () {
|
||||||
|
return (Object.entries(INTERNATIONAL_TELEPHONE_CODES)
|
||||||
|
.map(function (_a) {
|
||||||
|
var value = _a[0], _b = _a[1], code = _b.code, unicodeFlag = _b.unicodeFlag, svgFlag = _b.svgFlag;
|
||||||
|
var name = regionName(value, i18n.locale);
|
||||||
|
return {
|
||||||
|
value: value,
|
||||||
|
name: name,
|
||||||
|
code: code,
|
||||||
|
label: "".concat(name, " ").concat(code),
|
||||||
|
unicodeFlag: unicodeFlag,
|
||||||
|
svgFlag: svgFlag,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// boost the default value to the top, then sort by name
|
||||||
|
.sort(function (a, b) {
|
||||||
|
if (a.value === defaultCountry)
|
||||||
|
return -1;
|
||||||
|
if (b.value === defaultCountry)
|
||||||
|
return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}));
|
||||||
|
}, [i18n.locale, defaultCountry]);
|
||||||
|
var selected = useMemo(function () {
|
||||||
|
return items.find(function (item) { return item.value === value; });
|
||||||
|
}, [value, items]);
|
||||||
|
return (_jsxs(Select.Root, { value: value, onValueChange: onChange, children: [_jsxs(Select.Trigger, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Select telephone code"], ["Select telephone code"])))), children: [_jsx(Select.ValueText, { placeholder: "+...", webOverrideValue: selected, children: function (selected) { return (_jsxs(_Fragment, { children: [_jsx(Flag, __assign({}, selected)), selected.code] })); } }), _jsx(Select.Icon, {})] }), _jsx(Select.Content, { label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Country code"], ["Country code"])))), items: items, renderItem: function (item) { return (_jsxs(Fragment, { children: [_jsxs(Select.Item, { value: item.value, label: item.label, children: [_jsx(Select.ItemIndicator, {}), _jsxs(Select.ItemText, { style: [a.flex_1], emoji: true, children: [IS_WEB ? _jsx(Flag, __assign({}, item)) : item.unicodeFlag + ' ', item.name] }), _jsxs(Select.ItemText, { style: [a.text_right], children: [' ', item.code] })] }), item.value === defaultCountry && _jsx(Select.Separator, {})] }, item.value)); } })] }));
|
||||||
|
}
|
||||||
|
function Flag(_a) {
|
||||||
|
var unicodeFlag = _a.unicodeFlag, svgFlag = _a.svgFlag;
|
||||||
|
if (IS_WEB) {
|
||||||
|
return (_jsx(Image, { source: svgFlag, style: [
|
||||||
|
a.rounded_2xs,
|
||||||
|
{ height: 13, aspectRatio: 4 / 3, marginRight: 6 },
|
||||||
|
web({ verticalAlign: 'bottom' }),
|
||||||
|
], accessibilityIgnoresInvertColors: true }));
|
||||||
|
}
|
||||||
|
return _jsx(RNText, { style: [{ lineHeight: 21 }], children: unicodeFlag + ' ' });
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function KeepAwake() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||||
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { moderateProfile, } from '@atproto/api';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import { makeProfileLink } from '#/lib/routes/links';
|
||||||
|
import { sanitizeDisplayName } from '#/lib/strings/display-names';
|
||||||
|
import { UserAvatar } from '#/view/com/util/UserAvatar';
|
||||||
|
import { atoms as a, useTheme } from '#/alf';
|
||||||
|
import { Link } from '#/components/Link';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
var AVI_SIZE = 30;
|
||||||
|
var AVI_SIZE_SMALL = 20;
|
||||||
|
var AVI_BORDER = 1;
|
||||||
|
/**
|
||||||
|
* Shared logic to determine if `KnownFollowers` should be shown.
|
||||||
|
*
|
||||||
|
* Checks the # of actual returned users instead of the `count` value, because
|
||||||
|
* `count` includes blocked users and `followers` does not.
|
||||||
|
*/
|
||||||
|
export function shouldShowKnownFollowers(knownFollowers) {
|
||||||
|
return knownFollowers && knownFollowers.followers.length > 0;
|
||||||
|
}
|
||||||
|
export function KnownFollowers(_a) {
|
||||||
|
var _b;
|
||||||
|
var profile = _a.profile, moderationOpts = _a.moderationOpts, onLinkPress = _a.onLinkPress, minimal = _a.minimal, showIfEmpty = _a.showIfEmpty;
|
||||||
|
var cache = React.useRef(new Map());
|
||||||
|
/*
|
||||||
|
* Results for `knownFollowers` are not sorted consistently, so when
|
||||||
|
* revalidating we can see a flash of this data updating. This cache prevents
|
||||||
|
* this happening for screens that remain in memory. When pushing a new
|
||||||
|
* screen, or once this one is popped, this cache is empty, so new data is
|
||||||
|
* displayed.
|
||||||
|
*/
|
||||||
|
if (((_b = profile.viewer) === null || _b === void 0 ? void 0 : _b.knownFollowers) && !cache.current.has(profile.did)) {
|
||||||
|
cache.current.set(profile.did, profile.viewer.knownFollowers);
|
||||||
|
}
|
||||||
|
var cachedKnownFollowers = cache.current.get(profile.did);
|
||||||
|
if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) {
|
||||||
|
return (_jsx(KnownFollowersInner, { profile: profile, cachedKnownFollowers: cachedKnownFollowers, moderationOpts: moderationOpts, onLinkPress: onLinkPress, minimal: minimal, showIfEmpty: showIfEmpty }));
|
||||||
|
}
|
||||||
|
return _jsx(EmptyFallback, { show: showIfEmpty });
|
||||||
|
}
|
||||||
|
function KnownFollowersInner(_a) {
|
||||||
|
var profile = _a.profile, moderationOpts = _a.moderationOpts, cachedKnownFollowers = _a.cachedKnownFollowers, onLinkPress = _a.onLinkPress, minimal = _a.minimal, showIfEmpty = _a.showIfEmpty;
|
||||||
|
var t = useTheme();
|
||||||
|
var _ = useLingui()._;
|
||||||
|
var textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium];
|
||||||
|
var slice = cachedKnownFollowers.followers.slice(0, 3).map(function (f) {
|
||||||
|
var moderation = moderateProfile(f, moderationOpts);
|
||||||
|
return {
|
||||||
|
profile: __assign(__assign({}, f), { displayName: sanitizeDisplayName(f.displayName || f.handle, moderation.ui('displayName')) }),
|
||||||
|
moderation: moderation,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Does not have blocks applied. Always >= slices.length
|
||||||
|
var serverCount = cachedKnownFollowers.count;
|
||||||
|
/*
|
||||||
|
* We check above too, but here for clarity and a reminder to _check for
|
||||||
|
* valid indices_
|
||||||
|
*/
|
||||||
|
if (slice.length === 0)
|
||||||
|
return _jsx(EmptyFallback, { show: showIfEmpty });
|
||||||
|
var SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE;
|
||||||
|
return (_jsx(Link, { label: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Press to view followers of this account that you also follow"], ["Press to view followers of this account that you also follow"])))), onPress: onLinkPress, to: makeProfileLink(profile, 'known-followers'), style: [
|
||||||
|
a.max_w_full,
|
||||||
|
a.flex_row,
|
||||||
|
minimal ? a.gap_sm : a.gap_md,
|
||||||
|
a.align_center,
|
||||||
|
{ marginLeft: -AVI_BORDER },
|
||||||
|
], children: function (_a) {
|
||||||
|
var hovered = _a.hovered, pressed = _a.pressed;
|
||||||
|
return (_jsxs(_Fragment, { children: [_jsx(View, { style: [
|
||||||
|
a.flex_row,
|
||||||
|
{
|
||||||
|
height: SIZE,
|
||||||
|
},
|
||||||
|
pressed && {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
], children: slice.map(function (_a, i) {
|
||||||
|
var _b;
|
||||||
|
var prof = _a.profile, moderation = _a.moderation;
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
borderWidth: AVI_BORDER,
|
||||||
|
borderColor: t.atoms.bg.backgroundColor,
|
||||||
|
width: SIZE + AVI_BORDER * 2,
|
||||||
|
height: SIZE + AVI_BORDER * 2,
|
||||||
|
zIndex: AVI_BORDER - i,
|
||||||
|
marginLeft: i > 0 ? -8 : 0,
|
||||||
|
},
|
||||||
|
], children: _jsx(UserAvatar, { size: SIZE, avatar: prof.avatar, moderation: moderation.ui('avatar'), type: ((_b = prof.associated) === null || _b === void 0 ? void 0 : _b.labeler) ? 'labeler' : 'user', noBorder: true }) }, prof.did));
|
||||||
|
}) }), _jsx(Text, { style: [
|
||||||
|
a.flex_shrink,
|
||||||
|
textStyle,
|
||||||
|
hovered && {
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
textDecorationColor: t.atoms.text_contrast_medium.color,
|
||||||
|
},
|
||||||
|
pressed && {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
], numberOfLines: 2, children: slice.length >= 2 ? (
|
||||||
|
// 2-n followers, including blocks
|
||||||
|
serverCount > 2 ? (_jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did), ",", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[1].profile.displayName }, slice[1].profile.did), ", and", ' ', _jsx(Plural, { value: serverCount - 2, one: "# other", other: "# others" })] }) // only 2
|
||||||
|
) : (_jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did), ' ', "and", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[1].profile.displayName }, slice[1].profile.did)] }))) : serverCount > 1 ? (
|
||||||
|
// 1-n followers, including blocks
|
||||||
|
_jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did), ' ', "and", ' ', _jsx(Plural, { value: serverCount - 1, one: "# other", other: "# others" })] })) : (
|
||||||
|
// only 1
|
||||||
|
_jsxs(Trans, { children: ["Followed by", ' ', _jsx(Text, { emoji: true, style: textStyle, children: slice[0].profile.displayName }, slice[0].profile.did)] })) })] }));
|
||||||
|
} }));
|
||||||
|
}
|
||||||
|
function EmptyFallback(_a) {
|
||||||
|
var show = _a.show;
|
||||||
|
var t = useTheme();
|
||||||
|
if (!show)
|
||||||
|
return null;
|
||||||
|
return (_jsx(Text, { style: [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium], children: _jsx(Trans, { children: "Not followed by anyone you're following" }) }));
|
||||||
|
}
|
||||||
|
var templateObject_1;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
||||||
|
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
||||||
|
return cooked;
|
||||||
|
};
|
||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import { getLabelingServiceTitle } from '#/lib/moderation';
|
||||||
|
import { sanitizeHandle } from '#/lib/strings/handles';
|
||||||
|
import { useLabelerInfoQuery } from '#/state/queries/labeler';
|
||||||
|
import { UserAvatar } from '#/view/com/util/UserAvatar';
|
||||||
|
import { atoms as a, useTheme } from '#/alf';
|
||||||
|
import { Flag_Stroke2_Corner0_Rounded as Flag } from '#/components/icons/Flag';
|
||||||
|
import { Link as InternalLink } from '#/components/Link';
|
||||||
|
import { RichText } from '#/components/RichText';
|
||||||
|
import { Text } from '#/components/Typography';
|
||||||
|
import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRight } from '../icons/Chevron';
|
||||||
|
export function Outer(_a) {
|
||||||
|
var children = _a.children, style = _a.style;
|
||||||
|
return (_jsx(View, { style: [
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_md,
|
||||||
|
a.w_full,
|
||||||
|
a.p_lg,
|
||||||
|
a.pr_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
style,
|
||||||
|
], children: children }));
|
||||||
|
}
|
||||||
|
export function Avatar(_a) {
|
||||||
|
var avatar = _a.avatar;
|
||||||
|
return _jsx(UserAvatar, { type: "labeler", size: 40, avatar: avatar });
|
||||||
|
}
|
||||||
|
export function Title(_a) {
|
||||||
|
var value = _a.value;
|
||||||
|
return (_jsx(Text, { emoji: true, style: [a.text_md, a.font_semi_bold, a.leading_tight], children: value }));
|
||||||
|
}
|
||||||
|
export function Description(_a) {
|
||||||
|
var value = _a.value, handle = _a.handle;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
return value ? (_jsx(Text, { numberOfLines: 2, children: _jsx(RichText, { value: value }) })) : (_jsx(Text, { emoji: true, style: [a.leading_snug], children: _(msg(templateObject_1 || (templateObject_1 = __makeTemplateObject(["By ", ""], ["By ", ""])), sanitizeHandle(handle, '@'))) }));
|
||||||
|
}
|
||||||
|
export function RegionalNotice() {
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(View, { style: [
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_xs,
|
||||||
|
a.pt_2xs,
|
||||||
|
{ marginLeft: -2 },
|
||||||
|
], children: [_jsx(Flag, { fill: t.atoms.text_contrast_low.color, size: "sm" }), _jsx(Text, { style: [a.italic, a.leading_snug], children: _jsx(Trans, { children: "Required in your region" }) })] }));
|
||||||
|
}
|
||||||
|
export function LikeCount(_a) {
|
||||||
|
var likeCount = _a.likeCount;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsx(Text, { style: [
|
||||||
|
a.mt_sm,
|
||||||
|
a.text_sm,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
{ fontWeight: '600' },
|
||||||
|
], children: _jsxs(Trans, { children: ["Liked by ", _jsx(Plural, { value: likeCount, one: "# user", other: "# users" })] }) }));
|
||||||
|
}
|
||||||
|
export function Content(_a) {
|
||||||
|
var children = _a.children;
|
||||||
|
var t = useTheme();
|
||||||
|
return (_jsxs(View, { style: [
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_md,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
], children: [_jsx(View, { style: [a.gap_2xs, a.flex_1], children: children }), _jsx(ChevronRight, { size: "md", style: [a.z_10, t.atoms.text_contrast_low] })] }));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The canonical view for a labeling service. Use this or compose your own.
|
||||||
|
*/
|
||||||
|
export function Default(_a) {
|
||||||
|
var labeler = _a.labeler, style = _a.style;
|
||||||
|
return (_jsxs(Outer, { style: style, children: [_jsx(Avatar, { avatar: labeler.creator.avatar }), _jsxs(Content, { children: [_jsx(Title, { value: getLabelingServiceTitle({
|
||||||
|
displayName: labeler.creator.displayName,
|
||||||
|
handle: labeler.creator.handle,
|
||||||
|
}) }), _jsx(Description, { value: labeler.creator.description, handle: labeler.creator.handle }), labeler.likeCount ? _jsx(LikeCount, { likeCount: labeler.likeCount }) : null] })] }));
|
||||||
|
}
|
||||||
|
export function Link(_a) {
|
||||||
|
var children = _a.children, labeler = _a.labeler;
|
||||||
|
var _ = useLingui()._;
|
||||||
|
return (_jsx(InternalLink, { to: {
|
||||||
|
screen: 'Profile',
|
||||||
|
params: {
|
||||||
|
name: labeler.creator.did,
|
||||||
|
},
|
||||||
|
}, label: _(msg(templateObject_2 || (templateObject_2 = __makeTemplateObject(["View the labeling service provided by @", ""], ["View the labeling service provided by @", ""])), labeler.creator.handle)), children: children }));
|
||||||
|
}
|
||||||
|
// TODO not finished yet
|
||||||
|
export function DefaultSkeleton() {
|
||||||
|
return (_jsx(View, { children: _jsx(Text, { children: "Loading" }) }));
|
||||||
|
}
|
||||||
|
export function Loader(_a) {
|
||||||
|
var did = _a.did, _b = _a.loading, LoadingComponent = _b === void 0 ? DefaultSkeleton : _b, ErrorComponent = _a.error, Component = _a.component;
|
||||||
|
var _c = useLabelerInfoQuery({ did: did }), isLoading = _c.isLoading, data = _c.data, error = _c.error;
|
||||||
|
return isLoading ? (LoadingComponent ? (_jsx(LoadingComponent, {})) : null) : error || !data ? (ErrorComponent ? (_jsx(ErrorComponent, { error: (error === null || error === void 0 ? void 0 : error.message) || 'Unknown error' })) : null) : (_jsx(Component, { labeler: data }));
|
||||||
|
}
|
||||||
|
var templateObject_1, templateObject_2;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user