diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-05-25 21:17:11 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2023-05-25 21:17:11 -0500 |
commit | 7b6948e6171b448e271f0564efd1f186ccadb9b8 (patch) | |
tree | e0d1b6e9f9c863cbff53f8832fd55d03cb670a83 /src/lib | |
parent | 15c1b6ee157471807a723161066ba4ce5e12c0b5 (diff) | |
parent | e832352e9844002408b45291396a3c495be23276 (diff) | |
download | voidsky-7b6948e6171b448e271f0564efd1f186ccadb9b8.tar.zst |
Merge branch 'custom-algos' into main
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/api/feed-manip.ts | 3 | ||||
-rw-r--r-- | src/lib/api/index.ts | 93 | ||||
-rw-r--r-- | src/lib/async/revertible.ts | 16 | ||||
-rw-r--r-- | src/lib/constants.ts | 43 | ||||
-rw-r--r-- | src/lib/haptics.ts | 40 | ||||
-rw-r--r-- | src/lib/hooks/useCustomFeed.ts | 27 | ||||
-rw-r--r-- | src/lib/hooks/useDraggableScrollView.ts | 84 | ||||
-rw-r--r-- | src/lib/hooks/useNavigationTabState.ts | 4 | ||||
-rw-r--r-- | src/lib/hooks/useOnMainScroll.ts | 61 | ||||
-rw-r--r-- | src/lib/icons.tsx | 80 | ||||
-rw-r--r-- | src/lib/link-meta/bsky.ts | 27 | ||||
-rw-r--r-- | src/lib/merge-refs.ts | 27 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 12 | ||||
-rw-r--r-- | src/lib/strings/url-helpers.ts | 12 | ||||
-rw-r--r-- | src/lib/styles.ts | 9 |
15 files changed, 471 insertions, 67 deletions
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 7f31fb292..035b36096 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -143,9 +143,6 @@ export class FeedTuner { } } - // sort by slice roots' timestamps - slices.sort((a, b) => b.ts.localeCompare(a.ts)) - for (const slice of slices) { for (const item of slice.items) { this.seenUris.add(item.post.uri) diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 3877b3ef7..81b61a444 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -18,6 +18,7 @@ export interface ExternalEmbedDraft { uri: string isLoading: boolean meta?: LinkMeta + embed?: AppBskyEmbedRecord.Main localThumb?: ImageModel } @@ -135,40 +136,54 @@ export async function post(store: RootStoreModel, opts: PostOpts) { } if (opts.extLink && !opts.images?.length) { - let thumb - if (opts.extLink.localThumb) { - opts.onStateChange?.('Uploading link thumbnail...') - let encoding - if (opts.extLink.localThumb.mime) { - encoding = opts.extLink.localThumb.mime - } else if (opts.extLink.localThumb.path.endsWith('.png')) { - encoding = 'image/png' - } else if ( - opts.extLink.localThumb.path.endsWith('.jpeg') || - opts.extLink.localThumb.path.endsWith('.jpg') - ) { - encoding = 'image/jpeg' - } else { - store.log.warn( - 'Unexpected image format for thumbnail, skipping', - opts.extLink.localThumb.path, - ) - } - if (encoding) { - const thumbUploadRes = await uploadBlob( - store, - opts.extLink.localThumb.path, - encoding, - ) - thumb = thumbUploadRes.data.blob + if (opts.extLink.embed) { + embed = opts.extLink.embed + } else { + let thumb + if (opts.extLink.localThumb) { + opts.onStateChange?.('Uploading link thumbnail...') + let encoding + if (opts.extLink.localThumb.mime) { + encoding = opts.extLink.localThumb.mime + } else if (opts.extLink.localThumb.path.endsWith('.png')) { + encoding = 'image/png' + } else if ( + opts.extLink.localThumb.path.endsWith('.jpeg') || + opts.extLink.localThumb.path.endsWith('.jpg') + ) { + encoding = 'image/jpeg' + } else { + store.log.warn( + 'Unexpected image format for thumbnail, skipping', + opts.extLink.localThumb.path, + ) + } + if (encoding) { + const thumbUploadRes = await uploadBlob( + store, + opts.extLink.localThumb.path, + encoding, + ) + thumb = thumbUploadRes.data.blob + } } - } - if (opts.quote) { - embed = { - $type: 'app.bsky.embed.recordWithMedia', - record: embed, - media: { + if (opts.quote) { + embed = { + $type: 'app.bsky.embed.recordWithMedia', + record: embed, + media: { + $type: 'app.bsky.embed.external', + external: { + uri: opts.extLink.uri, + title: opts.extLink.meta?.title || '', + description: opts.extLink.meta?.description || '', + thumb, + }, + } as AppBskyEmbedExternal.Main, + } as AppBskyEmbedRecordWithMedia.Main + } else { + embed = { $type: 'app.bsky.embed.external', external: { uri: opts.extLink.uri, @@ -176,18 +191,8 @@ export async function post(store: RootStoreModel, opts: PostOpts) { description: opts.extLink.meta?.description || '', thumb, }, - } as AppBskyEmbedExternal.Main, - } as AppBskyEmbedRecordWithMedia.Main - } else { - embed = { - $type: 'app.bsky.embed.external', - external: { - uri: opts.extLink.uri, - title: opts.extLink.meta?.title || '', - description: opts.extLink.meta?.description || '', - thumb, - }, - } as AppBskyEmbedExternal.Main + } as AppBskyEmbedExternal.Main + } } } diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts index 3c8e3e8f9..43383b61e 100644 --- a/src/lib/async/revertible.ts +++ b/src/lib/async/revertible.ts @@ -4,6 +4,22 @@ import set from 'lodash.set' const ongoingActions = new Set<any>() +/** + * This is a TypeScript function that optimistically updates data on the client-side before sending a + * request to the server and rolling back changes if the request fails. + * @param {T} model - The object or record that needs to be updated optimistically. + * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It + * can be used to perform any necessary actions or updates on the model or UI before the server update + * is initiated. + * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server + * update operation. This function is called after the previous state of the model has been recorded + * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate` + * function is called with the result + * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the + * server update is successful. It takes in the response from the server update as its parameter. If + * this parameter is not provided, nothing will happen after the server update. + * @returns A Promise that resolves to `void`. + */ export const updateDataOptimistically = async < T extends Record<string, any>, U, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6d0d4797b..41aab1431 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -94,6 +94,49 @@ export function SUGGESTED_FOLLOWS(serviceUrl: string) { } } +export const STAGING_DEFAULT_FEED = (rkey: string) => + `at://did:plc:wqzurwm3kmaig6e6hnc2gqwo/app.bsky.feed.generator/${rkey}` +export const PROD_DEFAULT_FEED = (rkey: string) => + `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}` +export async function DEFAULT_FEEDS( + serviceUrl: string, + resolveHandle: (name: string) => Promise<string>, +) { + if (serviceUrl.includes('localhost')) { + // local dev + const aliceDid = await resolveHandle('alice.test') + return { + pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], + saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], + } + } else if (serviceUrl.includes('staging')) { + // staging + return { + pinned: [STAGING_DEFAULT_FEED('whats-hot')], + saved: [ + STAGING_DEFAULT_FEED('bsky-team'), + STAGING_DEFAULT_FEED('with-friends'), + STAGING_DEFAULT_FEED('whats-hot'), + STAGING_DEFAULT_FEED('hot-classic'), + ], + } + } else { + // production + return { + pinned: [ + PROD_DEFAULT_FEED('whats-hot'), + PROD_DEFAULT_FEED('with-friends'), + ], + saved: [ + PROD_DEFAULT_FEED('bsky-team'), + PROD_DEFAULT_FEED('with-friends'), + PROD_DEFAULT_FEED('whats-hot'), + PROD_DEFAULT_FEED('hot-classic'), + ], + } + } +} + export const POST_IMG_MAX = { width: 2000, height: 2000, diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts new file mode 100644 index 000000000..516940c1c --- /dev/null +++ b/src/lib/haptics.ts @@ -0,0 +1,40 @@ +import {isIOS, isWeb} from 'platform/detection' +import ReactNativeHapticFeedback, { + HapticFeedbackTypes, +} from 'react-native-haptic-feedback' + +const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s + +export class Haptics { + static default() { + if (isWeb) { + return + } + ReactNativeHapticFeedback.trigger(hapticImpact) + } + static impact(type: HapticFeedbackTypes = hapticImpact) { + if (isWeb) { + return + } + ReactNativeHapticFeedback.trigger(type) + } + static selection() { + if (isWeb) { + return + } + ReactNativeHapticFeedback.trigger('selection') + } + static notification = (type: 'success' | 'warning' | 'error') => { + if (isWeb) { + return + } + switch (type) { + case 'success': + return ReactNativeHapticFeedback.trigger('notificationSuccess') + case 'warning': + return ReactNativeHapticFeedback.trigger('notificationWarning') + case 'error': + return ReactNativeHapticFeedback.trigger('notificationError') + } + } +} diff --git a/src/lib/hooks/useCustomFeed.ts b/src/lib/hooks/useCustomFeed.ts new file mode 100644 index 000000000..d7a27050d --- /dev/null +++ b/src/lib/hooks/useCustomFeed.ts @@ -0,0 +1,27 @@ +import {useEffect, useState} from 'react' +import {useStores} from 'state/index' +import {CustomFeedModel} from 'state/models/feeds/custom-feed' + +export function useCustomFeed(uri: string): CustomFeedModel | undefined { + const store = useStores() + const [item, setItem] = useState<CustomFeedModel | undefined>() + useEffect(() => { + async function fetchView() { + const res = await store.agent.app.bsky.feed.getFeedGenerator({ + feed: uri, + }) + const view = res.data.view + return view + } + async function buildFeedItem() { + const view = await fetchView() + if (view) { + const temp = new CustomFeedModel(store, view) + setItem(temp) + } + } + buildFeedItem() + }, [store, uri]) + + return item +} diff --git a/src/lib/hooks/useDraggableScrollView.ts b/src/lib/hooks/useDraggableScrollView.ts new file mode 100644 index 000000000..b0f7465d7 --- /dev/null +++ b/src/lib/hooks/useDraggableScrollView.ts @@ -0,0 +1,84 @@ +import {useEffect, useRef, useMemo, ForwardedRef} from 'react' +import {Platform, findNodeHandle} from 'react-native' +import type {ScrollView} from 'react-native' +import {mergeRefs} from 'lib/merge-refs' + +type Props<Scrollable extends ScrollView = ScrollView> = { + cursor?: string + outerRef?: ForwardedRef<Scrollable> +} + +export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({ + outerRef, + cursor = 'grab', +}: Props<Scrollable> = {}) { + const ref = useRef<Scrollable>(null) + + useEffect(() => { + if (Platform.OS !== 'web' || !ref.current) { + return + } + const slider = findNodeHandle(ref.current) as unknown as HTMLDivElement + if (!slider) { + return + } + let isDragging = false + let isMouseDown = false + let startX = 0 + let scrollLeft = 0 + + const mouseDown = (e: MouseEvent) => { + isMouseDown = true + startX = e.pageX - slider.offsetLeft + scrollLeft = slider.scrollLeft + + slider.style.cursor = cursor + } + + const mouseUp = () => { + if (isDragging) { + slider.addEventListener('click', e => e.stopPropagation(), {once: true}) + } + + isMouseDown = false + isDragging = false + slider.style.cursor = 'default' + } + + const mouseMove = (e: MouseEvent) => { + if (!isMouseDown) { + return + } + + // Require n pixels momement before start of drag (3 in this case ) + const x = e.pageX - slider.offsetLeft + if (Math.abs(x - startX) < 3) { + return + } + + isDragging = true + e.preventDefault() + const walk = x - startX + slider.scrollLeft = scrollLeft - walk + } + + slider.addEventListener('mousedown', mouseDown) + window.addEventListener('mouseup', mouseUp) + window.addEventListener('mousemove', mouseMove) + + return () => { + slider.removeEventListener('mousedown', mouseDown) + window.removeEventListener('mouseup', mouseUp) + window.removeEventListener('mousemove', mouseMove) + } + }, [cursor]) + + const refs = useMemo( + () => mergeRefs(outerRef ? [ref, outerRef] : [ref]), + [ref, outerRef], + ) + + return { + refs, + } +} diff --git a/src/lib/hooks/useNavigationTabState.ts b/src/lib/hooks/useNavigationTabState.ts index fb3662152..3a05fe524 100644 --- a/src/lib/hooks/useNavigationTabState.ts +++ b/src/lib/hooks/useNavigationTabState.ts @@ -6,14 +6,16 @@ export function useNavigationTabState() { const res = { isAtHome: getTabState(state, 'Home') !== TabState.Outside, isAtSearch: getTabState(state, 'Search') !== TabState.Outside, + isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside, isAtNotifications: getTabState(state, 'Notifications') !== TabState.Outside, isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside, } if ( !res.isAtHome && - !res.isAtNotifications && !res.isAtSearch && + !res.isAtFeeds && + !res.isAtNotifications && !res.isAtMyProfile ) { // HACK for some reason useNavigationState will give us pre-hydration results diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 41b35dd4f..12e42aca5 100644 --- a/src/lib/hooks/useOnMainScroll.ts +++ b/src/lib/hooks/useOnMainScroll.ts @@ -1,25 +1,56 @@ -import {useState} from 'react' +import {useState, useCallback, useRef} from 'react' import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' import {RootStoreModel} from 'state/index' +import {s} from 'lib/styles' +import {isDesktopWeb} from 'platform/detection' + +const DY_LIMIT = isDesktopWeb ? 30 : 10 export type OnScrollCb = ( event: NativeSyntheticEvent<NativeScrollEvent>, ) => void +export type ResetCb = () => void + +export function useOnMainScroll( + store: RootStoreModel, +): [OnScrollCb, boolean, ResetCb] { + let lastY = useRef(0) + let [isScrolledDown, setIsScrolledDown] = useState(false) + return [ + useCallback( + (event: NativeSyntheticEvent<NativeScrollEvent>) => { + const y = event.nativeEvent.contentOffset.y + const dy = y - (lastY.current || 0) + lastY.current = y -export function useOnMainScroll(store: RootStoreModel) { - let [lastY, setLastY] = useState(0) - let isMinimal = store.shell.minimalShellMode - return function onMainScroll(event: NativeSyntheticEvent<NativeScrollEvent>) { - const y = event.nativeEvent.contentOffset.y - const dy = y - (lastY || 0) - setLastY(y) + if (!store.shell.minimalShellMode && y > 10 && dy > DY_LIMIT) { + store.shell.setMinimalShellMode(true) + } else if ( + store.shell.minimalShellMode && + (y <= 10 || dy < DY_LIMIT * -1) + ) { + store.shell.setMinimalShellMode(false) + } - if (!isMinimal && y > 10 && dy > 10) { - store.shell.setMinimalShellMode(true) - isMinimal = true - } else if (isMinimal && (y <= 10 || dy < -10)) { + if ( + !isScrolledDown && + event.nativeEvent.contentOffset.y > s.window.height + ) { + setIsScrolledDown(true) + } else if ( + isScrolledDown && + event.nativeEvent.contentOffset.y < s.window.height + ) { + setIsScrolledDown(false) + } + }, + [store, isScrolledDown], + ), + isScrolledDown, + useCallback(() => { + setIsScrolledDown(false) store.shell.setMinimalShellMode(false) - isMinimal = false - } - } + lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf + }, [store, setIsScrolledDown]), + ] } diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx index 06f195011..47a38fb63 100644 --- a/src/lib/icons.tsx +++ b/src/lib/icons.tsx @@ -1,6 +1,6 @@ import React from 'react' import {StyleProp, TextStyle, ViewStyle} from 'react-native' -import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg' +import Svg, {Path, Rect, Line, Ellipse, Circle} from 'react-native-svg' export function GridIcon({ style, @@ -472,7 +472,7 @@ export function HeartIcon({ size = 24, strokeWidth = 1.5, }: { - style?: StyleProp<ViewStyle> + style?: StyleProp<TextStyle> size?: string | number strokeWidth: number }) { @@ -493,7 +493,7 @@ export function HeartIconSolid({ style, size = 24, }: { - style?: StyleProp<ViewStyle> + style?: StyleProp<TextStyle> size?: string | number }) { return ( @@ -883,3 +883,77 @@ export function HandIcon({ </Svg> ) } + +export function SatelliteDishIconSolid({ + style, + size, + strokeWidth = 1.5, +}: { + style?: StyleProp<ViewStyle> + size?: string | number + strokeWidth?: number +}) { + return ( + <Svg + width={size || 24} + height={size || 24} + viewBox="0 0 22 22" + style={style} + fill="none" + stroke="none"> + <Path + d="M16 19.6622C14.5291 20.513 12.8214 21 11 21C5.47715 21 1 16.5229 1 11C1 9.17858 1.48697 7.47088 2.33782 6.00002C3.18867 4.52915 6 7.66219 6 7.66219L14.5 16.1622C14.5 16.1622 17.4709 18.8113 16 19.6622Z" + fill="currentColor" + /> + <Path + d="M8 1.62961C9.04899 1.22255 10.1847 1 11.3704 1C16.6887 1 21 5.47715 21 11C21 12.0452 20.8456 13.053 20.5592 14" + stroke="currentColor" + strokeWidth={strokeWidth} + strokeLinecap="round" + /> + <Path + d="M9 5.38745C9.64553 5.13695 10.3444 5 11.0741 5C14.3469 5 17 7.75517 17 11.1538C17 11.797 16.905 12.4172 16.7287 13" + stroke="currentColor" + strokeWidth={strokeWidth} + strokeLinecap="round" + /> + <Circle cx="10" cy="12" r="2" fill="currentColor" /> + </Svg> + ) +} + +export function SatelliteDishIcon({ + style, + size, + strokeWidth = 1.5, +}: { + style?: StyleProp<TextStyle> + size?: string | number + strokeWidth?: number +}) { + return ( + <Svg + fill="none" + viewBox="0 0 22 22" + strokeWidth={strokeWidth} + stroke="currentColor" + width={size} + height={size} + style={style}> + <Path d="M5.25593 8.3303L5.25609 8.33047L5.25616 8.33056L5.25621 8.33061L5.27377 8.35018L5.29289 8.3693L13.7929 16.8693L13.8131 16.8895L13.8338 16.908L13.834 16.9081L13.8342 16.9083L13.8342 16.9083L13.8345 16.9086L13.8381 16.9118L13.8574 16.9294C13.8752 16.9458 13.9026 16.9711 13.9377 17.0043C14.0081 17.0708 14.1088 17.1683 14.2258 17.2881C14.4635 17.5315 14.7526 17.8509 14.9928 18.1812C15.2067 18.4755 15.3299 18.7087 15.3817 18.8634C14.0859 19.5872 12.5926 20 11 20C6.02944 20 2 15.9706 2 11C2 9.4151 2.40883 7.9285 3.12619 6.63699C3.304 6.69748 3.56745 6.84213 3.89275 7.08309C4.24679 7.34534 4.58866 7.65673 4.84827 7.9106C4.97633 8.03583 5.08062 8.14337 5.152 8.21863C5.18763 8.25619 5.21487 8.28551 5.23257 8.30473L5.25178 8.32572L5.25571 8.33006L5.25593 8.3303ZM3.00217 6.60712C3.00217 6.6071 3.00267 6.6071 3.00372 6.60715C3.00271 6.60716 3.00218 6.60714 3.00217 6.60712Z" /> + <Path + d="M8 1.62961C9.04899 1.22255 10.1847 1 11.3704 1C16.6887 1 21 5.47715 21 11C21 12.0452 20.8456 13.053 20.5592 14" + stroke-linecap="round" + /> + <Path + d="M9 5.38745C9.64553 5.13695 10.3444 5 11.0741 5C14.3469 5 17 7.75517 17 11.1538C17 11.797 16.905 12.4172 16.7287 13" + stroke-linecap="round" + /> + <Path + d="M12 12C12 12.7403 11.5978 13.3866 11 13.7324L8.26756 11C8.61337 10.4022 9.25972 10 10 10C11.1046 10 12 10.8954 12 12Z" + fill="currentColor" + stroke="none" + /> + </Svg> + ) +} diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index f4a96a22f..cf43feca8 100644 --- a/src/lib/link-meta/bsky.ts +++ b/src/lib/link-meta/bsky.ts @@ -1,3 +1,4 @@ +import * as apilib from 'lib/api/index' import {LikelyType, LinkMeta} from './link-meta' // import {match as matchRoute} from 'view/routes' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' @@ -128,3 +129,29 @@ export async function getPostAsQuote( }, } } + +export async function getFeedAsEmbed( + store: RootStoreModel, + url: string, +): Promise<apilib.ExternalEmbedDraft> { + url = convertBskyAppUrlIfNeeded(url) + const [_0, user, _1, rkey] = url.split('/').filter(Boolean) + const feed = makeRecordUri(user, 'app.bsky.feed.generator', rkey) + const res = await store.agent.app.bsky.feed.getFeedGenerator({feed}) + return { + isLoading: false, + uri: feed, + meta: { + url: feed, + likelyType: LikelyType.AtpData, + title: res.data.view.displayName, + }, + embed: { + $type: 'app.bsky.embed.record', + record: { + uri: res.data.view.uri, + cid: res.data.view.cid, + }, + }, + } +} diff --git a/src/lib/merge-refs.ts b/src/lib/merge-refs.ts new file mode 100644 index 000000000..4617b5260 --- /dev/null +++ b/src/lib/merge-refs.ts @@ -0,0 +1,27 @@ +/** + * This TypeScript function merges multiple React refs into a single ref callback. + * When developing low level UI components, it is common to have to use a local ref + * but also support an external one using React.forwardRef. + * Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility. + * Today a ref can be a function or an object, tomorrow it could be another thing, who knows. + * This utility handles compatibility for you. + * This function is inspired by https://github.com/gregberge/react-merge-refs + * @param refs - An array of React refs, which can be either `React.MutableRefObject<T>` or + * `React.LegacyRef<T>`. These refs are used to store references to DOM elements or React components. + * The `mergeRefs` function takes in an array of these refs and returns a callback function that + * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and + * returns a ref callback function that can be used to merge multiple refs into a single ref. + */ +export function mergeRefs<T = any>( + refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>, +): React.RefCallback<T> { + return value => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value) + } else if (ref != null) { + ;(ref as React.MutableRefObject<T | null>).current = value + } + }) + } +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 56775deee..4eb5e29d2 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -9,6 +9,7 @@ export type CommonNavigatorParams = { ModerationMuteLists: undefined ModerationMutedAccounts: undefined ModerationBlockedAccounts: undefined + DiscoverFeeds: undefined Settings: undefined Profile: {name: string; hideBackButton?: boolean} ProfileFollowers: {name: string} @@ -17,6 +18,8 @@ export type CommonNavigatorParams = { PostThread: {name: string; rkey: string} PostLikedBy: {name: string; rkey: string} PostRepostedBy: {name: string; rkey: string} + CustomFeed: {name: string; rkey: string} + CustomFeedLikedBy: {name: string; rkey: string} Debug: undefined Log: undefined Support: undefined @@ -25,11 +28,13 @@ export type CommonNavigatorParams = { CommunityGuidelines: undefined CopyrightPolicy: undefined AppPasswords: undefined + SavedFeeds: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { HomeTab: undefined SearchTab: undefined + FeedsTab: undefined NotificationsTab: undefined MyProfileTab: undefined } @@ -42,6 +47,10 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & { Search: {q?: string} } +export type FeedsTabNavigatorParams = CommonNavigatorParams & { + Feeds: undefined +} + export type NotificationsTabNavigatorParams = CommonNavigatorParams & { Notifications: undefined } @@ -53,6 +62,7 @@ export type MyProfileTabNavigatorParams = CommonNavigatorParams & { export type FlatNavigatorParams = CommonNavigatorParams & { Home: undefined Search: {q?: string} + Feeds: undefined Notifications: undefined } @@ -61,6 +71,8 @@ export type AllNavigatorParams = CommonNavigatorParams & { Home: undefined SearchTab: undefined Search: {q?: string} + FeedsTab: undefined + Feeds: undefined NotificationsTab: undefined Notifications: undefined MyProfileTab: undefined diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index a5412920e..d6d43b89d 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -82,6 +82,18 @@ export function isBskyPostUrl(url: string): boolean { return false } +export function isBskyCustomFeedUrl(url: string): boolean { + if (isBskyAppUrl(url)) { + try { + const urlp = new URL(url) + return /profile\/(?<name>[^/]+)\/feed\/(?<rkey>[^/]+)/i.test( + urlp.pathname, + ) + } catch {} + } + return false +} + export function convertBskyAppUrlIfNeeded(url: string): string { if (isBskyAppUrl(url)) { try { diff --git a/src/lib/styles.ts b/src/lib/styles.ts index 00a8638f9..fb631c0bf 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -1,4 +1,4 @@ -import {StyleProp, StyleSheet, TextStyle} from 'react-native' +import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native' import {Theme, TypographyVariant} from './ThemeContext' import {isMobileWeb} from 'platform/detection' @@ -52,6 +52,7 @@ export const colors = { green5: '#082b03', unreadNotifBg: '#ebf6ff', + brandBlue: '#0066FF', } export const gradients = { @@ -169,6 +170,10 @@ export const s = StyleSheet.create({ w100pct: {width: '100%'}, h100pct: {height: '100%'}, hContentRegion: isMobileWeb ? {flex: 1} : {height: '100%'}, + window: { + width: Dimensions.get('window').width, + height: Dimensions.get('window').height, + }, // text align textLeft: {textAlign: 'left'}, @@ -214,6 +219,8 @@ export const s = StyleSheet.create({ green3: {color: colors.green3}, green4: {color: colors.green4}, green5: {color: colors.green5}, + + brandBlue: {color: colors.brandBlue}, }) export function lh( |