From 22884b53ad4daa2932aa8ed34fc5d5b928f8094d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 20 Apr 2023 17:16:56 -0500 Subject: [PATCH 001/374] Thread muting [APP-29] (#500) * Implement thread muting * Apply filtering on background fetched notifs * Implement thread-muting tests --- __e2e__/tests/thread-muting.test.ts | 116 +++++++++++ src/state/models/content/post-thread.ts | 40 ++++ src/state/models/content/post.ts | 19 ++ src/state/models/feeds/notifications.ts | 46 +++-- src/state/models/feeds/posts.ts | 19 ++ src/state/models/muted-threads.ts | 29 +++ src/state/models/root-store.ts | 6 + src/view/com/notifications/Feed.tsx | 5 +- src/view/com/notifications/FeedItem.tsx | 7 +- src/view/com/post-thread/PostThreadItem.tsx | 24 +++ src/view/com/post/Post.tsx | 18 ++ src/view/com/posts/FeedItem.tsx | 29 +-- src/view/com/util/PostCtrls.tsx | 4 + src/view/com/util/Selector.tsx | 5 +- src/view/com/util/forms/DropdownButton.tsx | 203 ++++++++++++-------- src/view/index.ts | 8 +- 16 files changed, 470 insertions(+), 108 deletions(-) create mode 100644 __e2e__/tests/thread-muting.test.ts create mode 100644 src/state/models/muted-threads.ts diff --git a/__e2e__/tests/thread-muting.test.ts b/__e2e__/tests/thread-muting.test.ts new file mode 100644 index 0000000000..a5cefdb26b --- /dev/null +++ b/__e2e__/tests/thread-muting.test.ts @@ -0,0 +1,116 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Thread muting', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&follows') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login, create a thread, and log out', async () => { + await login(service, 'alice', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + await element(by.id('composeFAB')).tap() + await element(by.id('composerTextInput')).typeText('Test thread') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, reply to the thread, and log out', async () => { + await login(service, 'bob', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + const alicePosts = by.id('feedItem-by-alice.test') + await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText('Reply 1') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, confirm notification exists, mute thread, and log out', async () => { + await login(service, 'alice', 'hunter2') + + await element(by.id('bottomBarNotificationsBtn')).tap() + const bobNotifs = by.id('feedItem-by-bob.test') + await expect( + element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), + ).toHaveText('Reply 1') + await element(by.id('postDropdownBtn').withAncestor(bobNotifs)) + .atIndex(0) + .tap() + await element(by.id('postDropdownMuteThreadBtn')).tap() + // have to wait for the toast to clear + await waitFor(element(by.id('viewHeaderDrawerBtn'))) + .toBeVisible() + .withTimeout(5000) + + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, reply to the thread twice, and log out', async () => { + await login(service, 'bob', 'hunter2') + + await element(by.id('bottomBarProfileBtn')).tap() + await element(by.id('selector-1')).tap() + const bobPosts = by.id('feedItem-by-bob.test') + await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText('Reply 2') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + + const alicePosts = by.id('feedItem-by-alice.test') + await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText('Reply 3') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + + await element(by.id('bottomBarHomeBtn')).tap() + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => { + await login(service, 'alice', 'hunter2') + + await element(by.id('bottomBarNotificationsBtn')).tap() + const bobNotifs = by.id('feedItem-by-bob.test') + await expect( + element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), + ).not.toExist() + + await element(by.id('bottomBarHomeBtn')).tap() + const alicePosts = by.id('feedItem-by-alice.test') + await element(by.id('postDropdownBtn').withAncestor(alicePosts)) + .atIndex(0) + .tap() + await element(by.id('postDropdownMuteThreadBtn')).tap() + + // TODO + // the swipe down to trigger PTR isnt working and I dont want to block on this + // -prf + // await element(by.id('bottomBarNotificationsBtn')).tap() + // await element(by.id('notifsFeed')).swipe('down', 'fast') + // await waitFor(element(by.id('postText').withAncestor(bobNotifs))) + // .toBeVisible() + // .withTimeout(5000) + // await expect( + // element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), + // ).toHaveText('Reply 2') + // await expect( + // element(by.id('postText').withAncestor(bobNotifs)).atIndex(1), + // ).toHaveText('Reply 3') + // await expect( + // element(by.id('postText').withAncestor(bobNotifs)).atIndex(2), + // ).toHaveText('Reply 1') + }) +}) diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index 794beae205..acc9bffa98 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -42,6 +42,17 @@ export class PostThreadItemModel { return this.postRecord?.reply?.parent.uri } + get rootUri(): string { + if (this.postRecord?.reply?.root.uri) { + return this.postRecord.reply.root.uri + } + return this.uri + } + + get isThreadMuted() { + return this.rootStore.mutedThreads.uris.has(this.rootUri) + } + constructor( public rootStore: RootStoreModel, reactKey: string, @@ -188,6 +199,14 @@ export class PostThreadItemModel { } } + async toggleThreadMute() { + if (this.isThreadMuted) { + this.rootStore.mutedThreads.uris.delete(this.rootUri) + } else { + this.rootStore.mutedThreads.uris.add(this.rootUri) + } + } + async delete() { await this.rootStore.agent.deletePost(this.post.uri) this.rootStore.emitPostDeleted(this.post.uri) @@ -230,6 +249,19 @@ export class PostThreadModel { return this.error !== '' } + get rootUri(): string { + if (this.thread) { + if (this.thread.postRecord?.reply?.root.uri) { + return this.thread.postRecord.reply.root.uri + } + } + return this.resolvedUri + } + + get isThreadMuted() { + return this.rootStore.mutedThreads.uris.has(this.rootUri) + } + // public api // = @@ -279,6 +311,14 @@ export class PostThreadModel { this.refresh() } + async toggleThreadMute() { + if (this.isThreadMuted) { + this.rootStore.mutedThreads.uris.delete(this.rootUri) + } else { + this.rootStore.mutedThreads.uris.add(this.rootUri) + } + } + // state transitions // = diff --git a/src/state/models/content/post.ts b/src/state/models/content/post.ts index b5d95bf01c..7ba633366c 100644 --- a/src/state/models/content/post.ts +++ b/src/state/models/content/post.ts @@ -48,6 +48,17 @@ export class PostModel implements RemoveIndex { return this.hasLoaded && !this.hasContent } + get rootUri(): string { + if (this.reply?.root.uri) { + return this.reply.root.uri + } + return this.uri + } + + get isThreadMuted() { + return this.rootStore.mutedThreads.uris.has(this.rootUri) + } + // public api // = @@ -55,6 +66,14 @@ export class PostModel implements RemoveIndex { await this._load() } + async toggleThreadMute() { + if (this.isThreadMuted) { + this.rootStore.mutedThreads.uris.delete(this.rootUri) + } else { + this.rootStore.mutedThreads.uris.add(this.rootUri) + } + } + // state transitions // = diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index ff77ab9796..e2a18ea04b 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -160,6 +160,13 @@ export class NotificationsFeedItemModel { return '' } + get reasonSubjectRootUri(): string | undefined { + if (this.additionalPost) { + return this.additionalPost.rootUri + } + return undefined + } + toSupportedRecord(v: unknown): SupportedRecord | undefined { for (const ns of [ AppBskyFeedPost, @@ -227,7 +234,7 @@ export class NotificationsFeedModel { // data notifications: NotificationsFeedItemModel[] = [] - queuedNotifications: undefined | ListNotifications.Notification[] = undefined + queuedNotifications: undefined | NotificationsFeedItemModel[] = undefined unreadCount = 0 // this is used to help trigger push notifications @@ -354,7 +361,13 @@ export class NotificationsFeedModel { queue.push(notif) } - this._setQueued(this._filterNotifications(queue)) + // NOTE + // because filtering depends on the added information we have to fetch + // the full models here. this is *not* ideal performance and we need + // to update the notifications route to give all the info we need + // -prf + const queueModels = await this._fetchItemModels(queue) + this._setQueued(this._filterNotifications(queueModels)) this._countUnread() } catch (e) { this.rootStore.log.error('NotificationsModel:syncQueue failed', {e}) @@ -452,7 +465,8 @@ export class NotificationsFeedModel { res.data.notifications[0], ) await notif.fetchAdditionalData() - return notif + const filtered = this._filterNotifications([notif]) + return filtered[0] } // state transitions @@ -505,23 +519,26 @@ export class NotificationsFeedModel { } _filterNotifications( - items: ListNotifications.Notification[], - ): ListNotifications.Notification[] { + items: NotificationsFeedItemModel[], + ): NotificationsFeedItemModel[] { return items.filter(item => { - return ( - this.rootStore.preferences.getLabelPreference(item.labels).pref !== + const hideByLabel = + this.rootStore.preferences.getLabelPreference(item.labels).pref === 'hide' + let mutedThread = !!( + item.reasonSubjectRootUri && + this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri) ) + return !hideByLabel && !mutedThread }) } - async _processNotifications( + async _fetchItemModels( items: ListNotifications.Notification[], ): Promise { const promises = [] const itemModels: NotificationsFeedItemModel[] = [] - items = this._filterNotifications(items) - for (const item of groupNotifications(items)) { + for (const item of items) { const itemModel = new NotificationsFeedItemModel( this.rootStore, `item-${_idCounter++}`, @@ -541,7 +558,14 @@ export class NotificationsFeedModel { return itemModels } - _setQueued(queued: undefined | ListNotifications.Notification[]) { + async _processNotifications( + items: ListNotifications.Notification[], + ): Promise { + const itemModels = await this._fetchItemModels(groupNotifications(items)) + return this._filterNotifications(itemModels) + } + + _setQueued(queued: undefined | NotificationsFeedItemModel[]) { this.queuedNotifications = queued } diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 38faf658a3..58167284da 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -72,6 +72,17 @@ export class PostsFeedItemModel { makeAutoObservable(this, {rootStore: false}) } + get rootUri(): string { + if (this.reply?.root.uri) { + return this.reply.root.uri + } + return this.post.uri + } + + get isThreadMuted() { + return this.rootStore.mutedThreads.uris.has(this.rootUri) + } + copy(v: FeedViewPost) { this.post = v.post this.reply = v.reply @@ -145,6 +156,14 @@ export class PostsFeedItemModel { } } + async toggleThreadMute() { + if (this.isThreadMuted) { + this.rootStore.mutedThreads.uris.delete(this.rootUri) + } else { + this.rootStore.mutedThreads.uris.add(this.rootUri) + } + } + async delete() { await this.rootStore.agent.deletePost(this.post.uri) this.rootStore.emitPostDeleted(this.post.uri) diff --git a/src/state/models/muted-threads.ts b/src/state/models/muted-threads.ts new file mode 100644 index 0000000000..e6f2027452 --- /dev/null +++ b/src/state/models/muted-threads.ts @@ -0,0 +1,29 @@ +/** + * This is a temporary client-side system for storing muted threads + * When the system lands on prod we should switch to that + */ + +import {makeAutoObservable} from 'mobx' +import {isObj, hasProp, isStrArray} from 'lib/type-guards' + +export class MutedThreads { + uris: Set = new Set() + + constructor() { + makeAutoObservable( + this, + {serialize: false, hydrate: false}, + {autoBind: true}, + ) + } + + serialize() { + return {uris: Array.from(this.uris)} + } + + hydrate(v: unknown) { + if (isObj(v) && hasProp(v, 'uris') && isStrArray(v.uris)) { + this.uris = new Set(v.uris) + } + } +} diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 9207f27ba6..b3e744a40b 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -20,6 +20,7 @@ import {InvitedUsers} from './invited-users' import {PreferencesModel} from './ui/preferences' import {resetToTab} from '../../Navigation' import {ImageSizesCache} from './cache/image-sizes' +import {MutedThreads} from './muted-threads' export const appInfo = z.object({ build: z.string(), @@ -41,6 +42,7 @@ export class RootStoreModel { profiles = new ProfilesCache(this) linkMetas = new LinkMetasCache(this) imageSizes = new ImageSizesCache() + mutedThreads = new MutedThreads() constructor(agent: BskyAgent) { this.agent = agent @@ -64,6 +66,7 @@ export class RootStoreModel { shell: this.shell.serialize(), preferences: this.preferences.serialize(), invitedUsers: this.invitedUsers.serialize(), + mutedThreads: this.mutedThreads.serialize(), } } @@ -90,6 +93,9 @@ export class RootStoreModel { if (hasProp(v, 'invitedUsers')) { this.invitedUsers.hydrate(v.invitedUsers) } + if (hasProp(v, 'mutedThreads')) { + this.mutedThreads.hydrate(v.mutedThreads) + } } } diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 33bde1955c..50bdc5dc93 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -135,8 +135,9 @@ export const Feed = observer(function Feed({ /> )} - {data.length && ( + {data.length ? ( item._reactKey} @@ -155,7 +156,7 @@ export const Feed = observer(function Feed({ onScroll={onScroll} contentContainerStyle={s.contentContainer} /> - )} + ) : null} ) }) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 34df2a8edb..b05111ffc2 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -85,7 +85,11 @@ export const FeedItem = observer(function FeedItem({ return } return ( - + { return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) }, [item, store]) + const onPressToggleLike = React.useCallback(() => { return item .toggleLike() .catch(e => store.log.error('Failed to toggle like', e)) }, [item, store]) + const onCopyPostText = React.useCallback(() => { Clipboard.setString(record?.text || '') Toast.show('Copied to clipboard') }, [record]) + const onOpenTranslate = React.useCallback(() => { Linking.openURL( encodeURI(`https://translate.google.com/#auto|en|${record?.text || ''}`), ) }, [record]) + + const onToggleThreadMute = React.useCallback(async () => { + try { + await item.toggleThreadMute() + if (item.isThreadMuted) { + Toast.show('You will no longer received notifications for this thread') + } else { + Toast.show('You will now receive notifications for this thread') + } + } catch (e) { + store.log.error('Failed to toggle thread mute', e) + } + }, [item, store]) + const onDeletePost = React.useCallback(() => { item.delete().then( () => { @@ -175,8 +193,10 @@ export const PostThreadItem = observer(function PostThreadItem({ itemHref={itemHref} itemTitle={itemTitle} isAuthor={item.post.author.did === store.me.did} + isThreadMuted={item.isThreadMuted} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} + onToggleThreadMute={onToggleThreadMute} onDeletePost={onDeletePost}> @@ -357,11 +379,13 @@ export const PostThreadItem = observer(function PostThreadItem({ likeCount={item.post.likeCount} isReposted={!!item.post.viewer?.repost} isLiked={!!item.post.viewer?.like} + isThreadMuted={item.isThreadMuted} onPressReply={onPressReply} onPressToggleRepost={onPressToggleRepost} onPressToggleLike={onPressToggleLike} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} + onToggleThreadMute={onToggleThreadMute} onDeletePost={onDeletePost} /> diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 60d46f5cc5..81f3b8c45a 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -174,6 +174,21 @@ const PostLoaded = observer( ) }, [record]) + const onToggleThreadMute = React.useCallback(async () => { + try { + await item.toggleThreadMute() + if (item.isThreadMuted) { + Toast.show( + 'You will no longer received notifications for this thread', + ) + } else { + Toast.show('You will now receive notifications for this thread') + } + } catch (e) { + store.log.error('Failed to toggle thread mute', e) + } + }, [item, store]) + const onDeletePost = React.useCallback(() => { item.delete().then( () => { @@ -237,6 +252,7 @@ const PostLoaded = observer( {item.richText?.text ? ( diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index c2baa4d4d4..18481d4cb3 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -101,6 +101,20 @@ export const FeedItem = observer(function ({ ) }, [record]) + const onToggleThreadMute = React.useCallback(async () => { + track('FeedItem:ThreadMute') + try { + await item.toggleThreadMute() + if (item.isThreadMuted) { + Toast.show('You will no longer receive notifications for this thread') + } else { + Toast.show('You will now receive notifications for this thread') + } + } catch (e) { + store.log.error('Failed to toggle thread mute', e) + } + }, [track, item, store]) + const onDeletePost = React.useCallback(() => { track('FeedItem:PostDelete') item.delete().then( @@ -120,7 +134,6 @@ export const FeedItem = observer(function ({ } const isSmallTop = isThreadChild - const isNoTop = false //isChild && !item._isThreadChild const isMuted = item.post.author.viewer?.muted && ignoreMuteFor !== item.post.author.did const outerStyles = [ @@ -128,7 +141,6 @@ export const FeedItem = observer(function ({ pal.view, {borderColor: pal.colors.border}, isSmallTop ? styles.outerSmallTop : undefined, - isNoTop ? styles.outerNoTop : undefined, isThreadParent ? styles.outerNoBottom : undefined, ] @@ -146,11 +158,7 @@ export const FeedItem = observer(function ({ )} {isThreadParent && ( )} {item.reasonRepost && ( @@ -260,11 +268,13 @@ export const FeedItem = observer(function ({ likeCount={item.post.likeCount} isReposted={!!item.post.viewer?.repost} isLiked={!!item.post.viewer?.like} + isThreadMuted={item.isThreadMuted} onPressReply={onPressReply} onPressToggleRepost={onPressToggleRepost} onPressToggleLike={onPressToggleLike} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} + onToggleThreadMute={onToggleThreadMute} onDeletePost={onDeletePost} /> @@ -280,10 +290,6 @@ const styles = StyleSheet.create({ paddingRight: 15, paddingBottom: 8, }, - outerNoTop: { - borderTopWidth: 0, - paddingTop: 0, - }, outerSmallTop: { borderTopWidth: 0, }, @@ -304,7 +310,6 @@ const styles = StyleSheet.create({ bottom: 0, borderLeftWidth: 2, }, - bottomReplyLineNoTop: {top: 64}, includeReason: { flexDirection: 'row', paddingLeft: 50, diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index 6441d3c77d..07a67fd8a6 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -48,11 +48,13 @@ interface PostCtrlsOpts { likeCount?: number isReposted: boolean isLiked: boolean + isThreadMuted: boolean onPressReply: () => void onPressToggleRepost: () => Promise onPressToggleLike: () => Promise onCopyPostText: () => void onOpenTranslate: () => void + onToggleThreadMute: () => void onDeletePost: () => void } @@ -255,8 +257,10 @@ export function PostCtrls(opts: PostCtrlsOpts) { itemHref={opts.itemHref} itemTitle={opts.itemTitle} isAuthor={opts.isAuthor} + isThreadMuted={opts.isThreadMuted} onCopyPostText={opts.onCopyPostText} onOpenTranslate={opts.onOpenTranslate} + onToggleThreadMute={opts.onToggleThreadMute} onDeletePost={opts.onDeletePost}> { const selected = i === selectedIndex return ( - onPressItem(i)}> + onPressItem(i)}> void } +export interface DropdownItemSeparator { + sep: true +} +export type DropdownItem = DropdownItemButton | DropdownItemSeparator type MaybeDropdownItem = DropdownItem | false | undefined export type DropdownButtonType = ButtonType | 'bare' @@ -59,10 +65,12 @@ export function DropdownButton({ rightOffset?: number bottomOffset?: number }) { - const ref = useRef(null) + const ref1 = useRef(null) + const ref2 = useRef(null) const onPress = () => { - ref.current?.measure( + const ref = ref1.current || ref2.current + ref?.measure( ( _x: number, _y: number, @@ -75,7 +83,14 @@ export function DropdownButton({ menuWidth = 200 } const winHeight = Dimensions.get('window').height - const estimatedMenuHeight = items.length * ESTIMATED_MENU_ITEM_HEIGHT + let estimatedMenuHeight = 0 + for (const item of items) { + if (item && isSep(item)) { + estimatedMenuHeight += ESTIMATED_SEP_HEIGHT + } else if (item && isBtn(item)) { + estimatedMenuHeight += ESTIMATED_BTN_HEIGHT + } + } const newX = openToRight ? pageX + width + rightOffset : pageX + width - menuWidth @@ -100,13 +115,13 @@ export function DropdownButton({ style={style} onPress={onPress} hitSlop={HITSLOP} - ref={ref}> + ref={ref1}> {children} ) } return ( - + @@ -122,8 +137,10 @@ export function PostDropdownBtn({ itemCid, itemHref, isAuthor, + isThreadMuted, onCopyPostText, onOpenTranslate, + onToggleThreadMute, onDeletePost, }: { testID?: string @@ -134,8 +151,10 @@ export function PostDropdownBtn({ itemHref: string itemTitle: string isAuthor: boolean + isThreadMuted: boolean onCopyPostText: () => void onOpenTranslate: () => void + onToggleThreadMute: () => void onDeletePost: () => void }) { const store = useStores() @@ -174,6 +193,16 @@ export function PostDropdownBtn({ } }, }, + {sep: true}, + { + testID: 'postDropdownMuteThreadBtn', + icon: 'comment-slash', + label: isThreadMuted ? 'Unmute thread' : 'Mute thread', + onPress() { + onToggleThreadMute() + }, + }, + {sep: true}, { testID: 'postDropdownReportBtn', icon: 'circle-exclamation', @@ -186,21 +215,19 @@ export function PostDropdownBtn({ }) }, }, - isAuthor - ? { - testID: 'postDropdownDeleteBtn', - icon: ['far', 'trash-can'], - label: 'Delete post', - onPress() { - store.shell.openModal({ - name: 'confirm', - title: 'Delete this post?', - message: 'Are you sure? This can not be undone.', - onPressConfirm: onDeletePost, - }) - }, - } - : undefined, + isAuthor && { + testID: 'postDropdownDeleteBtn', + icon: ['far', 'trash-can'], + label: 'Delete post', + onPress() { + store.shell.openModal({ + name: 'confirm', + title: 'Delete this post?', + message: 'Are you sure? This can not be undone.', + onPressConfirm: onDeletePost, + }) + }, + }, ].filter(Boolean) as DropdownItem[] return ( @@ -208,7 +235,7 @@ export function PostDropdownBtn({ testID={testID} style={style} items={dropdownItems} - menuWidth={200}> + menuWidth={isWeb ? 220 : 200}> {children} ) @@ -222,7 +249,10 @@ function createDropdownMenu( ): RootSiblings { const onPressItem = (index: number) => { sibling.destroy() - items[index].onPress() + const item = items[index] + if (isBtn(item)) { + item.onPress() + } } const onOuterPress = () => sibling.destroy() const sibling = new RootSiblings( @@ -240,6 +270,74 @@ function createDropdownMenu( return sibling } +type DropDownItemProps = { + onOuterPress: () => void + x: number + y: number + width: number + items: DropdownItem[] + onPressItem: (index: number) => void +} + +const DropdownItems = ({ + onOuterPress, + x, + y, + width, + items, + onPressItem, +}: DropDownItemProps) => { + const pal = usePalette('default') + const theme = useTheme() + const dropDownBackgroundColor = + theme.colorScheme === 'dark' ? pal.btn : pal.view + + return ( + <> + + + + + {items.map((item, index) => { + if (isBtn(item)) { + return ( + onPressItem(index)}> + {item.icon && ( + + )} + {item.label} + + ) + } else if (isSep(item)) { + return + } + return null + })} + + + ) +} + +function isSep(item: DropdownItem): item is DropdownItemSeparator { + return 'sep' in item && item.sep +} +function isBtn(item: DropdownItem): item is DropdownItemButton { + return !isSep(item) +} + const styles = StyleSheet.create({ bg: { position: 'absolute', @@ -277,57 +375,8 @@ const styles = StyleSheet.create({ label: { fontSize: 18, }, + separator: { + borderTopWidth: 1, + marginVertical: 8, + }, }) -type DropDownItemProps = { - onOuterPress: () => void - x: number - y: number - width: number - items: DropdownItem[] - onPressItem: (index: number) => void -} - -const DropdownItems = ({ - onOuterPress, - x, - y, - width, - items, - onPressItem, -}: DropDownItemProps) => { - const pal = usePalette('default') - const theme = useTheme() - const dropDownBackgroundColor = - theme.colorScheme === 'dark' ? pal.btn : pal.view - - return ( - <> - - - - - {items.map((item, index) => ( - onPressItem(index)}> - {item.icon && ( - - )} - {item.label} - - ))} - - - ) -} diff --git a/src/view/index.ts b/src/view/index.ts index e6e3426974..93c6fccc52 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -8,10 +8,7 @@ import {faAngleUp} from '@fortawesome/free-solid-svg-icons/faAngleUp' import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight' import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp' -import { - faArrowRightFromBracket, - faQuoteLeft, -} from '@fortawesome/free-solid-svg-icons' +import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowRightFromBracket' import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotateLeft' @@ -30,6 +27,7 @@ import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser' import {faClone} from '@fortawesome/free-solid-svg-icons/faClone' import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone' import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' +import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash' import {faCompass} from '@fortawesome/free-regular-svg-icons/faCompass' import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis' import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope' @@ -55,6 +53,7 @@ import {faPen} from '@fortawesome/free-solid-svg-icons/faPen' import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' +import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft' import {faShare} from '@fortawesome/free-solid-svg-icons/faShare' import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' @@ -104,6 +103,7 @@ export function setup() { faClone, farClone, faComment, + faCommentSlash, faCompass, faEllipsis, faEnvelope, From f2fe4abdce71fffa36419b8642289b7b86af7377 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 20 Apr 2023 17:34:45 -0500 Subject: [PATCH 002/374] Fix positioning of the load more button (#502) --- src/view/com/util/load-latest/LoadLatestBtn.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx index 22a8fbadaa..1b6f18b622 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx @@ -39,7 +39,7 @@ const styles = StyleSheet.create({ left: '50vw', // @ts-ignore web only -prf transform: 'translateX(-50%)', - top: 30, + top: 60, shadowColor: '#000', shadowOpacity: 0.2, shadowOffset: {width: 0, height: 2}, From e02c926c8a18f9cf296544b23ad343a59919acff Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 20 Apr 2023 17:36:25 -0500 Subject: [PATCH 003/374] Improvements to notifications screen [APP-520] (#501) * Refresh or sync notifications when the tab is navigated to * Fix to bad textnode render * Speed up initial session load * Fix lint * Restore updateSessionState() on session resumption --- src/lib/hooks/useTabFocusEffect.ts | 27 +++++++++++++++++++++++++++ src/state/models/me.ts | 14 ++++++-------- src/view/screens/Notifications.tsx | 22 ++++++++++++++++++++++ src/view/shell/desktop/LeftNav.tsx | 4 ++-- 4 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 src/lib/hooks/useTabFocusEffect.ts diff --git a/src/lib/hooks/useTabFocusEffect.ts b/src/lib/hooks/useTabFocusEffect.ts new file mode 100644 index 0000000000..e446084c5a --- /dev/null +++ b/src/lib/hooks/useTabFocusEffect.ts @@ -0,0 +1,27 @@ +import {useEffect, useState} from 'react' +import {useNavigation} from '@react-navigation/native' +import {getTabState, TabState} from 'lib/routes/helpers' + +export function useTabFocusEffect( + tabName: string, + cb: (isInside: boolean) => void, +) { + const [isInside, setIsInside] = useState(false) + + // get root navigator state + let nav = useNavigation() + while (nav.getParent()) { + nav = nav.getParent() + } + const state = nav.getState() + + useEffect(() => { + // check if inside + let v = getTabState(state, tabName) !== TabState.Outside + if (v !== isInside) { + // fire + setIsInside(v) + cb(v) + } + }, [state, isInside, setIsInside, tabName, cb]) +} diff --git a/src/state/models/me.ts b/src/state/models/me.ts index e8b8e1ed05..b993637900 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -99,14 +99,12 @@ export class MeModel { this.handle = sess.currentSession?.handle || '' await this.fetchProfile() this.mainFeed.clear() - await Promise.all([ - this.mainFeed.setup().catch(e => { - this.rootStore.log.error('Failed to setup main feed model', e) - }), - this.notifications.setup().catch(e => { - this.rootStore.log.error('Failed to setup notifications model', e) - }), - ]) + /* dont await */ this.mainFeed.setup().catch(e => { + this.rootStore.log.error('Failed to setup main feed model', e) + }) + /* dont await */ this.notifications.setup().catch(e => { + this.rootStore.log.error('Failed to setup notifications model', e) + }) this.rootStore.emitSessionLoaded() await this.fetchInviteCodes() } else { diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 3e34a9fabe..d93666aa8f 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -13,6 +13,7 @@ import {InvitedUsers} from '../com/notifications/InvitedUsers' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {useStores} from 'state/index' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' @@ -58,6 +59,27 @@ export const NotificationsScreen = withAuthRequired( } }, [store, screen, onPressLoadLatest]), ) + useTabFocusEffect( + 'Notifications', + React.useCallback( + isInside => { + // on mobile: + // fires with `isInside=true` when the user navigates to the root tab + // but not when the user goes back to the screen by pressing back + // on web: + // essentially equivalent to useFocusEffect because we dont used tabbed + // navigation + if (isInside) { + if (store.me.notifications.unreadCount > 0) { + store.me.notifications.refresh() + } else { + store.me.notifications.syncQueue() + } + } + }, + [store], + ), + ) return ( diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index bcff844f1d..b4b219023e 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -95,11 +95,11 @@ const NavItem = observer( {isCurrent ? iconFilled : icon} - {typeof count === 'string' && count && ( + {typeof count === 'string' && count ? ( {count} - )} + ) : null} {label} From 9f9bd314b34037a02e863df84dd642c2819b3472 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 20 Apr 2023 17:43:01 -0500 Subject: [PATCH 004/374] 1.19 --- app.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index ba2c6958fe..8e830395de 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Bluesky", "slug": "bluesky", "owner": "blueskysocial", - "version": "1.18.0", + "version": "1.19.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -34,7 +34,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 3, + "versionCode": 4, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" diff --git a/package.json b/package.json index 57a6d02ac0..4fa046b874 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.18.0", + "version": "1.19.0", "private": true, "scripts": { "postinstall": "patch-package", From 0f5735b616e3565c1c739e4c8007f4ea4aedba92 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 21 Apr 2023 12:21:38 -0500 Subject: [PATCH 005/374] Fix profile link 404s on session change & handle change [APP 523] (#507) * Use DID to link to the user profile to gracefully handle... handle changes * Reset nav state on active profile change --- src/Navigation.tsx | 20 +++++++++++++++++--- src/state/models/root-store.ts | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index e868dd3b08..3973b9dfa3 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1,8 +1,10 @@ import * as React from 'react' import {StyleSheet} from 'react-native' +import {observer} from 'mobx-react-lite' import { NavigationContainer, createNavigationContainerRef, + CommonActions, StackActions, } from '@react-navigation/native' import {createNativeStackNavigator} from '@react-navigation/native-stack' @@ -163,7 +165,7 @@ function NotificationsTabNavigator() { ) } -function MyProfileTabNavigator() { +const MyProfileTabNavigator = observer(() => { const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) const store = useStores() return ( @@ -180,14 +182,14 @@ function MyProfileTabNavigator() { // @ts-ignore // TODO: fix this broken type in ProfileScreen component={ProfileScreen} initialParams={{ - name: store.me.handle, + name: store.me.did, hideBackButton: true, }} /> {commonScreens(MyProfileTab as typeof HomeTab)} ) -} +}) /** * The FlatNavigator is used by Web to represent the routes @@ -281,6 +283,17 @@ function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') { } } +function reset() { + if (navigationRef.isReady()) { + navigationRef.dispatch( + CommonActions.reset({ + index: 0, + routes: [{name: isNative ? 'HomeTab' : 'Home'}], + }), + ) + } +} + function handleLink(url: string) { let path if (url.startsWith('/')) { @@ -326,6 +339,7 @@ const styles = StyleSheet.create({ export { navigate, resetToTab, + reset, handleLink, TabsNavigator, FlatNavigator, diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index b3e744a40b..6f919a4bfa 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -21,6 +21,7 @@ import {PreferencesModel} from './ui/preferences' import {resetToTab} from '../../Navigation' import {ImageSizesCache} from './cache/image-sizes' import {MutedThreads} from './muted-threads' +import {reset as resetNavigation} from '../../Navigation' export const appInfo = z.object({ build: z.string(), @@ -123,6 +124,7 @@ export class RootStoreModel { this.agent = agent this.me.clear() await this.me.load() + resetNavigation() } /** From f0706dbe9ffb758d2aa1f75c51cfa0c61cc84482 Mon Sep 17 00:00:00 2001 From: Ollie Hsieh Date: Fri, 21 Apr 2023 14:20:06 -0700 Subject: [PATCH 006/374] Add alt text support and rework image layout (#503) * Add alt text support and rework image layout * Add additional BottomSheet implementation to account for nested Composer modal * Use mobile gallery layout on mobile web * Missing key * Fix lint * Move altimage modal into the standard modal system * Fix overflow wrapping of images * Fixes to the alt-image modal * Remove unnecessary switch * Restore old imagelayoutgrid code --------- Co-authored-by: Paul Frazee --- package.json | 2 +- src/lib/api/index.ts | 11 +- src/lib/constants.ts | 4 + src/lib/media/alt-text.ts | 16 +++ src/state/models/media/gallery.ts | 4 + src/state/models/media/image.ts | 14 ++ src/state/models/ui/shell.ts | 39 ++++-- src/view/com/composer/Composer.tsx | 2 +- src/view/com/composer/photos/Gallery.tsx | 123 ++++++++++++------ src/view/com/modals/AltImage.tsx | 106 +++++++++++++++ src/view/com/modals/Modal.tsx | 8 +- src/view/com/modals/Modal.web.tsx | 3 + src/view/com/notifications/FeedItem.tsx | 5 +- src/view/com/util/images/AutoSizedImage.tsx | 43 +++++-- src/view/com/util/images/ImageHorzList.tsx | 22 ++-- src/view/com/util/images/ImageLayoutGrid.tsx | 128 +++++++++++++------ src/view/com/util/post-embeds/index.tsx | 4 +- src/view/shell/index.tsx | 2 +- yarn.lock | 8 +- 19 files changed, 412 insertions(+), 132 deletions(-) create mode 100644 src/lib/media/alt-text.ts create mode 100644 src/view/com/modals/AltImage.tsx diff --git a/package.json b/package.json index 4fa046b874..aef018a817 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "expo-build-properties": "~0.5.1", "expo-camera": "~13.2.1", "expo-dev-client": "~2.1.1", - "expo-image": "~1.0.0", + "expo-image": "^1.2.1", "expo-image-picker": "~14.1.1", "expo-localization": "~14.1.1", "expo-media-library": "~15.2.3", diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 1b12f29c5b..3877b3ef7c 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -10,15 +10,15 @@ import { import {AtUri} from '@atproto/api' import {RootStoreModel} from 'state/models/root-store' import {isNetworkError} from 'lib/strings/errors' -import {Image} from 'lib/media/types' import {LinkMeta} from '../link-meta/link-meta' import {isWeb} from 'platform/detection' +import {ImageModel} from 'state/models/media/image' export interface ExternalEmbedDraft { uri: string isLoading: boolean meta?: LinkMeta - localThumb?: Image + localThumb?: ImageModel } export async function resolveName(store: RootStoreModel, didOrHandle: string) { @@ -61,7 +61,7 @@ interface PostOpts { cid: string } extLink?: ExternalEmbedDraft - images?: string[] + images?: ImageModel[] knownHandles?: Set onStateChange?: (state: string) => void } @@ -109,10 +109,11 @@ export async function post(store: RootStoreModel, opts: PostOpts) { const images: AppBskyEmbedImages.Image[] = [] for (const image of opts.images) { opts.onStateChange?.(`Uploading image #${images.length + 1}...`) - const res = await uploadBlob(store, image, 'image/jpeg') + const path = image.compressed?.path ?? image.path + const res = await uploadBlob(store, path, 'image/jpeg') images.push({ image: res.data.blob, - alt: '', // TODO supply alt text + alt: image.altText ?? '', }) } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d49d8c75cf..12bdc55439 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -4,6 +4,10 @@ export const FEEDBACK_FORM_URL = export const MAX_DISPLAY_NAME = 64 export const MAX_DESCRIPTION = 256 +// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html +// but adding buffer room to account for languages like German +export const MAX_ALT_TEXT = 120 + export const PROD_TEAM_HANDLES = [ 'jay.bsky.social', 'pfrazee.com', diff --git a/src/lib/media/alt-text.ts b/src/lib/media/alt-text.ts new file mode 100644 index 0000000000..9f9f907bff --- /dev/null +++ b/src/lib/media/alt-text.ts @@ -0,0 +1,16 @@ +import {RootStoreModel} from 'state/index' + +export async function openAltTextModal(store: RootStoreModel): Promise { + return new Promise((resolve, reject) => { + store.shell.openModal({ + name: 'alt-text-image', + onAltTextSet: (altText?: string) => { + if (altText) { + resolve(altText) + } else { + reject(new Error('Canceled')) + } + }, + }) + }) +} diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts index fbe6c92a0a..97b1ac1d87 100644 --- a/src/state/models/media/gallery.ts +++ b/src/state/models/media/gallery.ts @@ -65,6 +65,10 @@ export class GalleryModel { }) } + setAltText(image: ImageModel) { + image.setAltText() + } + crop(image: ImageModel) { image.crop() } diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index 584bf90cc9..3585bb083c 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -5,6 +5,7 @@ import {makeAutoObservable, runInAction} from 'mobx' import {openCropper} from 'lib/media/picker' import {POST_IMG_MAX} from 'lib/constants' import {scaleDownDimensions} from 'lib/media/util' +import {openAltTextModal} from 'lib/media/alt-text' // TODO: EXIF embed // Cases to consider: ExternalEmbed @@ -14,6 +15,7 @@ export class ImageModel implements RNImage { width: number height: number size: number + altText?: string = undefined cropped?: RNImage = undefined compressed?: RNImage = undefined scaledWidth: number = POST_IMG_MAX.width @@ -41,6 +43,18 @@ export class ImageModel implements RNImage { this.scaledHeight = height } + async setAltText() { + try { + const altText = await openAltTextModal(this.rootStore) + + runInAction(() => { + this.altText = altText + }) + } catch (err) { + this.rootStore.log.error('Failed to set alt text', err) + } + } + async crop() { try { const cropped = await openCropper(this.rootStore, { diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 47cc0aa825..b717fe05cd 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -3,7 +3,7 @@ import {RootStoreModel} from '../root-store' import {makeAutoObservable} from 'mobx' import {ProfileModel} from '../content/profile' import {isObj, hasProp} from 'lib/type-guards' -import {Image} from 'lib/media/types' +import {Image as RNImage} from 'react-native-image-crop-picker' export interface ConfirmModal { name: 'confirm' @@ -38,7 +38,12 @@ export interface ReportAccountModal { export interface CropImageModal { name: 'crop-image' uri: string - onSelect: (img?: Image) => void + onSelect: (img?: RNImage) => void +} + +export interface AltTextImageModal { + name: 'alt-text-image' + onAltTextSet: (altText?: string) => void } export interface DeleteAccountModal { @@ -70,18 +75,30 @@ export interface ContentFilteringSettingsModal { } export type Modal = - | ConfirmModal - | EditProfileModal - | ServerInputModal - | ReportPostModal - | ReportAccountModal - | CropImageModal - | DeleteAccountModal - | RepostModal + // Account | ChangeHandleModal + | DeleteAccountModal + | EditProfileModal + + // Curation + | ContentFilteringSettingsModal + + // Reporting + | ReportAccountModal + | ReportPostModal + + // Posting + | AltTextImageModal + | CropImageModal + | ServerInputModal + | RepostModal + + // Bluesky access | WaitlistModal | InviteCodesModal - | ContentFilteringSettingsModal + + // Generic + | ConfirmModal interface LightboxModel {} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 08f977f790..2750013096 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -142,7 +142,7 @@ export const ComposePost = observer(function ComposePost({ await apilib.post(store, { rawText: rt.text, replyTo: replyTo?.uri, - images: gallery.paths, + images: gallery.images, quote: quote, extLink: extLink, onStateChange: setProcessingState, diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index f4dfc88fad..98f0824fdc 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -1,4 +1,5 @@ import React, {useCallback} from 'react' +import {ImageStyle, Keyboard} from 'react-native' import {GalleryModel} from 'state/models/media/gallery' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -6,6 +7,8 @@ import {colors} from 'lib/styles' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {ImageModel} from 'state/models/media/image' import {Image} from 'expo-image' +import {Text} from 'view/com/util/text/Text' +import {isDesktopWeb} from 'platform/detection' interface Props { gallery: GalleryModel @@ -13,17 +16,28 @@ interface Props { export const Gallery = observer(function ({gallery}: Props) { const getImageStyle = useCallback(() => { - switch (gallery.size) { - case 1: - return styles.image250 - case 2: - return styles.image175 - default: - return styles.image85 + let side: number + + if (gallery.size === 1) { + side = 250 + } else { + side = (isDesktopWeb ? 560 : 350) / gallery.size + } + + return { + height: side, + width: side, } }, [gallery]) const imageStyle = getImageStyle() + const handleAddImageAltText = useCallback( + (image: ImageModel) => { + Keyboard.dismiss() + gallery.setAltText(image) + }, + [gallery], + ) const handleRemovePhoto = useCallback( (image: ImageModel) => { gallery.remove(image) @@ -38,14 +52,68 @@ export const Gallery = observer(function ({gallery}: Props) { [gallery], ) + const isOverflow = !isDesktopWeb && gallery.size > 2 + + const imageControlLabelStyle = { + borderRadius: 5, + paddingHorizontal: 10, + position: 'absolute' as const, + width: 46, + zIndex: 1, + ...(isOverflow + ? { + left: 4, + bottom: 4, + } + : isDesktopWeb && gallery.size < 3 + ? { + left: 8, + top: 8, + } + : { + left: 4, + top: 4, + }), + } + + const imageControlsSubgroupStyle = { + display: 'flex' as const, + flexDirection: 'row' as const, + position: 'absolute' as const, + ...(isOverflow + ? { + top: 4, + right: 4, + gap: 4, + } + : isDesktopWeb && gallery.size < 3 + ? { + top: 8, + right: 8, + gap: 8, + } + : { + top: 4, + right: 4, + gap: 4, + }), + zIndex: 1, + } + return !gallery.isEmpty ? ( {gallery.images.map(image => image.compressed !== undefined ? ( - - + + { + handleAddImageAltText(image) + }} + style={[styles.imageControl, imageControlLabelStyle]}> + ALT + + { @@ -72,7 +140,7 @@ export const Gallery = observer(function ({gallery}: Props) { void +} + +export function Component({onAltTextSet}: Props) { + const pal = usePalette('default') + const store = useStores() + const theme = useTheme() + const [altText, setAltText] = useState('') + + const onPressSave = useCallback(() => { + onAltTextSet(altText) + store.shell.closeModal() + }, [store, altText, onAltTextSet]) + + const onPressCancel = () => { + store.shell.closeModal() + } + + return ( + + Add alt text + setAltText(enforceLen(text, MAX_ALT_TEXT))} + /> + + + + + Save + + + + + + + Cancel + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + gap: 18, + bottom: 0, + paddingVertical: 18, + paddingHorizontal: isDesktopWeb ? 0 : 12, + width: '100%', + }, + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + }, + textArea: { + borderWidth: 1, + borderRadius: 6, + paddingTop: 10, + paddingHorizontal: 12, + fontSize: 16, + height: 100, + textAlignVertical: 'top', + }, + button: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + borderRadius: 32, + padding: 10, + }, + buttonControls: { + gap: 8, + }, +}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 3f10ec8365..a83cdfdae6 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -1,5 +1,5 @@ import React, {useRef, useEffect} from 'react' -import {StyleSheet, View} from 'react-native' +import {StyleSheet} from 'react-native' import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' import {useStores} from 'state/index' @@ -11,6 +11,7 @@ import * as EditProfileModal from './EditProfile' import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' import * as RepostModal from './Repost' +import * as AltImageModal from './AltImage' import * as ReportAccountModal from './ReportAccount' import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' @@ -68,6 +69,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'repost') { snapPoints = RepostModal.snapPoints element = + } else if (activeModal?.name === 'alt-text-image') { + snapPoints = AltImageModal.snapPoints + element = } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = @@ -81,7 +85,7 @@ export const ModalsContainer = observer(function ModalsContainer() { snapPoints = ContentFilteringSettingsModal.snapPoints element = } else { - return + return null } return ( diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 25fed69a49..1effee69b7 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -14,6 +14,7 @@ import * as ReportAccountModal from './ReportAccount' import * as DeleteAccountModal from './DeleteAccount' import * as RepostModal from './Repost' import * as CropImageModal from './crop-image/CropImage.web' +import * as AltTextImageModal from './AltImage' import * as ChangeHandleModal from './ChangeHandle' import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' @@ -78,6 +79,8 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'content-filtering-settings') { element = + } else if (modal.name === 'alt-text-image') { + element = } else { return null } diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index b05111ffc2..02dea42048 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -369,10 +369,7 @@ function AdditionalPostText({ <> {text?.length > 0 && {text}} {images && images?.length > 0 && ( - img.thumb)} - style={styles.additionalPostImages} - /> + )} ) diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 17e3e809b2..8c31f56146 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -9,29 +9,33 @@ import { import {Image} from 'expo-image' import {clamp} from 'lib/numbers' import {useStores} from 'state/index' -import {Dim} from 'lib/media/manip' +import {Dimensions} from 'lib/media/types' export const DELAY_PRESS_IN = 500 const MIN_ASPECT_RATIO = 0.33 // 1/3 const MAX_ASPECT_RATIO = 5 // 5/1 -export function AutoSizedImage({ - uri, - onPress, - onLongPress, - onPressIn, - style, - children = null, -}: { +interface Props { + alt?: string uri: string onPress?: () => void onLongPress?: () => void onPressIn?: () => void style?: StyleProp children?: React.ReactNode -}) { +} + +export function AutoSizedImage({ + alt, + uri, + onPress, + onLongPress, + onPressIn, + style, + children = null, +}: Props) { const store = useStores() - const [dim, setDim] = React.useState( + const [dim, setDim] = React.useState( store.imageSizes.get(uri), ) const [aspectRatio, setAspectRatio] = React.useState( @@ -59,20 +63,31 @@ export function AutoSizedImage({ onPressIn={onPressIn} delayPressIn={DELAY_PRESS_IN} style={[styles.container, style]}> - + {children} ) } + return ( - + {children} ) } -function calc(dim: Dim) { +function calc(dim: Dimensions) { if (dim.width === 0 || dim.height === 0) { return 1 } diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 40f1948d69..5c232e0b46 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -7,21 +7,25 @@ import { ViewStyle, } from 'react-native' import {Image} from 'expo-image' +import {AppBskyEmbedImages} from '@atproto/api' -export function ImageHorzList({ - uris, - onPress, - style, -}: { - uris: string[] +interface Props { + images: AppBskyEmbedImages.ViewImage[] onPress?: (index: number) => void style?: StyleProp -}) { +} + +export function ImageHorzList({images, onPress, style}: Props) { return ( - {uris.map((uri, i) => ( + {images.map(({thumb, alt}, i) => ( onPress?.(i)}> - + ))} diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index f4fe59522e..51bb04fe94 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -9,26 +9,25 @@ import { } from 'react-native' import {Image, ImageStyle} from 'expo-image' import {Dimensions} from 'lib/media/types' +import {AppBskyEmbedImages} from '@atproto/api' export const DELAY_PRESS_IN = 500 -export type ImageLayoutGridType = number - -export function ImageLayoutGrid({ - type, - uris, - onPress, - onLongPress, - onPressIn, - style, -}: { - type: ImageLayoutGridType - uris: string[] +interface ImageLayoutGridProps { + images: AppBskyEmbedImages.ViewImage[] onPress?: (index: number) => void onLongPress?: (index: number) => void onPressIn?: (index: number) => void style?: StyleProp -}) { +} + +export function ImageLayoutGrid({ + images, + onPress, + onLongPress, + onPressIn, + style, +}: ImageLayoutGridProps) { const [containerInfo, setContainerInfo] = useState() const onLayout = (evt: LayoutChangeEvent) => { @@ -42,8 +41,7 @@ export function ImageLayoutGrid({ {containerInfo ? ( void onLongPress?: (index: number) => void onPressIn?: (index: number) => void containerInfo: Dimensions -}) { +} + +function ImageLayoutGridInner({ + images, + onPress, + onLongPress, + onPressIn, + containerInfo, +}: ImageLayoutGridInnerProps) { + const count = images.length const size1 = useMemo(() => { - if (type === 3) { + if (count === 3) { const size = (containerInfo.width - 10) / 3 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} } else { const size = (containerInfo.width - 5) / 2 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} } - }, [type, containerInfo]) + }, [count, containerInfo]) const size2 = React.useMemo(() => { - if (type === 3) { + if (count === 3) { const size = ((containerInfo.width - 10) / 3) * 2 + 5 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} } else { const size = (containerInfo.width - 5) / 2 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} } - }, [type, containerInfo]) + }, [count, containerInfo]) - if (type === 2) { + if (count === 2) { return ( onPress?.(0)} onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress?.(0)}> - + onPress?.(1)} onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress?.(1)}> - + ) } - if (type === 3) { + if (count === 3) { return ( onPress?.(0)} onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress?.(0)}> - + @@ -126,7 +140,12 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(1)} onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress?.(1)}> - + onPress?.(2)} onPressIn={() => onPressIn?.(2)} onLongPress={() => onLongPress?.(2)}> - + ) } - if (type === 4) { + if (count === 4) { return ( @@ -149,7 +173,12 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(0)} onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress?.(0)}> - + onPress?.(2)} onPressIn={() => onPressIn?.(2)} onLongPress={() => onLongPress?.(2)}> - + @@ -167,7 +201,12 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(1)} onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress?.(1)}> - + onPress?.(3)} onPressIn={() => onPressIn?.(3)} onLongPress={() => onLongPress?.(3)}> - + diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index c15986b76f..f37fba342d 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -112,6 +112,7 @@ export function PostEmbeds({ return ( openLightbox(0)} onLongPress={() => onLongPress(0)} @@ -124,8 +125,7 @@ export function PostEmbeds({ return ( img.thumb)} + images={embed.images} onPress={openLightbox} onLongPress={onLongPress} onPressIn={onPressIn} diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index eab050fd0a..e0abec777f 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -54,7 +54,6 @@ const ShellInner = observer(() => { - { onPost={store.shell.composerOpts?.onPost} quote={store.shell.composerOpts?.quote} /> + ) }) diff --git a/yarn.lock b/yarn.lock index f1cb70cf8c..635de6c181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8364,10 +8364,10 @@ expo-image-picker@~14.1.1: dependencies: expo-image-loader "~4.1.0" -expo-image@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.0.0.tgz#a3670d20815d99e2527307a33761c9b0088823b1" - integrity sha512-A1amVExKhBa/eRXuceauYtPkf9izeje5AbxEWL09tgK91rf3GSIZXM5PSDGlIM0s7dpCV+Iet2jhwcFUfWaZrw== +expo-image@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.2.1.tgz#3f377cb3142de2107903f4e4f88a7f44785dee18" + integrity sha512-pYZFN0ctuIBA+sqUiw70rHQQ04WDyEcF549ObArdj0MNgSUCBJMFmu/jrWDmxOpEMF40lfLVIZKigJT7Bw+GYA== expo-json-utils@~0.5.0: version "0.5.1" From aa56f4a5e2c4236b7ae74ab61e75e419a86ed83d Mon Sep 17 00:00:00 2001 From: Ollie Hsieh Date: Fri, 21 Apr 2023 14:40:41 -0700 Subject: [PATCH 007/374] Move border positioning to FlatList and ScrollView (#509) * Move border positioning to FlatList and ScrollView * Fix mobile web tab bar border --- src/view/com/pager/FeedsTabBar.web.tsx | 3 +-- src/view/com/pager/FeedsTabBarMobile.tsx | 3 ++- src/view/com/util/Views.web.tsx | 22 ++++++++++++++++--- src/view/shell/index.web.tsx | 28 ------------------------ 4 files changed, 22 insertions(+), 34 deletions(-) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index d80b140ce2..0fc1b73101 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -63,11 +63,10 @@ const styles = StyleSheet.create({ position: 'absolute', zIndex: 1, left: '50%', - width: 640, + width: 598, top: 0, flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 18, }, tabBarAvi: { marginTop: 1, diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 76e0a6fc6b..e7d2ec1042 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -33,7 +33,7 @@ export const FeedsTabBar = observer( }, [store]) return ( - + ( }: React.PropsWithChildren>, ref: React.Ref, ) { + const pal = usePalette('default') contentContainerStyle = addStyle( contentContainerStyle, styles.containerScroll, @@ -61,7 +63,11 @@ export const FlatList = React.forwardRef(function ( return ( , ref: React.Ref, ) { + const pal = usePalette('default') + contentContainerStyle = addStyle( contentContainerStyle, styles.containerScroll, ) return ( @@ -87,6 +99,11 @@ export const ScrollView = React.forwardRef(function ( }) const styles = StyleSheet.create({ + contentContainer: { + borderLeftWidth: 1, + borderRightWidth: 1, + minHeight: '100vh', + }, container: { width: '100%', maxWidth: 600, @@ -95,7 +112,6 @@ const styles = StyleSheet.create({ }, containerScroll: { width: '100%', - maxHeight: '100vh', maxWidth: 600, marginLeft: 'auto', marginRight: 'auto', diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 5d7ed259a3..3d790febc9 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -14,11 +14,9 @@ import {RoutesContainer, FlatNavigator} from '../../Navigation' import {DrawerContent} from './Drawer' import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' import {BottomBarWeb} from './bottom-bar/BottomBarWeb' -import {usePalette} from 'lib/hooks/usePalette' const ShellInner = observer(() => { const store = useStores() - const pal = usePalette('default') const {isDesktop} = useWebMediaQueries() return ( @@ -32,20 +30,6 @@ const ShellInner = observer(() => { <> - - )} Date: Fri, 21 Apr 2023 16:55:29 -0700 Subject: [PATCH 008/374] [APP-522] Create & revoke App Passwords within settings (#505) * create and delete app passwords * add randomly generated name * Tweak copy and layout of app passwords * Improve app passwords on desktop web * Rearrange settings * Change app-passwords route and add to backend * Fix link * Fix some more desktop web * Remove log --------- Co-authored-by: Paul Frazee --- bskyweb/cmd/bskyweb/server.go | 1 + src/Navigation.tsx | 2 + src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/state/models/me.ts | 61 +++++- src/state/models/ui/shell.ts | 5 + src/view/com/modals/AddAppPasswords.tsx | 216 +++++++++++++++++++ src/view/com/modals/Modal.tsx | 4 + src/view/com/util/ViewHeader.tsx | 25 ++- src/view/screens/AppPasswords.tsx | 275 ++++++++++++++++++++++++ src/view/screens/PostLikedBy.tsx | 2 +- src/view/screens/PostRepostedBy.tsx | 2 +- src/view/screens/ProfileFollowers.tsx | 2 +- src/view/screens/ProfileFollows.tsx | 2 +- src/view/screens/Settings.tsx | 16 +- 15 files changed, 607 insertions(+), 8 deletions(-) create mode 100644 src/view/com/modals/AddAppPasswords.tsx create mode 100644 src/view/screens/AppPasswords.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 0c8e9f8d2a..3339cccc10 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -92,6 +92,7 @@ func serve(cctx *cli.Context) error { e.GET("/search", server.WebGeneric) e.GET("/notifications", server.WebGeneric) e.GET("/settings", server.WebGeneric) + e.GET("/settings/app-passwords", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 3973b9dfa3..186432c8ca 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -46,6 +46,7 @@ import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' import {usePalette} from 'lib/hooks/usePalette' import {useStores} from './state' +import {AppPasswords} from 'view/screens/AppPasswords' const navigationRef = createNavigationContainerRef() @@ -84,6 +85,7 @@ function commonScreens(Stack: typeof HomeTab) { component={CommunityGuidelinesScreen} /> + ) } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index f8698f1cc1..eeb97ba7a9 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -19,6 +19,7 @@ export type CommonNavigatorParams = { TermsOfService: undefined CommunityGuidelines: undefined CopyrightPolicy: undefined + AppPasswords: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/routes.ts b/src/routes.ts index 7ae281424b..6762cde9d6 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -13,6 +13,7 @@ export const router = new Router({ PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', Debug: '/sys/debug', Log: '/sys/log', + AppPasswords: '/settings/app-passwords', Support: '/support', PrivacyPolicy: '/support/privacy', TermsOfService: '/support/tos', diff --git a/src/state/models/me.ts b/src/state/models/me.ts index b993637900..ba2dc6f32a 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -1,5 +1,8 @@ import {makeAutoObservable, runInAction} from 'mobx' -import {ComAtprotoServerDefs} from '@atproto/api' +import { + ComAtprotoServerDefs, + ComAtprotoServerListAppPasswords, +} from '@atproto/api' import {RootStoreModel} from './root-store' import {PostsFeedModel} from './feeds/posts' import {NotificationsFeedModel} from './feeds/notifications' @@ -21,6 +24,7 @@ export class MeModel { notifications: NotificationsFeedModel follows: MyFollowsCache invites: ComAtprotoServerDefs.InviteCode[] = [] + appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = [] lastProfileStateUpdate = Date.now() lastNotifsUpdate = Date.now() @@ -37,7 +41,7 @@ export class MeModel { this.mainFeed = new PostsFeedModel(this.rootStore, 'home', { algorithm: 'reverse-chronological', }) - this.notifications = new NotificationsFeedModel(this.rootStore, {}) + this.notifications = new NotificationsFeedModel(this.rootStore) this.follows = new MyFollowsCache(this.rootStore) } @@ -51,6 +55,7 @@ export class MeModel { this.description = '' this.avatar = '' this.invites = [] + this.appPasswords = [] } serialize(): unknown { @@ -107,6 +112,7 @@ export class MeModel { }) this.rootStore.emitSessionLoaded() await this.fetchInviteCodes() + await this.fetchAppPasswords() } else { this.clear() } @@ -118,6 +124,7 @@ export class MeModel { this.lastProfileStateUpdate = Date.now() await this.fetchProfile() await this.fetchInviteCodes() + await this.fetchAppPasswords() } if (Date.now() - this.lastNotifsUpdate > NOTIFS_UPDATE_INTERVAL) { this.lastNotifsUpdate = Date.now() @@ -171,6 +178,56 @@ export class MeModel { await this.rootStore.invitedUsers.fetch(this.invites) } } + + async fetchAppPasswords() { + if (this.rootStore.session) { + try { + const res = + await this.rootStore.agent.com.atproto.server.listAppPasswords({}) + runInAction(() => { + this.appPasswords = res.data.passwords + }) + } catch (e) { + this.rootStore.log.error('Failed to fetch user app passwords', e) + } + } + } + + async createAppPassword(name: string) { + if (this.rootStore.session) { + try { + if (this.appPasswords.find(p => p.name === name)) { + // TODO: this should be handled by the backend but it's not + throw new Error('App password with this name already exists') + } + const res = + await this.rootStore.agent.com.atproto.server.createAppPassword({ + name, + }) + runInAction(() => { + this.appPasswords.push(res.data) + }) + return res.data + } catch (e) { + this.rootStore.log.error('Failed to create app password', e) + } + } + } + + async deleteAppPassword(name: string) { + if (this.rootStore.session) { + try { + await this.rootStore.agent.com.atproto.server.revokeAppPassword({ + name: name, + }) + runInAction(() => { + this.appPasswords = this.appPasswords.filter(p => p.name !== name) + }) + } catch (e) { + this.rootStore.log.error('Failed to delete app password', e) + } + } + } } function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index b717fe05cd..6c58262d8a 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -70,6 +70,10 @@ export interface InviteCodesModal { name: 'invite-codes' } +export interface AddAppPasswordModal { + name: 'add-app-password' +} + export interface ContentFilteringSettingsModal { name: 'content-filtering-settings' } @@ -79,6 +83,7 @@ export type Modal = | ChangeHandleModal | DeleteAccountModal | EditProfileModal + | AddAppPasswordModal // Curation | ContentFilteringSettingsModal diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx new file mode 100644 index 0000000000..1d2f80ff0e --- /dev/null +++ b/src/view/com/modals/AddAppPasswords.tsx @@ -0,0 +1,216 @@ +import React, {useState} from 'react' +import {StyleSheet, TextInput, View, TouchableOpacity} from 'react-native' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {s} from 'lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import Clipboard from '@react-native-clipboard/clipboard' +import * as Toast from '../util/Toast' + +export const snapPoints = ['70%'] + +const shadesOfBlue: string[] = [ + 'AliceBlue', + 'Aqua', + 'Aquamarine', + 'Azure', + 'BabyBlue', + 'Blue', + 'BlueViolet', + 'CadetBlue', + 'CornflowerBlue', + 'Cyan', + 'DarkBlue', + 'DarkCyan', + 'DarkSlateBlue', + 'DeepSkyBlue', + 'DodgerBlue', + 'ElectricBlue', + 'LightBlue', + 'LightCyan', + 'LightSkyBlue', + 'LightSteelBlue', + 'MediumAquaMarine', + 'MediumBlue', + 'MediumSlateBlue', + 'MidnightBlue', + 'Navy', + 'PowderBlue', + 'RoyalBlue', + 'SkyBlue', + 'SlateBlue', + 'SteelBlue', + 'Teal', + 'Turquoise', +] + +export function Component({}: {}) { + const pal = usePalette('default') + const store = useStores() + const [name, setName] = useState( + shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], + ) + const [appPassword, setAppPassword] = useState() + const [wasCopied, setWasCopied] = useState(false) + + const onCopy = React.useCallback(() => { + if (appPassword) { + Clipboard.setString(appPassword) + Toast.show('Copied to clipboard') + setWasCopied(true) + } + }, [appPassword]) + + const onDone = React.useCallback(() => { + store.shell.closeModal() + }, [store]) + + const createAppPassword = async () => { + try { + const newPassword = await store.me.createAppPassword(name) + if (newPassword) { + setAppPassword(newPassword.password) + } else { + Toast.show('Failed to create app password.') + // TODO: better error handling (?) + } + } catch (e) { + Toast.show('Failed to create app password.') + store.log.error('Failed to create app password', {e}) + } + } + + return ( + + + {!appPassword ? ( + + Please enter a unique name for this App Password. We have generated + a random name for you. + + ) : ( + + Here is your app password. Use this to + sign into the other app along with your handle. + + )} + {!appPassword ? ( + + + + ) : ( + + {appPassword} + {wasCopied ? ( + Copied + ) : ( + + )} + + )} + + {appPassword ? ( + + For security reasons, you won't be able to view this again. If you + lose this password, you'll need to generate a new one. + + ) : null} + + )} @@ -115,11 +111,10 @@ const styles = StyleSheet.create({ marginBottom: 10, }, errorIcon: { - borderRadius: 30, + borderRadius: 25, width: 50, height: 50, alignItems: 'center', justifyContent: 'center', - marginRight: 5, }, }) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index cd6c72ff55..4e4e3040be 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -145,8 +145,7 @@ export const ProfileScreen = withAuthRequired( ) : uiState.profile.hasLoaded ? ( From e99c324f14f44ab00427648d74c8332d34c1dc1c Mon Sep 17 00:00:00 2001 From: Ansh Date: Tue, 25 Apr 2023 18:46:34 -0700 Subject: [PATCH 034/374] add pal.text to onboarding screens (#538) --- src/view/com/auth/create/Step2.tsx | 2 +- src/view/com/auth/create/StepHeader.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index 5a70e31a03..375f807961 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -46,7 +46,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => { )} {!model.inviteCode && model.isInviteCodeRequired ? ( - + Don't have an invite code?{' '} Join the waitlist diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx index 8c852b640a..4b4eb5d23b 100644 --- a/src/view/com/auth/create/StepHeader.tsx +++ b/src/view/com/auth/create/StepHeader.tsx @@ -7,10 +7,12 @@ export function StepHeader({step, title}: {step: string; title: string}) { const pal = usePalette('default') return ( - + {step === '3' ? 'Last step!' : <>Step {step} of 3} - {title} + + {title} + ) } From e1fd50d014749fc7757a322839ea46b4156ba8c4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 25 Apr 2023 20:46:47 -0500 Subject: [PATCH 035/374] Add web network failure error detection (close APP-83) (#537) --- src/lib/strings/errors.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts index 0efcad335c..0c11a6706c 100644 --- a/src/lib/strings/errors.ts +++ b/src/lib/strings/errors.ts @@ -19,5 +19,9 @@ export function cleanError(str: any): string { export function isNetworkError(e: unknown) { const str = String(e) - return str.includes('Abort') || str.includes('Network request failed') + return ( + str.includes('Abort') || + str.includes('Network request failed') || + str.includes('Failed to fetch') + ) } From f33a355a1ac1fb3e3d91e7e55a9fe9df53313e66 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 25 Apr 2023 20:47:07 -0500 Subject: [PATCH 036/374] [APP-562] Persist 'copied' state of invite codes (#535) * Persist 'copied' state of invite codes (close APP-562) * Dont show copied message if invite used --- src/state/models/invited-users.ts | 20 +++++++- src/view/com/modals/InviteCodes.tsx | 77 ++++++++++++++--------------- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/state/models/invited-users.ts b/src/state/models/invited-users.ts index 121161a320..a28e0309a6 100644 --- a/src/state/models/invited-users.ts +++ b/src/state/models/invited-users.ts @@ -4,6 +4,7 @@ import {RootStoreModel} from './root-store' import {isObj, hasProp, isStrArray} from 'lib/type-guards' export class InvitedUsers { + copiedInvites: string[] = [] seenDids: string[] = [] profiles: AppBskyActorDefs.ProfileViewDetailed[] = [] @@ -20,13 +21,20 @@ export class InvitedUsers { } serialize() { - return {seenDids: this.seenDids} + return {seenDids: this.seenDids, copiedInvites: this.copiedInvites} } hydrate(v: unknown) { if (isObj(v) && hasProp(v, 'seenDids') && isStrArray(v.seenDids)) { this.seenDids = v.seenDids } + if ( + isObj(v) && + hasProp(v, 'copiedInvites') && + isStrArray(v.copiedInvites) + ) { + this.copiedInvites = v.copiedInvites + } } async fetch(invites: ComAtprotoServerDefs.InviteCode[]) { @@ -63,6 +71,16 @@ export class InvitedUsers { } } + isInviteCopied(invite: string) { + return this.copiedInvites.includes(invite) + } + + setInviteCopied(invite: string) { + if (!this.isInviteCopied(invite)) { + this.copiedInvites.push(invite) + } + } + markSeen(did: string) { this.seenDids.push(did) this.profiles = this.profiles.filter(profile => profile.did !== did) diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index 5e31e16a8b..8d54a50b12 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -1,5 +1,6 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {observer} from 'mobx-react-lite' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -82,46 +83,42 @@ export function Component({}: {}) { ) } -function InviteCode({ - testID, - code, - used, -}: { - testID: string - code: string - used?: boolean -}) { - const pal = usePalette('default') - const [wasCopied, setWasCopied] = React.useState(false) +const InviteCode = observer( + ({testID, code, used}: {testID: string; code: string; used?: boolean}) => { + const pal = usePalette('default') + const store = useStores() - const onPress = React.useCallback(() => { - Clipboard.setString(code) - Toast.show('Copied to clipboard') - setWasCopied(true) - }, [code]) + const onPress = React.useCallback(() => { + Clipboard.setString(code) + Toast.show('Copied to clipboard') + store.invitedUsers.setInviteCopied(code) + }, [store, code]) - return ( - - - {code} - - {wasCopied ? ( - Copied - ) : !used ? ( - - ) : undefined} - - ) -} + return ( + + + {code} + + + {!used && store.invitedUsers.isInviteCopied(code) && ( + Copied + )} + {!used && ( + + )} + + ) + }, +) const styles = StyleSheet.create({ container: { @@ -163,11 +160,13 @@ const styles = StyleSheet.create({ inviteCode: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14, }, + codeCopied: { + marginRight: 8, + }, strikeThrough: { textDecorationLine: 'line-through', textDecorationStyle: 'solid', From 9b86cb5c36da993c99dfa36760667bded4c71b15 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 25 Apr 2023 20:47:37 -0500 Subject: [PATCH 037/374] Fix: dont request more than 25 posts at a time (close [APP-561]) (#534) --- src/state/models/feeds/notifications.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 0bbbe215c1..220e04bce2 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -10,6 +10,7 @@ import { ComAtprotoLabelDefs, } from '@atproto/api' import AwaitLock from 'await-lock' +import chunk from 'lodash.chunk' import {bundleAsync} from 'lib/async/bundle' import {RootStoreModel} from '../root-store' import {PostThreadModel} from '../content/post-thread' @@ -554,10 +555,15 @@ export class NotificationsFeedModel { // fetch additional data if (addedPostMap.size > 0) { - const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({ - uris: Array.from(addedPostMap.keys()), - }) - for (const post of postsRes.data.posts) { + const uriChunks = chunk(Array.from(addedPostMap.keys()), 25) + const postsChunks = await Promise.all( + uriChunks.map(uris => + this.rootStore.agent.app.bsky.feed + .getPosts({uris}) + .then(res => res.data.posts), + ), + ) + for (const post of postsChunks.flat()) { const models = addedPostMap.get(post.uri) if (models?.length) { for (const model of models) { From fc19ffba3881cf38d8a6a47a62c6ed5feff5aa4b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 25 Apr 2023 21:04:50 -0500 Subject: [PATCH 038/374] Update report modal to use new groupings (close [APP-567]) (#533) --- src/view/com/modals/ReportAccount.tsx | 70 +++++++++++----- src/view/com/modals/ReportPost.tsx | 107 +++++++++++++++++++----- src/view/com/util/forms/RadioButton.tsx | 14 ++-- src/view/com/util/forms/RadioGroup.tsx | 2 +- 4 files changed, 142 insertions(+), 51 deletions(-) diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx index 601bccbd13..e03f06bde5 100644 --- a/src/view/com/modals/ReportAccount.tsx +++ b/src/view/com/modals/ReportAccount.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react' +import React, {useState, useMemo} from 'react' import { ActivityIndicator, StyleSheet, @@ -15,12 +15,7 @@ import * as Toast from '../util/Toast' import {ErrorMessage} from '../util/error/ErrorMessage' import {cleanError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' - -const ITEMS: RadioGroupItem[] = [ - {key: 'spam', label: 'Spam or excessive repeat posts'}, - {key: 'abuse', label: 'Abusive, rude, or hateful'}, - {key: 'illegal', label: 'Posts illegal content'}, -] +import {isDesktopWeb} from 'platform/detection' export const snapPoints = ['50%'] @@ -31,6 +26,39 @@ export function Component({did}: {did: string}) { const [error, setError] = useState('') const [issue, setIssue] = useState('') const onSelectIssue = (v: string) => setIssue(v) + + const ITEMS: RadioGroupItem[] = useMemo( + () => [ + { + key: ComAtprotoModerationDefs.REASONMISLEADING, + label: ( + + + Misleading Account + + + Impersonation or false claims about identity or affiliation + + + ), + }, + { + key: ComAtprotoModerationDefs.REASONSPAM, + label: ( + + + Frequently Posts Unwanted Content + + + Spam; excessive mentions or replies + + + ), + }, + ], + [pal], + ) + const onPress = async () => { setError('') if (!issue) { @@ -38,15 +66,8 @@ export function Component({did}: {did: string}) { } setIsProcessing(true) try { - // NOTE: we should update the lexicon of reasontype to include more options -prf - let reasonType = ComAtprotoModerationDefs.REASONOTHER - if (issue === 'spam') { - reasonType = ComAtprotoModerationDefs.REASONSPAM - } - const reason = ITEMS.find(item => item.key === issue)?.label || '' await store.agent.com.atproto.moderation.createReport({ - reasonType, - reason, + reasonType: issue, subject: { $type: 'com.atproto.admin.defs#repoRef', did, @@ -61,11 +82,11 @@ export function Component({did}: {did: string}) { } } return ( - - Report account - + + + Report account + + What is the issue with this account? + + For other issues, please report specific posts. + {error ? ( @@ -101,15 +125,17 @@ export function Component({did}: {did: string}) { } const styles = StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: isDesktopWeb ? 0 : 10, + }, title: { textAlign: 'center', fontWeight: 'bold', - fontSize: 24, marginBottom: 12, }, description: { textAlign: 'center', - fontSize: 17, paddingHorizontal: 22, marginBottom: 10, }, diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx index 01a132af09..c2c89202be 100644 --- a/src/view/com/modals/ReportPost.tsx +++ b/src/view/com/modals/ReportPost.tsx @@ -1,6 +1,7 @@ -import React, {useState} from 'react' +import React, {useState, useMemo} from 'react' import { ActivityIndicator, + Linking, StyleSheet, TouchableOpacity, View, @@ -16,14 +17,9 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {cleanError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' -const ITEMS: RadioGroupItem[] = [ - {key: 'spam', label: 'Spam or excessive repeat posts'}, - {key: 'abuse', label: 'Abusive, rude, or hateful'}, - {key: 'copyright', label: 'Contains copyrighted material'}, - {key: 'illegal', label: 'Contains illegal content'}, -] +const DMCA_LINK = 'https://bsky.app/support/copyright' -export const snapPoints = ['50%'] +export const snapPoints = [500] export function Component({ postUri, @@ -38,6 +34,74 @@ export function Component({ const [error, setError] = useState('') const [issue, setIssue] = useState('') const onSelectIssue = (v: string) => setIssue(v) + + const ITEMS: RadioGroupItem[] = useMemo( + () => [ + { + key: ComAtprotoModerationDefs.REASONSPAM, + label: ( + + + Spam + + Excessive mentions or replies + + ), + }, + { + key: ComAtprotoModerationDefs.REASONSEXUAL, + label: ( + + + Unwanted Sexual Content + + + Nudity or pornography not labeled as such + + + ), + }, + { + key: '__copyright__', + label: ( + + + Copyright Violation + + Contains copyrighted material + + ), + }, + { + key: ComAtprotoModerationDefs.REASONVIOLATION, + label: ( + + + Illegal and Urgent + + + Glaring violations of law or terms of service + + + ), + }, + { + key: ComAtprotoModerationDefs.REASONOTHER, + label: ( + + + Other + + + An issue not included in these options + + + ), + }, + ], + [pal], + ) + const onPress = async () => { setError('') if (!issue) { @@ -45,22 +109,19 @@ export function Component({ } setIsProcessing(true) try { - // NOTE: we should update the lexicon of reasontype to include more options -prf - let reasonType = ComAtprotoModerationDefs.REASONOTHER - if (issue === 'spam') { - reasonType = ComAtprotoModerationDefs.REASONSPAM + if (issue === '__copyright__') { + Linking.openURL(DMCA_LINK) + } else { + await store.agent.createModerationReport({ + reasonType: issue, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: postUri, + cid: postCid, + }, + }) + Toast.show("Thank you for your report! We'll look into it promptly.") } - const reason = ITEMS.find(item => item.key === issue)?.label || '' - await store.agent.createModerationReport({ - reasonType, - reason, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postUri, - cid: postCid, - }, - }) - Toast.show("Thank you for your report! We'll look into it promptly.") store.shell.closeModal() return } catch (e: any) { diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx index f5696a76d5..9d1cb47497 100644 --- a/src/view/com/util/forms/RadioButton.tsx +++ b/src/view/com/util/forms/RadioButton.tsx @@ -15,7 +15,7 @@ export function RadioButton({ }: { testID?: string type?: ButtonType - label: string + label: string | JSX.Element isSelected: boolean style?: StyleProp onPress: () => void @@ -47,7 +47,7 @@ export function RadioButton({ borderColor: theme.palette.default.border, }, 'default-light': { - borderColor: theme.palette.default.border, + borderColor: theme.palette.default.borderDark, }, }) const circleFillStyle = choose>( @@ -128,9 +128,13 @@ export function RadioButton({ ) : undefined} - - {label} - + {typeof label === 'string' ? ( + + {label} + + ) : ( + {label} + )} ) diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx index 071540b73e..14599e6490 100644 --- a/src/view/com/util/forms/RadioGroup.tsx +++ b/src/view/com/util/forms/RadioGroup.tsx @@ -5,7 +5,7 @@ import {ButtonType} from './Button' import {s} from 'lib/styles' export interface RadioGroupItem { - label: string + label: string | JSX.Element key: string } From c8541a72436506a4e59f40bcd9c760f15c4c1cfa Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 25 Apr 2023 21:25:49 -0500 Subject: [PATCH 039/374] Fix e2e --- __e2e__/tests/home-screen.test.ts | 4 +++- __e2e__/tests/profile-screen.test.ts | 8 ++++++-- __e2e__/tests/thread-screen.test.ts | 8 ++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts index 1ec1774f3e..7fa9ff28c5 100644 --- a/__e2e__/tests/home-screen.test.ts +++ b/__e2e__/tests/home-screen.test.ts @@ -57,7 +57,9 @@ describe('Home screen', () => { .tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts index cf9debb59a..a7bb93656b 100644 --- a/__e2e__/tests/profile-screen.test.ts +++ b/__e2e__/tests/profile-screen.test.ts @@ -120,7 +120,9 @@ describe('Profile screen', () => { await element(by.id('profileHeaderDropdownBtn')).tap() await element(by.id('profileHeaderDropdownReportBtn')).tap() await expect(element(by.id('reportAccountModal'))).toBeVisible() - await element(by.id('reportAccountRadios-spam')).tap() + await element( + by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportAccountModal'))).not.toBeVisible() }) @@ -166,7 +168,9 @@ describe('Profile screen', () => { await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts index f84c339cef..8d3eacc884 100644 --- a/__e2e__/tests/thread-screen.test.ts +++ b/__e2e__/tests/thread-screen.test.ts @@ -106,7 +106,9 @@ describe('Thread screen', () => { await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) @@ -116,7 +118,9 @@ describe('Thread screen', () => { await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) From 8d8b3f53a082cd3737c49ab56fbbe9fd20cb3695 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 25 Apr 2023 21:26:33 -0500 Subject: [PATCH 040/374] 1.23 --- app.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index 8c019a1b46..9b83e55d59 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Bluesky", "slug": "bluesky", "owner": "blueskysocial", - "version": "1.22.0", + "version": "1.23.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -34,7 +34,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 7, + "versionCode": 8, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" diff --git a/package.json b/package.json index 707e70e5a7..c07b3d7918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.22.0", + "version": "1.23.0", "private": true, "scripts": { "postinstall": "patch-package", From a81158bd2a01ba976fb7d06a4d8e6ffa9416c9d3 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 26 Apr 2023 09:33:56 -0500 Subject: [PATCH 041/374] Tune some copy --- src/view/com/modals/EditProfile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 5a1ba3638e..9bd572cc02 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -182,7 +182,7 @@ export function Component({ Date: Thu, 27 Apr 2023 07:27:33 -0700 Subject: [PATCH 042/374] Add undo to web composer (#542) --- package.json | 1 + src/view/com/composer/text-input/TextInput.web.tsx | 2 ++ yarn.lock | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/package.json b/package.json index c07b3d7918..1d19d609ff 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@segment/sovran-react-native": "^0.4.5", "@tiptap/core": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220", + "@tiptap/extension-history": "^2.0.3", "@tiptap/extension-link": "^2.0.0-beta.220", "@tiptap/extension-mention": "^2.0.0-beta.220", "@tiptap/extension-paragraph": "^2.0.0-beta.220", diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index f21d4ac1a3..ef7676617d 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native' import {RichText} from '@atproto/api' import {useEditor, EditorContent, JSONContent} from '@tiptap/react' import {Document} from '@tiptap/extension-document' +import History from '@tiptap/extension-history' import {Link} from '@tiptap/extension-link' import {Mention} from '@tiptap/extension-mention' import {Paragraph} from '@tiptap/extension-paragraph' @@ -70,6 +71,7 @@ export const TextInput = React.forwardRef( placeholder, }), Text, + History, ], editorProps: { attributes: { diff --git a/yarn.lock b/yarn.lock index fb45b87a06..a6f174a251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4193,6 +4193,11 @@ dependencies: tippy.js "^6.3.7" +"@tiptap/extension-history@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.3.tgz#8936c15aa46f2ddeada1c3d9abe2888d58d08c30" + integrity sha512-00KHIcJ8kivn2ARI6NQYphv2LfllVCXViHGm0EhzDW6NQxCrriJKE3tKDcTFCu7LlC5doMpq9Z6KXdljc4oVeQ== + "@tiptap/extension-link@^2.0.0-beta.220": version "2.0.0-beta.220" resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.220.tgz#c9954613cd1e0a0f1527853b732ef50dff734eac" From 996dba759513c81ab57ee6167bb840340a468498 Mon Sep 17 00:00:00 2001 From: Ollie Hsieh Date: Thu, 27 Apr 2023 07:30:47 -0700 Subject: [PATCH 043/374] Close lightbox on web with escape key (#543) * Close lightbox on web with escape key * Lint --- src/view/com/lightbox/Lightbox.web.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index f10548351e..c17356d943 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useCallback, useEffect} from 'react' import { Image, TouchableOpacity, @@ -73,6 +73,20 @@ function LightboxInner({ } } + const onEscape = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose() + } + }, + [onClose], + ) + + useEffect(() => { + window.addEventListener('keydown', onEscape) + return () => window.removeEventListener('keydown', onEscape) + }, [onEscape]) + return ( From 62b07f93fdfaad750dca5ebaf79706d0d3b36630 Mon Sep 17 00:00:00 2001 From: Ollie Hsieh Date: Thu, 27 Apr 2023 07:31:14 -0700 Subject: [PATCH 044/374] Support Ctrl + Enter for non-Mac (#544) --- src/view/com/composer/text-input/TextInput.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index ef7676617d..3f98a3595b 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -87,7 +87,7 @@ export const TextInput = React.forwardRef( getImageFromUri(items, onPhotoPasted) }, handleKeyDown: (_, event) => { - if (event.metaKey && event.code === 'Enter') { + if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { // Workaround relying on previous state from `setRichText` to // get the updated text content during editor initialization setRichText((state: RichText) => { From c8e51a7d48587d977a7c005caeb9a1b215fa0ab5 Mon Sep 17 00:00:00 2001 From: Ollie Hsieh Date: Thu, 27 Apr 2023 07:38:28 -0700 Subject: [PATCH 045/374] Fix Android sharing (#545) --- src/lib/sharing.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/sharing.ts b/src/lib/sharing.ts index 95ebf8ee9a..b294d74649 100644 --- a/src/lib/sharing.ts +++ b/src/lib/sharing.ts @@ -1,4 +1,4 @@ -import {isNative} from 'platform/detection' +import {isIOS, isAndroid} from 'platform/detection' // import * as Sharing from 'expo-sharing' import Clipboard from '@react-native-clipboard/clipboard' import * as Toast from '../view/com/util/Toast' @@ -11,8 +11,10 @@ import {Share} from 'react-native' * clipboard. */ export async function shareUrl(url: string) { - if (isNative) { - Share.share({url: url}) + if (isAndroid) { + Share.share({message: url}) + } else if (isIOS) { + Share.share({url}) } else { // React Native Share is not supported by web. Web Share API // has increasing but not full support, so default to clipboard From 7a2c21026db702d028c20796a8d0bccd68de5464 Mon Sep 17 00:00:00 2001 From: Ollie Hsieh Date: Thu, 27 Apr 2023 07:51:47 -0700 Subject: [PATCH 046/374] Load previous state in alt text modal (#546) --- src/lib/media/alt-text.ts | 6 +++++- src/state/models/media/image.ts | 4 ++-- src/state/models/ui/shell.ts | 1 + src/view/com/modals/AltImage.tsx | 5 +++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/lib/media/alt-text.ts b/src/lib/media/alt-text.ts index 9f9f907bff..77b0be4461 100644 --- a/src/lib/media/alt-text.ts +++ b/src/lib/media/alt-text.ts @@ -1,9 +1,13 @@ import {RootStoreModel} from 'state/index' -export async function openAltTextModal(store: RootStoreModel): Promise { +export async function openAltTextModal( + store: RootStoreModel, + prevAltText: string, +): Promise { return new Promise((resolve, reject) => { store.shell.openModal({ name: 'alt-text-image', + prevAltText, onAltTextSet: (altText?: string) => { if (altText) { resolve(altText) diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index 3585bb083c..d989380d17 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -15,7 +15,7 @@ export class ImageModel implements RNImage { width: number height: number size: number - altText?: string = undefined + altText = '' cropped?: RNImage = undefined compressed?: RNImage = undefined scaledWidth: number = POST_IMG_MAX.width @@ -45,7 +45,7 @@ export class ImageModel implements RNImage { async setAltText() { try { - const altText = await openAltTextModal(this.rootStore) + const altText = await openAltTextModal(this.rootStore, this.altText) runInAction(() => { this.altText = altText diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index a2891d9bbe..797d53f816 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -43,6 +43,7 @@ export interface CropImageModal { export interface AltTextImageModal { name: 'alt-text-image' + prevAltText: string onAltTextSet: (altText?: string) => void } diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index e6e2ed8318..639303c980 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -15,14 +15,15 @@ import {isDesktopWeb} from 'platform/detection' export const snapPoints = ['80%'] interface Props { + prevAltText: string onAltTextSet: (altText?: string | undefined) => void } -export function Component({onAltTextSet}: Props) { +export function Component({prevAltText, onAltTextSet}: Props) { const pal = usePalette('default') const store = useStores() const theme = useTheme() - const [altText, setAltText] = useState('') + const [altText, setAltText] = useState(prevAltText) const onPressSave = useCallback(() => { onAltTextSet(altText) From 51be8474db5e8074b1af233609b5eb455af31692 Mon Sep 17 00:00:00 2001 From: Ollie Hsieh Date: Thu, 27 Apr 2023 10:31:03 -0700 Subject: [PATCH 047/374] Update invite code copy (#549) --- src/view/com/modals/InviteCodes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index 8d54a50b12..992439ebcf 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -57,7 +57,7 @@ export function Component({}: {}) { code works once! - ( We'll send you more periodically. ) + ( You'll receive one invite code every two weeks. ) {store.me.invites.map((invite, i) => ( From 1d50ddb378d5c6954d4cf8a6145b4486b9497107 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 27 Apr 2023 12:38:23 -0500 Subject: [PATCH 048/374] Refactor moderation to apply to accounts, profiles, and posts correctly (#548) * Add ScreenHider component * Add blur attribute to UserAvatar and UserBanner * Remove dead suggested posts component and model * Bump @atproto/api@0.2.10 * Rework moderation tooling to give a more precise DSL * Add label mocks * Apply finer grained moderation controls * Refactor ProfileCard to just take the profile object * Apply moderation to user listings and banner * Apply moderation to notifications * Fix lint * Tune avatar & banner blur settings per platform * 1.24 --- __e2e__/mock-server.ts | 114 +++++++ app.json | 4 +- jest/test-pds.ts | 149 ++++++++- package.json | 4 +- src/lib/labeling/const.ts | 20 +- src/lib/labeling/helpers.ts | 303 +++++++++++++++++- src/lib/labeling/types.ts | 58 ++++ src/state/models/content/post-thread.ts | 22 ++ src/state/models/content/post.ts | 122 ------- src/state/models/content/profile.ts | 18 ++ src/state/models/discovery/suggested-posts.ts | 88 ----- src/state/models/feeds/notifications.ts | 54 +++- src/state/models/feeds/posts.ts | 22 ++ src/view/com/discover/SuggestedPosts.tsx | 66 ---- src/view/com/notifications/FeedItem.tsx | 55 ++-- src/view/com/post-thread/PostLikedBy.tsx | 10 +- src/view/com/post-thread/PostRepostedBy.tsx | 10 +- src/view/com/post-thread/PostThreadItem.tsx | 25 +- src/view/com/post/Post.tsx | 7 +- src/view/com/post/PostText.tsx | 62 ---- src/view/com/posts/FeedItem.tsx | 12 +- src/view/com/profile/ProfileCard.tsx | 246 +++++++------- src/view/com/profile/ProfileFollowers.tsx | 10 +- src/view/com/profile/ProfileFollows.tsx | 10 +- src/view/com/profile/ProfileHeader.tsx | 8 +- src/view/com/search/SearchResults.tsx | 10 +- src/view/com/search/Suggestions.tsx | 34 +- src/view/com/util/PostMeta.tsx | 2 +- src/view/com/util/UserAvatar.tsx | 17 +- src/view/com/util/UserBanner.tsx | 9 +- src/view/com/util/error/ErrorScreen.tsx | 2 +- src/view/com/util/moderation/ContentHider.tsx | 25 +- src/view/com/util/moderation/PostHider.tsx | 85 +++-- .../util/moderation/ProfileHeaderLabels.tsx | 55 ---- .../util/moderation/ProfileHeaderWarnings.tsx | 44 +++ src/view/com/util/moderation/ScreenHider.tsx | 129 ++++++++ src/view/screens/Profile.tsx | 9 +- src/view/screens/SearchMobile.tsx | 21 +- src/view/shell/desktop/Search.tsx | 9 +- yarn.lock | 8 +- 40 files changed, 1195 insertions(+), 763 deletions(-) create mode 100644 src/lib/labeling/types.ts delete mode 100644 src/state/models/content/post.ts delete mode 100644 src/state/models/discovery/suggested-posts.ts delete mode 100644 src/view/com/discover/SuggestedPosts.tsx delete mode 100644 src/view/com/post/PostText.tsx delete mode 100644 src/view/com/util/moderation/ProfileHeaderLabels.tsx create mode 100644 src/view/com/util/moderation/ProfileHeaderWarnings.tsx create mode 100644 src/view/com/util/moderation/ScreenHider.tsx diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts index 7bcad47f32..858ac5e086 100644 --- a/__e2e__/mock-server.ts +++ b/__e2e__/mock-server.ts @@ -63,6 +63,120 @@ async function main() { }, }) } + if ('labels' in url.query) { + console.log('Generating naughty users with labels') + + const anchorPost = await server.mocker.createPost( + 'alice', + 'Anchor post', + ) + + for (const user of [ + 'csam-account', + 'csam-profile', + 'csam-posts', + 'porn-account', + 'porn-profile', + 'porn-posts', + 'nudity-account', + 'nudity-profile', + 'nudity-posts', + 'muted-account', + ]) { + await server.mocker.createUser(user) + await server.mocker.follow('alice', user) + await server.mocker.follow(user, 'alice') + await server.mocker.createPost(user, `Unlabeled post from ${user}`) + await server.mocker.createReply( + user, + `Unlabeled reply from ${user}`, + anchorPost, + ) + await server.mocker.like(user, anchorPost) + } + + await server.mocker.labelAccount('csam', 'csam-account') + await server.mocker.labelProfile('csam', 'csam-profile') + await server.mocker.labelPost( + 'csam', + await server.mocker.createPost('csam-posts', 'csam post'), + ) + await server.mocker.labelPost( + 'csam', + await server.mocker.createQuotePost( + 'csam-posts', + 'csam quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'csam', + await server.mocker.createReply( + 'csam-posts', + 'csam reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount('porn', 'porn-account') + await server.mocker.labelProfile('porn', 'porn-profile') + await server.mocker.labelPost( + 'porn', + await server.mocker.createPost('porn-posts', 'porn post'), + ) + await server.mocker.labelPost( + 'porn', + await server.mocker.createQuotePost( + 'porn-posts', + 'porn quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'porn', + await server.mocker.createReply( + 'porn-posts', + 'porn reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount('nudity', 'nudity-account') + await server.mocker.labelProfile('nudity', 'nudity-profile') + await server.mocker.labelPost( + 'nudity', + await server.mocker.createPost('nudity-posts', 'nudity post'), + ) + await server.mocker.labelPost( + 'nudity', + await server.mocker.createQuotePost( + 'nudity-posts', + 'nudity quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'nudity', + await server.mocker.createReply( + 'nudity-posts', + 'nudity reply', + anchorPost, + ), + ) + + await server.mocker.users.alice.agent.mute('muted-account.test') + await server.mocker.createPost('muted-account', 'muted post') + await server.mocker.createQuotePost( + 'muted-account', + 'account quote post', + anchorPost, + ) + await server.mocker.createReply( + 'muted-account', + 'account reply', + anchorPost, + ) + } } console.log('Ready') return res.writeHead(200).end(server.pdsUrl) diff --git a/app.json b/app.json index 9b83e55d59..f4b70f0a72 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Bluesky", "slug": "bluesky", "owner": "blueskysocial", - "version": "1.23.0", + "version": "1.24.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -34,7 +34,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 8, + "versionCode": 9, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" diff --git a/jest/test-pds.ts b/jest/test-pds.ts index 649638989e..7f8d202323 100644 --- a/jest/test-pds.ts +++ b/jest/test-pds.ts @@ -2,6 +2,7 @@ import {AddressInfo} from 'net' import os from 'os' import net from 'net' import path from 'path' +import fs from 'fs' import * as crypto from '@atproto/crypto' import {PDS, ServerConfig, Database, MemoryBlobStore} from '@atproto/pds' import * as plc from '@did-plc/lib' @@ -104,9 +105,13 @@ export async function createServer( await pds.start() const pdsUrl = `http://localhost:${port}` + const profilePic = fs.readFileSync( + path.join(__dirname, '..', 'assets', 'default-avatar.jpg'), + ) + return { pdsUrl, - mocker: new Mocker(pdsUrl), + mocker: new Mocker(pds, pdsUrl, profilePic), async close() { await pds.destroy() await plcServer.destroy() @@ -118,7 +123,11 @@ class Mocker { agent: BskyAgent users: Record = {} - constructor(public service: string) { + constructor( + public pds: PDS, + public service: string, + public profilePic: Uint8Array, + ) { this.agent = new BskyAgent({service}) } @@ -152,6 +161,15 @@ class Mocker { handle: name + '.test', password: 'hunter2', }) + await agent.upsertProfile(async () => { + const blob = await agent.uploadBlob(this.profilePic, { + encoding: 'image/jpeg', + }) + return { + displayName: name, + avatar: blob.data.blob, + } + }) this.users[name] = { did: res.data.did, email, @@ -192,6 +210,133 @@ class Mocker { await this.follow('carla', 'alice') await this.follow('carla', 'bob') } + + async createPost(user: string, text: string) { + const agent = this.users[user]?.agent + if (!agent) { + throw new Error(`Not a user: ${user}`) + } + return await agent.post({ + text, + createdAt: new Date().toISOString(), + }) + } + + async createQuotePost( + user: string, + text: string, + {uri, cid}: {uri: string; cid: string}, + ) { + const agent = this.users[user]?.agent + if (!agent) { + throw new Error(`Not a user: ${user}`) + } + return await agent.post({ + text, + embed: {$type: 'app.bsky.embed.record', record: {uri, cid}}, + createdAt: new Date().toISOString(), + }) + } + + async createReply( + user: string, + text: string, + {uri, cid}: {uri: string; cid: string}, + ) { + const agent = this.users[user]?.agent + if (!agent) { + throw new Error(`Not a user: ${user}`) + } + return await agent.post({ + text, + reply: {root: {uri, cid}, parent: {uri, cid}}, + createdAt: new Date().toISOString(), + }) + } + + async like(user: string, {uri, cid}: {uri: string; cid: string}) { + const agent = this.users[user]?.agent + if (!agent) { + throw new Error(`Not a user: ${user}`) + } + return await agent.like(uri, cid) + } + + async labelAccount(label: string, user: string) { + const did = this.users[user]?.did + if (!did) { + throw new Error(`Invalid user: ${user}`) + } + const ctx = this.pds.ctx + if (!ctx) { + throw new Error('Invalid PDS') + } + + await ctx.db.db + .insertInto('label') + .values([ + { + src: ctx.cfg.labelerDid, + uri: did, + cid: '', + val: label, + neg: 0, + cts: new Date().toISOString(), + }, + ]) + .execute() + } + + async labelProfile(label: string, user: string) { + const agent = this.users[user]?.agent + const did = this.users[user]?.did + if (!did) { + throw new Error(`Invalid user: ${user}`) + } + + const profile = await agent.app.bsky.actor.profile.get({ + repo: user + '.test', + rkey: 'self', + }) + + const ctx = this.pds.ctx + if (!ctx) { + throw new Error('Invalid PDS') + } + await ctx.db.db + .insertInto('label') + .values([ + { + src: ctx.cfg.labelerDid, + uri: profile.uri, + cid: profile.cid, + val: label, + neg: 0, + cts: new Date().toISOString(), + }, + ]) + .execute() + } + + async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) { + const ctx = this.pds.ctx + if (!ctx) { + throw new Error('Invalid PDS') + } + await ctx.db.db + .insertInto('label') + .values([ + { + src: ctx.cfg.labelerDid, + uri, + cid, + val: label, + neg: 0, + cts: new Date().toISOString(), + }, + ]) + .execute() + } } const checkAvailablePort = (port: number) => diff --git a/package.json b/package.json index 1d19d609ff..939c62b6f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.23.0", + "version": "1.24.0", "private": true, "scripts": { "postinstall": "patch-package", @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.2.9", + "@atproto/api": "0.2.10", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index f68353222c..6670e5413f 100644 --- a/src/lib/labeling/const.ts +++ b/src/lib/labeling/const.ts @@ -1,23 +1,20 @@ import {LabelPreferencesModel} from 'state/models/ui/preferences' - -export interface LabelValGroup { - id: keyof LabelPreferencesModel | 'illegal' | 'unknown' - title: string - subtitle?: string - warning?: string - values: string[] -} +import {LabelValGroup} from './types' export const ILLEGAL_LABEL_GROUP: LabelValGroup = { id: 'illegal', title: 'Illegal Content', + warning: 'Illegal Content', values: ['csam', 'dmca-violation', 'nudity-nonconsentual'], + imagesOnly: false, // not applicable } export const UNKNOWN_LABEL_GROUP: LabelValGroup = { id: 'unknown', title: 'Unknown Label', + warning: 'Content Warning', values: [], + imagesOnly: false, } export const CONFIGURABLE_LABEL_GROUPS: Record< @@ -30,6 +27,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'i.e. Pornography', warning: 'Sexually Explicit', values: ['porn'], + imagesOnly: false, // apply to whole thing }, nudity: { id: 'nudity', @@ -37,6 +35,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Including non-sexual and artistic', warning: 'Nudity', values: ['nudity'], + imagesOnly: true, }, suggestive: { id: 'suggestive', @@ -44,6 +43,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Does not include nudity', warning: 'Sexually Suggestive', values: ['sexual'], + imagesOnly: true, }, gore: { id: 'gore', @@ -51,12 +51,14 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Gore, self-harm, torture', warning: 'Violence', values: ['gore', 'self-harm', 'torture'], + imagesOnly: true, }, hate: { id: 'hate', title: 'Political Hate-Groups', warning: 'Hate', values: ['icon-kkk', 'icon-nazi'], + imagesOnly: false, }, spam: { id: 'spam', @@ -64,6 +66,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Excessive low-quality posts', warning: 'Spam', values: ['spam'], + imagesOnly: false, }, impersonation: { id: 'impersonation', @@ -71,5 +74,6 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Accounts falsely claiming to be people or orgs', warning: 'Impersonation', values: ['impersonation'], + imagesOnly: false, }, } diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index b2057ff18c..bac98c6a28 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -1,9 +1,33 @@ import { - LabelValGroup, + AppBskyActorDefs, + AppBskyEmbedRecordWithMedia, + AppBskyEmbedRecord, + AppBskyFeedPost, + AppBskyEmbedImages, + AppBskyEmbedExternal, +} from '@atproto/api' +import { CONFIGURABLE_LABEL_GROUPS, ILLEGAL_LABEL_GROUP, UNKNOWN_LABEL_GROUP, } from './const' +import { + Label, + LabelValGroup, + ModerationBehaviorCode, + PostModeration, + ProfileModeration, + PostLabelInfo, + ProfileLabelInfo, +} from './types' +import {RootStoreModel} from 'state/index' + +type Embed = + | AppBskyEmbedRecord.View + | AppBskyEmbedImages.View + | AppBskyEmbedExternal.View + | AppBskyEmbedRecordWithMedia.View + | {$type: string; [k: string]: unknown} export function getLabelValueGroup(labelVal: string): LabelValGroup { let id: keyof typeof CONFIGURABLE_LABEL_GROUPS @@ -17,3 +41,280 @@ export function getLabelValueGroup(labelVal: string): LabelValGroup { } return UNKNOWN_LABEL_GROUP } + +export function getPostModeration( + store: RootStoreModel, + postInfo: PostLabelInfo, +): PostModeration { + const accountPref = store.preferences.getLabelPreference( + postInfo.accountLabels, + ) + const profilePref = store.preferences.getLabelPreference( + postInfo.profileLabels, + ) + const postPref = store.preferences.getLabelPreference(postInfo.postLabels) + + // avatar + let avatar = { + warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', + blur: + accountPref.pref === 'hide' || + accountPref.pref === 'warn' || + profilePref.pref === 'hide' || + profilePref.pref === 'warn', + } + + // hide no-override cases + if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') { + return hidePostNoOverride(accountPref.desc.warning) + } + if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') { + return hidePostNoOverride(profilePref.desc.warning) + } + if (postPref.pref === 'hide' && postPref.desc.id === 'illegal') { + return hidePostNoOverride(postPref.desc.warning) + } + + // hide cases + if (accountPref.pref === 'hide') { + return { + avatar, + list: hide(accountPref.desc.warning), + thread: hide(accountPref.desc.warning), + view: warn(accountPref.desc.warning), + } + } + if (profilePref.pref === 'hide') { + return { + avatar, + list: hide(profilePref.desc.warning), + thread: hide(profilePref.desc.warning), + view: warn(profilePref.desc.warning), + } + } + if (postPref.pref === 'hide') { + return { + avatar, + list: hide(postPref.desc.warning), + thread: hide(postPref.desc.warning), + view: warn(postPref.desc.warning), + } + } + + // muting + if (postInfo.isMuted) { + return { + avatar, + list: hide('Post from an account you muted.'), + thread: warn('Post from an account you muted.'), + view: warn('Post from an account you muted.'), + } + } + + // warning cases + if (postPref.pref === 'warn') { + if (postPref.desc.imagesOnly) { + return { + avatar, + list: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + thread: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + view: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + } + } + return { + avatar, + list: warnContent(postPref.desc.warning), + thread: warnContent(postPref.desc.warning), + view: warnContent(postPref.desc.warning), + } + } + if (accountPref.pref === 'warn') { + return { + avatar, + list: warnContent(accountPref.desc.warning), + thread: warnContent(accountPref.desc.warning), + view: warnContent(accountPref.desc.warning), + } + } + + return { + avatar, + list: show(), + thread: show(), + view: show(), + } +} + +export function getProfileModeration( + store: RootStoreModel, + profileLabels: ProfileLabelInfo, +): ProfileModeration { + const accountPref = store.preferences.getLabelPreference( + profileLabels.accountLabels, + ) + const profilePref = store.preferences.getLabelPreference( + profileLabels.profileLabels, + ) + + // avatar + let avatar = { + warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', + blur: + accountPref.pref === 'hide' || + accountPref.pref === 'warn' || + profilePref.pref === 'hide' || + profilePref.pref === 'warn', + } + + // hide no-override cases + if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') { + return hideProfileNoOverride(accountPref.desc.warning) + } + if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') { + return hideProfileNoOverride(profilePref.desc.warning) + } + + // hide cases + if (accountPref.pref === 'hide') { + return { + avatar, + list: hide(accountPref.desc.warning), + view: hide(accountPref.desc.warning), + } + } + if (profilePref.pref === 'hide') { + return { + avatar, + list: hide(profilePref.desc.warning), + view: hide(profilePref.desc.warning), + } + } + + // warn cases + if (accountPref.pref === 'warn') { + return { + avatar, + list: warn(accountPref.desc.warning), + view: warn(accountPref.desc.warning), + } + } + // we don't warn for this + // if (profilePref.pref === 'warn') { + // return { + // avatar, + // list: warn(profilePref.desc.warning), + // view: warn(profilePref.desc.warning), + // } + // } + + return { + avatar, + list: show(), + view: show(), + } +} + +export function getProfileViewBasicLabelInfo( + profile: AppBskyActorDefs.ProfileViewBasic, +): ProfileLabelInfo { + return { + accountLabels: filterAccountLabels(profile.labels), + profileLabels: filterProfileLabels(profile.labels), + isMuted: profile.viewer?.muted || false, + } +} + +export function getEmbedLabels(embed?: Embed): Label[] { + if (!embed) { + return [] + } + if ( + AppBskyEmbedRecordWithMedia.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record.record) && + AppBskyFeedPost.isRecord(embed.record.record.value) && + AppBskyFeedPost.validateRecord(embed.record.record.value).success + ) { + return embed.record.record.labels || [] + } + return [] +} + +export function filterAccountLabels(labels?: Label[]): Label[] { + if (!labels) { + return [] + } + return labels.filter( + label => !label.uri.endsWith('/app.bsky.actor.profile/self'), + ) +} + +export function filterProfileLabels(labels?: Label[]): Label[] { + if (!labels) { + return [] + } + return labels.filter(label => + label.uri.endsWith('/app.bsky.actor.profile/self'), + ) +} + +// internal methods +// = + +function show() { + return { + behavior: ModerationBehaviorCode.Show, + } +} + +function hidePostNoOverride(reason: string) { + return { + avatar: {warn: true, blur: true}, + list: hideNoOverride(reason), + thread: hideNoOverride(reason), + view: hideNoOverride(reason), + } +} + +function hideProfileNoOverride(reason: string) { + return { + avatar: {warn: true, blur: true}, + list: hideNoOverride(reason), + view: hideNoOverride(reason), + } +} + +function hideNoOverride(reason: string) { + return { + behavior: ModerationBehaviorCode.Hide, + reason, + noOverride: true, + } +} + +function hide(reason: string) { + return { + behavior: ModerationBehaviorCode.Hide, + reason, + } +} + +function warn(reason: string) { + return { + behavior: ModerationBehaviorCode.Warn, + reason, + } +} + +function warnContent(reason: string) { + return { + behavior: ModerationBehaviorCode.WarnContent, + reason, + } +} + +function warnImages(reason: string) { + return { + behavior: ModerationBehaviorCode.WarnImages, + reason, + } +} diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts new file mode 100644 index 0000000000..d4efb499a3 --- /dev/null +++ b/src/lib/labeling/types.ts @@ -0,0 +1,58 @@ +import {ComAtprotoLabelDefs} from '@atproto/api' +import {LabelPreferencesModel} from 'state/models/ui/preferences' + +export type Label = ComAtprotoLabelDefs.Label + +export interface LabelValGroup { + id: keyof LabelPreferencesModel | 'illegal' | 'unknown' + title: string + imagesOnly: boolean + subtitle?: string + warning: string + values: string[] +} + +export interface PostLabelInfo { + postLabels: Label[] + accountLabels: Label[] + profileLabels: Label[] + isMuted: boolean +} + +export interface ProfileLabelInfo { + accountLabels: Label[] + profileLabels: Label[] + isMuted: boolean +} + +export enum ModerationBehaviorCode { + Show, + Hide, + Warn, + WarnContent, + WarnImages, +} + +export interface ModerationBehavior { + behavior: ModerationBehaviorCode + noOverride?: boolean + reason?: string +} + +export interface AvatarModeration { + warn: boolean + blur: boolean +} + +export interface PostModeration { + avatar: AvatarModeration + list: ModerationBehavior + thread: ModerationBehavior + view: ModerationBehavior +} + +export interface ProfileModeration { + avatar: AvatarModeration + list: ModerationBehavior + view: ModerationBehavior +} diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index 76cab5c619..8f9a550327 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -10,6 +10,13 @@ import {RootStoreModel} from '../root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' import {updateDataOptimistically} from 'lib/async/revertible' +import {PostLabelInfo, PostModeration} from 'lib/labeling/types' +import { + getEmbedLabels, + filterAccountLabels, + filterProfileLabels, + getPostModeration, +} from 'lib/labeling/helpers' export class PostThreadItemModel { // ui state @@ -46,6 +53,21 @@ export class PostThreadItemModel { return this.rootStore.mutedThreads.uris.has(this.rootUri) } + get labelInfo(): PostLabelInfo { + return { + postLabels: (this.post.labels || []).concat( + getEmbedLabels(this.post.embed), + ), + accountLabels: filterAccountLabels(this.post.author.labels), + profileLabels: filterProfileLabels(this.post.author.labels), + isMuted: this.post.author.viewer?.muted || false, + } + } + + get moderation(): PostModeration { + return getPostModeration(this.rootStore, this.labelInfo) + } + constructor( public rootStore: RootStoreModel, v: AppBskyFeedDefs.ThreadViewPost, diff --git a/src/state/models/content/post.ts b/src/state/models/content/post.ts deleted file mode 100644 index 7ba633366c..0000000000 --- a/src/state/models/content/post.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {AppBskyFeedPost as Post} from '@atproto/api' -import {AtUri} from '@atproto/api' -import {RootStoreModel} from '../root-store' -import {cleanError} from 'lib/strings/errors' - -type RemoveIndex = { - [P in keyof T as string extends P - ? never - : number extends P - ? never - : P]: T[P] -} -export class PostModel implements RemoveIndex { - // state - isLoading = false - hasLoaded = false - error = '' - uri: string = '' - - // data - text: string = '' - entities?: Post.Entity[] - reply?: Post.ReplyRef - createdAt: string = '' - - constructor(public rootStore: RootStoreModel, uri: string) { - makeAutoObservable( - this, - { - rootStore: false, - uri: false, - }, - {autoBind: true}, - ) - this.uri = uri - } - - get hasContent() { - return this.createdAt !== '' - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - get rootUri(): string { - if (this.reply?.root.uri) { - return this.reply.root.uri - } - return this.uri - } - - get isThreadMuted() { - return this.rootStore.mutedThreads.uris.has(this.rootUri) - } - - // public api - // = - - async setup() { - await this._load() - } - - async toggleThreadMute() { - if (this.isThreadMuted) { - this.rootStore.mutedThreads.uris.delete(this.rootUri) - } else { - this.rootStore.mutedThreads.uris.add(this.rootUri) - } - } - - // state transitions - // = - - _xLoading() { - this.isLoading = true - this.error = '' - } - - _xIdle(err?: any) { - this.isLoading = false - this.hasLoaded = true - this.error = cleanError(err) - if (err) { - this.rootStore.log.error('Failed to fetch post', err) - } - } - - // loader functions - // = - - async _load() { - this._xLoading() - try { - const urip = new AtUri(this.uri) - const res = await this.rootStore.agent.getPost({ - repo: urip.host, - rkey: urip.rkey, - }) - // TODO - // if (!res.valid) { - // throw new Error(res.error) - // } - this._replaceAll(res.value) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } - - _replaceAll(res: Post.Record) { - this.text = res.text - this.entities = res.entities - this.reply = res.reply - this.createdAt = res.createdAt - } -} diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index c26dc8749f..ea75d19c62 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -10,6 +10,12 @@ import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' import {FollowState} from '../cache/my-follows' import {Image as RNImage} from 'react-native-image-crop-picker' +import {ProfileLabelInfo, ProfileModeration} from 'lib/labeling/types' +import { + getProfileModeration, + filterAccountLabels, + filterProfileLabels, +} from 'lib/labeling/helpers' export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser' @@ -75,6 +81,18 @@ export class ProfileModel { return this.hasLoaded && !this.hasContent } + get labelInfo(): ProfileLabelInfo { + return { + accountLabels: filterAccountLabels(this.labels), + profileLabels: filterProfileLabels(this.labels), + isMuted: this.viewer?.muted || false, + } + } + + get moderation(): ProfileModeration { + return getProfileModeration(this.rootStore, this.labelInfo) + } + // public api // = diff --git a/src/state/models/discovery/suggested-posts.ts b/src/state/models/discovery/suggested-posts.ts deleted file mode 100644 index 6c8de3023c..0000000000 --- a/src/state/models/discovery/suggested-posts.ts +++ /dev/null @@ -1,88 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {RootStoreModel} from '../root-store' -import {PostsFeedItemModel} from '../feeds/posts' -import {cleanError} from 'lib/strings/errors' -import {TEAM_HANDLES} from 'lib/constants' -import { - getMultipleAuthorsPosts, - mergePosts, -} from 'lib/api/build-suggested-posts' - -export class SuggestedPostsModel { - // state - isLoading = false - hasLoaded = false - error = '' - - // data - posts: PostsFeedItemModel[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - get hasContent() { - return this.posts.length > 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - // public api - // = - - async setup() { - this._xLoading() - try { - const responses = await getMultipleAuthorsPosts( - this.rootStore, - TEAM_HANDLES(String(this.rootStore.agent.service)), - undefined, - 30, - ) - runInAction(() => { - const finalPosts = mergePosts(responses, {repostsOnly: true}) - // hydrate into models - this.posts = finalPosts.map((post, i) => { - // strip the reasons to hide that these are reposts - delete post.reason - return new PostsFeedItemModel(this.rootStore, `post-${i}`, post) - }) - }) - this._xIdle() - } catch (e: any) { - this.rootStore.log.error('SuggestedPostsView: Failed to load posts', { - e, - }) - this._xIdle() // dont bubble to the user - } - } - - // state transitions - // = - - _xLoading() { - this.isLoading = true - this.error = '' - } - - _xIdle(err?: any) { - this.isLoading = false - this.hasLoaded = true - this.error = cleanError(err) - if (err) { - this.rootStore.log.error('Failed to fetch suggested posts', err) - } - } -} diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 220e04bce2..02f58819f9 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -15,6 +15,16 @@ import {bundleAsync} from 'lib/async/bundle' import {RootStoreModel} from '../root-store' import {PostThreadModel} from '../content/post-thread' import {cleanError} from 'lib/strings/errors' +import { + PostLabelInfo, + PostModeration, + ModerationBehaviorCode, +} from 'lib/labeling/types' +import { + getPostModeration, + filterAccountLabels, + filterProfileLabels, +} from 'lib/labeling/helpers' const GROUPABLE_REASONS = ['like', 'repost', 'follow'] const PAGE_SIZE = 30 @@ -90,6 +100,24 @@ export class NotificationsFeedItemModel { } } + get labelInfo(): PostLabelInfo { + const addedInfo = this.additionalPost?.thread?.labelInfo + return { + postLabels: (this.labels || []).concat(addedInfo?.postLabels || []), + accountLabels: filterAccountLabels(this.author.labels).concat( + addedInfo?.accountLabels || [], + ), + profileLabels: filterProfileLabels(this.author.labels).concat( + addedInfo?.profileLabels || [], + ), + isMuted: this.author.viewer?.muted || addedInfo?.isMuted || false, + } + } + + get moderation(): PostModeration { + return getPostModeration(this.rootStore, this.labelInfo) + } + get numUnreadInGroup(): number { if (this.additional?.length) { return ( @@ -520,16 +548,22 @@ export class NotificationsFeedModel { _filterNotifications( items: NotificationsFeedItemModel[], ): NotificationsFeedItemModel[] { - return items.filter(item => { - const hideByLabel = - this.rootStore.preferences.getLabelPreference(item.labels).pref === - 'hide' - let mutedThread = !!( - item.reasonSubjectRootUri && - this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri) - ) - return !hideByLabel && !mutedThread - }) + return items + .filter(item => { + const hideByLabel = + item.moderation.list.behavior === ModerationBehaviorCode.Hide + let mutedThread = !!( + item.reasonSubjectRootUri && + this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri) + ) + return !hideByLabel && !mutedThread + }) + .map(item => { + if (item.additional?.length) { + item.additional = this._filterNotifications(item.additional) + } + return item + }) } async _fetchItemModels( diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index cbff707d07..62c6da3de4 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -20,6 +20,13 @@ import { } from 'lib/api/build-suggested-posts' import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip' import {updateDataOptimistically} from 'lib/async/revertible' +import {PostLabelInfo, PostModeration} from 'lib/labeling/types' +import { + getEmbedLabels, + getPostModeration, + filterAccountLabels, + filterProfileLabels, +} from 'lib/labeling/helpers' type FeedViewPost = AppBskyFeedDefs.FeedViewPost type ReasonRepost = AppBskyFeedDefs.ReasonRepost @@ -83,6 +90,21 @@ export class PostsFeedItemModel { return this.rootStore.mutedThreads.uris.has(this.rootUri) } + get labelInfo(): PostLabelInfo { + return { + postLabels: (this.post.labels || []).concat( + getEmbedLabels(this.post.embed), + ), + accountLabels: filterAccountLabels(this.post.author.labels), + profileLabels: filterProfileLabels(this.post.author.labels), + isMuted: this.post.author.viewer?.muted || false, + } + } + + get moderation(): PostModeration { + return getPostModeration(this.rootStore, this.labelInfo) + } + copy(v: FeedViewPost) { this.post = v.post this.reply = v.reply diff --git a/src/view/com/discover/SuggestedPosts.tsx b/src/view/com/discover/SuggestedPosts.tsx deleted file mode 100644 index 6d2f39636e..0000000000 --- a/src/view/com/discover/SuggestedPosts.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react' -import {ActivityIndicator, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' -import {SuggestedPostsModel} from 'state/models/discovery/suggested-posts' -import {s} from 'lib/styles' -import {FeedItem as Post} from '../posts/FeedItem' -import {Text} from '../util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' - -export const SuggestedPosts = observer(() => { - const pal = usePalette('default') - const store = useStores() - const suggestedPostsView = React.useMemo( - () => new SuggestedPostsModel(store), - [store], - ) - - React.useEffect(() => { - if (!suggestedPostsView.hasLoaded) { - suggestedPostsView.setup() - } - }, [store, suggestedPostsView]) - - return ( - <> - {(suggestedPostsView.hasContent || suggestedPostsView.isLoading) && ( - - Recently, on Bluesky... - - )} - {suggestedPostsView.hasContent && ( - <> - - {suggestedPostsView.posts.map(item => ( - - ))} - - - )} - {suggestedPostsView.isLoading && ( - - - - )} - - ) -}) - -const styles = StyleSheet.create({ - heading: { - fontWeight: 'bold', - paddingHorizontal: 12, - paddingTop: 16, - paddingBottom: 8, - }, - - bottomBorder: { - borderBottomWidth: 1, - }, - - loadMore: { - paddingLeft: 12, - paddingVertical: 10, - }, -}) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index a5c0ecba0c..8a6578a3ce 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -8,7 +8,7 @@ import { View, } from 'react-native' import {AppBskyEmbedImages} from '@atproto/api' -import {AtUri, ComAtprotoLabelDefs} from '@atproto/api' +import {AtUri} from '@atproto/api' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -26,8 +26,14 @@ import {UserAvatar} from '../util/UserAvatar' import {ImageHorzList} from '../util/images/ImageHorzList' import {Post} from '../post/Post' import {Link, TextLink} from '../util/Link' +import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import { + getProfileViewBasicLabelInfo, + getProfileModeration, +} from 'lib/labeling/helpers' +import {ProfileModeration} from 'lib/labeling/types' const MAX_AUTHORS = 5 @@ -38,14 +44,15 @@ interface Author { handle: string displayName?: string avatar?: string - labels?: ComAtprotoLabelDefs.Label[] + moderation: ProfileModeration } -export const FeedItem = observer(function FeedItem({ +export const FeedItem = observer(function ({ item, }: { item: NotificationsFeedItemModel }) { + const store = useStores() const pal = usePalette('default') const [isAuthorsExpanded, setAuthorsExpanded] = useState(false) const itemHref = useMemo(() => { @@ -81,27 +88,25 @@ export const FeedItem = observer(function FeedItem({ handle: item.author.handle, displayName: item.author.displayName, avatar: item.author.avatar, - labels: item.author.labels, + moderation: getProfileModeration( + store, + getProfileViewBasicLabelInfo(item.author), + ), }, - ...(item.additional?.map( - ({author: {avatar, labels, handle, displayName}}) => { - return { - href: `/profile/${handle}`, - handle, - displayName, - avatar, - labels, - } - }, - ) || []), + ...(item.additional?.map(({author}) => { + return { + href: `/profile/${author.handle}`, + handle: author.handle, + displayName: author.displayName, + avatar: author.avatar, + moderation: getProfileModeration( + store, + getProfileViewBasicLabelInfo(author), + ), + } + }) || []), ] - }, [ - item.additional, - item.author.avatar, - item.author.displayName, - item.author.handle, - item.author.labels, - ]) + }, [store, item.additional, item.author]) if (item.additionalPost?.notFound) { // don't render anything if the target post was deleted or unfindable @@ -264,7 +269,7 @@ function CondensedAuthorsList({ @@ -277,7 +282,7 @@ function CondensedAuthorsList({ ))} @@ -335,7 +340,7 @@ function ExpandedAuthorsList({ diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx index dc090e7ad1..80dd59072b 100644 --- a/src/view/com/post-thread/PostLikedBy.tsx +++ b/src/view/com/post-thread/PostLikedBy.tsx @@ -47,15 +47,7 @@ export const PostLikedBy = observer(function ({uri}: {uri: string}) { // loaded // = const renderItem = ({item}: {item: LikeItem}) => ( - + ) return ( ( - + ) return ( + style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} + moderation={item.moderation.thread}> @@ -218,9 +214,7 @@ export const PostThreadItem = observer(function PostThreadItem({ - + {item.richText?.text ? ( - + ) } else { return ( @@ -309,8 +303,7 @@ export const PostThreadItem = observer(function PostThreadItem({ testID={`postThreadItem-by-${item.post.author.handle}`} href={itemHref} style={[styles.outer, {borderColor: pal.colors.border}, pal.view]} - isMuted={item.post.author.viewer?.muted === true} - labels={item.post.labels}> + moderation={item.moderation.thread}> {item._showParentReplyLine && ( @@ -347,7 +340,7 @@ export const PostThreadItem = observer(function PostThreadItem({ did={item.post.author.did} /> {item.richText?.text ? ( diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 81f3b8c45a..af78a951b4 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -206,8 +206,7 @@ const PostLoaded = observer( + moderation={item.moderation.list}> {showReplyLine && } @@ -215,7 +214,7 @@ const PostLoaded = observer( @@ -247,7 +246,7 @@ const PostLoaded = observer( )} {item.richText?.text ? ( diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx deleted file mode 100644 index 1a56a5dbf0..0000000000 --- a/src/view/com/post/PostText.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, {useState, useEffect} from 'react' -import {observer} from 'mobx-react-lite' -import {StyleProp, StyleSheet, TextStyle, View} from 'react-native' -import {LoadingPlaceholder} from '../util/LoadingPlaceholder' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {Text} from '../util/text/Text' -import {PostModel} from 'state/models/content/post' -import {useStores} from 'state/index' - -export const PostText = observer(function PostText({ - uri, - style, -}: { - uri: string - style?: StyleProp -}) { - const store = useStores() - const [model, setModel] = useState() - - useEffect(() => { - if (model?.uri === uri) { - return // no change needed? or trigger refresh? - } - const newModel = new PostModel(store, uri) - setModel(newModel) - newModel.setup().catch(err => store.log.error('Failed to fetch post', err)) - }, [uri, model?.uri, store]) - - // loading - // = - if (!model || model.isLoading || model.uri !== uri) { - return ( - - - - - - ) - } - - // error - // = - if (model.hasError) { - return ( - - - - ) - } - - // loaded - // = - return ( - - {model.text} - - ) -}) - -const styles = StyleSheet.create({ - mt6: {marginTop: 6}, -}) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 18481d4cb3..10fc775c57 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -30,14 +30,13 @@ export const FeedItem = observer(function ({ isThreadChild, isThreadParent, showFollowBtn, - ignoreMuteFor, }: { item: PostsFeedItemModel isThreadChild?: boolean isThreadParent?: boolean showReplyLine?: boolean showFollowBtn?: boolean - ignoreMuteFor?: string + ignoreMuteFor?: string // NOTE currently disabled, will be addressed in the next PR -prf }) { const store = useStores() const pal = usePalette('default') @@ -134,8 +133,6 @@ export const FeedItem = observer(function ({ } const isSmallTop = isThreadChild - const isMuted = - item.post.author.viewer?.muted && ignoreMuteFor !== item.post.author.did const outerStyles = [ styles.outer, pal.view, @@ -149,8 +146,7 @@ export const FeedItem = observer(function ({ testID={`feedItem-by-${item.post.author.handle}`} style={outerStyles} href={itemHref} - isMuted={isMuted} - labels={item.post.labels}> + moderation={item.moderation.list}> {isThreadChild && ( @@ -236,7 +232,7 @@ export const FeedItem = observer(function ({ )} {item.richText?.text ? ( diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 07bf4e291c..1543443886 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -1,7 +1,7 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {AppBskyActorDefs, ComAtprotoLabelDefs} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' @@ -10,143 +10,159 @@ import {usePalette} from 'lib/hooks/usePalette' import {useStores} from 'state/index' import {FollowButton} from './FollowButton' import {sanitizeDisplayName} from 'lib/strings/display-names' +import { + getProfileViewBasicLabelInfo, + getProfileModeration, +} from 'lib/labeling/helpers' +import {ModerationBehaviorCode} from 'lib/labeling/types' -export function ProfileCard({ - testID, - handle, - displayName, - avatar, - description, - labels, - isFollowedBy, - noBg, - noBorder, - followers, - renderButton, -}: { - testID?: string - handle: string - displayName?: string - avatar?: string - description?: string - labels: ComAtprotoLabelDefs.Label[] | undefined - isFollowedBy?: boolean - noBg?: boolean - noBorder?: boolean - followers?: AppBskyActorDefs.ProfileView[] | undefined - renderButton?: () => JSX.Element -}) { - const pal = usePalette('default') - return ( - - - - - - - - {sanitizeDisplayName(displayName || handle)} - - - @{handle} - - {isFollowedBy && ( - - - - Follows You - +export const ProfileCard = observer( + ({ + testID, + profile, + noBg, + noBorder, + followers, + renderButton, + }: { + testID?: string + profile: AppBskyActorDefs.ProfileViewBasic + noBg?: boolean + noBorder?: boolean + followers?: AppBskyActorDefs.ProfileView[] | undefined + renderButton?: () => JSX.Element + }) => { + const store = useStores() + const pal = usePalette('default') + + const moderation = getProfileModeration( + store, + getProfileViewBasicLabelInfo(profile), + ) + + if (moderation.list.behavior === ModerationBehaviorCode.Hide) { + return null + } + + return ( + + + + + + + + {sanitizeDisplayName(profile.displayName || profile.handle)} + + + @{profile.handle} + + {!!profile.viewer?.followedBy && ( + + + + Follows You + + - - )} + )} + + {renderButton ? ( + {renderButton()} + ) : undefined} - {renderButton ? ( - {renderButton()} + {profile.description ? ( + + + {profile.description} + + ) : undefined} - - {description ? ( - - - {description} - - - ) : undefined} - {followers?.length ? ( - - - Followed by{' '} - {followers.map(f => f.displayName || f.handle).join(', ')} - - {followers.slice(0, 3).map(f => ( - - - - + + + ) + }, +) + +const FollowersList = observer( + ({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => { + const store = useStores() + const pal = usePalette('default') + if (!followers?.length) { + return null + } + + const followersWithMods = followers + .map(f => ({ + f, + mod: getProfileModeration(store, getProfileViewBasicLabelInfo(f)), + })) + .filter(({mod}) => mod.list.behavior !== ModerationBehaviorCode.Hide) + + return ( + + + Followed by{' '} + {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')} + + {followersWithMods.slice(0, 3).map(({f, mod}) => ( + + + - ))} - - ) : undefined} - - ) -} + + ))} + + ) + }, +) export const ProfileCardWithFollowBtn = observer( ({ - did, - handle, - displayName, - avatar, - description, - labels, - isFollowedBy, + profile, noBg, noBorder, followers, }: { - did: string - handle: string - displayName?: string - avatar?: string - description?: string - labels: ComAtprotoLabelDefs.Label[] | undefined - isFollowedBy?: boolean + profile: AppBskyActorDefs.ProfileViewBasic noBg?: boolean noBorder?: boolean followers?: AppBskyActorDefs.ProfileView[] | undefined }) => { const store = useStores() - const isMe = store.me.handle === handle + const isMe = store.me.handle === profile.handle return ( } + renderButton={ + isMe ? undefined : () => + } /> ) }, diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index cba1719253..aeb2fcba91 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -61,15 +61,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({ // loaded // = const renderItem = ({item}: {item: FollowerItem}) => ( - + ) return ( ( - + ) return ( - + {isMe ? ( @@ -332,7 +332,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({ richText={view.descriptionRichText} /> ) : undefined} - + {view.viewer.muted ? ( diff --git a/src/view/com/search/SearchResults.tsx b/src/view/com/search/SearchResults.tsx index 3b05f75eae..ca6a0dba26 100644 --- a/src/view/com/search/SearchResults.tsx +++ b/src/view/com/search/SearchResults.tsx @@ -99,15 +99,7 @@ const Profiles = observer(({model}: {model: SearchUIModel}) => { return ( {model.profiles.map(item => ( - + ))} diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx index aacab5c98f..ead17f72eb 100644 --- a/src/view/com/search/Suggestions.tsx +++ b/src/view/com/search/Suggestions.tsx @@ -144,18 +144,9 @@ export const Suggestions = observer( ) @@ -191,19 +173,9 @@ export const Suggestions = observer( ) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index d9dd11e05a..45651e4e5a 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -97,7 +97,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { )} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 9c0fe92973..7f55bf7735 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -13,8 +13,11 @@ import {useStores} from 'state/index' import {colors} from 'lib/styles' import {DropdownButton} from './forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' +import {isWeb, isAndroid} from 'platform/detection' import {Image as RNImage} from 'react-native-image-crop-picker' +import {AvatarModeration} from 'lib/labeling/types' + +const BLUR_AMOUNT = isWeb ? 5 : 100 function DefaultAvatar({size}: {size: number}) { return ( @@ -40,12 +43,12 @@ function DefaultAvatar({size}: {size: number}) { export function UserAvatar({ size, avatar, - hasWarning, + moderation, onSelectNewAvatar, }: { size: number avatar?: string | null - hasWarning?: boolean + moderation?: AvatarModeration onSelectNewAvatar?: (img: RNImage | null) => void }) { const store = useStores() @@ -114,7 +117,7 @@ export function UserAvatar({ ) const warning = useMemo(() => { - if (!hasWarning) { + if (!moderation?.warn) { return null } return ( @@ -126,7 +129,7 @@ export function UserAvatar({ /> ) - }, [hasWarning, size, pal]) + }, [moderation?.warn, size, pal]) // onSelectNewAvatar is only passed as prop on the EditProfile component return onSelectNewAvatar ? ( @@ -159,13 +162,15 @@ export function UserAvatar({ /> - ) : avatar ? ( + ) : avatar && + !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( {warning} diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index fcd66ca7a8..14459bf774 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -13,13 +13,16 @@ import { } from 'lib/hooks/usePermissions' import {DropdownButton} from './forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' +import {AvatarModeration} from 'lib/labeling/types' +import {isWeb, isAndroid} from 'platform/detection' export function UserBanner({ banner, + moderation, onSelectNewBanner, }: { banner?: string | null + moderation?: AvatarModeration onSelectNewBanner?: (img: TImage | null) => void }) { const store = useStores() @@ -107,12 +110,14 @@ export function UserBanner({ /> - ) : banner ? ( + ) : banner && + !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( ) : ( diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index 42a97cd347..74fb479ad7 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -6,32 +6,31 @@ import { View, ViewStyle, } from 'react-native' -import {ComAtprotoLabelDefs} from '@atproto/api' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {Text} from '../text/Text' import {addStyle} from 'lib/styles' +import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types' export function ContentHider({ testID, - isMuted, - labels, + moderation, style, containerStyle, children, }: React.PropsWithChildren<{ testID?: string - isMuted?: boolean - labels: ComAtprotoLabelDefs.Label[] | undefined + moderation: ModerationBehavior style?: StyleProp containerStyle?: StyleProp }>) { const pal = usePalette('default') const [override, setOverride] = React.useState(false) - const store = useStores() - const labelPref = store.preferences.getLabelPreference(labels) - if (!isMuted && labelPref.pref === 'show') { + if ( + moderation.behavior === ModerationBehaviorCode.Show || + moderation.behavior === ModerationBehaviorCode.Warn || + moderation.behavior === ModerationBehaviorCode.WarnImages + ) { return ( {children} @@ -39,7 +38,7 @@ export function ContentHider({ ) } - if (labelPref.pref === 'hide') { + if (moderation.behavior === ModerationBehaviorCode.Hide) { return null } @@ -52,11 +51,7 @@ export function ContentHider({ override && styles.descriptionOpen, ]}> - {isMuted ? ( - <>Post from an account you muted. - ) : ( - <>Warning: {labelPref.desc.warning || labelPref.desc.title} - )} + {moderation.reason || 'Content warning'} }>) { - const store = useStores() const pal = usePalette('default') const [override, setOverride] = React.useState(false) const bg = override ? pal.viewLight : pal.view - const labelPref = store.preferences.getLabelPreference(labels) - if (labelPref.pref === 'hide') { - return <> + if (moderation.behavior === ModerationBehaviorCode.Hide) { + return null } - if (!isMuted) { - // NOTE: any further label enforcement should occur in ContentContainer + if (moderation.behavior === ModerationBehaviorCode.Warn) { return ( - - {children} - + <> + + + + {moderation.reason || 'Content warning'} + + setOverride(v => !v)}> + + {override ? 'Hide' : 'Show'} post + + + + {override && ( + + + {children} + + + )} + ) } + // NOTE: any further label enforcement should occur in ContentContainer return ( - <> - - - - Post from an account you muted. - - setOverride(v => !v)}> - - {override ? 'Hide' : 'Show'} post - - - - {override && ( - - - {children} - - - )} - + + {children} + ) } diff --git a/src/view/com/util/moderation/ProfileHeaderLabels.tsx b/src/view/com/util/moderation/ProfileHeaderLabels.tsx deleted file mode 100644 index c6fbfaf6ba..0000000000 --- a/src/view/com/util/moderation/ProfileHeaderLabels.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {ComAtprotoLabelDefs} from '@atproto/api' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {Text} from '../text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {getLabelValueGroup} from 'lib/labeling/helpers' - -export function ProfileHeaderLabels({ - labels, -}: { - labels: ComAtprotoLabelDefs.Label[] | undefined -}) { - const palErr = usePalette('error') - if (!labels?.length) { - return null - } - return ( - <> - {labels.map((label, i) => { - const labelGroup = getLabelValueGroup(label?.val || '') - return ( - - - - This account has been flagged for{' '} - {(labelGroup.warning || labelGroup.title).toLocaleLowerCase()}. - - - ) - })} - - ) -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - gap: 10, - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 10, - paddingVertical: 8, - }, -}) diff --git a/src/view/com/util/moderation/ProfileHeaderWarnings.tsx b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx new file mode 100644 index 0000000000..7a1a8e295b --- /dev/null +++ b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {Text} from '../text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types' + +export function ProfileHeaderWarnings({ + moderation, +}: { + moderation: ModerationBehavior +}) { + const palErr = usePalette('error') + if (moderation.behavior === ModerationBehaviorCode.Show) { + return null + } + return ( + + + + This account has been flagged: {moderation.reason} + + + ) +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 10, + paddingVertical: 8, + }, +}) diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx new file mode 100644 index 0000000000..2e7b07e1a3 --- /dev/null +++ b/src/view/com/util/moderation/ScreenHider.tsx @@ -0,0 +1,129 @@ +import React from 'react' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {useNavigation} from '@react-navigation/native' +import {usePalette} from 'lib/hooks/usePalette' +import {NavigationProp} from 'lib/routes/types' +import {Text} from '../text/Text' +import {Button} from '../forms/Button' +import {isDesktopWeb} from 'platform/detection' +import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types' + +export function ScreenHider({ + testID, + screenDescription, + moderation, + style, + containerStyle, + children, +}: React.PropsWithChildren<{ + testID?: string + screenDescription: string + moderation: ModerationBehavior + style?: StyleProp + containerStyle?: StyleProp +}>) { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const [override, setOverride] = React.useState(false) + const navigation = useNavigation() + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + if (moderation.behavior !== ModerationBehaviorCode.Hide || override) { + return ( + + {children} + + ) + } + + return ( + + + + + + + + Content Warning + + + This {screenDescription} has been flagged:{' '} + {moderation.reason || 'Content warning'} + + {!isDesktopWeb && } + + + {!moderation.noOverride && ( + + )} + + + ) +} + +const styles = StyleSheet.create({ + spacer: { + flex: 1, + }, + container: { + flex: 1, + paddingTop: 100, + paddingBottom: 150, + }, + iconContainer: { + alignItems: 'center', + marginBottom: 10, + }, + icon: { + borderRadius: 25, + width: 50, + height: 50, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + textAlign: 'center', + marginBottom: 10, + }, + description: { + marginBottom: 10, + paddingHorizontal: 20, + textAlign: 'center', + }, + btnContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginVertical: 10, + gap: 10, + }, + btn: { + paddingHorizontal: 20, + paddingVertical: 14, + }, +}) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 4e4e3040be..4be1179326 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -6,6 +6,7 @@ import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewSelector} from '../com/util/ViewSelector' import {CenteredView} from '../com/util/Views' +import {ScreenHider} from 'view/com/util/moderation/ScreenHider' import {ProfileUiModel} from 'state/models/ui/profile' import {useStores} from 'state/index' import {PostsFeedSliceModel} from 'state/models/feeds/posts' @@ -140,7 +141,11 @@ export const ProfileScreen = withAuthRequired( ) return ( - + {uiState.profile.hasError ? ( } /> - + ) }), ) diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx index de64b2d67f..4522d79ee5 100644 --- a/src/view/screens/SearchMobile.tsx +++ b/src/view/screens/SearchMobile.tsx @@ -146,19 +146,14 @@ export const SearchScreen = withAuthRequired( scrollEventThrottle={100}> {query && autocompleteView.searchRes.length ? ( <> - {autocompleteView.searchRes.map( - ({did, handle, displayName, labels, avatar}, index) => ( - - ), - )} + {autocompleteView.searchRes.map((profile, index) => ( + + ))} ) : query && !autocompleteView.searchRes.length ? ( diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index 9954719443..5504e94155 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -85,14 +85,7 @@ export const DesktopSearch = observer(function DesktopSearch() { {autocompleteView.searchRes.length ? ( <> {autocompleteView.searchRes.map((item, i) => ( - + ))} ) : ( diff --git a/yarn.lock b/yarn.lock index a6f174a251..268d46fc5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,10 +30,10 @@ tlds "^1.234.0" typed-emitter "^2.1.0" -"@atproto/api@0.2.9": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.9.tgz#08e29da66d1a9001d9d3ce427548c1760d805e99" - integrity sha512-r00IqidX2YF3VUEa4MUO2Vxqp3+QhI1cSNcWgzT4LsANapzrwdDTM+rY2Ejp9na3F+unO4SWRW3o434cVmG5gw== +"@atproto/api@0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.10.tgz#19c4d695f88ab4e45e4c9f2f4db5fad61590a3d2" + integrity sha512-97UBtvIXhsgNO7bXhHk0JwDNwyqTcL1N0JT2rnXjUeLKNf2hDvomFtI50Y4RFU942uUS5W5VtM+JJuZO5Ryw5w== dependencies: "@atproto/common-web" "*" "@atproto/uri" "*" From da06b608f2992b4a18ca51b8e6919ef4d32aad7a Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 27 Apr 2023 13:14:54 -0500 Subject: [PATCH 049/374] 1.23.0 build 2 --- app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.json b/app.json index 9b83e55d59..8084f3fff5 100644 --- a/app.json +++ b/app.json @@ -13,7 +13,7 @@ "backgroundColor": "#ffffff" }, "ios": { - "buildNumber": "1", + "buildNumber": "2", "supportsTablet": false, "bundleIdentifier": "xyz.blueskyweb.app", "config": { @@ -34,7 +34,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 8, + "versionCode": 9, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" From 173e06f866281b4138d927ac6a577225c8fbd847 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 27 Apr 2023 15:01:38 -0500 Subject: [PATCH 050/374] Fix to embed labels lookup (#550) * Fix to embed labels lookup * Fix lint * Fix lint --- src/lib/labeling/helpers.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index bac98c6a28..0092b99e4f 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -2,7 +2,6 @@ import { AppBskyActorDefs, AppBskyEmbedRecordWithMedia, AppBskyEmbedRecord, - AppBskyFeedPost, AppBskyEmbedImages, AppBskyEmbedExternal, } from '@atproto/api' @@ -229,12 +228,10 @@ export function getEmbedLabels(embed?: Embed): Label[] { return [] } if ( - AppBskyEmbedRecordWithMedia.isView(embed) && - AppBskyEmbedRecord.isViewRecord(embed.record.record) && - AppBskyFeedPost.isRecord(embed.record.record.value) && - AppBskyFeedPost.validateRecord(embed.record.record.value).success + AppBskyEmbedRecord.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record) ) { - return embed.record.record.labels || [] + return embed.record.labels || [] } return [] } @@ -312,9 +309,10 @@ function warnContent(reason: string) { } } -function warnImages(reason: string) { - return { - behavior: ModerationBehaviorCode.WarnImages, - reason, - } -} +// TODO +// function warnImages(reason: string) { +// return { +// behavior: ModerationBehaviorCode.WarnImages, +// reason, +// } +// } From b754ed0e5a72412b5df8b397bcbf5fb964ea14a3 Mon Sep 17 00:00:00 2001 From: renahlee Date: Fri, 28 Apr 2023 17:12:17 -0700 Subject: [PATCH 051/374] Up Android snap point --- src/view/com/modals/Waitlist.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx index f3c3019377..2795dcffee 100644 --- a/src/view/com/modals/Waitlist.tsx +++ b/src/view/com/modals/Waitlist.tsx @@ -19,7 +19,7 @@ import {useTheme} from 'lib/ThemeContext' import {ErrorMessage} from '../util/error/ErrorMessage' import {cleanError} from 'lib/strings/errors' -export const snapPoints = ['60%'] +export const snapPoints = ['80%'] export function Component({}: {}) { const pal = usePalette('default') From a95c03e280ca153ba4a98d6b81ff9d743d4adcaa Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 28 Apr 2023 20:03:13 -0500 Subject: [PATCH 052/374] Implement blocks (#554) * Quick fix to prompt * Add blocked accounts screen * Add blocking tools to profile * Blur avis/banners of blocked users * Factor blocking state into moderation dsl * Filter post slices from the feed if any are hidden * Handle various block UIs * Filter in the client on blockedBy * Implement block list * Fix some copy * Bump deps * Fix lint --- bskyweb/cmd/bskyweb/server.go | 1 + package.json | 4 +- src/Navigation.tsx | 6 +- src/lib/labeling/helpers.ts | 94 +++- src/lib/labeling/types.ts | 4 + src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/state/models/content/post-thread.ts | 43 +- src/state/models/content/profile.ts | 32 ++ src/state/models/feeds/notifications.ts | 4 + src/state/models/feeds/posts.ts | 25 +- src/state/models/lists/blocked-accounts.ts | 106 ++++ src/view/com/composer/Composer.tsx | 6 +- src/view/com/modals/Confirm.tsx | 3 +- src/view/com/post-thread/PostThread.tsx | 64 ++- src/view/com/posts/FeedSlice.tsx | 4 + src/view/com/profile/ProfileCard.tsx | 7 +- src/view/com/profile/ProfileHeader.tsx | 617 ++++++++++++--------- src/view/index.ts | 2 + src/view/screens/AppPasswords.tsx | 2 +- src/view/screens/BlockedAccounts.tsx | 172 ++++++ src/view/screens/Profile.tsx | 25 +- src/view/screens/Settings.tsx | 22 +- yarn.lock | 20 +- 24 files changed, 974 insertions(+), 291 deletions(-) create mode 100644 src/state/models/lists/blocked-accounts.ts create mode 100644 src/view/screens/BlockedAccounts.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 3339cccc10..b901e226ce 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -93,6 +93,7 @@ func serve(cctx *cli.Context) error { e.GET("/notifications", server.WebGeneric) e.GET("/settings", server.WebGeneric) e.GET("/settings/app-passwords", server.WebGeneric) + e.GET("/settings/blocked-accounts", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) diff --git a/package.json b/package.json index 939c62b6f1..595b887b43 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.2.10", + "@atproto/api": "0.2.11", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", @@ -130,7 +130,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.1.4", + "@atproto/pds": "^0.1.5", "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index d5ffb15395..3a9392fb88 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -27,6 +27,8 @@ import {colors} from 'lib/styles' import {isNative} from 'platform/detection' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {router} from './routes' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from './state' import {HomeScreen} from './view/screens/Home' import {SearchScreen} from './view/screens/Search' @@ -46,9 +48,8 @@ import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy' import {TermsOfServiceScreen} from './view/screens/TermsOfService' import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' -import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from './state' import {AppPasswords} from 'view/screens/AppPasswords' +import {BlockedAccounts} from 'view/screens/BlockedAccounts' const navigationRef = createNavigationContainerRef() @@ -88,6 +89,7 @@ function commonScreens(Stack: typeof HomeTab) { /> + ) } diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index 0092b99e4f..5ec591cfb5 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -57,6 +57,7 @@ export function getPostModeration( let avatar = { warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', blur: + postInfo.isBlocking || accountPref.pref === 'hide' || accountPref.pref === 'warn' || profilePref.pref === 'hide' || @@ -75,6 +76,22 @@ export function getPostModeration( } // hide cases + if (postInfo.isBlocking) { + return { + avatar, + list: hide('Post from an account you blocked.'), + thread: hide('Post from an account you blocked.'), + view: warn('Post from an account you blocked.'), + } + } + if (postInfo.isBlockedBy) { + return { + avatar, + list: hide('Post from an account that has blocked you.'), + thread: hide('Post from an account that has blocked you.'), + view: warn('Post from an account that has blocked you.'), + } + } if (accountPref.pref === 'hide') { return { avatar, @@ -144,21 +161,45 @@ export function getPostModeration( } } +export function mergePostModerations( + moderations: PostModeration[], +): PostModeration { + const merged: PostModeration = { + avatar: {warn: false, blur: false}, + list: show(), + thread: show(), + view: show(), + } + for (const mod of moderations) { + if (mod.list.behavior === ModerationBehaviorCode.Hide) { + merged.list = mod.list + } + if (mod.thread.behavior === ModerationBehaviorCode.Hide) { + merged.thread = mod.thread + } + if (mod.view.behavior === ModerationBehaviorCode.Hide) { + merged.view = mod.view + } + } + return merged +} + export function getProfileModeration( store: RootStoreModel, - profileLabels: ProfileLabelInfo, + profileInfo: ProfileLabelInfo, ): ProfileModeration { const accountPref = store.preferences.getLabelPreference( - profileLabels.accountLabels, + profileInfo.accountLabels, ) const profilePref = store.preferences.getLabelPreference( - profileLabels.profileLabels, + profileInfo.profileLabels, ) // avatar let avatar = { warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', blur: + profileInfo.isBlocking || accountPref.pref === 'hide' || accountPref.pref === 'warn' || profilePref.pref === 'hide' || @@ -193,7 +234,10 @@ export function getProfileModeration( if (accountPref.pref === 'warn') { return { avatar, - list: warn(accountPref.desc.warning), + list: + profileInfo.isBlocking || profileInfo.isBlockedBy + ? hide('Blocked account') + : warn(accountPref.desc.warning), view: warn(accountPref.desc.warning), } } @@ -208,7 +252,7 @@ export function getProfileModeration( return { avatar, - list: show(), + list: profileInfo.isBlocking ? hide('Blocked account') : show(), view: show(), } } @@ -220,6 +264,7 @@ export function getProfileViewBasicLabelInfo( accountLabels: filterAccountLabels(profile.labels), profileLabels: filterProfileLabels(profile.labels), isMuted: profile.viewer?.muted || false, + isBlocking: !!profile.viewer?.blocking || false, } } @@ -236,6 +281,45 @@ export function getEmbedLabels(embed?: Embed): Label[] { return [] } +export function getEmbedMuted(embed?: Embed): boolean { + if (!embed) { + return false + } + if ( + AppBskyEmbedRecord.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record) + ) { + return !!embed.record.author.viewer?.muted + } + return false +} + +export function getEmbedBlocking(embed?: Embed): boolean { + if (!embed) { + return false + } + if ( + AppBskyEmbedRecord.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record) + ) { + return !!embed.record.author.viewer?.blocking + } + return false +} + +export function getEmbedBlockedBy(embed?: Embed): boolean { + if (!embed) { + return false + } + if ( + AppBskyEmbedRecord.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record) + ) { + return !!embed.record.author.viewer?.blockedBy + } + return false +} + export function filterAccountLabels(labels?: Label[]): Label[] { if (!labels) { return [] diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts index d4efb499a3..20ecaa5b58 100644 --- a/src/lib/labeling/types.ts +++ b/src/lib/labeling/types.ts @@ -17,12 +17,16 @@ export interface PostLabelInfo { accountLabels: Label[] profileLabels: Label[] isMuted: boolean + isBlocking: boolean + isBlockedBy: boolean } export interface ProfileLabelInfo { accountLabels: Label[] profileLabels: Label[] isMuted: boolean + isBlocking: boolean + isBlockedBy: boolean } export enum ModerationBehaviorCode { diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index eeb97ba7a9..3aff821174 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -20,6 +20,7 @@ export type CommonNavigatorParams = { CommunityGuidelines: undefined CopyrightPolicy: undefined AppPasswords: undefined + BlockedAccounts: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/routes.ts b/src/routes.ts index 6762cde9d6..15595775e2 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -14,6 +14,7 @@ export const router = new Router({ Debug: '/sys/debug', Log: '/sys/log', AppPasswords: '/settings/app-passwords', + BlockedAccounts: '/settings/blocked-accounts', Support: '/support', PrivacyPolicy: '/support/privacy', TermsOfService: '/support/tos', diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index 8f9a550327..18a42732cd 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -13,6 +13,9 @@ import {updateDataOptimistically} from 'lib/async/revertible' import {PostLabelInfo, PostModeration} from 'lib/labeling/types' import { getEmbedLabels, + getEmbedMuted, + getEmbedBlocking, + getEmbedBlockedBy, filterAccountLabels, filterProfileLabels, getPostModeration, @@ -30,7 +33,10 @@ export class PostThreadItemModel { // data post: AppBskyFeedDefs.PostView postRecord?: FeedPost.Record - parent?: PostThreadItemModel | AppBskyFeedDefs.NotFoundPost + parent?: + | PostThreadItemModel + | AppBskyFeedDefs.NotFoundPost + | AppBskyFeedDefs.BlockedPost replies?: (PostThreadItemModel | AppBskyFeedDefs.NotFoundPost)[] richText?: RichText @@ -60,7 +66,18 @@ export class PostThreadItemModel { ), accountLabels: filterAccountLabels(this.post.author.labels), profileLabels: filterProfileLabels(this.post.author.labels), - isMuted: this.post.author.viewer?.muted || false, + isMuted: + this.post.author.viewer?.muted || + getEmbedMuted(this.post.embed) || + false, + isBlocking: + !!this.post.author.viewer?.blocking || + getEmbedBlocking(this.post.embed) || + false, + isBlockedBy: + !!this.post.author.viewer?.blockedBy || + getEmbedBlockedBy(this.post.embed) || + false, } } @@ -114,6 +131,8 @@ export class PostThreadItemModel { this.parent = parentModel } else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) { this.parent = v.parent + } else if (AppBskyFeedDefs.isBlockedPost(v.parent)) { + this.parent = v.parent } } // replies @@ -218,6 +237,7 @@ export class PostThreadModel { // data thread?: PostThreadItemModel + isBlocked = false constructor( public rootStore: RootStoreModel, @@ -377,11 +397,17 @@ export class PostThreadModel { this._replaceAll(res) this._xIdle() } catch (e: any) { + console.log(e) this._xIdle(e) } } _replaceAll(res: GetPostThread.Response) { + this.isBlocked = AppBskyFeedDefs.isBlockedPost(res.data.thread) + if (this.isBlocked) { + return + } + pruneReplies(res.data.thread) sortThread(res.data.thread) const thread = new PostThreadItemModel( this.rootStore, @@ -399,7 +425,20 @@ export class PostThreadModel { type MaybePost = | AppBskyFeedDefs.ThreadViewPost | AppBskyFeedDefs.NotFoundPost + | AppBskyFeedDefs.BlockedPost | {[k: string]: unknown; $type: string} +function pruneReplies(post: MaybePost) { + if (post.replies) { + post.replies = (post.replies as MaybePost[]).filter((reply: MaybePost) => { + if (reply.blocked) { + return false + } + pruneReplies(reply) + return true + }) + } +} + function sortThread(post: MaybePost) { if (post.notFound) { return diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index ea75d19c62..dddf488a3c 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -1,5 +1,6 @@ import {makeAutoObservable, runInAction} from 'mobx' import { + AtUri, ComAtprotoLabelDefs, AppBskyActorGetProfile as GetProfile, AppBskyActorProfile, @@ -23,6 +24,8 @@ export class ProfileViewerModel { muted?: boolean following?: string followedBy?: string + blockedBy?: boolean + blocking?: string constructor() { makeAutoObservable(this) @@ -86,6 +89,8 @@ export class ProfileModel { accountLabels: filterAccountLabels(this.labels), profileLabels: filterProfileLabels(this.labels), isMuted: this.viewer?.muted || false, + isBlocking: !!this.viewer?.blocking || false, + isBlockedBy: !!this.viewer?.blockedBy || false, } } @@ -185,6 +190,33 @@ export class ProfileModel { await this.refresh() } + async blockAccount() { + const res = await this.rootStore.agent.app.bsky.graph.block.create( + { + repo: this.rootStore.me.did, + }, + { + subject: this.did, + createdAt: new Date().toISOString(), + }, + ) + this.viewer.blocking = res.uri + await this.refresh() + } + + async unblockAccount() { + if (!this.viewer.blocking) { + return + } + const {rkey} = new AtUri(this.viewer.blocking) + await this.rootStore.agent.app.bsky.graph.block.delete({ + repo: this.rootStore.me.did, + rkey, + }) + this.viewer.blocking = undefined + await this.refresh() + } + // state transitions // = diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 02f58819f9..3ffd10b99e 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -111,6 +111,10 @@ export class NotificationsFeedItemModel { addedInfo?.profileLabels || [], ), isMuted: this.author.viewer?.muted || addedInfo?.isMuted || false, + isBlocking: + !!this.author.viewer?.blocking || addedInfo?.isBlocking || false, + isBlockedBy: + !!this.author.viewer?.blockedBy || addedInfo?.isBlockedBy || false, } } diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 62c6da3de4..62047acbab 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -23,7 +23,11 @@ import {updateDataOptimistically} from 'lib/async/revertible' import {PostLabelInfo, PostModeration} from 'lib/labeling/types' import { getEmbedLabels, + getEmbedMuted, + getEmbedBlocking, + getEmbedBlockedBy, getPostModeration, + mergePostModerations, filterAccountLabels, filterProfileLabels, } from 'lib/labeling/helpers' @@ -97,7 +101,18 @@ export class PostsFeedItemModel { ), accountLabels: filterAccountLabels(this.post.author.labels), profileLabels: filterProfileLabels(this.post.author.labels), - isMuted: this.post.author.viewer?.muted || false, + isMuted: + this.post.author.viewer?.muted || + getEmbedMuted(this.post.embed) || + false, + isBlocking: + !!this.post.author.viewer?.blocking || + getEmbedBlocking(this.post.embed) || + false, + isBlockedBy: + !!this.post.author.viewer?.blockedBy || + getEmbedBlockedBy(this.post.embed) || + false, } } @@ -240,6 +255,10 @@ export class PostsFeedSliceModel { return this.items[0] } + get moderation() { + return mergePostModerations(this.items.map(item => item.moderation)) + } + containsUri(uri: string) { return !!this.items.find(item => item.post.uri === uri) } @@ -265,6 +284,8 @@ export class PostsFeedModel { isRefreshing = false hasNewLatest = false hasLoaded = false + isBlocking = false + isBlockedBy = false error = '' loadMoreError = '' params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams @@ -553,6 +574,8 @@ export class PostsFeedModel { this.isLoading = false this.isRefreshing = false this.hasLoaded = true + this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError + this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError this.error = cleanError(error) this.loadMoreError = cleanError(loadMoreError) if (error) { diff --git a/src/state/models/lists/blocked-accounts.ts b/src/state/models/lists/blocked-accounts.ts new file mode 100644 index 0000000000..20eef8affa --- /dev/null +++ b/src/state/models/lists/blocked-accounts.ts @@ -0,0 +1,106 @@ +import {makeAutoObservable} from 'mobx' +import { + AppBskyGraphGetBlocks as GetBlocks, + AppBskyActorDefs as ActorDefs, +} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' + +const PAGE_SIZE = 30 + +export class BlockedAccountsModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + hasMore = true + loadMoreCursor?: string + + // data + blocks: ActorDefs.ProfileView[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.blocks.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async refresh() { + return this.loadMore(true) + } + + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return + } + this._xLoading(replace) + try { + const res = await this.rootStore.agent.app.bsky.graph.getBlocks({ + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + // state transitions + // = + + _xLoading(isRefreshing = false) { + this.isLoading = true + this.isRefreshing = isRefreshing + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch user followers', err) + } + } + + // helper functions + // = + + _replaceAll(res: GetBlocks.Response) { + this.blocks = [] + this._appendAll(res) + } + + _appendAll(res: GetBlocks.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.blocks = this.blocks.concat(res.data.blocks) + } +} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index c30d881ec9..5ccc229d69 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -190,11 +190,7 @@ export const ComposePost = observer(function ComposePost({ const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH - const selectTextInputPlaceholder = replyTo - ? 'Write your reply' - : gallery.isEmpty - ? 'Write a comment' - : "What's up?" + const selectTextInputPlaceholder = replyTo ? 'Write your reply' : "What's up?" const canSelectImages = gallery.size < 4 const viewStyles = { diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index 63877fe5dd..6f7b062cfd 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -11,6 +11,7 @@ import {s, colors} from 'lib/styles' import {ErrorMessage} from '../util/error/ErrorMessage' import {cleanError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' export const snapPoints = [300] @@ -77,7 +78,7 @@ const styles = StyleSheet.create({ container: { flex: 1, padding: 10, - paddingBottom: 60, + paddingBottom: isDesktopWeb ? 0 : 60, }, title: { textAlign: 'center', diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 6e387b8d0f..fe1822acb9 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -7,6 +7,7 @@ import { TouchableOpacity, View, } from 'react-native' +import {AppBskyFeedDefs} from '@atproto/api' import {CenteredView, FlatList} from '../util/Views' import { PostThreadModel, @@ -27,11 +28,17 @@ import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false} +const DELETED = {_reactKey: '__deleted__', _isHighlightedPost: false} +const BLOCKED = {_reactKey: '__blocked__', _isHighlightedPost: false} const BOTTOM_COMPONENT = { _reactKey: '__bottom_component__', _isHighlightedPost: false, } -type YieldedItem = PostThreadItemModel | typeof REPLY_PROMPT +type YieldedItem = + | PostThreadItemModel + | typeof REPLY_PROMPT + | typeof DELETED + | typeof BLOCKED export const PostThread = observer(function PostThread({ uri, @@ -103,6 +110,22 @@ export const PostThread = observer(function PostThread({ ({item}: {item: YieldedItem}) => { if (item === REPLY_PROMPT) { return + } else if (item === DELETED) { + return ( + + + Deleted post. + + + ) + } else if (item === BLOCKED) { + return ( + + + Blocked post. + + + ) } else if (item === BOTTOM_COMPONENT) { // HACK // due to some complexities with how flatlist works, this is the easiest way @@ -177,6 +200,30 @@ export const PostThread = observer(function PostThread({ ) } + if (view.isBlocked) { + return ( + + + + Post hidden + + + You have blocked the author or you have been blocked by the author. + + + + + Back + + + + + ) + } // loaded // = @@ -208,8 +255,10 @@ function* flattenThread( isAscending = false, ): Generator { if (post.parent) { - if ('notFound' in post.parent && post.parent.notFound) { - // TODO render not found + if (AppBskyFeedDefs.isNotFoundPost(post.parent)) { + yield DELETED + } else if (AppBskyFeedDefs.isBlockedPost(post.parent)) { + yield BLOCKED } else { yield* flattenThread(post.parent as PostThreadItemModel, true) } @@ -220,8 +269,8 @@ function* flattenThread( } if (post.replies?.length) { for (const reply of post.replies) { - if ('notFound' in reply && reply.notFound) { - // TODO render not found + if (AppBskyFeedDefs.isNotFoundPost(reply)) { + yield DELETED } else { yield* flattenThread(reply as PostThreadItemModel) } @@ -238,6 +287,11 @@ const styles = StyleSheet.create({ paddingVertical: 14, borderRadius: 6, }, + missingItem: { + borderTop: 1, + paddingHorizontal: 18, + paddingVertical: 18, + }, bottomBorder: { borderBottomWidth: 1, }, diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index 651b69bff1..5a191ac102 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -7,6 +7,7 @@ import {Text} from '../util/text/Text' import Svg, {Circle, Line} from 'react-native-svg' import {FeedItem} from './FeedItem' import {usePalette} from 'lib/hooks/usePalette' +import {ModerationBehaviorCode} from 'lib/labeling/types' export function FeedSlice({ slice, @@ -17,6 +18,9 @@ export function FeedSlice({ showFollowBtn?: boolean ignoreMuteFor?: string }) { + if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) { + return null + } if (slice.isThread && slice.items.length > 3) { const last = slice.items.length - 1 return ( diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 1543443886..66c1721413 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -23,6 +23,7 @@ export const ProfileCard = observer( noBg, noBorder, followers, + overrideModeration, renderButton, }: { testID?: string @@ -30,6 +31,7 @@ export const ProfileCard = observer( noBg?: boolean noBorder?: boolean followers?: AppBskyActorDefs.ProfileView[] | undefined + overrideModeration?: boolean renderButton?: () => JSX.Element }) => { const store = useStores() @@ -40,7 +42,10 @@ export const ProfileCard = observer( getProfileViewBasicLabelInfo(profile), ) - if (moderation.list.behavior === ModerationBehaviorCode.Hide) { + if ( + moderation.list.behavior === ModerationBehaviorCode.Hide && + !overrideModeration + ) { return null } diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index d1104d184e..719b84e20a 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -96,281 +96,377 @@ export const ProfileHeader = observer( }, ) -const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({ - view, - onRefreshAll, - hideBackButton = false, -}: Props) { - const pal = usePalette('default') - const store = useStores() - const navigation = useNavigation() - const {track} = useAnalytics() +const ProfileHeaderLoaded = observer( + ({view, onRefreshAll, hideBackButton = false}: Props) => { + const pal = usePalette('default') + const store = useStores() + const navigation = useNavigation() + const {track} = useAnalytics() - const onPressBack = React.useCallback(() => { - navigation.goBack() - }, [navigation]) + const onPressBack = React.useCallback(() => { + navigation.goBack() + }, [navigation]) - const onPressAvi = React.useCallback(() => { - if (view.avatar) { - store.shell.openLightbox(new ProfileImageLightbox(view)) - } - }, [store, view]) + const onPressAvi = React.useCallback(() => { + if (view.avatar) { + store.shell.openLightbox(new ProfileImageLightbox(view)) + } + }, [store, view]) - const onPressToggleFollow = React.useCallback(() => { - view?.toggleFollowing().then( - () => { - Toast.show( - `${ - view.viewer.following ? 'Following' : 'No longer following' - } ${sanitizeDisplayName(view.displayName || view.handle)}`, - ) - }, - err => store.log.error('Failed to toggle follow', err), + const onPressToggleFollow = React.useCallback(() => { + view?.toggleFollowing().then( + () => { + Toast.show( + `${ + view.viewer.following ? 'Following' : 'No longer following' + } ${sanitizeDisplayName(view.displayName || view.handle)}`, + ) + }, + err => store.log.error('Failed to toggle follow', err), + ) + }, [view, store]) + + const onPressEditProfile = React.useCallback(() => { + track('ProfileHeader:EditProfileButtonClicked') + store.shell.openModal({ + name: 'edit-profile', + profileView: view, + onUpdate: onRefreshAll, + }) + }, [track, store, view, onRefreshAll]) + + const onPressFollowers = React.useCallback(() => { + track('ProfileHeader:FollowersButtonClicked') + navigation.push('ProfileFollowers', {name: view.handle}) + }, [track, navigation, view]) + + const onPressFollows = React.useCallback(() => { + track('ProfileHeader:FollowsButtonClicked') + navigation.push('ProfileFollows', {name: view.handle}) + }, [track, navigation, view]) + + const onPressShare = React.useCallback(async () => { + track('ProfileHeader:ShareButtonClicked') + const url = toShareUrl(`/profile/${view.handle}`) + shareUrl(url) + }, [track, view]) + + const onPressMuteAccount = React.useCallback(async () => { + track('ProfileHeader:MuteAccountButtonClicked') + try { + await view.muteAccount() + Toast.show('Account muted') + } catch (e: any) { + store.log.error('Failed to mute account', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, [track, view, store]) + + const onPressUnmuteAccount = React.useCallback(async () => { + track('ProfileHeader:UnmuteAccountButtonClicked') + try { + await view.unmuteAccount() + Toast.show('Account unmuted') + } catch (e: any) { + store.log.error('Failed to unmute account', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, [track, view, store]) + + const onPressBlockAccount = React.useCallback(async () => { + track('ProfileHeader:BlockAccountButtonClicked') + store.shell.openModal({ + name: 'confirm', + title: 'Block Account', + message: + 'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours.', + onPressConfirm: async () => { + try { + await view.blockAccount() + onRefreshAll() + Toast.show('Account blocked') + } catch (e: any) { + store.log.error('Failed to block account', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, + }) + }, [track, view, store, onRefreshAll]) + + const onPressUnblockAccount = React.useCallback(async () => { + track('ProfileHeader:UnblockAccountButtonClicked') + store.shell.openModal({ + name: 'confirm', + title: 'Unblock Account', + message: + 'The account will be able to interact with you after unblocking. (You can always block again in the future.)', + onPressConfirm: async () => { + try { + await view.unblockAccount() + onRefreshAll() + Toast.show('Account unblocked') + } catch (e: any) { + store.log.error('Failed to block unaccount', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, + }) + }, [track, view, store, onRefreshAll]) + + const onPressReportAccount = React.useCallback(() => { + track('ProfileHeader:ReportAccountButtonClicked') + store.shell.openModal({ + name: 'report-account', + did: view.did, + }) + }, [track, store, view]) + + const isMe = React.useMemo( + () => store.me.did === view.did, + [store.me.did, view.did], ) - }, [view, store]) + const dropdownItems: DropdownItem[] = React.useMemo(() => { + let items: DropdownItem[] = [ + { + testID: 'profileHeaderDropdownShareBtn', + label: 'Share', + onPress: onPressShare, + }, + ] + if (!isMe) { + items.push({sep: true}) + if (!view.viewer.blocking) { + items.push({ + testID: 'profileHeaderDropdownMuteBtn', + label: view.viewer.muted ? 'Unmute Account' : 'Mute Account', + onPress: view.viewer.muted + ? onPressUnmuteAccount + : onPressMuteAccount, + }) + } + items.push({ + testID: 'profileHeaderDropdownBlockBtn', + label: view.viewer.blocking ? 'Unblock Account' : 'Block Account', + onPress: view.viewer.blocking + ? onPressUnblockAccount + : onPressBlockAccount, + }) + items.push({ + testID: 'profileHeaderDropdownReportBtn', + label: 'Report Account', + onPress: onPressReportAccount, + }) + } + return items + }, [ + isMe, + view.viewer.muted, + view.viewer.blocking, + onPressShare, + onPressUnmuteAccount, + onPressMuteAccount, + onPressUnblockAccount, + onPressBlockAccount, + onPressReportAccount, + ]) - const onPressEditProfile = React.useCallback(() => { - track('ProfileHeader:EditProfileButtonClicked') - store.shell.openModal({ - name: 'edit-profile', - profileView: view, - onUpdate: onRefreshAll, - }) - }, [track, store, view, onRefreshAll]) + const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy) - const onPressFollowers = React.useCallback(() => { - track('ProfileHeader:FollowersButtonClicked') - navigation.push('ProfileFollowers', {name: view.handle}) - }, [track, navigation, view]) - - const onPressFollows = React.useCallback(() => { - track('ProfileHeader:FollowsButtonClicked') - navigation.push('ProfileFollows', {name: view.handle}) - }, [track, navigation, view]) - - const onPressShare = React.useCallback(async () => { - track('ProfileHeader:ShareButtonClicked') - const url = toShareUrl(`/profile/${view.handle}`) - shareUrl(url) - }, [track, view]) - - const onPressMuteAccount = React.useCallback(async () => { - track('ProfileHeader:MuteAccountButtonClicked') - try { - await view.muteAccount() - Toast.show('Account muted') - } catch (e: any) { - store.log.error('Failed to mute account', e) - Toast.show(`There was an issue! ${e.toString()}`) - } - }, [track, view, store]) - - const onPressUnmuteAccount = React.useCallback(async () => { - track('ProfileHeader:UnmuteAccountButtonClicked') - try { - await view.unmuteAccount() - Toast.show('Account unmuted') - } catch (e: any) { - store.log.error('Failed to unmute account', e) - Toast.show(`There was an issue! ${e.toString()}`) - } - }, [track, view, store]) - - const onPressReportAccount = React.useCallback(() => { - track('ProfileHeader:ReportAccountButtonClicked') - store.shell.openModal({ - name: 'report-account', - did: view.did, - }) - }, [track, store, view]) - - const isMe = React.useMemo( - () => store.me.did === view.did, - [store.me.did, view.did], - ) - const dropdownItems: DropdownItem[] = React.useMemo(() => { - let items: DropdownItem[] = [ - { - testID: 'profileHeaderDropdownSahreBtn', - label: 'Share', - onPress: onPressShare, - }, - ] - if (!isMe) { - items.push({ - testID: 'profileHeaderDropdownMuteBtn', - label: view.viewer.muted ? 'Unmute Account' : 'Mute Account', - onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount, - }) - items.push({ - testID: 'profileHeaderDropdownReportBtn', - label: 'Report Account', - onPress: onPressReportAccount, - }) - } - return items - }, [ - isMe, - view.viewer.muted, - onPressShare, - onPressUnmuteAccount, - onPressMuteAccount, - onPressReportAccount, - ]) - return ( - - - - - {isMe ? ( - - - Edit Profile - - - ) : ( + return ( + + + + + {isMe ? ( + + + Edit Profile + + + ) : view.viewer.blocking ? ( + + + Unblock + + + ) : !view.viewer.blockedBy ? ( + <> + {store.me.follows.getFollowState(view.did) === + FollowState.Following ? ( + + + + Following + + + ) : ( + + + + Follow + + + )} + + ) : null} + {dropdownItems?.length ? ( + + + + ) : undefined} + + + + {sanitizeDisplayName(view.displayName || view.handle)} + + + + {view.viewer.followedBy && !blockHide ? ( + + + Follows you + + + ) : undefined} + @{view.handle} + + {!blockHide && ( <> - {store.me.follows.getFollowState(view.did) === - FollowState.Following ? ( + - - - Following + testID="profileHeaderFollowersButton" + style={[s.flexRow, s.mr10]} + onPress={onPressFollowers}> + + {view.followersCount} + + + {pluralize(view.followersCount, 'follower')} - ) : ( - - - Follow + testID="profileHeaderFollowsButton" + style={[s.flexRow, s.mr10]} + onPress={onPressFollows}> + + {view.followsCount} + + + following - )} + + + {view.postsCount} + + + {pluralize(view.postsCount, 'post')} + + + + {view.descriptionRichText ? ( + + ) : undefined} )} - {dropdownItems?.length ? ( - - - - ) : undefined} + + + {view.viewer.blocking ? ( + + + + Account blocked + + + ) : view.viewer.muted ? ( + + + + Account muted + + + ) : undefined} + {view.viewer.blockedBy && ( + + + + This account has blocked you + + + )} + - - - {sanitizeDisplayName(view.displayName || view.handle)} - - - - {view.viewer.followedBy ? ( - - - Follows you - + {!isDesktopWeb && !hideBackButton && ( + + + + + - ) : undefined} - @{view.handle} - - - - - {view.followersCount} - - - {pluralize(view.followersCount, 'follower')} - - - - - {view.followsCount} - - - following - - - - - {view.postsCount} - - - {pluralize(view.postsCount, 'post')} - - - - {view.descriptionRichText ? ( - - ) : undefined} - - {view.viewer.muted ? ( + + )} + - + - - Account muted - - - ) : undefined} - - {!isDesktopWeb && !hideBackButton && ( - - - - - - )} - - - - - - - ) -}) + + ) + }, +) const styles = StyleSheet.create({ banner: { @@ -460,6 +556,19 @@ const styles = StyleSheet.create({ paddingVertical: 2, }, + moderationLines: { + gap: 6, + }, + + moderationNotice: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + }, + br40: {borderRadius: 40}, br50: {borderRadius: 50}, }) diff --git a/src/view/index.ts b/src/view/index.ts index 93c6fccc52..8de0358683 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -15,6 +15,7 @@ import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotate import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate' import {faAt} from '@fortawesome/free-solid-svg-icons/faAt' import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' +import {faBan} from '@fortawesome/free-solid-svg-icons/faBan' import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark' @@ -90,6 +91,7 @@ export function setup() { faArrowRotateLeft, faArrowsRotate, faAt, + faBan, faBars, faBell, farBell, diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx index f957a45e0f..4e20558b7b 100644 --- a/src/view/screens/AppPasswords.tsx +++ b/src/view/screens/AppPasswords.tsx @@ -27,7 +27,7 @@ export const AppPasswords = withAuthRequired( useFocusEffect( React.useCallback(() => { - screen('Settings') + screen('AppPasswords') store.shell.setMinimalShellMode(false) }, [screen, store]), ) diff --git a/src/view/screens/BlockedAccounts.tsx b/src/view/screens/BlockedAccounts.tsx new file mode 100644 index 0000000000..1950685109 --- /dev/null +++ b/src/view/screens/BlockedAccounts.tsx @@ -0,0 +1,172 @@ +import React, {useMemo} from 'react' +import { + ActivityIndicator, + FlatList, + RefreshControl, + StyleSheet, + View, +} from 'react-native' +import {AppBskyActorDefs as ActorDefs} from '@atproto/api' +import {Text} from '../com/util/text/Text' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {observer} from 'mobx-react-lite' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {CommonNavigatorParams} from 'lib/routes/types' +import {BlockedAccountsModel} from 'state/models/lists/blocked-accounts' +import {useAnalytics} from 'lib/analytics' +import {useFocusEffect} from '@react-navigation/native' +import {ViewHeader} from '../com/util/ViewHeader' +import {CenteredView} from 'view/com/util/Views' +import {ProfileCard} from 'view/com/profile/ProfileCard' + +type Props = NativeStackScreenProps +export const BlockedAccounts = withAuthRequired( + observer(({}: Props) => { + const pal = usePalette('default') + const store = useStores() + const {screen} = useAnalytics() + const blockedAccounts = useMemo( + () => new BlockedAccountsModel(store), + [store], + ) + + useFocusEffect( + React.useCallback(() => { + screen('BlockedAccounts') + store.shell.setMinimalShellMode(false) + blockedAccounts.refresh() + }, [screen, store, blockedAccounts]), + ) + + const onRefresh = React.useCallback(() => { + blockedAccounts.refresh() + }, [blockedAccounts]) + const onEndReached = React.useCallback(() => { + blockedAccounts + .loadMore() + .catch(err => + store.log.error('Failed to load more blocked accounts', err), + ) + }, [blockedAccounts, store]) + + const renderItem = ({ + item, + index, + }: { + item: ActorDefs.ProfileView + index: number + }) => ( + + ) + return ( + + + + Blocked accounts cannot reply in your threads, mention you, or + otherwise interact with you. You will not see their content and they + will be prevented from seeing yours. + + {!blockedAccounts.hasContent ? ( + + + + You have not blocked any accounts yet. To block an account, go + to their profile and selected "Block account" from the menu on + their account. + + + + ) : ( + item.did} + refreshControl={ + + } + onEndReached={onEndReached} + renderItem={renderItem} + initialNumToRender={15} + ListFooterComponent={() => ( + + {blockedAccounts.isLoading && } + + )} + extraData={blockedAccounts.isLoading} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + )} + + ) + }), +) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 100, + }, + containerDesktop: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + title: { + textAlign: 'center', + marginTop: 12, + marginBottom: 12, + }, + description: { + textAlign: 'center', + paddingHorizontal: 30, + marginBottom: 14, + }, + descriptionDesktop: { + marginTop: 14, + }, + + flex1: { + flex: 1, + }, + empty: { + paddingHorizontal: 20, + paddingVertical: 20, + borderRadius: 16, + marginHorizontal: 24, + marginTop: 10, + }, + emptyText: { + textAlign: 'center', + }, + + footer: { + height: 200, + paddingTop: 20, + }, +}) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 4be1179326..5fb212554b 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -116,6 +116,24 @@ export const ProfileScreen = withAuthRequired( } else if (item === ProfileUiModel.LOADING_ITEM) { return } else if (item._reactKey === '__error__') { + if (uiState.feed.isBlocking) { + return ( + + ) + } + if (uiState.feed.isBlockedBy) { + return ( + + ) + } return ( }, - [onPressTryAgain, uiState.profile.did], + [ + onPressTryAgain, + uiState.profile.did, + uiState.feed.isBlocking, + uiState.feed.isBlockedBy, + ], ) return ( diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 89e2d78b46..ef02e81891 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -255,7 +255,7 @@ export const SettingsScreen = withAuthRequired( - Advanced + Moderation + + + + + + Blocked accounts + + + + + + + Advanced + Date: Fri, 28 Apr 2023 20:03:55 -0500 Subject: [PATCH 053/374] 1.25 --- app.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app.json b/app.json index cae5c05665..119208ff62 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Bluesky", "slug": "bluesky", "owner": "blueskysocial", - "version": "1.24.0", + "version": "1.25.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -13,7 +13,7 @@ "backgroundColor": "#ffffff" }, "ios": { - "buildNumber": "2", + "buildNumber": "1", "supportsTablet": false, "bundleIdentifier": "xyz.blueskyweb.app", "config": { @@ -34,7 +34,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 9, + "versionCode": 10, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" diff --git a/package.json b/package.json index 595b887b43..2dc4e99137 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.24.0", + "version": "1.25.0", "private": true, "scripts": { "postinstall": "patch-package", From 7171d0404ed61fbfb6d593aae3030834ad885072 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 1 May 2023 13:13:58 -0500 Subject: [PATCH 054/374] App store fixes (permission descriptions) (#557) * Fix permission usage descriptions on iOS * Bump ios build number --- app.json | 8 ++++++-- package.json | 6 +++--- yarn.lock | 46 +++++++++++++++++++++++----------------------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/app.json b/app.json index 119208ff62..2d69204488 100644 --- a/app.json +++ b/app.json @@ -13,7 +13,7 @@ "backgroundColor": "#ffffff" }, "ios": { - "buildNumber": "1", + "buildNumber": "2", "supportsTablet": false, "bundleIdentifier": "xyz.blueskyweb.app", "config": { @@ -26,7 +26,11 @@ ], "BGTaskSchedulerPermittedIdentifiers": [ "com.transistorsoft.fetch" - ] + ], + "NSCameraUsageDescription": "Used for profile pictures, posts, and other kinds of content.", + "NSMicrophoneUsageDescription": "Used for posts and other kinds of content.", + "NSPhotoLibraryAddUsageDescription": "Used to save images to your library.", + "NSPhotoLibraryUsageDescription": "Used for profile pictures, posts, and other kinds of content" } }, "androidStatusBar": { diff --git a/package.json b/package.json index 2dc4e99137..5595139918 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "await-lock": "^2.2.2", "base64-js": "^1.5.1", "email-validator": "^2.0.4", - "expo": "~48.0.11", + "expo": "~48.0.15", "expo-build-properties": "~0.5.1", "expo-camera": "~13.2.1", "expo-dev-client": "~2.1.1", @@ -70,7 +70,7 @@ "expo-localization": "~14.1.1", "expo-media-library": "~15.2.3", "expo-sharing": "~11.2.2", - "expo-splash-screen": "~0.18.1", + "expo-splash-screen": "~0.18.2", "expo-status-bar": "~1.4.4", "expo-system-ui": "~2.2.1", "expo-updates": "~0.16.4", @@ -99,7 +99,7 @@ "react-avatar-editor": "^13.0.0", "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", - "react-native": "0.71.6", + "react-native": "0.71.7", "react-native-appstate-hook": "^1.0.6", "react-native-background-fetch": "^4.1.8", "react-native-drawer-layout": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 979d6bf633..0889771535 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1535,10 +1535,10 @@ mv "~2" safe-json-stringify "~1" -"@expo/cli@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.7.0.tgz#2a16873ced05c1f3b7f3990d7b410e9853600f45" - integrity sha512-9gjr3pRgwWzUDW/P7B4tA0QevKb+hCrvTmVc3Ce5w7CjdM3zNoBcro8vwviRHqkiB1IifG7zQh0PPStSbK+FRQ== +"@expo/cli@0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.7.1.tgz#8b2e32867452b4dad006759dd438b5f7fc4bc047" + integrity sha512-414sC4phJA5p96+bgPsyaPNwsepcOsGeErxFXp9OhqwgiQpw+H0uN9mVrvNIKLDHMVWHrW9bAFUEcpoL6VkzbQ== dependencies: "@babel/runtime" "^7.20.0" "@expo/code-signing-certificates" "0.0.5" @@ -1551,7 +1551,7 @@ "@expo/osascript" "^2.0.31" "@expo/package-manager" "~1.0.0" "@expo/plist" "^0.0.20" - "@expo/prebuild-config" "6.0.0" + "@expo/prebuild-config" "6.0.1" "@expo/rudder-sdk-node" "1.1.1" "@expo/spawn-async" "1.5.0" "@expo/xcpretty" "^4.2.1" @@ -1859,10 +1859,10 @@ base64-js "^1.2.3" xmlbuilder "^14.0.0" -"@expo/prebuild-config@6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-6.0.0.tgz#c8e7f634f3ecf2272673f371c47d5d22950129a4" - integrity sha512-UW0QKAoRelsalVMhAG1tmegwS+2tbefvUi6/0QiKPlMLg8GFDQ5ZnzsSmuljD0SzT5yGg8oSpKYhnrXJ6pRmIQ== +"@expo/prebuild-config@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-6.0.1.tgz#e3a5bbf5892859e71ac6a2408b1cc8ba6ca3f58f" + integrity sha512-WK3FDht1tdXZGCvtG5s7HSwzhsc7Tyu2DdqV9jVUsLtGD42oqUepk13mEWlU9LOTBgLsoEueKjoSK4EXOXFctw== dependencies: "@expo/config" "~8.0.0" "@expo/config-plugins" "~6.0.0" @@ -8437,13 +8437,13 @@ expo-sharing@~11.2.2: resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-11.2.2.tgz#7d9e387f1a902e6dd6838c22d9599dae9e7432cf" integrity sha512-4Lhm1eS/CFIzX+JPuxMUTWBt9rv/WdvJvpQ9y+71bL/9w9dhvsdt9tv0SsNZATz4hk0tbrYD8ZEUsgiHiT1KkQ== -expo-splash-screen@~0.18.1: - version "0.18.1" - resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.18.1.tgz#e090b045a7f8c5d9597b7a96910caa4eae1fcf3b" - integrity sha512-1di1kuh14likGUs3fyVZWAqEMxhmdAjpmf9T8Qk5OzUa5oPEMEDYB2e2VprddWnJNBVVe/ojBDSCY8w56/LS0Q== +expo-splash-screen@~0.18.2: + version "0.18.2" + resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.18.2.tgz#dde246204da875785ba40c7143a70013cdefdbb6" + integrity sha512-fsiKmyn/lbJtV6Uor6wSvl21fScOidFzmB/HHShQJJOu2TBN/vqMvhPu/r0bF5NVk8Wi64r98hiWY1EEsbW03w== dependencies: "@expo/configure-splash-screen" "^0.6.0" - "@expo/prebuild-config" "6.0.0" + "@expo/prebuild-config" "6.0.1" expo-status-bar@~1.4.4: version "1.4.4" @@ -8485,13 +8485,13 @@ expo-updates@~0.16.4: fbemitter "^3.0.0" resolve-from "^5.0.0" -expo@~48.0.11: - version "48.0.11" - resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.11.tgz#afd43c7a5ddce3d02a3f27263c95f8d01e1fb84d" - integrity sha512-KX1RCHhdhdT4DjCeRqYJpZXhdCTuqxHHdNIRoFkmCgkUARYlZbB+Y1U8/KMz8fBAlFoEq99cF/KyRr87VAxRCw== +expo@~48.0.15: + version "48.0.15" + resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.15.tgz#28194c03ac85f7f5a87b7493b8cef0eb405eccbe" + integrity sha512-me2Xxr7Faxf60BiKq8WBSwkYV9BVbS+VqeHRFdXduVA0Uj2zp1a0zYB5eblmWqpRco75VBUgOa9M+/eR1YVZmw== dependencies: "@babel/runtime" "^7.20.0" - "@expo/cli" "0.7.0" + "@expo/cli" "0.7.1" "@expo/config" "8.0.2" "@expo/config-plugins" "6.0.1" "@expo/vector-icons" "^13.0.0" @@ -14871,10 +14871,10 @@ react-native-web@^0.18.11: postcss-value-parser "^4.2.0" styleq "^0.1.2" -react-native@0.71.6: - version "0.71.6" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.6.tgz#e8f07baf55abd1015eaa7040ceaa4aa632c2c04f" - integrity sha512-gHrDj7qaAaiE41JwaFCh3AtvOqOLuRgZtHKzNiwxakG/wvPAYmG73ECfWHGxjxIx/QT17Hp37Da3ipCei/CayQ== +react-native@0.71.7: + version "0.71.7" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.7.tgz#d0ae409f6ee4fc7e7a876b4ca9d8d28934133228" + integrity sha512-Id6iRLS581fJMFGbBl1jP5uSmjExtGOvw5Gvh7694zISXjsRAsFMmU+izs0pyCLqDBoHK7y4BT7WGPGw693nYw== dependencies: "@jest/create-cache-key-function" "^29.2.1" "@react-native-community/cli" "10.2.2" From 0ec98c77ef65ff74e83b314d8eed9ef9b07d47d3 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Mon, 1 May 2023 11:31:00 -0700 Subject: [PATCH 055/374] Format large numbers (#556) --- src/view/com/profile/ProfileHeader.tsx | 5 +++-- src/view/com/util/numeric/format.ts | 5 +++++ src/view/shell/Drawer.tsx | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 src/view/com/util/numeric/format.ts diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 719b84e20a..4accd7abac 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -33,6 +33,7 @@ import {NavigationProp} from 'lib/routes/types' import {isDesktopWeb} from 'platform/detection' import {FollowState} from 'state/models/cache/my-follows' import {shareUrl} from 'lib/sharing' +import {formatCount} from '../util/numeric/format' const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30} @@ -364,7 +365,7 @@ const ProfileHeaderLoaded = observer( style={[s.flexRow, s.mr10]} onPress={onPressFollowers}> - {view.followersCount} + {formatCount(view.followersCount)} {pluralize(view.followersCount, 'follower')} @@ -375,7 +376,7 @@ const ProfileHeaderLoaded = observer( style={[s.flexRow, s.mr10]} onPress={onPressFollows}> - {view.followsCount} + {formatCount(view.followsCount)} following diff --git a/src/view/com/util/numeric/format.ts b/src/view/com/util/numeric/format.ts new file mode 100644 index 0000000000..f0e90217f5 --- /dev/null +++ b/src/view/com/util/numeric/format.ts @@ -0,0 +1,5 @@ +export const formatCount = (num: number) => + Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, + }).format(num) diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 7128d42132..81ee005c81 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -39,6 +39,7 @@ import {getTabState, TabState} from 'lib/routes/helpers' import {NavigationProp} from 'lib/routes/types' import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' import {isWeb} from 'platform/detection' +import {formatCount} from 'view/com/util/numeric/format' export const DrawerContent = observer(() => { const theme = useTheme() @@ -133,11 +134,11 @@ export const DrawerContent = observer(() => { type="xl" style={[pal.textLight, styles.profileCardFollowers]}> - {store.me.followersCount || 0} + {formatCount(store.me.followersCount ?? 0)} {' '} {pluralize(store.me.followersCount || 0, 'follower')} ·{' '} - {store.me.followsCount || 0} + {formatCount(store.me.followsCount ?? 0)} {' '} following From dbb3c5c15524c517291356a4918d043348906aad Mon Sep 17 00:00:00 2001 From: Ollie H Date: Mon, 1 May 2023 11:59:17 -0700 Subject: [PATCH 056/374] Image alt text view modal (#551) * Image alt text view modal * Minor style tweaks --------- Co-authored-by: Paul Frazee --- src/state/models/ui/shell.ts | 8 +- src/view/com/modals/AltImageRead.tsx | 75 +++++++ src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/Modal.web.tsx | 3 + src/view/com/util/images/Gallery.tsx | 76 +++++++ src/view/com/util/images/ImageLayoutGrid.tsx | 204 ++++--------------- src/view/com/util/post-embeds/index.tsx | 94 ++++++--- 7 files changed, 272 insertions(+), 192 deletions(-) create mode 100644 src/view/com/modals/AltImageRead.tsx create mode 100644 src/view/com/util/images/Gallery.tsx diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 797d53f816..98e98ef8eb 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -47,6 +47,11 @@ export interface AltTextImageModal { onAltTextSet: (altText?: string) => void } +export interface AltTextImageReadModal { + name: 'alt-text-image-read' + altText: string +} + export interface DeleteAccountModal { name: 'delete-account' } @@ -93,8 +98,9 @@ export type Modal = | ReportAccountModal | ReportPostModal - // Posting + // Posts | AltTextImageModal + | AltTextImageReadModal | CropImageModal | ServerInputModal | RepostModal diff --git a/src/view/com/modals/AltImageRead.tsx b/src/view/com/modals/AltImageRead.tsx new file mode 100644 index 0000000000..e7b4797eee --- /dev/null +++ b/src/view/com/modals/AltImageRead.tsx @@ -0,0 +1,75 @@ +import React, {useCallback} from 'react' +import {StyleSheet, View} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {gradients, s} from 'lib/styles' +import {Text} from '../util/text/Text' +import {TouchableOpacity} from 'react-native-gesture-handler' +import LinearGradient from 'react-native-linear-gradient' +import {useStores} from 'state/index' +import {isDesktopWeb} from 'platform/detection' + +export const snapPoints = ['70%'] + +interface Props { + altText: string +} + +export function Component({altText}: Props) { + const pal = usePalette('default') + const store = useStores() + + const onPress = useCallback(() => { + store.shell.closeModal() + }, [store]) + + return ( + + Image description + + {altText} + + + + + Done + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + gap: 18, + paddingVertical: isDesktopWeb ? 0 : 18, + paddingHorizontal: isDesktopWeb ? 0 : 12, + height: '100%', + width: '100%', + }, + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + }, + text: { + borderRadius: 5, + marginVertical: 18, + paddingHorizontal: 18, + paddingVertical: 16, + }, + button: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + borderRadius: 32, + padding: 10, + }, +}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index df7d7f0420..2e053e3add 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -13,6 +13,7 @@ import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' import * as RepostModal from './Repost' import * as AltImageModal from './AltImage' +import * as AltImageReadModal from './AltImageRead' import * as ReportAccountModal from './ReportAccount' import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' @@ -74,6 +75,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'alt-text-image') { snapPoints = AltImageModal.snapPoints element = + } else if (activeModal?.name === 'alt-text-image-read') { + snapPoints = AltImageReadModal.snapPoints + element = } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 07d5168eda..de748b3a8b 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -15,6 +15,7 @@ import * as DeleteAccountModal from './DeleteAccount' import * as RepostModal from './Repost' import * as CropImageModal from './crop-image/CropImage.web' import * as AltTextImageModal from './AltImage' +import * as AltTextImageReadModal from './AltImageRead' import * as ChangeHandleModal from './ChangeHandle' import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' @@ -84,6 +85,8 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'alt-text-image') { element = + } else if (modal.name === 'alt-text-image-read') { + element = } else { return null } diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx new file mode 100644 index 0000000000..78ced0668e --- /dev/null +++ b/src/view/com/util/images/Gallery.tsx @@ -0,0 +1,76 @@ +import {AppBskyEmbedImages} from '@atproto/api' +import React, {ComponentProps, FC, useCallback} from 'react' +import {Pressable, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import {Image} from 'expo-image' +import {useStores} from 'state/index' + +type EventFunction = (index: number) => void + +interface GalleryItemProps { + images: AppBskyEmbedImages.ViewImage[] + index: number + onPress?: EventFunction + onLongPress?: EventFunction + onPressIn?: EventFunction + imageStyle: ComponentProps['style'] +} + +const DELAY_PRESS_IN = 500 + +export const GalleryItem: FC = ({ + images, + index, + imageStyle, + onPress, + onPressIn, + onLongPress, +}) => { + const image = images[index] + const store = useStores() + + const onPressAltText = useCallback(() => { + store.shell.openModal({ + name: 'alt-text-image-read', + altText: image.alt, + }) + }, [image.alt, store.shell]) + + return ( + + onPress?.(index)} + onPressIn={() => onPressIn?.(index)} + onLongPress={() => onLongPress?.(index)}> + + + {image.alt === '' ? null : ( + + ALT + + )} + + ) +} + +const styles = StyleSheet.create({ + alt: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + color: 'white', + fontSize: 12, + fontWeight: 'bold', + letterSpacing: 1, + paddingHorizontal: 10, + paddingVertical: 3, + position: 'absolute', + left: 10, + top: -26, + width: 46, + }, +}) diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 51bb04fe94..4c09013043 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -3,15 +3,13 @@ import { LayoutChangeEvent, StyleProp, StyleSheet, - TouchableOpacity, View, ViewStyle, } from 'react-native' -import {Image, ImageStyle} from 'expo-image' +import {ImageStyle} from 'expo-image' import {Dimensions} from 'lib/media/types' import {AppBskyEmbedImages} from '@atproto/api' - -export const DELAY_PRESS_IN = 500 +import {GalleryItem} from './Gallery' interface ImageLayoutGridProps { images: AppBskyEmbedImages.ViewImage[] @@ -21,32 +19,21 @@ interface ImageLayoutGridProps { style?: StyleProp } -export function ImageLayoutGrid({ - images, - onPress, - onLongPress, - onPressIn, - style, -}: ImageLayoutGridProps) { +export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { const [containerInfo, setContainerInfo] = useState() const onLayout = (evt: LayoutChangeEvent) => { + const {width, height} = evt.nativeEvent.layout setContainerInfo({ - width: evt.nativeEvent.layout.width, - height: evt.nativeEvent.layout.height, + width, + height, }) } return ( {containerInfo ? ( - + ) : undefined} ) @@ -61,13 +48,10 @@ interface ImageLayoutGridInnerProps { } function ImageLayoutGridInner({ - images, - onPress, - onLongPress, - onPressIn, containerInfo, + ...props }: ImageLayoutGridInnerProps) { - const count = images.length + const count = props.images.length const size1 = useMemo(() => { if (count === 3) { const size = (containerInfo.width - 10) / 3 @@ -87,149 +71,43 @@ function ImageLayoutGridInner({ } }, [count, containerInfo]) - if (count === 2) { - return ( - - onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - - - - onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - - - - ) - } - if (count === 3) { - return ( - - onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - - - - - onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - - - - onPress?.(2)} - onPressIn={() => onPressIn?.(2)} - onLongPress={() => onLongPress?.(2)}> - - + switch (count) { + case 2: + return ( + + + - - ) - } - if (count === 4) { - return ( - - - onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - - - - onPress?.(2)} - onPressIn={() => onPressIn?.(2)} - onLongPress={() => onLongPress?.(2)}> - - + ) + case 3: + return ( + + + + + + - - - onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - - - - onPress?.(3)} - onPressIn={() => onPressIn?.(3)} - onLongPress={() => onLongPress?.(3)}> - - + ) + case 4: + return ( + + + + + + + + + - - ) + ) + default: + return null } - return } const styles = StyleSheet.create({ - flexRow: {flexDirection: 'row'}, - wSpace: {width: 5}, - hSpace: {height: 5}, + flexRow: {flexDirection: 'row', gap: 5}, + flexColumn: {flexDirection: 'column', gap: 5}, }) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index f37fba342d..6a77598408 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -1,10 +1,12 @@ -import React from 'react' +import React, {useCallback} from 'react' import { StyleSheet, StyleProp, View, ViewStyle, Image as RNImage, + Pressable, + Text, } from 'react-native' import { AppBskyEmbedImages, @@ -14,7 +16,6 @@ import { AppBskyFeedPost, } from '@atproto/api' import {Link} from '../Link' -import {AutoSizedImage} from '../images/AutoSizedImage' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImagesLightbox} from 'state/models/ui/shell' import {useStores} from 'state/index' @@ -24,6 +25,7 @@ import {YoutubeEmbed} from './YoutubeEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {getYoutubeVideoId} from 'lib/strings/url-helpers' import QuoteEmbed from './QuoteEmbed' +import {AutoSizedImage} from '../images/AutoSizedImage' type Embed = | AppBskyEmbedRecord.View @@ -42,6 +44,16 @@ export function PostEmbeds({ const pal = usePalette('default') const store = useStores() + const onPressAltText = useCallback( + (alt: string) => { + store.shell.openModal({ + name: 'alt-text-image-read', + altText: alt, + }) + }, + [store.shell], + ) + if ( AppBskyEmbedRecordWithMedia.isView(embed) && AppBskyEmbedRecord.isViewRecord(embed.record.record) && @@ -88,7 +100,9 @@ export function PostEmbeds({ } if (AppBskyEmbedImages.isView(embed)) { - if (embed.images.length > 0) { + const {images} = embed + + if (images.length > 0) { const uris = embed.images.map(img => img.fullsize) const openLightbox = (index: number) => { store.shell.openLightbox(new ImagesLightbox(uris, index)) @@ -107,32 +121,42 @@ export function PostEmbeds({ }) } - switch (embed.images.length) { - case 1: - return ( - - openLightbox(0)} - onLongPress={() => onLongPress(0)} - onPressIn={() => onPressIn(0)} - style={styles.singleImage} - /> - - ) - default: - return ( - - - - ) + if (images.length === 1) { + const {alt, thumb} = images[0] + return ( + + openLightbox(0)} + onLongPress={() => onLongPress(0)} + onPressIn={() => onPressIn(0)} + style={styles.singleImage}> + {alt === '' ? null : ( + { + onPressAltText(alt) + }}> + ALT + + )} + + + ) } + + return ( + + + + ) + // } } } @@ -172,4 +196,18 @@ const styles = StyleSheet.create({ borderRadius: 8, marginTop: 4, }, + alt: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + color: 'white', + fontSize: 12, + fontWeight: 'bold', + letterSpacing: 1, + paddingHorizontal: 10, + paddingVertical: 3, + position: 'absolute', + left: 10, + top: -26, + width: 46, + }, }) From c75c888de2407d3314cad07989174201313facaa Mon Sep 17 00:00:00 2001 From: Ansh Date: Mon, 1 May 2023 12:42:31 -0700 Subject: [PATCH 057/374] [APP-527] setup sentry (#532) * setup sentry * add sentry to transformIgnorePatterns to fix jest issues * update README with sourcemap instructions * only enable integrations on native * fix sentry web * remove testing code * fix sentry authToken * Switch over to paul's auth tokens temporarily (lol) --------- Co-authored-by: Paul Frazee --- README.md | 24 +++ app.json | 14 +- eas.json | 15 +- package.json | 7 +- src/App.native.tsx | 4 +- src/App.web.tsx | 1 + src/Navigation.tsx | 13 +- src/lib/sentry.ts | 46 +++++ yarn.lock | 431 +++++++++++++++++++++++++++++++++++++++++++-- 9 files changed, 531 insertions(+), 24 deletions(-) create mode 100644 src/lib/sentry.ts diff --git a/README.md b/README.md index f29b49e1dd..a0a8da1961 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,27 @@ To open the [Developer Menu](https://docs.expo.dev/debugging/tools/#developer-me `./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes: - TextEncoder / TextDecoder + + +### Sentry sourcemaps +Sourcemaps should automatically be updated when a signed build is created using `eas build` and published using `eas submit` due to the postPublish hook setup in `app.json`. However, if an update is created and published OTA using `eas update`, we need to the take the following steps to upload sourcemaps to Sentry: +- Run eas update. This will generate a dist folder in your project root, which contains your JavaScript bundles and source maps. This command will also output the 'Android update ID' and 'iOS update ID' that we'll need in the next step. +- Copy or rename the bundle names in the `dist/bundles` folder to match `index.android.bundle` (Android) or `main.jsbundle` (iOS). +- Next, you can use the Sentry CLI to upload your bundles and source maps: + - release name should be set to `${bundleIdentifier}@${version}+${buildNumber}` (iOS) or `${androidPackage}@${version}+${versionCode}` (Android), so for example `com.domain.myapp@1.0.0+1`. + - `dist` should be set to the Update ID that `eas update` generated. +- Command for Android: +`node_modules/@sentry/cli/bin/sentry-cli releases \ + files \ + upload-sourcemaps \ + --dist \ + --rewrite \ + dist/bundles/index.android.bundle dist/bundles/android-.map` +- Command for iOS: + `node_modules/@sentry/cli/bin/sentry-cli releases \ + files \ + upload-sourcemaps \ + --dist \ + --rewrite \ + dist/bundles/main.jsbundle dist/bundles/ios-.map` + diff --git a/app.json b/app.json index 2d69204488..f2ac0eb506 100644 --- a/app.json +++ b/app.json @@ -64,12 +64,24 @@ { "username": "blueskysocial" } - ] + ], + "sentry-expo" ], "extra": { "eas": { "projectId": "55bd077a-d905-4184-9c7f-94789ba0f302" } + }, + "hooks": { + "postPublish": [ + { + "file": "sentry-expo/upload-sourcemaps", + "config": { + "organization": "blueskyweb", + "project": "react-native" + } + } + ] } } } diff --git a/eas.json b/eas.json index 93db51cbea..37671c0868 100644 --- a/eas.json +++ b/eas.json @@ -10,20 +10,29 @@ "ios": { "resourceClass": "medium" }, - "channel": "development" + "channel": "development", + "env": { + "SENTRY_AUTH_TOKEN": "89c975413cd543fbb683b11bec984fc2163d9a77312c41c0b4480a570f3daa65" + } }, "preview": { "distribution": "internal", "ios": { "resourceClass": "medium" }, - "channel": "preview" + "channel": "preview", + "env": { + "SENTRY_AUTH_TOKEN": "89c975413cd543fbb683b11bec984fc2163d9a77312c41c0b4480a570f3daa65" + } }, "production": { "ios": { "resourceClass": "medium" }, - "channel": "production" + "channel": "production", + "env": { + "SENTRY_AUTH_TOKEN": "89c975413cd543fbb683b11bec984fc2163d9a77312c41c0b4480a570f3daa65" + } } }, "submit": { diff --git a/package.json b/package.json index 5595139918..d35bd0efdf 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@segment/analytics-react": "^1.0.0-rc1", "@segment/analytics-react-native": "^2.10.1", "@segment/sovran-react-native": "^0.4.5", + "@sentry/react-native": "4.13.0", "@tiptap/core": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220", "@tiptap/extension-history": "^2.0.3", @@ -62,9 +63,12 @@ "base64-js": "^1.5.1", "email-validator": "^2.0.4", "expo": "~48.0.15", + "expo-application": "~5.1.1", "expo-build-properties": "~0.5.1", "expo-camera": "~13.2.1", + "expo-constants": "~14.2.1", "expo-dev-client": "~2.1.1", + "expo-device": "~5.2.1", "expo-image": "^1.2.1", "expo-image-picker": "~14.1.1", "expo-localization": "~14.1.1", @@ -125,6 +129,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-responsive": "^9.0.2", "rn-fetch-blob": "^0.12.0", + "sentry-expo": "~6.1.0", "tippy.js": "^6.3.7", "tlds": "^1.234.0", "zod": "^3.20.2" @@ -197,7 +202,7 @@ "node" ], "transformIgnorePatterns": [ - "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg)" + "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo)" ], "modulePathIgnorePatterns": [ "__tests__/.*/__mocks__", diff --git a/src/App.native.tsx b/src/App.native.tsx index e0e030cbc1..f330cfa046 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -1,5 +1,7 @@ import 'react-native-url-polyfill/auto' import React, {useState, useEffect} from 'react' +import 'lib/sentry' // must be relatively on top +import {withSentry} from 'lib/sentry' import {Linking} from 'react-native' import {RootSiblingParent} from 'react-native-root-siblings' import * as SplashScreen from 'expo-splash-screen' @@ -64,4 +66,4 @@ const App = observer(() => { ) }) -export default App +export default withSentry(App) diff --git a/src/App.web.tsx b/src/App.web.tsx index e259a48e99..4293282769 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,4 +1,5 @@ import React, {useState, useEffect} from 'react' +import 'lib/sentry' // must be relatively on top import {SafeAreaProvider} from 'react-native-safe-area-context' import {RootSiblingParent} from 'react-native-root-siblings' import * as view from './view/index' diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 3a9392fb88..412c63f338 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -50,6 +50,7 @@ import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' import {AppPasswords} from 'view/screens/AppPasswords' import {BlockedAccounts} from 'view/screens/BlockedAccounts' +import {getRoutingInstrumentation} from 'lib/sentry' const navigationRef = createNavigationContainerRef() @@ -262,7 +263,17 @@ const LINKING = { function RoutesContainer({children}: React.PropsWithChildren<{}>) { const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) return ( - + { + // Register the navigation container with the Sentry instrumentation (only works on native) + if (isNative) { + const routingInstrumentation = getRoutingInstrumentation() + routingInstrumentation.registerNavigationContainer(navigationRef) + } + }}> {children} ) diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts new file mode 100644 index 0000000000..c5d1d3eb61 --- /dev/null +++ b/src/lib/sentry.ts @@ -0,0 +1,46 @@ +import {isNative, isWeb} from 'platform/detection' +import {FC} from 'react' +import * as Sentry from 'sentry-expo' + +// Sentry Initialization + +export const getRoutingInstrumentation = () => { + return new Sentry.Native.ReactNavigationInstrumentation() // initialize this in `onReady` prop of NavigationContainer +} + +Sentry.init({ + dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432', + enableInExpoDevelopment: false, // if true, Sentry will try to send events/errors in development mode. + debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production + environment: __DEV__ ? 'development' : 'production', // Set the environment + enableAutoPerformanceTracking: true, // Enable auto performance tracking + tracesSampleRate: 0.5, // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. // TODO: this might be too much in production + integrations: isNative + ? [ + new Sentry.Native.ReactNativeTracing({ + shouldCreateSpanForRequest: url => { + // Do not create spans for outgoing requests to a `/logs` endpoint as it is too noisy due to expo + return !url.match(/\/logs$/) + }, + routingInstrumentation: getRoutingInstrumentation(), + }), + ] + : [], // no integrations for web, yet +}) + +// if web, use Browser client, otherwise use Native client +export function getSentryClient() { + if (isWeb) { + return Sentry.Browser + } + return Sentry.Native +} + +// wrap root App component with Sentry for automatic touch event tracking and performance monitoring +export function withSentry(Component: FC) { + if (isWeb) { + return Component // .wrap is not required or available for web + } + const sentryClient = getSentryClient() + return sentryClient.wrap(Component) +} diff --git a/yarn.lock b/yarn.lock index 0889771535..a54cab39d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1900,7 +1900,7 @@ dependencies: cross-spawn "^6.0.5" -"@expo/spawn-async@^1.5.0": +"@expo/spawn-async@^1.5.0", "@expo/spawn-async@^1.7.0": version "1.7.2" resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58" integrity sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew== @@ -3134,6 +3134,147 @@ dset "^3.1.1" tiny-hashes "^1.0.1" +"@sentry/browser@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.29.0.tgz#eb162b50adec33ac49ecd3dc930bdffbfda8098e" + integrity sha512-Af+dIcntaw405Wt7myDOMGDxiszfy4aBdshrEKYbGgcfHjgXBIdF3iKlNatvl6nrOm+IOVuKgSpCLOr2hiCwzw== + dependencies: + "@sentry/core" "7.29.0" + "@sentry/replay" "7.29.0" + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + tslib "^1.9.3" + +"@sentry/cli@1.74.4": + version "1.74.4" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.4.tgz#7df82f68045a155e1885bfcbb5d303e5259eb18e" + integrity sha512-BMfzYiedbModsNBJlKeBOLVYUtwSi99LJ8gxxE4Bp5N8hyjNIN0WVrozAVZ27mqzAuy6151Za3dpmOLO86YlGw== + dependencies: + https-proxy-agent "^5.0.0" + mkdirp "^0.5.5" + node-fetch "^2.6.7" + npmlog "^4.1.2" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + +"@sentry/cli@^1.72.0": + version "1.75.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.0.tgz#4a5e71b5619cd4e9e6238cc77857c66f6b38d86a" + integrity sha512-vT8NurHy00GcN8dNqur4CMIYvFH3PaKdkX3qllVvi4syybKqjwoz+aWRCvprbYv0knweneFkLt1SmBWqazUMfA== + dependencies: + https-proxy-agent "^5.0.0" + mkdirp "^0.5.5" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + +"@sentry/core@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.29.0.tgz#bc4b54d56cf7652598d4430cf43ea97cc069f6fe" + integrity sha512-+e9aIp2ljtT4EJq3901z6TfEVEeqZd5cWzbKEuQzPn2UO6If9+Utd7kY2Y31eQYb4QnJgZfiIEz1HonuYY6zqQ== + dependencies: + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + tslib "^1.9.3" + +"@sentry/hub@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.29.0.tgz#916f818617b3c3993853737db3e752c21f8f8445" + integrity sha512-nIV2NtTn16VukTtWFhROHJ35NyUIXgEGtesG8a1i7D4iRSvkfLkLrQ9i6D0BAE2huqKqQemO3zGEPR00szqsiA== + dependencies: + "@sentry/core" "7.29.0" + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + tslib "^1.9.3" + +"@sentry/integrations@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.29.0.tgz#12595ac8d964b8006148618b8d5fad294e623c7f" + integrity sha512-BkZe3ALij320VtC5bNkeSz3OUhT9oxZsj2lf5rCuRFqcqw4tvVNADF/Y98mf0L4VCy582M9MlNXmwfewJjxGOA== + dependencies: + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + localforage "^1.8.1" + tslib "^1.9.3" + +"@sentry/react-native@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-4.13.0.tgz#d1b532f481080aed16532ac2778b20c1275391af" + integrity sha512-CxQd5jWPKEPgR1SH5ppf555h7DMhSBZMU3eSZ/VNT+BocgzxxBnf/tcJj92+gpwrzt2m7MiZ3uDfyfQOgyMc8Q== + dependencies: + "@sentry/browser" "7.29.0" + "@sentry/cli" "1.74.4" + "@sentry/core" "7.29.0" + "@sentry/hub" "7.29.0" + "@sentry/integrations" "7.29.0" + "@sentry/react" "7.29.0" + "@sentry/tracing" "7.29.0" + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + "@sentry/wizard" "1.4.0" + +"@sentry/react@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.29.0.tgz#a1c2ef522a4ccf1e948d77584e59e1254e09c92b" + integrity sha512-pJ138QTChfAiYzFrCgycBgXrAVARV6TdVvLB8z/HsqbHzPq17RhyF9M1xPE4ffeLDQAEuSudwED9CLOpJqKnAw== + dependencies: + "@sentry/browser" "7.29.0" + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + hoist-non-react-statics "^3.3.2" + tslib "^1.9.3" + +"@sentry/replay@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.29.0.tgz#75d5bb9df39e0a31994be245032c9998af62a304" + integrity sha512-Gw7HgviJQu6pX5RFQGVY38Av4qFn9otrZdwSSl/QK5hIyg6yhlh5h7U0ydZkrYYGiW6Z6SYYRpEWCJc/Wbh+ZQ== + dependencies: + "@sentry/core" "7.29.0" + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + +"@sentry/tracing@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.29.0.tgz#767f309cbff46ab12bec6ab3c266f7f03fec91fd" + integrity sha512-MAN/G6XROtRhzo/KDjddb6VJn/Q1TaPLwdyj9vvfkUkBNtlt5k16oXp+u7eHWX0uujER9wnZtj2ivXaPeqq0VA== + dependencies: + "@sentry/core" "7.29.0" + "@sentry/types" "7.29.0" + "@sentry/utils" "7.29.0" + tslib "^1.9.3" + +"@sentry/types@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.29.0.tgz#ed829b6014ee19049035fec6af2b4fea44ff28b8" + integrity sha512-DmoEpoqHPty3VxqubS/5gxarwebHRlcBd/yuno+PS3xy++/i9YPjOWLZhU2jYs1cW68M9R6CcCOiC9f2ckJjdw== + +"@sentry/utils@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.29.0.tgz#cbf8f87dd851b0fdc7870db9c68014c321c3bab8" + integrity sha512-ICcBwTiBGK8NQA8H2BJo0JcMN6yCeKLqNKNMVampRgS6wSfSk1edvcTdhRkW3bSktIGrIPZrKskBHyMwDGF2XQ== + dependencies: + "@sentry/types" "7.29.0" + tslib "^1.9.3" + +"@sentry/wizard@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@sentry/wizard/-/wizard-1.4.0.tgz#9356ae2cb9e81ee6fa64418d15638607f1a957bd" + integrity sha512-Q/f9wJAAAr/YB6oWUzMQP/y5LIgx9la1SanMHNr3hMtVPKkMhvIZO5UWVn2G763yi85zARqSCLDx31/tZd4new== + dependencies: + "@sentry/cli" "^1.72.0" + chalk "^2.4.1" + glob "^7.1.3" + inquirer "^6.2.0" + lodash "^4.17.15" + opn "^5.4.0" + r2 "^2.0.1" + read-env "^1.3.0" + semver "^7.3.5" + xcode "3.0.1" + yargs "^16.2.0" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -5187,7 +5328,7 @@ anser@^1.4.9: resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== -ansi-escapes@^3.1.0: +ansi-escapes@^3.1.0, ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -5225,6 +5366,16 @@ ansi-regex@5.0.1, ansi-regex@^5.0.0, ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + ansi-regex@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" @@ -5277,6 +5428,19 @@ application-config-path@^0.1.0: resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.1.tgz#8b5ac64ff6afdd9bd70ce69f6f64b6998f5f756e" integrity sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw== +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + arg@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" @@ -6200,6 +6364,11 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelcase@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -6235,6 +6404,11 @@ case-sensitive-paths-webpack-plugin@^2.4.0: resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== +caseless@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + cborg@^1.6.0: version "1.10.1" resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.1.tgz#24cfe52c69ec0f66f95e23dc57f2086954c8d718" @@ -6275,6 +6449,11 @@ char-regex@^2.0.0: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e" integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + charenc@0.0.2, charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" @@ -6387,6 +6566,11 @@ cli-spinners@^2.0.0, cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== +cli-width@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" + integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== + cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -6447,6 +6631,11 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -6642,6 +6831,11 @@ connect@^3.6.5, connect@^3.7.0: parseurl "~1.3.3" utils-merge "1.0.1" +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -7272,6 +7466,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + denodeify@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" @@ -8338,6 +8537,13 @@ expo-dev-menu@2.1.3: expo-dev-menu-interface "1.1.1" semver "^7.3.5" +expo-device@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-5.2.1.tgz#2962abdb9682e5b991a82836667f2e7d7103d9ef" + integrity sha512-ZWGph+fGQPxo9v2e0YygPb45Hl+ZR3mh4tpLY5AOYK/sNjQy+Lu3T/sLGIdi2TOcYNL2oZwzZ6eGvwVYmdIfLg== + dependencies: + ua-parser-js "^0.7.33" + expo-eas-client@~0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.5.1.tgz#3ef80dbbde13abe35be4e2a2e29b73d2f7fdf27a" @@ -8570,6 +8776,15 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -8715,6 +8930,13 @@ fetch-retry@^4.1.1: resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-4.1.1.tgz#fafe0bb22b54f4d0a9c788dff6dd7f8673ca63f3" integrity sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA== +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -9081,6 +9303,20 @@ funpermaproxy@^1.1.0: resolved "https://registry.yarnpkg.com/funpermaproxy/-/funpermaproxy-1.1.0.tgz#39cb0b8bea908051e4608d8a414f1d87b55bf557" integrity sha512-2Sp1hWuO8m5fqeFDusyhKqYPT+7rGLw34N3qonDcdRP8+n7M7Gl/yKp/q7oCxnnJ6pWCectOmLFJpsMU/++KrQ== +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -9399,6 +9635,11 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -9468,7 +9709,7 @@ history@^5.3.0: dependencies: "@babel/runtime" "^7.7.6" -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -9667,7 +9908,7 @@ hyphenate-style-name@^1.0.0, hyphenate-style-name@^1.0.3: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -9713,6 +9954,11 @@ image-size@^0.6.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.6.3.tgz#e7e5c65bb534bd7cdcedd6cb5166272a85f75fb2" integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immer@^9.0.7: version "9.0.19" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b" @@ -9788,6 +10034,25 @@ inline-style-prefixer@^6.0.1: css-in-js-utils "^3.1.0" fast-loops "^1.1.3" +inquirer@^6.2.0: + version "6.5.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" + integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== + dependencies: + ansi-escapes "^3.2.0" + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^2.0.0" + lodash "^4.17.12" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^2.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + internal-ip@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -9995,6 +10260,13 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== + dependencies: + number-is-nan "^1.0.0" + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -11646,6 +11918,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -11687,6 +11966,13 @@ loader-utils@^3.2.0: resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== +localforage@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -11809,7 +12095,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: +lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12519,7 +12805,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.1, mkdirp@~0.5.1: +mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -12592,6 +12878,11 @@ multipipe@^4.0.0: duplexer2 "^0.1.2" object-assign "^4.1.0" +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ== + mv@~2: version "2.1.1" resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" @@ -12723,7 +13014,7 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.9" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== @@ -12833,6 +13124,16 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -12852,6 +13153,11 @@ nullthrows@^1.1.1: resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== + nwsapi@^2.2.0, nwsapi@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" @@ -13064,6 +13370,13 @@ opencollective-postinstall@^2.0.3: resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== +opn@^5.4.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -14250,7 +14563,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -progress@2.0.3: +progress@2.0.3, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -14547,6 +14860,15 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +r2@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/r2/-/r2-2.0.1.tgz#94cd802ecfce9a622549c8182032d8e4a2b2e612" + integrity sha512-EEmxoxYCe3LHzAUhRIRxdCKERpeRNmlLj6KLUSORqnK6dWl/K5ShmDGZqM2lRZQeqJgF+wyqk0s1M7SWUveNOQ== + dependencies: + caseless "^0.12.0" + node-fetch "^2.0.0-alpha.8" + typedarray-to-buffer "^3.1.2" + raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -15017,7 +15339,14 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.3.6: +read-env@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/read-env/-/read-env-1.3.0.tgz#e26e1e446992b3216e9a3c6f6ac51064fe91fdff" + integrity sha512-DbCgZ8oHwZreK/E2E27RGk3EUPapMhYGSGIt02k9sX6R3tCFc4u4tkltKvkCvzEQ3SOLUaiYHAnGb+TdsnPp0A== + dependencies: + camelcase "5.0.0" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -15449,6 +15778,11 @@ rtl-detect@^1.0.2: resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6" integrity sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ== +run-async@^2.2.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -15456,6 +15790,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@^6.4.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + rxjs@^7.5.2: version "7.8.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" @@ -15645,6 +15986,19 @@ send@0.18.0, send@^0.18.0: range-parser "~1.2.1" statuses "2.0.1" +sentry-expo@~6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/sentry-expo/-/sentry-expo-6.1.1.tgz#e5cb74523ef09b7cdc185ba696333c69d87ed703" + integrity sha512-eNrWvvDY/Z6Yba+jjjYWX6s5Qk3jzCaSAs8I6EkXUFiXqEi3ONJ+LKanf9Wuy0pjjtWtxHyPHTrb+93Kn0cVmg== + dependencies: + "@expo/spawn-async" "^1.7.0" + "@sentry/integrations" "7.29.0" + "@sentry/react" "7.29.0" + "@sentry/react-native" "4.13.0" + "@sentry/types" "7.29.0" + mkdirp "^1.0.4" + rimraf "^3.0.2" + serialize-error@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-6.0.0.tgz#ccfb887a1dd1c48d6d52d7863b92544331fd752b" @@ -15701,7 +16055,7 @@ serve-static@1.15.0, serve-static@^1.13.1: parseurl "~1.3.3" send "0.18.0" -set-blocking@^2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== @@ -16164,7 +16518,16 @@ string-similarity@^4.0.1: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16173,6 +16536,14 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" @@ -16237,7 +16608,21 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.2.0: +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== @@ -16668,7 +17053,7 @@ through2@^2.0.1: readable-stream "~2.3.6" xtend "~4.0.1" -through@2: +through@2, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -16857,7 +17242,7 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1: +tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -16964,7 +17349,7 @@ typed-emitter@^2.1.0: optionalDependencies: rxjs "^7.5.2" -typedarray-to-buffer@^3.1.5: +typedarray-to-buffer@^3.1.2, typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== @@ -16981,6 +17366,11 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.34.tgz#afb439e2e3e394bdc90080acb661a39c685b67d7" integrity sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ== +ua-parser-js@^0.7.33: + version "0.7.35" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" + integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -17649,13 +18039,20 @@ which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -17917,7 +18314,7 @@ ws@^8.11.0, ws@^8.12.1, ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== -xcode@^3.0.0, xcode@^3.0.1: +xcode@3.0.1, xcode@^3.0.0, xcode@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c" integrity sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA== From 83959c595d52ceb7aa4e3f68441c5ac41c389ebc Mon Sep 17 00:00:00 2001 From: Ollie H Date: Mon, 1 May 2023 18:38:47 -0700 Subject: [PATCH 058/374] React Native accessibility (#539) * React Native accessibility * First round of changes * Latest update * Checkpoint * Wrap up * Lint * Remove unhelpful image hints * Fix navigation * Fix rebase and lint * Mitigate an known issue with the password entry in login * Fix composer dismiss * Remove focus on input elements for web * Remove i and npm * pls work * Remove stray declaration * Regenerate yarn.lock --------- Co-authored-by: Paul Frazee --- .eslintrc.js | 2 +- bskyweb/templates/base.html | 3 +- package.json | 1 + src/lib/strings/display-names.ts | 2 +- src/lib/styles.ts | 2 + src/view/com/auth/SplashScreen.tsx | 10 +- src/view/com/auth/SplashScreen.web.tsx | 13 +- src/view/com/auth/create/CreateAccount.tsx | 20 +- src/view/com/auth/create/Step1.tsx | 14 +- src/view/com/auth/create/Step2.tsx | 33 +- src/view/com/auth/create/Step3.tsx | 3 + src/view/com/auth/login/Login.tsx | 100 +- src/view/com/auth/util/TextInput.tsx | 25 +- src/view/com/composer/Composer.tsx | 286 +- src/view/com/composer/ExternalEmbed.tsx | 8 +- src/view/com/composer/Prompt.tsx | 5 +- src/view/com/composer/photos/Gallery.tsx | 11 + .../com/composer/photos/OpenCameraBtn.tsx | 16 +- .../com/composer/photos/SelectPhotoBtn.tsx | 16 +- .../com/composer/text-input/TextInput.tsx | 41 +- .../com/composer/text-input/TextInput.web.tsx | 4 +- .../text-input/mobile/Autocomplete.tsx | 4 +- .../components/ImageDefaultHeader.tsx | 6 +- .../components/ImageItem/ImageItem.ios.tsx | 3 +- src/view/com/lightbox/ImageViewing/index.tsx | 7 +- src/view/com/lightbox/Lightbox.web.tsx | 23 +- src/view/com/modals/AddAppPasswords.tsx | 8 +- src/view/com/modals/AltImage.tsx | 19 +- src/view/com/modals/AltImageRead.tsx | 7 +- src/view/com/modals/ChangeHandle.tsx | 37 +- src/view/com/modals/Confirm.tsx | 7 +- .../com/modals/ContentFilteringSettings.tsx | 47 +- src/view/com/modals/DeleteAccount.tsx | 36 +- src/view/com/modals/EditProfile.tsx | 17 +- src/view/com/modals/InviteCodes.tsx | 10 +- src/view/com/modals/Modal.web.tsx | 3 + src/view/com/modals/ReportAccount.tsx | 5 +- src/view/com/modals/ReportPost.tsx | 5 +- src/view/com/modals/Repost.tsx | 19 +- src/view/com/modals/ServerInput.tsx | 25 +- src/view/com/modals/Waitlist.tsx | 16 +- .../com/modals/crop-image/CropImage.web.tsx | 35 +- src/view/com/notifications/FeedItem.tsx | 83 +- src/view/com/pager/FeedsTabBarMobile.tsx | 5 +- src/view/com/post-thread/PostThread.tsx | 12 +- src/view/com/post-thread/PostThreadItem.tsx | 13 +- src/view/com/profile/ProfileHeader.tsx | 48 +- src/view/com/search/HeaderWithInput.tsx | 21 +- .../com/util/BottomSheetCustomBackdrop.tsx | 14 +- src/view/com/util/Link.tsx | 34 +- src/view/com/util/Picker.tsx | 157 -- src/view/com/util/PostCtrls.tsx | 159 +- src/view/com/util/Selector.tsx | 6 +- src/view/com/util/UserAvatar.tsx | 7 +- src/view/com/util/UserBanner.tsx | 8 +- src/view/com/util/ViewHeader.tsx | 13 +- src/view/com/util/ViewSelector.tsx | 7 +- src/view/com/util/error/ErrorMessage.tsx | 5 +- src/view/com/util/error/ErrorScreen.tsx | 4 +- src/view/com/util/fab/FABInner.tsx | 18 +- src/view/com/util/forms/Button.tsx | 4 +- src/view/com/util/forms/DropdownButton.tsx | 62 +- src/view/com/util/images/AutoSizedImage.tsx | 9 +- src/view/com/util/images/Gallery.tsx | 13 +- src/view/com/util/images/Image.tsx | 4 +- src/view/com/util/images/ImageHorzList.tsx | 22 +- .../util/load-latest/LoadLatestBtn.web.tsx | 5 +- .../util/load-latest/LoadLatestBtnMobile.tsx | 5 +- src/view/com/util/moderation/ContentHider.tsx | 9 +- src/view/com/util/moderation/PostHider.tsx | 3 +- src/view/com/util/post-embeds/index.tsx | 5 +- src/view/screens/AppPasswords.tsx | 6 +- src/view/screens/Home.tsx | 3 + src/view/screens/Log.tsx | 4 +- src/view/screens/SearchMobile.tsx | 4 +- src/view/screens/Settings.tsx | 39 +- src/view/shell/Composer.tsx | 5 +- src/view/shell/Composer.web.tsx | 2 +- src/view/shell/Drawer.tsx | 71 +- src/view/shell/bottom-bar/BottomBar.tsx | 35 +- src/view/shell/desktop/LeftNav.tsx | 62 +- src/view/shell/desktop/RightNav.tsx | 20 +- src/view/shell/desktop/Search.tsx | 8 +- src/view/shell/index.web.tsx | 4 +- web/index.html | 12 +- yarn.lock | 2322 +++++++++-------- 86 files changed, 2479 insertions(+), 1827 deletions(-) delete mode 100644 src/view/com/util/Picker.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 93348b0d03..2d59d36dd5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { root: true, - extends: '@react-native-community', + extends: ['@react-native-community', 'plugin:react-native-a11y/ios'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'detox'], ignorePatterns: [ diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 28b92958ef..d3d76ad0a9 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -57,8 +57,9 @@ } }*/ + /* OLLIE: TODO -- this is not accessible */ /* Remove focus state on inputs */ - *:focus { + input:focus { outline: 0; } /* Remove default link styling */ diff --git a/package.json b/package.json index d35bd0efdf..92077f4a1b 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "await-lock": "^2.2.2", "base64-js": "^1.5.1", "email-validator": "^2.0.4", + "eslint-plugin-react-native-a11y": "^3.3.0", "expo": "~48.0.15", "expo-application": "~5.1.1", "expo-build-properties": "~0.5.1", diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts index 5b58dec3d0..555151b553 100644 --- a/src/lib/strings/display-names.ts +++ b/src/lib/strings/display-names.ts @@ -6,7 +6,7 @@ const CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu export function sanitizeDisplayName(str: string): string { if (typeof str === 'string') { - return str.replace(CHECK_MARKS_RE, '') + return str.replace(CHECK_MARKS_RE, '').trim() } return '' } diff --git a/src/lib/styles.ts b/src/lib/styles.ts index 37d1696794..1ff2d520d2 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -118,6 +118,7 @@ export const s = StyleSheet.create({ mr2: {marginRight: 2}, mr5: {marginRight: 5}, mr10: {marginRight: 10}, + mr20: {marginRight: 20}, ml2: {marginLeft: 2}, ml5: {marginLeft: 5}, ml10: {marginLeft: 10}, @@ -149,6 +150,7 @@ export const s = StyleSheet.create({ pb5: {paddingBottom: 5}, pb10: {paddingBottom: 10}, pb20: {paddingBottom: 20}, + px5: {paddingHorizontal: 5}, // flex flexRow: {flexDirection: 'row'}, diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx index f98bed1203..41787bb5fc 100644 --- a/src/view/com/auth/SplashScreen.tsx +++ b/src/view/com/auth/SplashScreen.tsx @@ -28,7 +28,10 @@ export const SplashScreen = ({ + onPress={onPressCreateAccount} + accessibilityRole="button" + accessibilityLabel="Create new account" + accessibilityHint="Opens flow to create a new Bluesky account"> Create a new account @@ -36,7 +39,10 @@ export const SplashScreen = ({ + onPress={onPressSignin} + accessibilityRole="button" + accessibilityLabel="Sign in" + accessibilityHint="Opens flow to sign into your existing Bluesky account"> Sign in diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx index 7fac5a8c0d..9236968c43 100644 --- a/src/view/com/auth/SplashScreen.web.tsx +++ b/src/view/com/auth/SplashScreen.web.tsx @@ -43,7 +43,9 @@ export const SplashScreen = ({ + onPress={onPressCreateAccount} + // TODO: web accessibility + accessibilityRole="button"> Create a new account @@ -51,7 +53,9 @@ export const SplashScreen = ({ + onPress={onPressSignin} + // TODO: web accessibility + accessibilityRole="button"> Sign in @@ -60,7 +64,10 @@ export const SplashScreen = ({ style={[styles.notice, pal.textLight]} lineHeight={1.3}> Bluesky will launch soon.{' '} - + Join the waitlist diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx index 467b879487..ac03081dff 100644 --- a/src/view/com/auth/create/CreateAccount.tsx +++ b/src/view/com/auth/create/CreateAccount.tsx @@ -72,14 +72,24 @@ export const CreateAccount = observer( {model.step === 3 && } - + Back {model.canNext ? ( - + {model.isProcessing ? ( ) : ( @@ -91,7 +101,11 @@ export const CreateAccount = observer( ) : model.didServiceDescriptionFetchFail ? ( + onPress={onPressRetryConnect} + accessibilityRole="button" + accessibilityLabel="Retry" + accessibilityHint="Retries account creation" + accessibilityLiveRegion="polite"> Retry diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index ca964ede2b..ac0d706d74 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -57,7 +57,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => { - This is the company that keeps you online. + This is the service that keeps you online. )} - + diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index 301b900933..98a10b0f5d 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -13,7 +13,10 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) { onPressCompose()}> + onPress={() => onPressCompose()} + accessibilityRole="button" + accessibilityLabel="Compose reply" + accessibilityHint="Opens composer"> { handleAddImageAltText(image) }} @@ -116,6 +119,9 @@ export const Gallery = observer(function ({gallery}: Props) { { handleEditPhoto(image) }} @@ -128,6 +134,9 @@ export const Gallery = observer(function ({gallery}: Props) { handleRemovePhoto(image)} style={styles.imageControl}> ) : null, diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 809c41783b..bfcfa6b78d 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -1,5 +1,5 @@ import React, {useCallback} from 'react' -import {TouchableOpacity} from 'react-native' +import {TouchableOpacity, StyleSheet} from 'react-native' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -7,7 +7,6 @@ import { import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' import {useStores} from 'state/index' -import {s} from 'lib/styles' import {isDesktopWeb} from 'platform/detection' import {openCamera} from 'lib/media/picker' import {useCameraPermission} from 'lib/hooks/usePermissions' @@ -54,8 +53,11 @@ export function OpenCameraBtn({gallery}: Props) { + style={styles.button} + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel="Camera" + accessibilityHint="Opens camera on device"> ) } + +const styles = StyleSheet.create({ + button: { + paddingHorizontal: 15, + }, +}) diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index 9569e08ad0..0b8046a4b1 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -1,12 +1,11 @@ import React, {useCallback} from 'react' -import {TouchableOpacity} from 'react-native' +import {TouchableOpacity, StyleSheet} from 'react-native' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' -import {s} from 'lib/styles' import {isDesktopWeb} from 'platform/detection' import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' import {GalleryModel} from 'state/models/media/gallery' @@ -36,8 +35,11 @@ export function SelectPhotoBtn({gallery}: Props) { + style={styles.button} + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel="Gallery" + accessibilityHint="Opens device photo gallery"> ) } + +const styles = StyleSheet.create({ + button: { + paddingHorizontal: 15, + }, +}) diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 10ac52b5d8..7b09da93d1 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -1,7 +1,14 @@ -import React, {forwardRef, useCallback, useEffect, useRef, useMemo} from 'react' +import React, { + forwardRef, + useCallback, + useRef, + useMemo, + ComponentProps, +} from 'react' import { NativeSyntheticEvent, StyleSheet, + TextInput as RNTextInput, TextInputSelectionChangeEventData, View, } from 'react-native' @@ -27,14 +34,14 @@ export interface TextInputRef { blur: () => void } -interface TextInputProps { +interface TextInputProps extends ComponentProps { richtext: RichText placeholder: string suggestedLinks: Set autocompleteView: UserAutocompleteModel - setRichText: (v: RichText) => void + setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void - onPressPublish: (richtext: RichText) => Promise + onPressPublish: (richtext: RichText) => Promise onSuggestedLinksChanged: (uris: Set) => void onError: (err: string) => void } @@ -55,6 +62,7 @@ export const TextInput = forwardRef( onPhotoPasted, onSuggestedLinksChanged, onError, + ...props }: TextInputProps, ref, ) => { @@ -65,26 +73,11 @@ export const TextInput = forwardRef( React.useImperativeHandle(ref, () => ({ focus: () => textInput.current?.focus(), - blur: () => textInput.current?.blur(), + blur: () => { + textInput.current?.blur() + }, })) - useEffect(() => { - // HACK - // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view - // -prf - let to: NodeJS.Timeout | undefined - if (textInput.current) { - to = setTimeout(() => { - textInput.current?.focus() - }, 250) - } - return () => { - if (to) { - clearTimeout(to) - } - } - }, []) - const onChangeText = useCallback( async (newText: string) => { const newRt = new RichText({text: newText}) @@ -206,8 +199,10 @@ export const TextInput = forwardRef( placeholder={placeholder} placeholderTextColor={pal.colors.textLight} keyboardAppearance={theme.colorScheme} + autoFocus={true} multiline - style={[pal.text, styles.textInput, styles.textInputFormatting]}> + style={[pal.text, styles.textInput, styles.textInputFormatting]} + {...props}> {textDecorated} autocompleteView: UserAutocompleteModel - setRichText: (v: RichText) => void + setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void - onPressPublish: (richtext: RichText) => Promise + onPressPublish: (richtext: RichText) => Promise onSuggestedLinksChanged: (uris: Set) => void onError: (err: string) => void } diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index 879bac0711..7806241f13 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -50,7 +50,9 @@ export const Autocomplete = observer( testID="autocompleteButton" key={item.handle} style={[pal.border, styles.item]} - onPress={() => onSelect(item.handle)}> + onPress={() => onSelect(item.handle)} + accessibilityLabel={`Select ${item.handle}`} + accessibilityHint={`Autocompletes to ${item.handle}`}> {item.displayName || item.handle} diff --git a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx index 6880008e4a..84e5f90fb4 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx @@ -20,7 +20,11 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => ( + hitSlop={HIT_SLOP} + accessibilityRole="button" + accessibilityLabel="Close image" + accessibilityHint="Closes viewer for header image" + onAccessibilityEscape={onRequestClose}> diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index 12d37e283a..658735724b 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -127,7 +127,8 @@ const ImageItem = ({ + delayLongPress={delayLongPress} + accessibilityRole="image"> + diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index c17356d943..1d4a9c2153 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -89,13 +89,25 @@ function LightboxInner({ return ( - + - + {canGoLeft && ( + style={[styles.btn, styles.leftBtn]} + accessibilityRole="button" + accessibilityLabel="Go back" + accessibilityHint="Navigates to previous image in viewer"> + style={[styles.btn, styles.rightBtn]} + accessibilityRole="button" + accessibilityLabel="Go to next" + accessibilityHint="Navigates to next image in viewer"> ) : ( + onPress={onCopy} + accessibilityRole="button" + accessibilityLabel="Copy" + accessibilityHint="Copies app password"> {appPassword} diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index 639303c980..ba05a7d624 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -37,7 +37,8 @@ export function Component({prevAltText, onAltTextSet}: Props) { return ( + style={[pal.view, styles.container, s.flex1]} + nativeID="imageAltText"> Add alt text setAltText(enforceLen(text, MAX_ALT_TEXT))} + accessibilityLabel="Image alt text" + accessibilityHint="Sets image alt text for screenreaders" + accessibilityLabelledBy="imageAltText" /> - + + onPress={onPressCancel} + accessibilityRole="button" + accessibilityLabel="Cancel add image alt text" + accessibilityHint="Exits adding alt text to image" + onAccessibilityEscape={onPressCancel}> Cancel diff --git a/src/view/com/modals/AltImageRead.tsx b/src/view/com/modals/AltImageRead.tsx index e7b4797eee..4dde8f58b4 100644 --- a/src/view/com/modals/AltImageRead.tsx +++ b/src/view/com/modals/AltImageRead.tsx @@ -30,7 +30,12 @@ export function Component({altText}: Props) { {altText} - + void}) { - + Cancel @@ -148,13 +153,20 @@ export function Component({onChanged}: {onChanged: () => void}) { ) : error && !serviceDescription ? ( + onPress={onPressRetryConnect} + accessibilityRole="button" + accessibilityLabel="Retry change handle" + accessibilityHint={`Retries handle change to ${handle}`}> Retry ) : canSave ? ( - + Save @@ -245,6 +257,9 @@ function ProvidedHandleForm({ value={handle} onChangeText={onChangeHandle} editable={!isProcessing} + accessible={true} + accessibilityLabel="Handle" + accessibilityHint="Sets Bluesky username" /> @@ -253,7 +268,11 @@ function ProvidedHandleForm({ @{createFullHandle(handle, userDomain)} - + I have my own domain @@ -338,7 +357,7 @@ function CustomHandleForm({ // = return ( <> - + Enter the domain you want to use @@ -356,6 +375,9 @@ function CustomHandleForm({ value={handle} onChangeText={onChangeHandle} editable={!isProcessing} + accessibilityLabelledBy="customDomain" + accessibilityLabel="Custom domain" + accessibilityHint="Input your preferred hosting provider" /> @@ -421,7 +443,10 @@ function CustomHandleForm({ )} - + Nevermind, create a handle for me diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index 6f7b062cfd..f0c905d044 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -66,7 +66,12 @@ export function Component({ + style={[styles.btn]} + accessibilityRole="button" + accessibilityLabel="Confirm" + // TODO: This needs to be updated so that modal roles are clear; + // Currently there is only one usage for the confirm modal: post deletion + accessibilityHint="Confirms a potentially destructive action"> Confirm )} diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index 735de85a7b..c683e43f8f 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -34,7 +34,12 @@ export function Component({}: {}) { - + { const store = useStores() @@ -67,19 +73,20 @@ const ContentLabelPref = observer( store.preferences.setContentLabelPref(group, v)} + group={group} /> ) }, ) -function SelectGroup({ - current, - onChange, -}: { +interface SelectGroupProps { current: LabelPreference onChange: (v: LabelPreference) => void -}) { + group: keyof typeof CONFIGURABLE_LABEL_GROUPS +} + +function SelectGroup({current, onChange, group}: SelectGroupProps) { return ( ) } +interface SelectableBtnProps { + current: string + value: LabelPreference + label: string + left?: boolean + right?: boolean + onChange: (v: LabelPreference) => void + group: keyof typeof CONFIGURABLE_LABEL_GROUPS +} + function SelectableBtn({ current, value, @@ -113,14 +133,8 @@ function SelectableBtn({ left, right, onChange, -}: { - current: string - value: LabelPreference - label: string - left?: boolean - right?: boolean - onChange: (v: LabelPreference) => void -}) { + group, +}: SelectableBtnProps) { const pal = usePalette('default') const palPrimary = usePalette('inverted') return ( @@ -132,7 +146,10 @@ function SelectableBtn({ pal.border, current === value ? palPrimary.view : pal.view, ]} - onPress={() => onChange(value)}> + onPress={() => onChange(value)} + accessibilityRole="button" + accessibilityLabel={value} + accessibilityHint={`Set ${value} for ${group} content moderation policy`}> {label} diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index 353122163a..f1febc2eab 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -86,7 +86,10 @@ export function Component({}: {}) { <> + onPress={onPressSendEmail} + accessibilityRole="button" + accessibilityLabel="Send email" + accessibilityHint="Sends email with confirmation code for account deletion"> + onPress={onCancel} + accessibilityRole="button" + accessibilityLabel="Cancel account deletion" + accessibilityHint="" + onAccessibilityEscape={onCancel}> Cancel @@ -112,7 +119,11 @@ export function Component({}: {}) { ) : ( <> - + {/* TODO: Update this label to be more concise */} + Check your inbox for an email with the confirmation code to enter below: @@ -123,8 +134,11 @@ export function Component({}: {}) { keyboardAppearance={theme.colorScheme} value={confirmCode} onChangeText={setConfirmCode} + accessibilityLabelledBy="confirmationCode" + accessibilityLabel="Confirmation code" + accessibilityHint="Input confirmation code for account deletion" /> - + Please enter your password as well: {error ? ( @@ -149,14 +166,21 @@ export function Component({}: {}) { <> + onPress={onPressConfirmDelete} + accessibilityRole="button" + accessibilityLabel="Confirm delete account" + accessibilityHint=""> Delete my account + onPress={onCancel} + accessibilityRole="button" + accessibilityLabel="Cancel account deletion" + accessibilityHint="Exits account deletion process" + onAccessibilityEscape={onCancel}> Cancel diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 9bd572cc02..c26592fa98 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -175,6 +175,9 @@ export function Component({ onChangeText={v => setDisplayName(enforceLen(v, MAX_DISPLAY_NAME)) } + accessible={true} + accessibilityLabel="Display name" + accessibilityHint="Edit your display name" /> @@ -188,6 +191,9 @@ export function Component({ multiline value={description} onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} + accessible={true} + accessibilityLabel="Description" + accessibilityHint="Edit your profile description" /> {isProcessing ? ( @@ -198,7 +204,10 @@ export function Component({ + onPress={onPressSave} + accessibilityRole="button" + accessibilityLabel="Save" + accessibilityHint="Saves any changes to your profile"> + onPress={onPressCancel} + accessibilityRole="button" + accessibilityLabel="Cancel profile editing" + accessibilityHint="" + onAccessibilityEscape={onPressCancel}> Cancel diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index 992439ebcf..52d6fa46a8 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -87,6 +87,7 @@ const InviteCode = observer( ({testID, code, used}: {testID: string; code: string; used?: boolean}) => { const pal = usePalette('default') const store = useStores() + const {invitesAvailable} = store.me const onPress = React.useCallback(() => { Clipboard.setString(code) @@ -98,7 +99,14 @@ const InviteCode = observer( + onPress={onPress} + accessibilityRole="button" + accessibilityLabel={ + invitesAvailable === 1 + ? 'Invite codes: 1 available' + : `Invite codes: ${invitesAvailable} available` + } + accessibilityHint="Opens list of invite codes"> { + // TODO: can we use prevent default? // do nothing, we just want to stop it from bubbling } @@ -92,8 +93,10 @@ function Modal({modal}: {modal: ModalIface}) { } return ( + // eslint-disable-next-line + {/* eslint-disable-next-line */} + onPress={onPress} + accessibilityRole="button" + accessibilityLabel="Report account" + accessibilityHint={`Reports account with reason ${issue}`}> + onPress={onPress} + accessibilityRole="button" + accessibilityLabel="Report post" + accessibilityHint={`Reports post with reason ${issue}`}> void onQuote: () => void isReposted: boolean + // TODO: Add author into component }) { const store = useStores() const pal = usePalette('default') @@ -31,7 +32,10 @@ export function Component({ + onPress={onRepost} + accessibilityRole="button" + accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'} + accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}> {!isReposted ? 'Repost' : 'Undo repost'} @@ -40,14 +44,23 @@ export function Component({ + onPress={onQuote} + accessibilityRole="button" + accessibilityLabel="Quote post" + accessibilityHint=""> Quote Post - + void}) { doSelect(LOCAL_DEV_SERVICE)}> + onPress={() => doSelect(LOCAL_DEV_SERVICE)} + accessibilityRole="button"> Local dev server void}) { doSelect(STAGING_SERVICE)}> + onPress={() => doSelect(STAGING_SERVICE)} + accessibilityRole="button"> Staging void}) { ) : undefined} doSelect(PROD_SERVICE)}> + onPress={() => doSelect(PROD_SERVICE)} + accessibilityRole="button" + accessibilityLabel="Select Bluesky Social" + accessibilityHint="Sets Bluesky Social as your service provider"> Bluesky.Social void}) { keyboardAppearance={theme.colorScheme} value={customUrl} onChangeText={setCustomUrl} + accessibilityLabel="Custom domain" + // TODO: Simplify this wording further to be understandable by everyone + accessibilityHint="Use your domain as your Bluesky client service provider" /> doSelect(customUrl)}> + onPress={() => doSelect(customUrl)} + accessibilityRole="button" + accessibilityLabel={`Confirm service. ${ + customUrl === '' + ? 'Button disabled. Input custom domain to proceed.' + : '' + }`} + accessibilityHint="" + // TODO - accessibility: Need to inform state change on failure + disabled={customUrl === ''}> {error ? ( @@ -99,7 +102,10 @@ export function Component({}: {}) { ) : ( <> - + - + Cancel diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx index 8a9b4bf623..c5959cf4c1 100644 --- a/src/view/com/modals/crop-image/CropImage.web.tsx +++ b/src/view/com/modals/crop-image/CropImage.web.tsx @@ -4,12 +4,13 @@ import ImageEditor from 'react-avatar-editor' import {Slider} from '@miblanchard/react-native-slider' import LinearGradient from 'react-native-linear-gradient' import {Text} from 'view/com/util/text/Text' -import {Dimensions, Image} from 'lib/media/types' +import {Dimensions} from 'lib/media/types' import {getDataUriSize} from 'lib/media/util' import {s, gradients} from 'lib/styles' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons' +import {Image as RNImage} from 'react-native-image-crop-picker' enum AspectRatio { Square = 'square', @@ -30,7 +31,7 @@ export function Component({ onSelect, }: { uri: string - onSelect: (img?: Image) => void + onSelect: (img?: RNImage) => void }) { const store = useStores() const pal = usePalette('default') @@ -92,19 +93,31 @@ export function Component({ maximumValue={3} containerStyle={styles.slider} /> - + - + - + - + Cancel - + + noFeedback + accessible={false}> + noFeedback + accessible={(item.isLike && authors.length === 1) || item.isRepost}> + {/* TODO: Prevent conditional rendering and move toward composable + notifications for clearer accessibility labeling */} {icon === 'HeartIconSolid' ? ( ) : ( @@ -192,17 +197,18 @@ export const FeedItem = observer(function ({ 1 ? onToggleAuthorsExpanded : () => {}}> + onPress={authors.length > 1 ? onToggleAuthorsExpanded : undefined} + accessible={false}> - + {authors.length > 1 ? ( <> - and - + and + {authors.length - 1} {pluralize(authors.length - 1, 'other')} ) : undefined} - {action} - - {ago(item.indexedAt)} - - + {action} + {ago(item.indexedAt)} + {item.isLike || item.isRepost || item.isQuote ? ( @@ -245,7 +249,10 @@ function CondensedAuthorsList({ + onPress={onToggleAuthorsExpanded} + accessibilityRole="button" + accessibilityLabel="Hide user list" + accessibilityHint="Collapses list of users for a given notification"> - {authors.slice(0, MAX_AUTHORS).map(author => ( - - - - ))} - {authors.length > MAX_AUTHORS ? ( - - +{authors.length - MAX_AUTHORS} - - ) : undefined} - - + + + {authors.slice(0, MAX_AUTHORS).map(author => ( + + + + ))} + {authors.length > MAX_AUTHORS ? ( + + +{authors.length - MAX_AUTHORS} + + ) : undefined} + + + ) } @@ -426,9 +438,6 @@ const styles = StyleSheet.create({ paddingTop: 6, paddingBottom: 2, }, - metaItem: { - paddingRight: 3, - }, postText: { paddingBottom: 5, color: colors.black, diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index e7d2ec1042..725c44603e 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -37,7 +37,10 @@ export const FeedsTabBar = observer( + onPress={onPressAvi} + accessibilityRole="button" + accessibilityLabel="Open navigation" + accessibilityHint="Access profile and other navigation links"> The post may have been deleted. - + You have blocked the author or you have been blocked by the author. - + - + @@ -435,10 +440,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, layoutAvi: { - width: 70, paddingLeft: 10, paddingTop: 10, paddingBottom: 10, + marginRight: 10, }, layoutContent: { flex: 1, diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 4accd7abac..d8c4b9d8fc 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -282,7 +282,10 @@ const ProfileHeaderLoaded = observer( + style={[styles.btn, styles.mainBtn, pal.btn]} + accessibilityRole="button" + accessibilityLabel="Edit profile" + accessibilityHint="Opens editor for profile display name, avatar, background image, and description"> Edit Profile @@ -291,7 +294,10 @@ const ProfileHeaderLoaded = observer( + style={[styles.btn, styles.mainBtn, pal.btn]} + accessibilityRole="button" + accessibilityLabel="Unblock" + accessibilityHint=""> Unblock @@ -303,7 +309,10 @@ const ProfileHeaderLoaded = observer( + style={[styles.btn, styles.mainBtn, pal.btn]} + accessibilityRole="button" + accessibilityLabel={`Unfollow ${view.handle}`} + accessibilityHint={`Hides direct posts from ${view.handle} in your feed`}> + style={[styles.btn, styles.primaryBtn]} + accessibilityRole="button" + accessibilityLabel={`Follow ${view.handle}`} + accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}> + onPress={onPressFollowers} + accessibilityRole="button" + accessibilityLabel={`Show ${view.handle}'s followers`} + accessibilityHint={`Shows folks following ${view.handle}`}> {formatCount(view.followersCount)} @@ -374,7 +389,10 @@ const ProfileHeaderLoaded = observer( + onPress={onPressFollows} + accessibilityRole="button" + accessibilityLabel={`Show ${view.handle}'s follows`} + accessibilityHint={`Shows folks followed by ${view.handle}`}> {formatCount(view.followsCount)} @@ -382,14 +400,12 @@ const ProfileHeaderLoaded = observer( following - - - {view.postsCount} - + + {view.postsCount}{' '} {pluralize(view.postsCount, 'post')} - + {view.descriptionRichText ? ( + hitSlop={BACK_HITSLOP} + accessibilityRole="button" + accessibilityLabel="Go back" + accessibilityHint="Navigates to the previous screen"> @@ -450,7 +469,10 @@ const ProfileHeaderLoaded = observer( )} + onPress={onPressAvi} + accessibilityRole="image" + accessibilityLabel={`View ${view.handle}'s avatar`} + accessibilityHint={`Opens ${view.handle}'s avatar in an image viewer`}> + style={styles.headerMenuBtn} + accessibilityLabel="Go back" + accessibilityHint="Navigates to the previous screen"> setIsInputFocused(false)} onChangeText={onChangeQuery} onSubmitEditing={onSubmitQuery} + autoFocus={true} + accessibilityRole="search" /> {query ? ( - + {query || isInputFocused ? ( - + Cancel @@ -110,9 +120,10 @@ const styles = StyleSheet.create({ paddingVertical: 4, }, headerMenuBtn: { - width: 40, + width: 30, height: 30, - marginLeft: 6, + borderRadius: 30, + marginHorizontal: 6, }, headerSearchContainer: { flex: 1, diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx index e175b33a53..91379f1c98 100644 --- a/src/view/com/util/BottomSheetCustomBackdrop.tsx +++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react' -import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native' +import {TouchableWithoutFeedback} from 'react-native' import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' import Animated, { Extrapolate, @@ -8,7 +8,7 @@ import Animated, { } from 'react-native-reanimated' export function createCustomBackdrop( - onClose?: ((event: GestureResponderEvent) => void) | undefined, + onClose?: (() => void) | undefined, ): React.FC { const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { // animated variables @@ -27,7 +27,15 @@ export function createCustomBackdrop( ) return ( - + { + if (onClose !== undefined) { + onClose() + } + }}> ) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 5110acf486..503e22084d 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {ComponentProps} from 'react' import {observer} from 'mobx-react-lite' import { Linking, @@ -29,6 +29,16 @@ type Event = | React.MouseEvent | GestureResponderEvent +interface Props extends ComponentProps { + testID?: string + style?: StyleProp + href?: string + title?: string + children?: React.ReactNode + noFeedback?: boolean + asAnchor?: boolean +} + export const Link = observer(function Link({ testID, style, @@ -37,15 +47,9 @@ export const Link = observer(function Link({ children, noFeedback, asAnchor, -}: { - testID?: string - style?: StyleProp - href?: string - title?: string - children?: React.ReactNode - noFeedback?: boolean - asAnchor?: boolean -}) { + accessible, + ...props +}: Props) { const store = useStores() const navigation = useNavigation() @@ -64,7 +68,10 @@ export const Link = observer(function Link({ testID={testID} onPress={onPress} // @ts-ignore web only -prf - href={asAnchor ? sanitizeUrl(href) : undefined}> + href={asAnchor ? sanitizeUrl(href) : undefined} + accessible={accessible} + accessibilityRole="link" + {...props}> {children ? children : {title || 'link'}} @@ -76,8 +83,11 @@ export const Link = observer(function Link({ testID={testID} style={style} onPress={onPress} + accessible={accessible} + accessibilityRole="link" // @ts-ignore web only -prf - href={asAnchor ? sanitizeUrl(href) : undefined}> + href={asAnchor ? sanitizeUrl(href) : undefined} + {...props}> {children ? children : {title || 'link'}} ) diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx deleted file mode 100644 index 9007cb1f05..0000000000 --- a/src/view/com/util/Picker.tsx +++ /dev/null @@ -1,157 +0,0 @@ -// TODO: replaceme with something in the design system - -import React, {useRef} from 'react' -import { - StyleProp, - StyleSheet, - TextStyle, - TouchableOpacity, - TouchableWithoutFeedback, - View, - ViewStyle, -} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import RootSiblings from 'react-native-root-siblings' -import {Text} from './text/Text' -import {colors} from 'lib/styles' - -interface PickerItem { - value: string - label: string -} - -interface PickerOpts { - style?: StyleProp - labelStyle?: StyleProp - iconStyle?: FontAwesomeIconStyle - items: PickerItem[] - value: string - onChange: (value: string) => void - enabled?: boolean -} - -const MENU_WIDTH = 200 - -export function Picker({ - style, - labelStyle, - iconStyle, - items, - value, - onChange, - enabled, -}: PickerOpts) { - const ref = useRef(null) - const valueLabel = items.find(item => item.value === value)?.label || value - const onPress = () => { - if (!enabled) { - return - } - ref.current?.measure( - ( - _x: number, - _y: number, - width: number, - height: number, - pageX: number, - pageY: number, - ) => { - createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange) - }, - ) - } - return ( - - - - {valueLabel} - - - - - ) -} - -function createDropdownMenu( - x: number, - y: number, - width: number, - items: PickerItem[], - onChange: (value: string) => void, -): RootSiblings { - const onPressItem = (index: number) => { - sibling.destroy() - onChange(items[index].value) - } - const onOuterPress = () => sibling.destroy() - const sibling = new RootSiblings( - ( - <> - - - - - {items.map((item, index) => ( - onPressItem(index)}> - {item.label} - - ))} - - - ), - ) - return sibling -} - -const styles = StyleSheet.create({ - outer: { - flexDirection: 'row', - alignItems: 'center', - }, - label: { - marginRight: 5, - }, - icon: {}, - bg: { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - backgroundColor: '#000', - opacity: 0.1, - }, - menu: { - position: 'absolute', - backgroundColor: '#fff', - borderRadius: 14, - opacity: 1, - paddingVertical: 6, - }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 6, - paddingLeft: 15, - paddingRight: 30, - }, - menuItemBorder: { - borderTopWidth: 1, - borderTopColor: colors.gray2, - marginTop: 4, - paddingTop: 12, - }, - menuItemIcon: { - marginLeft: 6, - marginRight: 8, - }, - menuItemLabel: { - fontSize: 15, - }, -}) diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index 07a67fd8a6..725f3bbbe3 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -170,83 +170,94 @@ export function PostCtrls(opts: PostCtrlsOpts) { return ( - - - - {typeof opts.replyCount !== 'undefined' ? ( - - {opts.replyCount} - - ) : undefined} - - - - - + + {typeof opts.replyCount !== 'undefined' ? ( + + {opts.replyCount} + + ) : undefined} + + + ) + : defaultCtrlColor + } + strokeWidth={2.4} + size={opts.big ? 24 : 20} + /> + {typeof opts.repostCount !== 'undefined' ? ( + ) - : defaultCtrlColor - } - strokeWidth={2.4} - size={opts.big ? 24 : 20} + ? [s.bold, s.green3, s.f15, s.ml5] + : [defaultCtrlColor, s.f15, s.ml5] + }> + {opts.repostCount} + + ) : undefined} + + + {opts.isLiked ? ( + } + size={opts.big ? 22 : 16} /> - {typeof opts.repostCount !== 'undefined' ? ( - - {opts.repostCount} - - ) : undefined} - - - - - {opts.isLiked ? ( - } - size={opts.big ? 22 : 16} - /> - ) : ( - - )} - {typeof opts.likeCount !== 'undefined' ? ( - - {opts.likeCount} - - ) : undefined} - - + ) : ( + + )} + {typeof opts.likeCount !== 'undefined' ? ( + + {opts.likeCount} + + ) : undefined} + {opts.big ? undefined : ( onPressItem(i)}> + onPress={() => onPressItem(i)} + accessibilityLabel={`Select ${item}`} + accessibilityHint={`Select option ${i} of ${numItems}`}> ) : ( @@ -167,7 +168,11 @@ export function UserAvatar({ void + onSelectNewBanner?: (img: RNImage | null) => void }) { const store = useStores() const pal = usePalette('default') @@ -94,6 +94,8 @@ export function UserBanner({ testID="userBannerImage" style={styles.bannerImage} source={{uri: banner}} + accessible={true} + accessibilityIgnoresInvertColors /> ) : ( ) : ( + style={canGoBack ? styles.backBtn : styles.backBtnWide} + accessibilityRole="button" + accessibilityLabel={canGoBack ? 'Go back' : 'Go to menu'} + accessibilityHint={ + canGoBack + ? 'Navigates to the previous screen' + : 'Navigates to the menu' + }> {canGoBack ? ( onPressItem(i)}> + onPress={() => onPressItem(i)} + accessibilityLabel={item} + accessibilityHint={`Selects ${item}`} + // TODO: Modify the component API such that lint fails + // at the invocation site as well + > + onPress={onPressTryAgain} + accessibilityRole="button" + accessibilityLabel="Retry" + accessibilityHint="Retries the last action, which errored out"> + onPress={onPressTryAgain} + accessibilityLabel="Retry" + accessibilityHint="Retries the last action, which errored out"> void) | undefined -export interface FABProps { +export interface FABProps + extends ComponentProps { testID?: string icon: JSX.Element - onPress: OnPress } -export const FABInner = observer(({testID, icon, onPress}: FABProps) => { +export const FABInner = observer(({testID, icon, ...props}: FABProps) => { const store = useStores() const interp = useAnimatedValue(0) React.useEffect(() => { @@ -34,7 +28,7 @@ export const FABInner = observer(({testID, icon, onPress}: FABProps) => { transform: [{translateY: Animated.multiply(interp, 60)}], } return ( - + + testID={testID} + accessibilityRole="button"> {label ? ( {label} diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index 725d45c1b7..04346d91f4 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react' +import React, {PropsWithChildren, useMemo, useRef} from 'react' import { Dimensions, StyleProp, @@ -39,6 +39,19 @@ type MaybeDropdownItem = DropdownItem | false | undefined export type DropdownButtonType = ButtonType | 'bare' +interface DropdownButtonProps { + testID?: string + type?: DropdownButtonType + style?: StyleProp + items: MaybeDropdownItem[] + label?: string + menuWidth?: number + children?: React.ReactNode + openToRight?: boolean + rightOffset?: number + bottomOffset?: number +} + export function DropdownButton({ testID, type = 'bare', @@ -50,18 +63,7 @@ export function DropdownButton({ openToRight = false, rightOffset = 0, bottomOffset = 0, -}: { - testID?: string - type?: DropdownButtonType - style?: StyleProp - items: MaybeDropdownItem[] - label?: string - menuWidth?: number - children?: React.ReactNode - openToRight?: boolean - rightOffset?: number - bottomOffset?: number -}) { +}: PropsWithChildren) { const ref1 = useRef(null) const ref2 = useRef(null) @@ -105,6 +107,18 @@ export function DropdownButton({ ) } + const numItems = useMemo( + () => + items.filter(item => { + if (item === undefined || item === false) { + return false + } + + return isBtn(item) + }).length, + [items], + ) + if (type === 'bare') { return ( + ref={ref1} + accessibilityRole="button" + accessibilityLabel={`Opens ${numItems} options`} + accessibilityHint={`Opens ${numItems} options`}> {children} ) @@ -283,9 +300,20 @@ const DropdownItems = ({ const separatorColor = theme.colorScheme === 'dark' ? pal.borderDark : pal.border + const numItems = items.filter(isBtn).length + return ( <> - + + // and onPressItem(index)}> + onPress={() => onPressItem(index)} + accessibilityLabel={item.label} + accessibilityHint={`Option ${index + 1} of ${numItems}`}> {item.icon && ( + style={[styles.container, style]} + accessible={true} + accessibilityLabel="Share image" + accessibilityHint="Opens ways of sharing image"> {children} @@ -80,7 +85,9 @@ export function AutoSizedImage({ style={[styles.image, {aspectRatio}]} source={{uri}} accessible={true} // Must set for `accessibilityLabel` to work + accessibilityIgnoresInvertColors accessibilityLabel={alt} + accessibilityHint="" /> {children} diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 78ced0668e..5b6c3384d0 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -41,16 +41,25 @@ export const GalleryItem: FC = ({ delayPressIn={DELAY_PRESS_IN} onPress={() => onPress?.(index)} onPressIn={() => onPressIn?.(index)} - onLongPress={() => onLongPress?.(index)}> + onLongPress={() => onLongPress?.(index)} + accessibilityRole="button" + accessibilityLabel="View image" + accessibilityHint=""> {image.alt === '' ? null : ( - + ALT )} diff --git a/src/view/com/util/images/Image.tsx b/src/view/com/util/images/Image.tsx index e3d0d7fcc2..e779fa3787 100644 --- a/src/view/com/util/images/Image.tsx +++ b/src/view/com/util/images/Image.tsx @@ -8,5 +8,7 @@ export function HighPriorityImage({source, ...props}: HighPriorityImageProps) { const updatedSource = { uri: typeof source === 'object' && source ? source.uri : '', } satisfies ImageSource - return + return ( + + ) } diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 5c232e0b46..88494bba39 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -16,15 +16,33 @@ interface Props { } export function ImageHorzList({images, onPress, style}: Props) { + const numImages = images.length return ( {images.map(({thumb, alt}, i) => ( - onPress?.(i)}> + onPress?.(i)} + accessible={true} + accessibilityLabel={`Open image ${i} of ${numImages}`} + accessibilityHint="Opens image in viewer" + accessibilityActions={[{name: 'press', label: 'Press'}]} + onAccessibilityAction={action => { + switch (action.nativeEvent.actionName) { + case 'press': + onPress?.(0) + break + default: + break + } + }}> ))} diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx index 1b6f18b622..839685029d 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx @@ -23,7 +23,10 @@ export const LoadLatestBtn = ({ + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel={`Load new ${label}`} + accessibilityHint=""> Load new {label} diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 75a812760c..5279696a27 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -23,7 +23,10 @@ export const LoadLatestBtn = observer( }, ]} onPress={onPress} - hitSlop={HITSLOP}> + hitSlop={HITSLOP} + accessibilityRole="button" + accessibilityLabel={`Load new ${label}`} + accessibilityHint={`Loads new ${label}`}> setOverride(v => !v)}> + onPress={() => setOverride(v => !v)} + accessibilityLabel={override ? 'Hide post' : 'Show post'} + // TODO: The text labelling should be split up so controls have unique roles + accessibilityHint={ + override + ? 'Re-hide post' + : 'Shows post hidden based on your moderation settings' + }> {override ? 'Hide' : 'Show'} diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index b3c4c9593c..2cc7ea62b5 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -46,7 +46,8 @@ export function PostHider({ setOverride(v => !v)}> + onPress={() => setOverride(v => !v)} + accessibilityRole="button"> {override ? 'Hide' : 'Show'} post diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 6a77598408..929c85adcb 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -136,7 +136,10 @@ export function PostEmbeds({ { onPressAltText(alt) - }}> + }} + accessibilityRole="button" + accessibilityLabel="View alt text" + accessibilityHint="Opens modal with alt text"> ALT )} diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx index 4e20558b7b..a4bea68f7c 100644 --- a/src/view/screens/AppPasswords.tsx +++ b/src/view/screens/AppPasswords.tsx @@ -184,7 +184,10 @@ function AppPassword({ + onPress={onDelete} + accessibilityRole="button" + accessibilityLabel="Delete" + accessibilityHint="Deletes app password"> {name} @@ -250,7 +253,6 @@ const styles = StyleSheet.create({ pr10: { marginRight: 10, }, - btnContainer: { flexDirection: 'row', justifyContent: 'center', diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 53bef813d8..ba9b05c438 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -226,6 +226,9 @@ const FeedPage = observer( testID="composeFAB" onPress={onPressCompose} icon={} + accessibilityRole="button" + accessibilityLabel="Compose" + accessibilityHint="Opens post composer" /> ) diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index 8e0fe8dd3b..4a747e5bf7 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -46,7 +46,9 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps< + onPress={toggler(entry.id)} + accessibilityLabel="View debug entry" + accessibilityHint="Opens additional details for a debug entry"> {entry.type === 'debug' ? ( ) : ( diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx index 4522d79ee5..6152038d3d 100644 --- a/src/view/screens/SearchMobile.tsx +++ b/src/view/screens/SearchMobile.tsx @@ -118,10 +118,10 @@ export const SearchScreen = withAuthRequired( }, []) return ( - + + noFeedback + accessibilityLabel={`Signed in as ${store.me.handle}`} + accessibilityHint="Double tap to sign out"> @@ -176,7 +178,10 @@ export const SettingsScreen = withAuthRequired( + onPress={isSwitching ? undefined : onPressSignout} + accessibilityRole="button" + accessibilityLabel="Sign out" + accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}> Sign out @@ -191,7 +196,10 @@ export const SettingsScreen = withAuthRequired( style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]} onPress={ isSwitching ? undefined : () => onPressSwitchAccount(account) - }> + } + accessibilityRole="button" + accessibilityLabel={`Switch to ${account.handle}`} + accessibilityHint="Switches the account you are logged in to"> @@ -209,7 +217,10 @@ export const SettingsScreen = withAuthRequired( + onPress={isSwitching ? undefined : onPressAddAccount} + accessibilityRole="button" + accessibilityLabel="Add account" + accessibilityHint="Create a new Bluesky account"> + onPress={isSwitching ? undefined : onPressInviteCodes} + accessibilityRole="button" + accessibilityLabel="Invite" + accessibilityHint="Opens invite code list"> + onPress={isSwitching ? undefined : onPressContentFiltering} + accessibilityHint="Content moderation" + accessibilityLabel="Opens configurable content moderation settings"> + onPress={isSwitching ? undefined : onPressChangeHandle} + accessibilityRole="button" + accessibilityLabel="Change handle" + accessibilityHint="Choose a new Bluesky username or create"> + onPress={onPressDeleteAccount} + accessible={true} + accessibilityRole="button" + accessibilityLabel="Delete account" + accessibilityHint="Opens modal for account deletion confirmation. Requires email code."> + + { const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} = useNavigationTabState() + const {notifications} = store.me + // events // = @@ -120,7 +122,11 @@ export const DrawerContent = observer(() => { ]}> - + { ) } label="Search" + accessibilityLabel="Search" + accessibilityHint="Search through users and posts" bold={isAtSearch} onPress={onPressSearch} /> @@ -184,6 +192,8 @@ export const DrawerContent = observer(() => { ) } label="Home" + accessibilityLabel="Home" + accessibilityHint="Navigates to default feed" bold={isAtHome} onPress={onPressHome} /> @@ -204,7 +214,13 @@ export const DrawerContent = observer(() => { ) } label="Notifications" - count={store.me.notifications.unreadCountLabel} + accessibilityLabel={ + notifications.unreadCountLabel === '1' + ? 'Notifications: 1 unread notification' + : `Notifications: ${notifications.unreadCountLabel} unread notifications` + } + accessibilityHint="Opens notification feed" + count={notifications.unreadCountLabel} bold={isAtNotifications} onPress={onPressNotifications} /> @@ -225,6 +241,8 @@ export const DrawerContent = observer(() => { ) } label="Profile" + accessibilityLabel="Profile" + accessibilityHint="See profile display name, avatar, description, and other profile items" onPress={onPressProfile} /> { /> } label="Settings" + accessibilityLabel="Settings" + accessibilityHint="Manage settings for your account, like handle, content moderation, and app passwords" onPress={onPressSettings} /> @@ -243,6 +263,13 @@ export const DrawerContent = observer(() => { {!isWeb && ( { )} { ) }) -function MenuItem({ - icon, - label, - count, - bold, - onPress, -}: { +interface MenuItemProps extends ComponentProps { icon: JSX.Element label: string count?: string bold?: boolean - onPress: () => void -}) { +} + +function MenuItem({ + icon, + label, + accessibilityLabel, + count, + bold, + onPress, +}: MenuItemProps) { const pal = usePalette('default') return ( + onPress={onPress} + accessibilityRole="menuitem" + accessibilityLabel={accessibilityLabel} + accessibilityHint=""> {icon} {count ? ( @@ -332,6 +367,7 @@ const InviteCodes = observer(() => { const {track} = useAnalytics() const store = useStores() const pal = usePalette('default') + const {invitesAvailable} = store.me const onPress = React.useCallback(() => { track('Menu:ItemClicked', {url: '#invite-codes'}) store.shell.closeDrawer() @@ -341,7 +377,14 @@ const InviteCodes = observer(() => { + onPress={onPress} + accessibilityRole="button" + accessibilityLabel={ + invitesAvailable === 1 + ? 'Invite codes: 1 available' + : `Invite codes: ${invitesAvailable} available` + } + accessibilityHint="Opens list of invite codes"> { ) } onPress={onPressHome} + accessibilityLabel="Go home" + accessibilityHint="Navigates to feed home" /> { ) } onPress={onPressSearch} + accessibilityRole="search" /> { } onPress={onPressNotifications} notificationCount={store.me.notifications.unreadCountLabel} + accessibilityLabel="Notifications" + accessibilityHint="Navigates to notifications" /> { } onPress={onPressProfile} + accessibilityLabel="Profile" + accessibilityHint="Navigates to profile" /> ) }) +interface BtnProps + extends Pick< + ComponentProps, + 'accessibilityRole' | 'accessibilityHint' | 'accessibilityLabel' + > { + testID?: string + icon: JSX.Element + notificationCount?: string + onPress?: (event: GestureResponderEvent) => void + onLongPress?: (event: GestureResponderEvent) => void +} + function Btn({ testID, icon, notificationCount, onPress, onLongPress, -}: { - testID?: string - icon: JSX.Element - notificationCount?: string - onPress?: (event: GestureResponderEvent) => void - onLongPress?: (event: GestureResponderEvent) => void -}) { + accessibilityHint, + accessibilityLabel, +}: BtnProps) { return ( + onLongPress={onLongPress} + accessibilityLabel={accessibilityLabel} + accessibilityHint={accessibilityHint}> {notificationCount ? ( {notificationCount} diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index b4b219023e..86f1a3ef37 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -2,7 +2,11 @@ import React from 'react' import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {PressableWithHover} from 'view/com/util/PressableWithHover' -import {useNavigation, useNavigationState} from '@react-navigation/native' +import { + useLinkProps, + useNavigation, + useNavigationState, +} from '@react-navigation/native' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -59,7 +63,10 @@ function BackBtn() { + style={styles.backBtn} + accessibilityRole="button" + accessibilityLabel="Go back" + accessibilityHint="Navigates to the previous screen"> - - - {isCurrent ? iconFilled : icon} - {typeof count === 'string' && count ? ( - - {count} - - ) : null} - - - {label} - - + hoverStyle={pal.viewLight} + onPress={onPress} + accessibilityLabel={label} + accessibilityHint={`Navigates to ${label}`}> + + {isCurrent ? iconFilled : icon} + {typeof count === 'string' && count ? ( + + {count} + + ) : null} + + + {label} + ) }, @@ -115,7 +125,12 @@ function ComposeBtn() { const onPressCompose = () => store.shell.openComposer({}) return ( - + + onPress={onDarkmodePress} + accessibilityRole="button" + accessibilityLabel="Toggle dark mode" + accessibilityHint={ + mode === 'Dark' + ? 'Sets display to light mode' + : 'Sets display to dark mode' + }> @@ -78,13 +85,22 @@ const InviteCodes = observer(() => { const store = useStores() const pal = usePalette('default') + const {invitesAvailable} = store.me + const onPress = React.useCallback(() => { store.shell.openModal({name: 'invite-codes'}) }, [store]) return ( + onPress={onPress} + accessibilityRole="button" + accessibilityLabel={ + invitesAvailable === 1 + ? 'Invite codes: 1 available' + : `Invite codes: ${invitesAvailable} available` + } + accessibilityHint="Opens list of invite codes"> setIsInputFocused(false)} onChangeText={onChangeQuery} onSubmitEditing={onSubmit} + accessibilityRole="search" /> {query ? ( - + Cancel diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 3d790febc9..349376436c 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -46,7 +46,9 @@ const ShellInner = observer(() => { {!isDesktop && store.shell.isDrawerOpen && ( store.shell.closeDrawer()} - style={styles.drawerMask}> + style={styles.drawerMask} + accessibilityLabel="Close navigation footer" + accessibilityHint="Closes bottom navigation bar"> diff --git a/web/index.html b/web/index.html index b1b9d51ddd..ea08e9d555 100644 --- a/web/index.html +++ b/web/index.html @@ -60,10 +60,6 @@ } }*/ - /* Remove focus state on inputs */ - *:focus { - outline: 0; - } /* Remove default link styling */ a { color: inherit; @@ -102,6 +98,14 @@ color: #0085ff; cursor: pointer; } + /* OLLIE: TODO -- this is not accessible */ + /* Remove focus state on inputs */ + .ProseMirror-focused { + outline: 0; + } + input:focus { + outline: 0; + } .tippy-content .items { border-radius: 6px; background: #F3F3F8; diff --git a/yarn.lock b/yarn.lock index a54cab39d1..994ec7fb74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,12 +2,22 @@ # yarn lockfile v1 +"@0no-co/graphql.web@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@0no-co/graphql.web/-/graphql.web-1.0.1.tgz#db3da0d2cd41548b50f0583c0d2f4743c767e56b" + integrity sha512-6Yaxyv6rOwRkLIvFaL0NrLDgfNqC/Ng9QOPmTmlqW4mORXMEKmh5NYGkIvvt5Yw8fZesnMAqkj8cIqTj8f40cQ== + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + "@ampproject/remapping@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== dependencies: - "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" "@apideck/better-ajv-errors@^0.3.1": @@ -19,18 +29,7 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@*": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.0.tgz#4a60f8f1de91105ad93526d69abcf011bbeaa3be" - integrity sha512-AntqYOVrMalBJapnNBV0akh/PWcsKdWq8zfuvv8hZW/jwOkJTVPTRFOP2OHJFcfz4WezytX43ml/L2kSG9z4+Q== - dependencies: - "@atproto/common-web" "*" - "@atproto/uri" "*" - "@atproto/xrpc" "*" - tlds "^1.234.0" - typed-emitter "^2.1.0" - -"@atproto/api@0.2.11": +"@atproto/api@*", "@atproto/api@0.2.11": version "0.2.11" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.11.tgz#53b70b0f4942b2e2dd5cb46433f133cde83917bf" integrity sha512-5JY1Ii/81Bcy1ZTGRqALsaOdc8fIJTSlMNoSptpGH73uAPQE93weDrb8sc3KoxWi1G2ss3IIBSLPJWxALocJSQ== @@ -41,16 +40,6 @@ tlds "^1.234.0" typed-emitter "^2.1.0" -"@atproto/auth@*": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@atproto/auth/-/auth-0.0.1.tgz#0ae07bfb6e4e86605504a20f0302e448ba3f8b0e" - integrity sha512-eom7V/LmXttlFE31TcOJ0BInTszkm5ZBS2mqoLqbnA5ZTcTsgQsMKhGzARFf2zwBM9h8pbVa1XMI83gnrTHfxA== - dependencies: - "@atproto/crypto" "*" - "@atproto/did-resolver" "*" - "@ucans/core" "0.11.0" - uint8arrays "3.0.0" - "@atproto/common-web@*": version "0.1.0" resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.1.0.tgz#5529fa66f9533aa00cfd13f0a25757df7b26bd3d" @@ -61,14 +50,15 @@ zod "^3.14.2" "@atproto/common@*": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.1.tgz#ec33a3b4995c91d3ad2e90fc4cdbc65284ceff84" - integrity sha512-GYwot5wF/z8iYGSPjrLHuratLc0CVgovmwfJss7+BUOB6y2/Vw8+1Vw0n9DDI0gb5vmx3UI8z0uJgC8aa8yuJg== + version "0.2.0" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.2.0.tgz#e74502edf636f30e332f516dcb96f7342b71ff1b" + integrity sha512-PVYSC30pyonz2MOxuBLk27uGdwyZQ42gJfCA/NE9jLeuenVDmZnVrK5WqJ7eGg+F88rZj7NcGfRsZdP0GMykEQ== dependencies: + "@atproto/common-web" "*" "@ipld/dag-cbor" "^7.0.3" + cbor-x "^1.5.1" multiformats "^9.6.4" pino "^8.6.1" - zod "^3.14.2" "@atproto/common@0.1.0": version "0.1.0" @@ -80,7 +70,28 @@ pino "^8.6.1" zod "^3.14.2" -"@atproto/crypto@*", "@atproto/crypto@0.1.0": +"@atproto/common@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.1.tgz#ec33a3b4995c91d3ad2e90fc4cdbc65284ceff84" + integrity sha512-GYwot5wF/z8iYGSPjrLHuratLc0CVgovmwfJss7+BUOB6y2/Vw8+1Vw0n9DDI0gb5vmx3UI8z0uJgC8aa8yuJg== + dependencies: + "@ipld/dag-cbor" "^7.0.3" + multiformats "^9.6.4" + pino "^8.6.1" + zod "^3.14.2" + +"@atproto/crypto@*": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.1.tgz#54afad2124c3867091e4d9b271f22d375fcfdf9e" + integrity sha512-/7Ntn55dRZPtCnOd6dVo1IvZzpVut6YTAkZ8iFry9JW29l7ZeNkJd+NTnmWRz3aGQody10jngb4SNxQNi/f3+A== + dependencies: + "@noble/secp256k1" "^1.7.0" + big-integer "^1.6.51" + multiformats "^9.6.4" + one-webcrypto "^1.0.3" + uint8arrays "3.0.0" + +"@atproto/crypto@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568" integrity sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg== @@ -109,12 +120,16 @@ "@atproto/common-web" "*" "@atproto/lexicon@*": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.0.4.tgz#f0a6688ad54adb2ec4a8d1f11fcbf45e96203c4b" - integrity sha512-00lqIKJetVlxQzNmEhrFzZeT9k+zGPBsHwtYpG7rH4vZ211i5WiDkmQcBwwFs2g/qCBt+nVq0dlgl3JhCLJXQg== + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.1.0.tgz#e7784cc868c734314d5bf9af83487aba7ccae0b3" + integrity sha512-Iy+gV9w42xLhrZrmcbZh7VFoHjXuzWvecGHIfz44owNjjv7aE/d2P5BbOX/XicSkmQ8Qkpg0BqwYDD1XBVS+DQ== dependencies: + "@atproto/common-web" "*" + "@atproto/identifier" "*" "@atproto/nsid" "*" + "@atproto/uri" "*" iso-datestring-validator "^2.2.2" + multiformats "^9.6.4" zod "^3.14.2" "@atproto/nsid@*": @@ -162,12 +177,14 @@ uint8arrays "3.0.0" "@atproto/repo@*": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.0.1.tgz#41c63943a7e6a0942fc3e721c05d8c836c2fcfc2" - integrity sha512-tBZjaeaRL7fJynZCA5F+ZjRQuf5fpL7Cj5VqP6KtXYacuNP/LufwrHARSOwxJMMZpPOoWmwv4R8bETiQozehEA== + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.1.0.tgz#8c546af16c30fe5ba4c883ac73b68be9d7eca273" + integrity sha512-O4qs5WfSjEFvUtpOTB4n9cLcK6YP/w/ly6Qxc3S8IFevLGYX58NPPr5zlg3dxs64uLKbWWjzhQM7JAqO44MEKw== dependencies: - "@atproto/auth" "*" "@atproto/common" "*" + "@atproto/crypto" "*" + "@atproto/did-resolver" "*" + "@atproto/lexicon" "*" "@atproto/nsid" "*" "@ipld/car" "^3.2.3" "@ipld/dag-cbor" "^7.0.0" @@ -176,26 +193,32 @@ zod "^3.14.2" "@atproto/uri@*": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@atproto/uri/-/uri-0.0.1.tgz#bfab68eda17ec987647f10d102168d417bc8a326" - integrity sha512-Tm+20Bxdie+a4yvberrfWaDhrze/p3AvA5v5IV6XyZJYu2+fnionUrufUjkcs3PIWeSd6VMgVcRp3GaoiUvSvQ== + version "0.0.2" + resolved "https://registry.yarnpkg.com/@atproto/uri/-/uri-0.0.2.tgz#c6d3788e6f12d66ba72690d2d70fe6c291b4acfb" + integrity sha512-/6otLZF7BLpT9suSdHuXLbL12nINcWPsLmcOI+dctqovWUjH+XIRVNXDQgBYSrPVetxMiknuEwWelmnA33AEXg== + dependencies: + "@atproto/identifier" "*" + "@atproto/nsid" "*" "@atproto/xrpc-server@*": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.0.1.tgz#62891d8e24b0813a7006d8ba947716b7c69e5667" - integrity sha512-W9pb9k9wgDlZdDF3eIDMXhEs1trg3zSRd70f1BfN22h+Or4wsoq5dAxXg6q9os3+DNkVkD9BWeRwVppCF6FxGg== + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.1.0.tgz#2dd3172bb35fbfefb98c3d727d29be8eca5c3d9b" + integrity sha512-I7EjhnLUrlqQKTe2jDEnyAaOTvj26pg9NRjTXflbIOqCOkh+K9+5ztGSI0djF7TSQ7pegXroj3qRnmpVVCBr7Q== dependencies: "@atproto/common" "*" "@atproto/lexicon" "*" + cbor-x "^1.5.1" express "^4.17.2" http-errors "^2.0.0" mime-types "^2.1.35" + uint8arrays "3.0.0" + ws "^8.12.0" zod "^3.14.2" "@atproto/xrpc@*": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.0.4.tgz#d7dd45cdb21e29b9715ca30eb18320548f293413" - integrity sha512-Hxh+GgZx21Zvlb2RMlSlJDd3r3GR0vAS6OOZPW2xzWiVHsetb9ZlFB6D0AeAPj2R+U2UUkmdUR8G3U/nkgnQFA== + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.1.0.tgz#798569095538ac060475ae51f1b4c071ff8776d6" + integrity sha512-LhBeZkQwPezjEtricGYnG62udFglOqlnmMSS0KyWgEAPi4KMp4H2F4jNoXcf5NPtZ9S4N4hJaErHX4PJYv2lfA== dependencies: "@atproto/lexicon" "*" zod "^3.14.2" @@ -207,33 +230,33 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.8.3": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" + integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.0.tgz#c241dc454e5b5917e40d37e525e2f4530c399298" - integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.5": + version "7.21.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc" + integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== -"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.16.0", "@babel/core@^7.20.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.3.tgz#cf1c877284a469da5d1ce1d1e53665253fae712e" - integrity sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw== +"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.16.0", "@babel/core@^7.20.0", "@babel/core@^7.20.2", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.5.tgz#92f753e8b9f96e15d4b398dbe2f25d1408c9c426" + integrity sha512-9M398B/QH5DlfCOTKDZT1ozXr0x8uBEeFd+dJraGUZGiaNpGCDVGCc14hZexsMblw3XxltJ+6kSvogp9J+5a9g== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.21.3" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helpers" "^7.21.0" - "@babel/parser" "^7.21.3" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helpers" "^7.21.5" + "@babel/parser" "^7.21.5" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.3" - "@babel/types" "^7.21.3" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -249,12 +272,12 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.0" -"@babel/generator@^7.20.0", "@babel/generator@^7.21.3", "@babel/generator@^7.7.2": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.3.tgz#232359d0874b392df04045d72ce2fd9bb5045fce" - integrity sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA== +"@babel/generator@^7.20.0", "@babel/generator@^7.20.4", "@babel/generator@^7.21.5", "@babel/generator@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.5.tgz#c0c0e5449504c7b7de8236d99338c3e2a340745f" + integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w== dependencies: - "@babel/types" "^7.21.3" + "@babel/types" "^7.21.5" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -267,45 +290,46 @@ "@babel/types" "^7.18.6" "@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.21.5.tgz#817f73b6c59726ab39f6ba18c234268a519e5abb" + integrity sha512-uNrjKztPLkUk7bpCNC0jEKDJzzkvel/W+HguzbN8krA+LPfC1CEobJEvAvGka2A/M+ViOqXdcRL0GqPUJSjx9g== dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" + "@babel/types" "^7.21.5" -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" - integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366" + integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w== dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-validator-option" "^7.18.6" + "@babel/compat-data" "^7.21.5" + "@babel/helper-validator-option" "^7.21.0" browserslist "^4.21.3" lru-cache "^5.1.1" semver "^6.3.0" "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz#64f49ecb0020532f19b1d014b03bccaa1ab85fb9" - integrity sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.5.tgz#09a259305467d2020bd2492119ee1c1bc55029e9" + integrity sha512-yNSEck9SuDvPTEUYm4BSXl6ZVC7yO5ZLEMAhG3v3zi7RDxyL/nQDemWWZmw4L0stPWwhpnznRRyJHPRcbXR2jw== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-environment-visitor" "^7.21.5" "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" + "@babel/helper-member-expression-to-functions" "^7.21.5" "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-replace-supers" "^7.21.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" "@babel/helper-split-export-declaration" "^7.18.6" + semver "^6.3.0" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz#53ff78472e5ce10a52664272a239787107603ebb" - integrity sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.5.tgz#4ce6ffaf497a241aa6c62192416b273987a8daa3" + integrity sha512-1+DPMcln46eNAta/rPIqQYXYRGvQ/LRy6bRKnSt9Dzt/yLjNUbbsh+6yzD6fUHmtzc9kWvVnAhtcMSMyziHmUA== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" regexpu-core "^5.3.1" + semver "^6.3.0" "@babel/helper-define-polyfill-provider@^0.3.3": version "0.3.3" @@ -319,17 +343,10 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" + integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== "@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": version "7.21.0" @@ -346,33 +363,33 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" - integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== +"@babel/helper-member-expression-to-functions@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.5.tgz#3b1a009af932e586af77c1030fba9ee0bde396c0" + integrity sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg== dependencies: - "@babel/types" "^7.21.0" + "@babel/types" "^7.21.5" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af" + integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== dependencies: - "@babel/types" "^7.18.6" + "@babel/types" "^7.21.4" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" - integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420" + integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw== dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-module-imports" "^7.21.4" + "@babel/helper-simple-access" "^7.21.5" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.19.1" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.2" - "@babel/types" "^7.21.2" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" @@ -381,10 +398,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.21.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" + integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" @@ -396,24 +413,24 @@ "@babel/helper-wrap-function" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== +"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7", "@babel/helper-replace-supers@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.21.5.tgz#a6ad005ba1c7d9bc2973dfde05a1bba7065dde3c" + integrity sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg== dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-member-expression-to-functions" "^7.21.5" "@babel/helper-optimise-call-expression" "^7.18.6" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== +"@babel/helper-simple-access@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee" + integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg== dependencies: - "@babel/types" "^7.20.2" + "@babel/types" "^7.21.5" "@babel/helper-skip-transparent-expression-wrappers@^7.20.0": version "7.20.0" @@ -429,10 +446,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" + integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" @@ -454,14 +471,14 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" -"@babel/helpers@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" - integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== +"@babel/helpers@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08" + integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA== dependencies: "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.0" - "@babel/types" "^7.21.0" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" "@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": version "7.18.6" @@ -472,10 +489,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.3": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.3.tgz#1d285d67a19162ff9daa358d4cb41d50c06220b3" - integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.5.tgz#821bb520118fd25b982eaf8d37421cf5c64a312b" + integrity sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -484,7 +501,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1" integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== @@ -493,7 +510,7 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" "@babel/plugin-proposal-optional-chaining" "^7.20.7" -"@babel/plugin-proposal-async-generator-functions@^7.0.0", "@babel/plugin-proposal-async-generator-functions@^7.20.1": +"@babel/plugin-proposal-async-generator-functions@^7.0.0", "@babel/plugin-proposal-async-generator-functions@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== @@ -511,7 +528,7 @@ "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-proposal-class-static-block@^7.18.6": +"@babel/plugin-proposal-class-static-block@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz#77bdd66fb7b605f3a61302d224bdfacf5547977d" integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== @@ -563,7 +580,7 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": +"@babel/plugin-proposal-logical-assignment-operators@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== @@ -587,7 +604,7 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.12.13", "@babel/plugin-proposal-object-rest-spread@^7.20.2": +"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.12.13", "@babel/plugin-proposal-object-rest-spread@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== @@ -606,7 +623,7 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-proposal-optional-chaining@^7.0.0", "@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": +"@babel/plugin-proposal-optional-chaining@^7.0.0", "@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.20.7", "@babel/plugin-proposal-optional-chaining@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== @@ -623,7 +640,7 @@ "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-proposal-private-property-in-object@^7.18.6": +"@babel/plugin-proposal-private-property-in-object@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz#19496bd9883dd83c23c7d7fc45dcd9ad02dfa1dc" integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== @@ -698,11 +715,11 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1" - integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A== + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.21.4.tgz#3e37fca4f06d93567c1cd9b75156422e90a67107" + integrity sha512-l9xd3N+XG4fZRxEP3vXdK6RW7vN1Uf5dxzRC/09wV86wqZ/YYQooBIGNsiRdfNR3/q2/5pPzV4B54J/9ctX5jw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-import-assertions@^7.20.0": version "7.20.0" @@ -711,7 +728,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -725,12 +742,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.7.2": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" - integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== +"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.21.4", "@babel/plugin-syntax-jsx@^7.7.2": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2" + integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" @@ -789,20 +806,20 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.20.0", "@babel/plugin-syntax-typescript@^7.7.2": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" - integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8" + integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.18.6": +"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz#9bb42a53de447936a57ba256fbf537fc312b6929" + integrity sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA== + dependencies: + "@babel/helper-plugin-utils" "^7.21.5" + +"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354" integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== @@ -818,14 +835,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.20.2": +"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz#e737b91037e5186ee16b76e7ae093358a5634f02" integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.20.2": +"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665" integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== @@ -840,15 +857,15 @@ "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.18.9": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== +"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz#3a2d8bb771cd2ef1cd736435f6552fe502e11b44" + integrity sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/template" "^7.20.7" -"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.20.2": +"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.21.3": version "7.21.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz#73b46d0fd11cd6ef57dea8a381b1215f4959d401" integrity sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA== @@ -878,7 +895,7 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.16.0", "@babel/plugin-transform-flow-strip-types@^7.18.6": +"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.16.0", "@babel/plugin-transform-flow-strip-types@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz#6aeca0adcb81dc627c8986e770bfaa4d9812aff5" integrity sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w== @@ -886,12 +903,12 @@ "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-flow" "^7.18.6" -"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.18.8": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" - integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== +"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz#e890032b535f5a2e237a18535f56a9fdaa7b83fc" + integrity sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.18.9": version "7.18.9" @@ -916,7 +933,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-modules-amd@^7.19.6": +"@babel/plugin-transform-modules-amd@^7.20.11": version "7.20.11" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a" integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== @@ -924,16 +941,16 @@ "@babel/helper-module-transforms" "^7.20.11" "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.19.6": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7" - integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== +"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.19.6", "@babel/plugin-transform-modules-commonjs@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz#d69fb947eed51af91de82e4708f676864e5e47bc" + integrity sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ== dependencies: - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helper-plugin-utils" "^7.21.5" + "@babel/helper-simple-access" "^7.21.5" -"@babel/plugin-transform-modules-systemjs@^7.19.6": +"@babel/plugin-transform-modules-systemjs@^7.20.11": version "7.20.11" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e" integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== @@ -951,7 +968,7 @@ "@babel/helper-module-transforms" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-named-capturing-groups-regex@^7.0.0", "@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": +"@babel/plugin-transform-named-capturing-groups-regex@^7.0.0", "@babel/plugin-transform-named-capturing-groups-regex@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== @@ -981,7 +998,7 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-replace-supers" "^7.18.6" -"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": +"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.21.3": version "7.21.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz#18fc4e797cf6d6d972cb8c411dbe8a809fa157db" integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ== @@ -1031,15 +1048,15 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.12.17", "@babel/plugin-transform-react-jsx@^7.18.6": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz#656b42c2fdea0a6d8762075d58ef9d4e3c4ab8a2" - integrity sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.5.tgz#bd98f3b429688243e4fa131fe1cbb2ef31ce6f38" + integrity sha512-ELdlq61FpoEkHO6gFRpfj0kUgSwQTGoaEU8eMRoS8Dv3v6e7BjEAj5WMtIBRdHUeAioMhKP5HyxNzNnP+heKbA== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-jsx" "^7.18.6" - "@babel/types" "^7.21.0" + "@babel/helper-module-imports" "^7.21.4" + "@babel/helper-plugin-utils" "^7.21.5" + "@babel/plugin-syntax-jsx" "^7.21.4" + "@babel/types" "^7.21.5" "@babel/plugin-transform-react-pure-annotations@^7.18.6": version "7.18.6" @@ -1049,12 +1066,12 @@ "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-regenerator@^7.18.6": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== +"@babel/plugin-transform-regenerator@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz#576c62f9923f94bcb1c855adc53561fd7913724e" + integrity sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" regenerator-transform "^0.15.1" "@babel/plugin-transform-reserved-words@^7.18.6": @@ -1065,11 +1082,11 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.12.1", "@babel/plugin-transform-runtime@^7.16.4": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.0.tgz#2a884f29556d0a68cd3d152dcc9e6c71dfb6eee8" - integrity sha512-ReY6pxwSzEU0b3r2/T/VhqMKg/AkceBT19X0UptA3/tYi5Pe2eXgEUH+NNMC5nok6c6XQz5tyVTUpuezRfSMSg== + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.4.tgz#2e1da21ca597a7d01fc96b699b21d8d2023191aa" + integrity sha512-1J4dhrw1h1PqnNNpzwxQ2UBymJUF8KuPjAAnlLwZcGhHAIqUigFW7cdK6GHoB64ubY4qXQNYknoUeks4Wz7CUA== dependencies: - "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-module-imports" "^7.21.4" "@babel/helper-plugin-utils" "^7.20.2" babel-plugin-polyfill-corejs2 "^0.3.3" babel-plugin-polyfill-corejs3 "^0.6.0" @@ -1083,7 +1100,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.19.0": +"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e" integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== @@ -1112,7 +1129,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" -"@babel/plugin-transform-typescript@^7.21.0", "@babel/plugin-transform-typescript@^7.5.0": +"@babel/plugin-transform-typescript@^7.21.3", "@babel/plugin-transform-typescript@^7.5.0": version "7.21.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz#316c5be579856ea890a57ebc5116c5d064658f2b" integrity sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw== @@ -1122,12 +1139,12 @@ "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-typescript" "^7.20.0" -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== +"@babel/plugin-transform-unicode-escapes@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz#1e55ed6195259b0e9061d81f5ef45a9b009fb7f2" + integrity sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-unicode-regex@^7.0.0", "@babel/plugin-transform-unicode-regex@^7.18.6": version "7.18.6" @@ -1138,30 +1155,30 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.20.0": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" - integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.5.tgz#db2089d99efd2297716f018aeead815ac3decffb" + integrity sha512-wH00QnTTldTbf/IefEVyChtRdw5RJvODT/Vb4Vcxq1AZvtXj6T0YeX0cAcXhI6/BdGuiP3GcNIL4OQbI2DVNxg== dependencies: - "@babel/compat-data" "^7.20.1" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.18.6" + "@babel/compat-data" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-plugin-utils" "^7.21.5" + "@babel/helper-validator-option" "^7.21.0" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-async-generator-functions" "^7.20.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.20.7" + "@babel/plugin-proposal-async-generator-functions" "^7.20.7" "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.21.0" "@babel/plugin-proposal-dynamic-import" "^7.18.6" "@babel/plugin-proposal-export-namespace-from" "^7.18.9" "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-logical-assignment-operators" "^7.20.7" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.20.2" + "@babel/plugin-proposal-object-rest-spread" "^7.20.7" "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-optional-chaining" "^7.21.0" "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.21.0" "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" @@ -1169,6 +1186,7 @@ "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-import-assertions" "^7.20.0" + "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" @@ -1178,40 +1196,40 @@ "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.18.6" - "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.21.5" + "@babel/plugin-transform-async-to-generator" "^7.20.7" "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.20.2" - "@babel/plugin-transform-classes" "^7.20.2" - "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.20.2" + "@babel/plugin-transform-block-scoping" "^7.21.0" + "@babel/plugin-transform-classes" "^7.21.0" + "@babel/plugin-transform-computed-properties" "^7.21.5" + "@babel/plugin-transform-destructuring" "^7.21.3" "@babel/plugin-transform-dotall-regex" "^7.18.6" "@babel/plugin-transform-duplicate-keys" "^7.18.9" "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-for-of" "^7.21.5" "@babel/plugin-transform-function-name" "^7.18.9" "@babel/plugin-transform-literals" "^7.18.9" "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.19.6" - "@babel/plugin-transform-modules-commonjs" "^7.19.6" - "@babel/plugin-transform-modules-systemjs" "^7.19.6" + "@babel/plugin-transform-modules-amd" "^7.20.11" + "@babel/plugin-transform-modules-commonjs" "^7.21.5" + "@babel/plugin-transform-modules-systemjs" "^7.20.11" "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.20.5" "@babel/plugin-transform-new-target" "^7.18.6" "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.20.1" + "@babel/plugin-transform-parameters" "^7.21.3" "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.21.5" "@babel/plugin-transform-reserved-words" "^7.18.6" "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.19.0" + "@babel/plugin-transform-spread" "^7.20.7" "@babel/plugin-transform-sticky-regex" "^7.18.6" "@babel/plugin-transform-template-literals" "^7.18.9" "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-escapes" "^7.21.5" "@babel/plugin-transform-unicode-regex" "^7.18.6" "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.20.2" + "@babel/types" "^7.21.5" babel-plugin-polyfill-corejs2 "^0.3.3" babel-plugin-polyfill-corejs3 "^0.6.0" babel-plugin-polyfill-regenerator "^0.4.1" @@ -1219,13 +1237,13 @@ semver "^6.3.0" "@babel/preset-flow@^7.13.13": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.18.6.tgz#83f7602ba566e72a9918beefafef8ef16d2810cb" - integrity sha512-E7BDhL64W6OUqpuyHnSroLnqyRTcG6ZdOBl1OKI/QK/HJfplqK/S3sq1Cckx7oTodJ5yOXyfw7rEADJ6UjoQDQ== + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.21.4.tgz#a5de2a1cafa61f0e0b3af9b30ff0295d38d3608f" + integrity sha512-F24cSq4DIBmhq4OzK3dE63NHagb27OPE3eWR+HLekt4Z3Y5MzIIUGF3LlLgV0gN8vzbDViSY7HnrReNVCJXTeA== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-transform-flow-strip-types" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.21.0" + "@babel/plugin-transform-flow-strip-types" "^7.21.0" "@babel/preset-modules@^0.1.5": version "0.1.5" @@ -1251,13 +1269,15 @@ "@babel/plugin-transform-react-pure-annotations" "^7.18.6" "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.16.7": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz#bcbbca513e8213691fe5d4b23d9251e01f00ebff" - integrity sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz#68292c884b0e26070b4d66b202072d391358395f" + integrity sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/helper-validator-option" "^7.21.0" - "@babel/plugin-transform-typescript" "^7.21.0" + "@babel/plugin-syntax-jsx" "^7.21.4" + "@babel/plugin-transform-modules-commonjs" "^7.21.5" + "@babel/plugin-transform-typescript" "^7.21.3" "@babel/register@^7.13.16": version "7.21.0" @@ -1275,10 +1295,10 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" - integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" + integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== dependencies: regenerator-runtime "^0.13.11" @@ -1291,28 +1311,28 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.20.0", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.3", "@babel/traverse@^7.7.2", "@babel/traverse@^7.7.4": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.3.tgz#4747c5e7903d224be71f90788b06798331896f67" - integrity sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ== +"@babel/traverse@^7.20.0", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2", "@babel/traverse@^7.7.4": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" + integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.21.3" - "@babel/helper-environment-visitor" "^7.18.9" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-environment-visitor" "^7.21.5" "@babel/helper-function-name" "^7.21.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.3" - "@babel/types" "^7.21.3" + "@babel/parser" "^7.21.5" + "@babel/types" "^7.21.5" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.3", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.3.tgz#4865a5357ce40f64e3400b0f3b737dc6d4f64d05" - integrity sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg== +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== dependencies: - "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-string-parser" "^7.21.5" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" @@ -1331,6 +1351,36 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg== +"@cbor-extract/cbor-extract-darwin-arm64@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6" + integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA== + +"@cbor-extract/cbor-extract-darwin-x64@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f" + integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw== + +"@cbor-extract/cbor-extract-linux-arm64@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63" + integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ== + +"@cbor-extract/cbor-extract-linux-arm@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41" + integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ== + +"@cbor-extract/cbor-extract-linux-x64@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd" + integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA== + +"@cbor-extract/cbor-extract-win32-x64@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7" + integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -1445,11 +1495,24 @@ integrity sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g== "@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308" - integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw== + version "2.2.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" + integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== -"@did-plc/lib@*", "@did-plc/lib@^0.0.1": +"@did-plc/lib@*": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@did-plc/lib/-/lib-0.0.4.tgz#be5400dc9464ec3088294bd089631e8a8aa98215" + integrity sha512-Omeawq3b8G/c/5CtkTtzovSOnWuvIuCI4GTJNrt1AmCskwEQV7zbX5d6km1mjJNbE0gHuQPTVqZxLVqetNbfwA== + dependencies: + "@atproto/common" "0.1.1" + "@atproto/crypto" "0.1.0" + "@ipld/dag-cbor" "^7.0.3" + axios "^1.3.4" + multiformats "^9.6.4" + uint8arrays "3.0.0" + zod "^3.14.2" + +"@did-plc/lib@^0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@did-plc/lib/-/lib-0.0.1.tgz#5fd78c71901168ac05c5650af3a376c76461991c" integrity sha512-RkY5w9DbYMco3SjeepqIiMveqz35exjlVDipCs2gz9AXF4/cp9hvmrp9zUWEw2vny+FjV8vGEN7QpaXWaO6nhg== @@ -1494,25 +1557,25 @@ "@types/hammerjs" "^2.0.36" "@eslint-community/eslint-utils@^4.2.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz#a556790523a351b4e47e9d385f47265eaaf9780a" - integrity sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA== + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403" - integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ== + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== -"@eslint/eslintrc@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d" - integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw== +"@eslint/eslintrc@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02" + integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.0" + espree "^9.5.1" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1520,10 +1583,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.36.0": - version "8.36.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" - integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== +"@eslint/js@8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" + integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== "@expo/bunyan@4.0.0", "@expo/bunyan@^4.0.0": version "4.0.0" @@ -1913,13 +1976,13 @@ integrity sha512-TI+l71+5aSKnShYclFa14Kum+hQMZ86b95SH6tQUG3qZEmLTarvWpKwqtTwQKqvlJSJrpFiSFu3eCuZokY6zWA== "@expo/webpack-config@^18.0.1": - version "18.0.1" - resolved "https://registry.yarnpkg.com/@expo/webpack-config/-/webpack-config-18.0.1.tgz#e657ae4490052a9ada6bf703cfd721324a5be741" - integrity sha512-0C+wjmmQ0usySdhtzeRp0yYuf9zkUZ/kNgA6AHQ9N7eG4JIr0DM1c87g119smxcJTbd8N+//mv5znPxSJqBqmg== + version "18.0.4" + resolved "https://registry.yarnpkg.com/@expo/webpack-config/-/webpack-config-18.0.4.tgz#4743428a546c2affb7a551cfc01139b5973193ec" + integrity sha512-TfQSQCJ9o5MXat+y6cqTnix2adBA61fgYpotDXONxVB8aZ8xi81D/Wp3CBT6WnNeukEyKzQFCBqrX3BTfewyJQ== dependencies: - "@babel/core" "^7.16.0" + "@babel/core" "^7.20.2" "@expo/config" "6.0.20" - babel-loader "^8.2.3" + babel-loader "^8.3.0" chalk "^4.0.0" clean-webpack-plugin "^4.0.0" copy-webpack-plugin "^10.2.0" @@ -1951,31 +2014,31 @@ find-up "^5.0.0" js-yaml "^4.1.0" -"@fortawesome/fontawesome-common-types@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz#51f734e64511dbc3674cd347044d02f4dd26e86b" - integrity sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg== +"@fortawesome/fontawesome-common-types@6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b" + integrity sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ== "@fortawesome/fontawesome-svg-core@^6.1.1": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz#b6a17d48d231ac1fad93e43fca7271676bf316cf" - integrity sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw== + version "6.4.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz#3727552eff9179506e9203d72feb5b1063c11a21" + integrity sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw== dependencies: - "@fortawesome/fontawesome-common-types" "6.3.0" + "@fortawesome/fontawesome-common-types" "6.4.0" "@fortawesome/free-regular-svg-icons@^6.1.1": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz#286f87f777e6c96af59151e86647c81083029ee2" - integrity sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg== + version "6.4.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz#cacc53bd8d832d46feead412d9ea9ce80a55e13a" + integrity sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw== dependencies: - "@fortawesome/fontawesome-common-types" "6.3.0" + "@fortawesome/fontawesome-common-types" "6.4.0" "@fortawesome/free-solid-svg-icons@^6.1.1": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz#d3bd33ae18bb15fdfc3ca136e2fea05f32768a65" - integrity sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA== + version "6.4.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz#48c0e790847fa56299e2f26b82b39663b8ad7119" + integrity sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ== dependencies: - "@fortawesome/fontawesome-common-types" "6.3.0" + "@fortawesome/fontawesome-common-types" "6.4.0" "@fortawesome/react-native-fontawesome@^0.3.0": version "0.3.0" @@ -1991,9 +2054,9 @@ integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== "@gorhom/bottom-sheet@^4": - version "4.4.5" - resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.4.5.tgz#b9041b01ce1af9a936e7c0fc1d78f026d759eebe" - integrity sha512-Z5Z20wshLUB8lIdtMKoJaRnjd64wBR/q8EeVPThrg+skrcBwBPHfUwZJ2srB0rEszA/01ejSJy/ixyd7Ra7vUA== + version "4.4.6" + resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.4.6.tgz#49fa6728f10133de772baaeb67ebe0f63125cf80" + integrity sha512-okqJPtFQjfqPZdh6wGDzQKkMevG1IfplQeoWY0VqOFCp3E0p7WHNeW41voK7KXXCVTQaGXibPfd9GNGjXgFNyg== dependencies: "@gorhom/portal" "1.0.14" invariant "^2.2.4" @@ -2006,9 +2069,9 @@ nanoid "^3.3.1" "@graphql-typed-document-node/core@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04" - integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== "@hapi/hoek@^9.0.0": version "9.3.0" @@ -2494,46 +2557,48 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== dependencies: "@jridgewell/set-array" "^1.0.1" "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": +"@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== "@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" + integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== dependencies: "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@1.4.14": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -2543,9 +2608,9 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== dependencies: "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" @@ -2562,10 +2627,45 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== -"@linaria/core@3.0.0-beta.13": - version "3.0.0-beta.13" - resolved "https://registry.yarnpkg.com/@linaria/core/-/core-3.0.0-beta.13.tgz#049c5be5faa67e341e413a0f6b641d5d78d91056" - integrity sha512-3zEi5plBCOsEzUneRVuQb+2SAx3qaC1dj0FfFAI6zIJQoDWu0dlSwKijMRack7oO9tUWrchfj3OkKQAd1LBdVg== +"@linaria/core@4.2.9": + version "4.2.9" + resolved "https://registry.yarnpkg.com/@linaria/core/-/core-4.2.9.tgz#4917bde18d064a29cff4fd86aa99621f953a2a2c" + integrity sha512-ELcu37VNVOT/PU0L6WDIN+aLzNFyJrqoBYT0CucGOCAmODbojUMCv8oJYRbWzA3N34w1t199dN4UFdfRWFG2rg== + dependencies: + "@linaria/logger" "^4.0.0" + "@linaria/tags" "^4.3.4" + "@linaria/utils" "^4.3.3" + +"@linaria/logger@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@linaria/logger/-/logger-4.0.0.tgz#6f73eb3cc11d548967a7caf2e7997439e46fca0d" + integrity sha512-YnBq0JlDWMEkTOK+tMo5yEVR0f5V//6qMLToGcLhTyM9g9i+IDFn51Z+5q2hLk7RdG4NBPgbcCXYi2w4RKsPeg== + dependencies: + debug "^4.1.1" + picocolors "^1.0.0" + +"@linaria/tags@^4.3.4": + version "4.3.4" + resolved "https://registry.yarnpkg.com/@linaria/tags/-/tags-4.3.4.tgz#3c98108e4b48b8413662b4c62c2b2abdebacaca4" + integrity sha512-W8zaLKtC4YFCwkZ9DMu2enCiD/zGyYmFSTzEvJP7ZycdftMizoOrWNOyF9kITyjGdq+jZvAXJz0BZDT6axgIRg== + dependencies: + "@babel/generator" "^7.20.4" + "@linaria/logger" "^4.0.0" + "@linaria/utils" "^4.3.3" + +"@linaria/utils@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@linaria/utils/-/utils-4.3.3.tgz#9f66ae41187e8a2f2cc3471b44935128ebd1dab3" + integrity sha512-xSe/tod9A44aIMbtds9fWLNe2TT080lLdRSaoqX+UHsBWqClkrw5cXEt3lm8Vr4hZiXT2r/1AldjuHb9YbUlMg== + dependencies: + "@babel/core" "^7.20.2" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-modules-commonjs" "^7.19.6" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" + "@linaria/logger" "^4.0.0" + babel-merge "^3.0.0" "@lukeed/csprng@^1.1.0": version "1.1.0" @@ -2625,9 +2725,9 @@ fastq "^1.6.0" "@notifee/react-native@^7.4.0": - version "7.6.1" - resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.6.1.tgz#e215428787396ec57ea424106cc88666f7efe70d" - integrity sha512-OjhLPODh6FICYZmF9/0UZbcl2JPaPpcrWi1Cvs/OLFbPSJTIEwPZgXFrCHv/cA3wUX4YQCXreSqQGSVQgvNItQ== + version "7.7.1" + resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.7.1.tgz#ce3f982fb7354519406cb7716f8e861bab0056ce" + integrity sha512-E+W91ulI4dxdIrhK6YCyjWqXgrUsVNZYYCSn3gDADmveuR2Yd2uGvbbSW2vUIFU4N4gQQT/5HJdk9Jk83KHbVA== "@npmcli/fs@^1.0.0": version "1.1.1" @@ -2661,21 +2761,21 @@ source-map "^0.7.3" "@popperjs/core@^2.9.0": - version "2.11.6" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" - integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== + version "2.11.7" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7" + integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw== "@react-native-async-storage/async-storage@^1.15.15", "@react-native-async-storage/async-storage@^1.17.6": - version "1.17.12" - resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.12.tgz#a39e4df5b06795ce49b2ca5b7ca9b8faadf8e621" - integrity sha512-BXg4OxFdjPTRt+8MvN6jz4muq0/2zII3s7HeT/11e4Zeh3WCgk/BleLzUcDfVqF3OzFHUqEkSrb76d6Ndjd/Nw== + version "1.18.1" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.18.1.tgz#b1aea4f07fb1dba3325b857b770671517ddab221" + integrity sha512-70aFW8fVCKl+oA1AKPFDpE6s4t9pulj2QeLX+MabEmzfT3urd/3cckv45WJvtocdoIH/oXA3Y+YcCRJCcNa8mA== dependencies: merge-options "^3.0.4" "@react-native-camera-roll/camera-roll@^5.2.2": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.3.1.tgz#0b6d363c0f6c83fc93ff033826f8fa96274a01a7" - integrity sha512-2XKMkb/pLBC6vYkNh+bJ4UEj49V2ZSyWFHmaxsUJU9beLo1QbM3XJnySV6F1uv7aC+I2RBlDuAusCqNiTQiCOw== + version "5.4.0" + resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.4.0.tgz#f9dfb2fb37f6f88b70801e727282dd9157bf62be" + integrity sha512-SMEhc+2hQWubwzxR6Zac0CmrJ2rdoHHBo0ibG2iNMsxR0dnU5AdRGnYF/tyK9i20/i7ZNxn+qsEJ69shpkd6gg== "@react-native-clipboard/clipboard@^1.10.0": version "1.11.2" @@ -2683,9 +2783,9 @@ integrity sha512-bHyZVW62TuleiZsXNHS1Pv16fWc0fh8O9WvBzl4h2fykqZRW9a+Pv/RGTH56E3X2PqzHP38K5go8zmCZUoIsoQ== "@react-native-community/blur@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.0.tgz#e5018b3b0bd6de9632ac6cf34e9f8e0f1a9a28ec" - integrity sha512-d6phh39kKcbZ4IluDftiVWqfeFOgjl1AbQWzN47x+hLKQ5GvQJ6QhRvgAuDZ+xbJksrbXgNpMjVYkjsbcVehxg== + version "4.3.1" + resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.1.tgz#817a9b9762f738e578a2cd5306902f4510a6df34" + integrity sha512-XVjTKs+nSXG7DCmxIr7HSjeAB276OO9KZ7XUVCdjK+RGTlvlCRZIPV0ygi+WN87zsdvfWsQOTZv3k0/BI86gsA== "@react-native-community/cli-clean@^10.1.1": version "10.1.1" @@ -2948,40 +3048,40 @@ dependencies: nanoid "^3.1.23" -"@remirror/core-constants@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.0.tgz#a52f89059d93955e00810023cc76b4f7db9650bf" - integrity sha512-vpePPMecHJllBqCWXl6+FIcZqS+tRUM2kSCCKFeEo1H3XUEv3ocijBIPhnlSAa7g6maX+12ATTgxrOsLpWVr2g== - dependencies: - "@babel/runtime" "^7.13.10" - -"@remirror/core-helpers@^2.0.1": +"@remirror/core-constants@^2.0.1": version "2.0.1" - resolved "https://registry.yarnpkg.com/@remirror/core-helpers/-/core-helpers-2.0.1.tgz#6847666a009ada8c9b9f3a093c13a6d07a95d9bb" - integrity sha512-s8M1pn33aBUhduvD1QR02uUQMegnFkGaTr4c1iBzxTTyg0rbQstzuQ7Q8TkL6n64JtgCdJS9jLz2dONb2meBKQ== + resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.1.tgz#19b4ae221880762cd98452f44288fcc66baaec0f" + integrity sha512-ZR4aihtnnT9lMbhh5DEbsriJRlukRXmLZe7HmM+6ufJNNUDoazc75UX26xbgQlNUqgAqMcUdGFAnPc1JwgAdLQ== dependencies: - "@babel/runtime" "^7.13.10" - "@linaria/core" "3.0.0-beta.13" - "@remirror/core-constants" "^2.0.0" - "@remirror/types" "^1.0.0" + "@babel/runtime" "^7.21.0" + +"@remirror/core-helpers@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@remirror/core-helpers/-/core-helpers-2.0.3.tgz#fa4a0224a612016b9f16052ed0c5d817c69daa39" + integrity sha512-LqIPF4stGG69l9qu/FFicv9d9B+YaItzgDMC5A0CEvDQfKkGD3BfabLmfpnuWbsc06oKGdTduilgWcALLZoYLg== + dependencies: + "@babel/runtime" "^7.21.0" + "@linaria/core" "4.2.9" + "@remirror/core-constants" "^2.0.1" + "@remirror/types" "^1.0.1" "@types/object.omit" "^3.0.0" - "@types/object.pick" "^1.3.1" + "@types/object.pick" "^1.3.2" "@types/throttle-debounce" "^2.1.0" case-anything "^2.1.10" dash-get "^1.0.2" - deepmerge "^4.2.2" + deepmerge "^4.3.1" fast-deep-equal "^3.1.3" make-error "^1.3.6" object.omit "^3.0.0" object.pick "^1.3.0" throttle-debounce "^3.0.1" -"@remirror/types@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@remirror/types/-/types-1.0.0.tgz#cc8764440089a2ada71f149c409739575b73b12e" - integrity sha512-7HQbW7k8VxrAtfzs9FxwO6XSDabn8tSFDi1wwzShOnU+cvaYpfxu0ygyTk3TpXsag1hgFKY3ZIlAfB4WVz2LkQ== +"@remirror/types@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@remirror/types/-/types-1.0.1.tgz#768502497a0fbbc23338a1586b893f729310cf70" + integrity sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA== dependencies: - type-fest "^2.0.0" + type-fest "^2.19.0" "@rollup/plugin-babel@^5.2.0": version "5.3.1" @@ -3025,22 +3125,22 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== -"@segment/analytics-core@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.2.3.tgz#729c5b72d6d940341ea8cba9d3ff3eef39baef7a" - integrity sha512-/B4f4Hxmwd9WpEba/ChYkUwhILz5cPhG4Sto03IlLc8vbV7gAOCGH021EKvU3Wv70WlRK6EgJkuDLPnRl2a2aA== +"@segment/analytics-core@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.2.4.tgz#a01f0c87246292e0b9790e12c73d2f7e5fceb168" + integrity sha512-M16osD6+z/bQPSVCZdlU+BAhCk968ppi+SGxU2gVa4B196Qr8SEkBPr3NxUCGTSoULo4/T+k8Ea5cF+pXlgf6Q== dependencies: "@lukeed/uuid" "^2.0.0" dset "^3.1.2" tslib "^2.4.1" "@segment/analytics-next@^1.51.3": - version "1.51.3" - resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.51.3.tgz#4720691d2bac43bb8390d4a7a15881e52dc9529f" - integrity sha512-c22GDz6rrhliIsgtLQjEcRiZdqb70+0hEyfTI6YpRXZzEXBwdJybO5ZCD7NRlVFHf/qXp1qcjHuQ5xyOGr2lJg== + version "1.51.6" + resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.51.6.tgz#56c99782fc333025906dbf8f5efd9b7f9f87c197" + integrity sha512-SiuuCHLq2sWM3fwF0peQ9J9Ku+FAbsx5XsGl9pL4rfHoaItlQuBETxkmT3BD2YltFxhr4FCQ5+phdT0/X5QUFA== dependencies: "@lukeed/uuid" "^2.0.0" - "@segment/analytics-core" "1.2.3" + "@segment/analytics-core" "1.2.4" "@segment/analytics.js-video-plugins" "^0.2.1" "@segment/facade" "^3.4.9" "@segment/tsub" "1.0.1" @@ -3052,9 +3152,9 @@ unfetch "^4.1.0" "@segment/analytics-react-native@^2.10.1": - version "2.13.4" - resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.13.4.tgz#52216972bf0a1f8722ddf18088340c9d4d90ca5a" - integrity sha512-47z2TmODJpeA7Pf1P8kE5dNTiqmxJ7khQ/NgiFR3eoiSy/ir0QOpT49QFrwVMeG35fEl+wDGLXUoYWoAMvBy6w== + version "2.13.5" + resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.13.5.tgz#e8373d1584812afbe39e9fb935b83655d15ce750" + integrity sha512-uWezHOghP3yf3tgEfpe2OxP/54l9SM7+YNwkrFhumhoe4cw4xTptlFi6zU4p8lRdmmoJQgQ+/rh3AUP/i4yFTA== dependencies: "@segment/sovran-react-native" "^1" deepmerge "^4.2.2" @@ -3116,13 +3216,13 @@ shell-quote "1.7.3" "@segment/sovran-react-native@^1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-1.0.1.tgz#4311f0af2e2b606d2c17e535b293c096c6a3c2e8" - integrity sha512-7VZrIa7/VP59d4QDvAs0ZOhiadlJ+2YC8K8dKOF0fGwiFC0UmQUZVs4IN9GZfbBavXsagVVMgL2GzjVGLLQdBw== + version "1.0.2" + resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-1.0.2.tgz#b9b260368d95546b8dfd028970ba01756e63f20c" + integrity sha512-2zr37YDWD9vdBHvsTTP2yXJ6qtNH1gAvocHFkzZBQ9E4YcO3FbbvO4gUoscHqYfRyo/+kUovysRKtUsXCcrm/Q== dependencies: ansi-regex "5.0.1" deepmerge "^4.2.2" - shell-quote "1.7.3" + shell-quote "1.8.0" "@segment/tsub@1.0.1": version "1.0.1" @@ -3159,9 +3259,9 @@ which "^2.0.2" "@sentry/cli@^1.72.0": - version "1.75.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.0.tgz#4a5e71b5619cd4e9e6238cc77857c66f6b38d86a" - integrity sha512-vT8NurHy00GcN8dNqur4CMIYvFH3PaKdkX3qllVvi4syybKqjwoz+aWRCvprbYv0knweneFkLt1SmBWqazUMfA== + version "1.75.2" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.2.tgz#2c38647b38300e52c9839612d42b7c23f8d6455b" + integrity sha512-CG0CKH4VCKWzEaegouWfCLQt9SFN+AieFESCatJ7zSuJmzF05ywpMusjxqRul6lMwfUhRKjGKOzcRJ1jLsfTBw== dependencies: https-proxy-agent "^5.0.0" mkdirp "^0.5.5" @@ -4310,27 +4410,26 @@ pretty-format "^29.0.0" "@tiptap/core@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.220.tgz#ced4b8f13ad6361f957275510bd0c005de29d18c" - integrity sha512-F2Q666xJqijBU5o+GqekqseNgIEMTs6BhsLDaf9DwThhljGLS8RXKnSvQxrxLNrYEPpw39n/G3Qt8YAOk5qR6w== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.3.tgz#dfd55124b3e7b0482e5ccb8be46eb9c3189167e2" + integrity sha512-jLyVIWAdjjlNzrsRhSE2lVL/7N8228/1R1QtaVU85UlMIwHFAcdzhD8FeiKkqxpTnGpaDVaTy7VNEtEgaYdCyA== -"@tiptap/extension-bubble-menu@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.220.tgz#3fea0c846f73a237f562fdce05671ef1fa025943" - integrity sha512-wthyec7s0vZlTSEAAZEgoFfx/1Arwg1zxDUrrE+YAost/Yn+w4xQksz/ts5Bx90iOk2qsJ+jzzttLRV17Ku7lA== +"@tiptap/extension-bubble-menu@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.3.tgz#44b3c4e35fd478c42467d8fb7dbc9532614e5b18" + integrity sha512-lPt1ELrYCuoQrQEUukqjp9xt38EwgPUwaKHI3wwt2Rbv+C6q1gmRsK1yeO/KqCNmFxNqF2p9ZF9srOnug/RZDQ== dependencies: - lodash "^4.17.21" tippy.js "^6.3.7" "@tiptap/extension-document@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.220.tgz#15b4db7a92659eff7efc6d4d877dcf72e3fd61b6" - integrity sha512-2sja4ZvOb4iynHrzinnclCSFgLyo6fJc1fBV5fIYaOgZOYcvz9KK8fgKiq+wIpG58sJEmQ5kcwwBlkXv+NTK+g== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.3.tgz#b58af5b4f71c0acea953a7ebe8b1d24341bfaf68" + integrity sha512-PsYeNQQBYIU9ayz1R11Kv/kKNPFNIV8tApJ9pxelXjzcAhkjncNUazPN/dyho60mzo+WpsmS3ceTj/gK3bCtWA== -"@tiptap/extension-floating-menu@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.220.tgz#35eb154227533ada738c922be2f8cf18426fe4bf" - integrity sha512-+WfcBEedm82ntaVIEQAGz0Om96Rpav7a+4f7e8N4PrLKm6nZ3gBaEkZVQ6vjJ6S/1htiWCv1XosYIwRboPBG0w== +"@tiptap/extension-floating-menu@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.3.tgz#8d9943246aa3247442c1993f235617094fe705b5" + integrity sha512-zN1vRGRvyK3pO2aHRmQSOTpl4UJraXYwKYM009n6WviYKUNm0LPGo+VD4OAtdzUhPXyccnlsTv2p6LIqFty6Bg== dependencies: tippy.js "^6.3.7" @@ -4340,36 +4439,36 @@ integrity sha512-00KHIcJ8kivn2ARI6NQYphv2LfllVCXViHGm0EhzDW6NQxCrriJKE3tKDcTFCu7LlC5doMpq9Z6KXdljc4oVeQ== "@tiptap/extension-link@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.220.tgz#c9954613cd1e0a0f1527853b732ef50dff734eac" - integrity sha512-vjEA8cE37ZZVVgPHSpttw3kbJoClb+ya/BVukDtJ1h6C7mIR1rqzNxTgpbnXJuA8xww0JOjpa5dpzEgcs294fA== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.3.tgz#4714a4c23d04032e75b5b8364a9c532f7a385aba" + integrity sha512-H72tXQ5rkVCkAhFaf08fbEU7EBUCK0uocsqOF+4th9sOlrhfgyJtc8Jv5EXPDpxNgG5jixSqWBo0zKXQm9s9eg== dependencies: linkifyjs "^4.1.0" "@tiptap/extension-mention@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.220.tgz#c3745895096157b09412bd49544f4ae741e8d0da" - integrity sha512-mjFNBuLxLaZ48CaIp/AdyHB2X1UKptpv6NVG0JaP2vBxW22eUy709JmCbRnWjeYe8pHbJjW22WC4/M1C44SFWg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.3.tgz#7ef0968c31543b806e431982ca697161439410ce" + integrity sha512-mT+tMJyf15gN3kW7UfZrP+J0jlhlBnR50SHj0PnDWqGnJ70qKSZTxcHfohrxU6On6yaOFsd+5Omn5seGK4XFWA== "@tiptap/extension-paragraph@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.220.tgz#d552dfdeeab9856e9eb8f0a7cf850f37d7cced69" - integrity sha512-ZGCzNGFYV4wa3l1nXtDIaYp7O6f0DrGTSl3alKkDTQe3SOmzXS2HjgWl9yPw8VXpU9W5mMGhXd+nGn/jUk+f/A== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.3.tgz#88d332158c70622d36849256f90e43ca4d226dfe" + integrity sha512-a+tKtmj4bU3GVCH1NE8VHWnhVexxX5boTVxsHIr4yGG3UoKo1c5AO7YMaeX2W5xB5iIA+BQqOPCDPEAx34dd2A== "@tiptap/extension-placeholder@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.0-beta.220.tgz#1d6057e5ae950d9a1ed43c03d26df60c08368f87" - integrity sha512-Pq79BH/JqhjTNgxHkmbzcmwATsSJdRRSLHrnLx5upSmwEkQwCzqni9jL10rL2NM1ZyR+o25xC+r5loujx0aQ+Q== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.3.tgz#69575353f09fc7524c9cdbfbf16c04f73c29d154" + integrity sha512-Z42jo0termRAf0S0L8oxrts94IWX5waU4isS2CUw8xCUigYyCFslkhQXkWATO1qRbjNFLKN2C9qvCgGf4UeBrw== "@tiptap/extension-text@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.220.tgz#3f51d4aac11c16d79cf8ca22502898b67f5bc2f5" - integrity sha512-3tnffc2YMjNyv7Lbad6fx9wYDE/Buz8vhx76M2AOSrjYbzmTJf7mLkgdlPM0VTy7FGZD5CGgHJAgYNt5HIqPkQ== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.3.tgz#12b6400a31ac6d35cbaf1822600f4c425457902f" + integrity sha512-LvzChcTCcPSMNLUjZe/A9SHXWGDHtvk73fR7CBqAeNU0MxhBPEBI03GFQ6RzW3xX0CmDmjpZoDxFMB+hDEtW1A== "@tiptap/pm@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.0-beta.220.tgz#04e4c98e4d042ea8d67148ec6676f7078c6bac5a" - integrity sha512-O9mGcmwUpEr630HY9RylIyZJKnpXi3xWINWNiAEfRJ1br5j5pHRoVRJQ1HzU+6+Z+i/8qp3zRHGLTBqihaZETA== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.3.tgz#e8bb47df765fc1b7acd52f2800c52d7ff945c5ec" + integrity sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g== dependencies: prosemirror-changeset "^2.2.0" prosemirror-collab "^1.3.0" @@ -4391,17 +4490,17 @@ prosemirror-view "^1.28.2" "@tiptap/react@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.220.tgz#c79df680ee2002061078704e4f35b232588a4a20" - integrity sha512-AZWaCGjm2FcJWNl1dxRCHOjGYvUV8R39L7tAcnKxHGajOHdFk8JQHc0XbVZhdBi2YgwvwEr7Tw9G2lzi9e6/fg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.3.tgz#4b7155ed4bfe3fa9cb691adbbcf3713173ca7a6c" + integrity sha512-fiAh8Lk+/NBPAR/PE4Kc/aLiBUbUYI/CpAopz8DI9eInNyV8h8LAGa9uFILJQF/TNu0tclJ4rV0sWc7Se0FZMw== dependencies: - "@tiptap/extension-bubble-menu" "^2.0.0-beta.220" - "@tiptap/extension-floating-menu" "^2.0.0-beta.220" + "@tiptap/extension-bubble-menu" "^2.0.3" + "@tiptap/extension-floating-menu" "^2.0.3" "@tiptap/suggestion@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.220.tgz#2dc05f65e89006ffaad9f2b6a3468311a305e5ee" - integrity sha512-lYb2HOAKJLjEBbTx5VXA32wRryQiMwaKkNfr3v6UhlwoNgD6NkCYID08UJbpMV7iM+iFQp9408D/vVWFwvOuKg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.3.tgz#3f25e20f50de6748f2b65a88e264d9b5887ca16a" + integrity sha512-1y3palQStGZq13UtHjouZ50k4sotM+N56cIlFeygIv3gqdai2zGPaPQtqV9FOVVQizXpUbQMTlPSDC5Ej4SPnQ== "@tokenizer/token@^0.3.0": version "0.3.0" @@ -4475,9 +4574,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + version "7.18.5" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" + integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== dependencies: "@babel/types" "^7.3.0" @@ -4497,9 +4596,9 @@ "@types/node" "*" "@types/connect-history-api-fallback@^1.3.5": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" - integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + version "1.5.0" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#9fd20b3974bdc2bcd4ac6567e2e0f6885cb2cf41" + integrity sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig== dependencies: "@types/express-serve-static-core" "*" "@types/node" "*" @@ -4520,36 +4619,32 @@ "@types/estree" "*" "@types/eslint@*", "@types/eslint@^7.29.0 || ^8.4.1": - version "8.21.3" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.3.tgz#5794b3911f0f19e34e3a272c49cbdf48d6f543f2" - integrity sha512-fa7GkppZVEByMWGbTtE5MbmXWJTVbrjjaS8K6uQj+XtuuUv1fsuPAxhygfqLmsb/Ufb3CV8deFCpiMfAgi00Sw== + version "8.37.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.37.0.tgz#29cebc6c2a3ac7fea7113207bf5a828fdf4d7ef1" + integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== dependencies: "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" - integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== - "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.17.33" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" - integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + version "4.17.34" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz#c119e85b75215178bc127de588e93100698ab4cc" + integrity sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" "@types/express@*", "@types/express@^4.17.13": version "4.17.17" @@ -4592,9 +4687,9 @@ integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== "@types/http-proxy@^1.17.8": - version "1.17.10" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.10.tgz#e576c8e4a0cc5c6a138819025a88e167ebb38d6c" - integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== + version "1.17.11" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.11.tgz#0ca21949a5588d55ac2b659b69035c84bd5da293" + integrity sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA== dependencies: "@types/node" "*" @@ -4618,9 +4713,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^29.4.0": - version "29.5.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.0.tgz#337b90bbcfe42158f39c2fb5619ad044bbb518ac" - integrity sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg== + version "29.5.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47" + integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -4701,31 +4796,36 @@ "@types/lodash" "*" "@types/lodash@*": - version "4.14.191" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" - integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + version "4.14.194" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" + integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/node@*": - version "18.15.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" - integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw== + version "18.16.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01" + integrity sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q== "@types/object.omit@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.0.tgz#0d31e1208eac8fe2ad5c9499a1016a8273bbfafc" integrity sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw== -"@types/object.pick@^1.3.1": +"@types/object.pick@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.2.tgz#9eb28118240ad8f658b9c9c6caf35359fdb37150" integrity sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg== @@ -4768,9 +4868,9 @@ "@types/react" "*" "@types/react-native@^0.67.3": - version "0.67.19" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.19.tgz#8f2fb257bd9f7b56b07a98be488aab0d79f087fe" - integrity sha512-tk3D4HtJ4KBmnoOMiPWY5og0m34cwavCPSlV75hMqut2WgcDF9SXvkqZU0RP6qddHwvEstYIJSvSfLMPOak5vQ== + version "0.67.20" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.20.tgz#0a4293613f26e8ea468b7c4403fc8e9886575dce" + integrity sha512-a9i1+mpt4Jcztvqfx6SFQVh12vmd4AX2T8Q1NwBhMAFRQm+BPbXwYMjiO7GUS2H6FAx4qysROWMK/OZlfZQ1qA== dependencies: "@types/react" "^17" @@ -4789,9 +4889,9 @@ "@types/react" "^17" "@types/react@*", "@types/react@^17": - version "17.0.53" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.53.tgz#10d4d5999b8af3d6bc6a9369d7eb953da82442ab" - integrity sha512-1yIpQR2zdYu1Z/dc1OxC+MA6GR240u3gcnP4l6mvj/PJiVaqHsQPmWttsvHsfnhfPbU2FuGmo0wSITPygjBmsw== + version "17.0.58" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.58.tgz#c8bbc82114e5c29001548ebe8ed6c4ba4d3c9fb0" + integrity sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -4810,15 +4910,23 @@ integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== "@types/scheduler@*": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" - integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + version "0.16.3" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" + integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + "@types/serve-index@^1.9.1": version "1.9.1" resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" @@ -4888,21 +4996,21 @@ "@types/yargs-parser" "*" "@types/yargs@^17.0.8": - version "17.0.22" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" - integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== dependencies: "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.30.5", "@typescript-eslint/eslint-plugin@^5.48.2", "@typescript-eslint/eslint-plugin@^5.5.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz#bc2400c3a23305e8c9a9c04aa40933868aaaeb47" - integrity sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg== + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd" + integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.55.0" - "@typescript-eslint/type-utils" "5.55.0" - "@typescript-eslint/utils" "5.55.0" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/type-utils" "5.59.2" + "@typescript-eslint/utils" "5.59.2" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -4911,87 +5019,80 @@ tsutils "^3.21.0" "@typescript-eslint/experimental-utils@^5.0.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.55.0.tgz#ea2dd8737834af3a36b6a7be5bee57f57160c942" - integrity sha512-3ZqXIZhdGyGQAIIGATeMtg7prA6VlyxGtcy5hYIR/3qUqp3t18pWWUYhL9mpsDm7y8F9mr3ISMt83TiqCt7OPQ== + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.2.tgz#c2785247c4c8929cb6946e46280ea44f54d9cf79" + integrity sha512-JLw2UImsjHDuVukpA8Nt+UK7JKE/LQAeV3tU5f7wJo2/NNYVwcakzkWjoYzu/2qzWY/Z9c7zojngNDfecNt92g== dependencies: - "@typescript-eslint/utils" "5.55.0" + "@typescript-eslint/utils" "5.59.2" "@typescript-eslint/parser@^5.30.5", "@typescript-eslint/parser@^5.48.2", "@typescript-eslint/parser@^5.5.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.55.0.tgz#8c96a0b6529708ace1dcfa60f5e6aec0f5ed2262" - integrity sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw== + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8" + integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ== dependencies: - "@typescript-eslint/scope-manager" "5.55.0" - "@typescript-eslint/types" "5.55.0" - "@typescript-eslint/typescript-estree" "5.55.0" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.55.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.55.0.tgz#e863bab4d4183ddce79967fe10ceb6c829791210" - integrity sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw== +"@typescript-eslint/scope-manager@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c" + integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA== dependencies: - "@typescript-eslint/types" "5.55.0" - "@typescript-eslint/visitor-keys" "5.55.0" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" -"@typescript-eslint/type-utils@5.55.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.55.0.tgz#74bf0233523f874738677bb73cb58094210e01e9" - integrity sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA== +"@typescript-eslint/type-utils@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf" + integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ== dependencies: - "@typescript-eslint/typescript-estree" "5.55.0" - "@typescript-eslint/utils" "5.55.0" + "@typescript-eslint/typescript-estree" "5.59.2" + "@typescript-eslint/utils" "5.59.2" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.55.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.55.0.tgz#9830f8d3bcbecf59d12f821e5bc6960baaed41fd" - integrity sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug== +"@typescript-eslint/types@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655" + integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w== -"@typescript-eslint/typescript-estree@5.55.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.55.0.tgz#8db7c8e47ecc03d49b05362b8db6f1345ee7b575" - integrity sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ== +"@typescript-eslint/typescript-estree@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936" + integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q== dependencies: - "@typescript-eslint/types" "5.55.0" - "@typescript-eslint/visitor-keys" "5.55.0" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.55.0", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.43.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.55.0.tgz#34e97322e7ae5b901e7a870aabb01dad90023341" - integrity sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw== +"@typescript-eslint/utils@5.59.2", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4" + integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.55.0" - "@typescript-eslint/types" "5.55.0" - "@typescript-eslint/typescript-estree" "5.55.0" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.55.0": - version "5.55.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.55.0.tgz#01ad414fca8367706d76cdb94adf788dc5b664a2" - integrity sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw== +"@typescript-eslint/visitor-keys@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750" + integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig== dependencies: - "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/types" "5.59.2" eslint-visitor-keys "^3.3.0" -"@ucans/core@0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@ucans/core/-/core-0.11.0.tgz#8201680294d980f2b1f5edfaf77b42a86d3a5688" - integrity sha512-SHX67e313kKBaur5Cp+6WFeOLC7aBhkf1i1jIFpFb9f0f1cvM/lC3mjzOyUBeDg3QwmcN5QSZzaogVFvuVvzvg== - dependencies: - uint8arrays "3.0.0" - "@urql/core@2.3.6": version "2.3.6" resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.6.tgz#ee0a6f8fde02251e9560c5f17dce5cd90f948552" @@ -5001,11 +5102,12 @@ wonka "^4.0.14" "@urql/core@>=2.3.1": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@urql/core/-/core-3.2.2.tgz#2a44015b536d72981822f715c96393d8e0ddc576" - integrity sha512-i046Cz8cZ4xIzGMTyHZrbdgzcFMcKD7+yhCAH5FwWBRjcKrc+RjEOuR9X5AMuBvr8c6IAaE92xAqa4wmlGfWTQ== + version "4.0.7" + resolved "https://registry.yarnpkg.com/@urql/core/-/core-4.0.7.tgz#8918a956f8e2ffbaeb3aae58190d728813de5841" + integrity sha512-UtZ9oSbSFODXzFydgLCXpAQz26KGT1d6uEfcylKphiRWNXSWZi8k7vhJXNceNm/Dn0MiZ+kaaJHKcnGY1jvHRQ== dependencies: - wonka "^6.1.2" + "@0no-co/graphql.web" "^1.0.1" + wonka "^6.3.2" "@urql/exchange-retry@0.3.0": version "0.3.0" @@ -5015,125 +5117,125 @@ "@urql/core" ">=2.3.1" wonka "^4.0.14" -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.5.tgz#6e818036b94548c1fb53b754b5cae3c9b208281c" + integrity sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz#e85dfdb01cad16b812ff166b96806c050555f1b4" + integrity sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz#1e82fa7958c681ddcf4eabef756ce09d49d442d1" + integrity sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz#91381652ea95bb38bbfd270702351c0c89d69fba" + integrity sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz#23380c910d56764957292839006fecbe05e135a9" + integrity sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.5" + "@webassemblyjs/helper-api-error" "1.11.5" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz#e258a25251bc69a52ef817da3001863cc1c24b9f" + integrity sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz#966e855a6fae04d5570ad4ec87fbcf29b42ba78e" + integrity sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-buffer" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/wasm-gen" "1.11.5" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz#b2db1b33ce9c91e34236194c2b5cba9b25ca9d60" + integrity sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.5.tgz#482e44d26b6b949edf042a8525a66c649e38935a" + integrity sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== +"@webassemblyjs/utf8@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.5.tgz#83bef94856e399f3740e8df9f63bc47a987eae1a" + integrity sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ== -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz#93ee10a08037657e21c70de31c47fdad6b522b2d" + integrity sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-buffer" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/helper-wasm-section" "1.11.5" + "@webassemblyjs/wasm-gen" "1.11.5" + "@webassemblyjs/wasm-opt" "1.11.5" + "@webassemblyjs/wasm-parser" "1.11.5" + "@webassemblyjs/wast-printer" "1.11.5" -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== +"@webassemblyjs/wasm-gen@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz#ceb1c82b40bf0cf67a492c53381916756ef7f0b1" + integrity sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/ieee754" "1.11.5" + "@webassemblyjs/leb128" "1.11.5" + "@webassemblyjs/utf8" "1.11.5" -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== +"@webassemblyjs/wasm-opt@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz#b52bac29681fa62487e16d3bb7f0633d5e62ca0a" + integrity sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-buffer" "1.11.5" + "@webassemblyjs/wasm-gen" "1.11.5" + "@webassemblyjs/wasm-parser" "1.11.5" -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== +"@webassemblyjs/wasm-parser@1.11.5", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz#7ba0697ca74c860ea13e3ba226b29617046982e2" + integrity sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-api-error" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/ieee754" "1.11.5" + "@webassemblyjs/leb128" "1.11.5" + "@webassemblyjs/utf8" "1.11.5" -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== +"@webassemblyjs/wast-printer@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz#7a5e9689043f3eca82d544d7be7a8e6373a6fa98" + integrity sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA== dependencies: - "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/ast" "1.11.5" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^2.0.1": @@ -5146,15 +5248,15 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.1.tgz#eed745799c910d20081e06e5177c2b2569f166c0" integrity sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA== -"@webpack-cli/serve@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" - integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== +"@webpack-cli/serve@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.2.tgz#10aa290e44a182c02e173a89452781b1acbc86d9" + integrity sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A== "@xmldom/xmldom@~0.7.0", "@xmldom/xmldom@~0.7.7": - version "0.7.9" - resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.9.tgz#7f9278a50e737920e21b297b8a35286e9942c056" - integrity sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA== + version "0.7.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.10.tgz#b1f4a7dc63ac35b2750847644d5dacf5b4ead12f" + integrity sha512-hb9QhOg5MGmpVkFcoZ9XJMe1em5gd0e2eqqjK87O1dwULedXsnY/Zg/Ju6lcohA+t6jVkmKpe7I1etqhvdRdrQ== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -5227,16 +5329,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-node@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" - integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== - dependencies: - acorn "^7.0.0" - acorn-walk "^7.0.0" - xtend "^4.0.2" - -acorn-walk@^7.0.0, acorn-walk@^7.1.1: +acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== @@ -5246,7 +5339,7 @@ acorn-walk@^8.0.2, acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.0.0, acorn@^7.1.1: +acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -5296,7 +5389,7 @@ ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv-keywords@^5.0.0: +ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== @@ -5313,7 +5406,7 @@ ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.11.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.11.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.9.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -5341,9 +5434,9 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1: type-fest "^0.21.3" ansi-escapes@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.1.0.tgz#f2912cdaa10785f3f51f4b562a2497b885aadc5e" - integrity sha512-bQyg9bzRntwR/8b89DOEhGwctcwCrbWW/TuqTQnpqpy5Fz3aovcOTj5i8NJV6AHc8OGNdMaqdxAWww8pz2kiKg== + version "6.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.0.tgz#8a13ce75286f417f1963487d86ba9f90dccf9947" + integrity sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw== dependencies: type-fest "^3.0.0" @@ -5668,9 +5761,9 @@ await-lock@^2.2.2: integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== axe-core@^4.6.2: - version "4.6.3" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" - integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== + version "4.7.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== axios@^0.24.0: version "0.24.0" @@ -5680,9 +5773,9 @@ axios@^0.24.0: follow-redirects "^1.14.4" axios@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" - integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" @@ -5727,7 +5820,7 @@ babel-jest@^29.2.1, babel-jest@^29.4.2, babel-jest@^29.5.0: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@^8.2.3: +babel-loader@^8.2.3, babel-loader@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== @@ -5745,6 +5838,14 @@ babel-loader@^9.1.2: find-cache-dir "^3.3.2" schema-utils "^4.0.0" +babel-merge@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/babel-merge/-/babel-merge-3.0.0.tgz#9bd368d48116dab18b8f3e8022835479d80f3b50" + integrity sha512-eBOBtHnzt9xvnjpYNI5HmaPp/b2vMveE5XggzqHnQeHJ8mFIBrBv6WZEVIj5jJ2uwTItkqKo9gWzEEcBxEq0yw== + dependencies: + deepmerge "^2.2.1" + object.omit "^3.0.0" + babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -6089,9 +6190,9 @@ body-parser@^1.20.1: unpipe "1.0.0" bonjour-service@^1.0.11: - version "1.1.0" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.0.tgz#424170268d68af26ff83a5c640b95def01803a13" - integrity sha512-LVRinRB3k1/K0XzZ2p58COnWvkQknIY6sf0zF2rpErvcJXpMBttEPQSxK+HEXSS9VmpZlDoDnQWv8ftJT20B0Q== + version "1.1.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.1.tgz#960948fa0e0153f5d26743ab15baf8e33752c135" + integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== dependencies: array-flatten "^2.1.2" dns-equal "^1.0.0" @@ -6390,9 +6491,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001468" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001468.tgz#0101837c6a4e38e6331104c33dcfb3bdf367a4b7" - integrity sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A== + version "1.0.30001482" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz#8b3fad73dc35b2674a5c96df2d4f9f1c561435de" + integrity sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ== case-anything@^2.1.10: version "2.1.10" @@ -6409,6 +6510,27 @@ caseless@^0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +cbor-extract@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" + integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA== + dependencies: + node-gyp-build-optional-packages "5.0.3" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.1.1" + "@cbor-extract/cbor-extract-darwin-x64" "2.1.1" + "@cbor-extract/cbor-extract-linux-arm" "2.1.1" + "@cbor-extract/cbor-extract-linux-arm64" "2.1.1" + "@cbor-extract/cbor-extract-linux-x64" "2.1.1" + "@cbor-extract/cbor-extract-win32-x64" "2.1.1" + +cbor-x@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.2.tgz#ceabc48bda06185de1f3a078bb4a793e6e222de5" + integrity sha512-JArE6xcgj3eo13fpnShO42QFBUuXP2uG12RLeF2Nb+dJcETFYxkUa27gXQrRYp67Ahtaxyfbg+ihc62XTyQqsQ== + optionalDependencies: + cbor-extract "^2.1.1" + cborg@^1.6.0: version "1.10.1" resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.1.tgz#24cfe52c69ec0f66f95e23dc57f2086954c8d718" @@ -6562,9 +6684,9 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.0.0, cli-spinners@^2.5.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" - integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== + version "2.8.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.8.0.tgz#e97a3e2bd00e6d85aa0c13d7f9e3ce236f7787fc" + integrity sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ== cli-width@^2.0.0: version "2.2.1" @@ -6668,7 +6790,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -6700,9 +6822,9 @@ colorette@^1.0.7: integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== colorette@^2.0.10, colorette@^2.0.14: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== combined-stream@^1.0.8: version "1.0.8" @@ -6721,6 +6843,11 @@ commander@2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -6886,21 +7013,21 @@ copy-webpack-plugin@^10.2.0: serialize-javascript "^6.0.0" core-js-compat@^3.25.1: - version "3.29.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.29.1.tgz#15c0fb812ea27c973c18d425099afa50b934b41b" - integrity sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA== + version "3.30.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.1.tgz#961541e22db9c27fc48bfc13a3cafa8734171dfe" + integrity sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw== dependencies: browserslist "^4.21.5" core-js-pure@^3.23.3: - version "3.29.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.29.1.tgz#1be6ca2b8772f6b4df7fc4621743286e676c6162" - integrity sha512-4En6zYVi0i0XlXHVz/bi6l1XDjCqkKRq765NXuX+SnaIatlE96Odt5lMLjdxUiNI1v9OXI5DSLWYPlmTfkTktg== + version "3.30.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.30.1.tgz#7d93dc89e7d47b8ef05d7e79f507b0e99ea77eec" + integrity sha512-nXBEVpmUnNRhz83cHd9JRQC52cTMcuXAmR56+9dSMpRdpeA4I1PX6yjmhd71Eyc/wXNsdBdUDIj1QTIeZpU5Tg== core-js@^3.19.2: - version "3.29.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.1.tgz#40ff3b41588b091aaed19ca1aa5cb111803fa9a6" - integrity sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw== + version "3.30.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.1.tgz#fc9c5adcc541d8e9fa3e381179433cbf795628ba" + integrity sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ== core-util-is@~1.0.0: version "1.0.3" @@ -7023,9 +7150,9 @@ css-blank-pseudo@^3.0.3: postcss-selector-parser "^6.0.9" css-declaration-sorter@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec" - integrity sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w== + version "6.4.0" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz#630618adc21724484b3e9505bce812def44000ad" + integrity sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew== css-has-pseudo@^3.0.4: version "3.0.4" @@ -7141,9 +7268,9 @@ css-what@^6.0.1, css-what@^6.1.0: integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssdb@^7.1.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.4.1.tgz#61d55c0173126689922a219e15e131e4b5caf422" - integrity sha512-0Q8NOMpXJ3iTDDbUv9grcmQAfdDx4qz+fN/+Md2FGbevT+6+bJNQ2LjB2YIUlLbpBTM32idU1Sb+tb/uGt6/XQ== + version "7.5.4" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.5.4.tgz#e34dafee5184d67634604e345e389ca79ac179ea" + integrity sha512-fGD+J6Jlq+aurfE1VDXlLS4Pt0VtNlu2+YgfGOdMxRyl/HQ9bDiHTwSck1Yz8A97Dt/82izSK6Bp/4nVqacOsg== cssesc@^3.0.0: version "3.0.0" @@ -7229,9 +7356,9 @@ cssstyle@^2.3.0: cssom "~0.3.6" csstype@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" - integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== dag-map@~1.0.0: version "1.0.2" @@ -7325,15 +7452,16 @@ dedent@^0.7.0: integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== deep-equal@^2.0.5: - version "2.2.0" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" - integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== + version "2.2.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.1.tgz#c72ab22f3a7d3503a4ca87dde976fe9978816739" + integrity sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ== dependencies: + array-buffer-byte-length "^1.0.0" call-bind "^1.0.2" - es-get-iterator "^1.1.2" - get-intrinsic "^1.1.3" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.0" is-arguments "^1.1.1" - is-array-buffer "^3.0.1" + is-array-buffer "^3.0.2" is-date-object "^1.0.5" is-regex "^1.1.4" is-shared-array-buffer "^1.0.2" @@ -7341,7 +7469,7 @@ deep-equal@^2.0.5: object-is "^1.1.5" object-keys "^1.1.1" object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" + regexp.prototype.flags "^1.5.0" side-channel "^1.0.4" which-boxed-primitive "^1.0.2" which-collection "^1.0.1" @@ -7357,12 +7485,17 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + deepmerge@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.3.0.tgz#d3c47fd6f3a93d517b14426b0628a17b0125f5f7" integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA== -deepmerge@^4.2.2: +deepmerge@^4.2.2, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -7394,7 +7527,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3, define-properties@^1.1.4: +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== @@ -7424,11 +7557,6 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf" - integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== - del@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" @@ -7528,29 +7656,20 @@ detect-port-alt@^1.1.6: address "^1.0.1" debug "^2.6.0" -detective@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034" - integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== - dependencies: - acorn-node "^1.8.2" - defined "^1.0.0" - minimist "^1.2.6" - detox@^20.1.2: - version "20.5.0" - resolved "https://registry.yarnpkg.com/detox/-/detox-20.5.0.tgz#70f1aa7ed4a2b652b5787a806e680fafaab2fcb3" - integrity sha512-iFDqU5UZ5f1usgRowyiauO83ffMqvN7qFdF5+TVJelfcHTIVHRbZwI/D4MjtAnpuowljfwBhN0tYhTSEOMjCmg== + version "20.7.1" + resolved "https://registry.yarnpkg.com/detox/-/detox-20.7.1.tgz#3e3981a8eaa223135ca85d44aa9dc3b742b8ed46" + integrity sha512-a8y+M40g4goqWnyHZnestmVL/EII8Hq4utCK4kuSpvRHWkBA5KuQFlXErfNrrOcqXuXhPqdBnxqO9VsMAlLHFA== dependencies: ajv "^8.6.3" bunyan "^1.8.12" bunyan-debug-stream "^3.1.0" caf "^15.0.1" - chalk "^2.4.2" + chalk "^4.0.0" child-process-promise "^2.2.0" execa "^5.1.1" - find-up "^4.1.0" - fs-extra "^4.0.2" + find-up "^5.0.0" + fs-extra "^11.0.0" funpermaproxy "^1.1.0" glob "^8.0.3" ini "^1.3.4" @@ -7558,7 +7677,7 @@ detox@^20.1.2: lodash "^4.17.11" multi-sort-stream "^1.0.3" multipipe "^4.0.0" - node-ipc "^9.2.1" + node-ipc "9.2.1" proper-lockfile "^3.0.2" resolve-from "^5.0.0" sanitize-filename "^1.6.1" @@ -7573,8 +7692,8 @@ detox@^20.1.2: trace-event-lib "^1.3.1" which "^1.3.1" ws "^7.0.0" - yargs "^16.0.3" - yargs-parser "^20.2.9" + yargs "^17.0.0" + yargs-parser "^21.0.0" yargs-unparser "^2.0.0" did-resolver@^4.0.0: @@ -7620,9 +7739,9 @@ dns-equal@^1.0.0: integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== dns-packet@^5.2.2: - version "5.4.0" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" - integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== + version "5.6.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.0.tgz#2202c947845c7a63c23ece58f2f70ff6ab4c2f7d" + integrity sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ== dependencies: "@leichtgewicht/ip-codec" "^2.0.1" @@ -7704,7 +7823,7 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" -domhandler@^5.0.1, domhandler@^5.0.2: +domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== @@ -7729,13 +7848,13 @@ domutils@^2.5.2, domutils@^2.8.0: domhandler "^4.2.0" domutils@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" - integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== dependencies: dom-serializer "^2.0.0" domelementtype "^2.3.0" - domhandler "^5.0.1" + domhandler "^5.0.3" dot-case@^3.0.4: version "3.0.4" @@ -7809,9 +7928,9 @@ ejs@^3.1.6: jake "^10.8.5" electron-to-chromium@^1.4.284: - version "1.4.333" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.333.tgz#ebb21f860f8a29923717b06ec0cb54e77ed34c04" - integrity sha512-YyE8+GKyGtPEP1/kpvqsdhD6rA/TP1DUFDN4uiU/YI52NzDxmwHkEb3qjId8hLBa5siJvG0sfC3O66501jMruQ== + version "1.4.378" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.378.tgz#73431ffd5fffebc18b4e897fac2e7d4ae6d559d9" + integrity sha512-RfCD26kGStl6+XalfX3DGgt3z2DNwJS5DKRHCpkPq5T/PqpZMPB1moSRXuK9xhkt/sF57LlpzJgNoYl7mO7Z6w== email-validator@^2.0.4: version "2.0.4" @@ -7860,10 +7979,10 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== +enhanced-resolve@^5.13.0: + version "5.13.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz#26d1ecc448c02de997133217b5c1053f34a0a275" + integrity sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -7874,9 +7993,9 @@ entities@^2.0.0: integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== entities@^4.2.0, entities@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" - integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== entities@~3.0.1: version "3.0.1" @@ -7920,7 +8039,7 @@ errorhandler@^1.5.0: accepts "~1.3.7" escape-html "~1.0.3" -es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4: +es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: version "1.21.2" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== @@ -7965,7 +8084,7 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-get-iterator@^1.1.2: +es-get-iterator@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== @@ -7980,10 +8099,10 @@ es-get-iterator@^1.1.2: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" + integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== es-set-tostringtag@^2.0.1: version "2.0.1" @@ -8048,9 +8167,9 @@ escodegen@^2.0.0: source-map "~0.6.1" eslint-config-prettier@^8.5.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz#f1cc58a8afebc50980bd53475451df146c13182d" - integrity sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA== + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" + integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== eslint-config-react-app@^7.0.1: version "7.0.1" @@ -8082,9 +8201,9 @@ eslint-import-resolver-node@^0.3.7: resolve "^1.22.1" eslint-module-utils@^2.7.4: - version "2.7.4" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" - integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== dependencies: debug "^3.2.7" @@ -8188,6 +8307,15 @@ eslint-plugin-react-hooks@^4.3.0, eslint-plugin-react-hooks@^4.6.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== +eslint-plugin-react-native-a11y@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-a11y/-/eslint-plugin-react-native-a11y-3.3.0.tgz#0485a8f18474bf54ec68d004b50167f75ffbf201" + integrity sha512-21bIs/0yROcMq7KtAG+OVNDWAh8M+6scII0iXcO3i9NYHe2xZ443yPs5KSUMSvQJeRLLjuKB7V5saqNjoMWDHA== + dependencies: + "@babel/runtime" "^7.15.4" + ast-types-flow "^0.0.7" + jsx-ast-utils "^3.2.1" + eslint-plugin-react-native-globals@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz#ee1348bc2ceb912303ce6bdbd22e2f045ea86ea2" @@ -8223,11 +8351,11 @@ eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.30.1: string.prototype.matchall "^4.0.8" eslint-plugin-testing-library@^5.0.1: - version "5.10.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.2.tgz#12f231ad9b52b6aef45c801fd00aa129a932e0c2" - integrity sha512-f1DmDWcz5SDM+IpCkEX0lbFqrrTs8HRsEElzDEqN/EBI0hpRj8Cns5+IVANXswE8/LeybIJqPAOQIFu2j5Y5sw== + version "5.10.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.3.tgz#e613fbaf9a145e9eef115d080b32cb488fae622e" + integrity sha512-0yhsKFsjHLud5PM+f2dWr9K3rqYzMy4cSHs3lcmFYMa1CdSzRvHGgXvsFarBjZ41gU8jhTdMIkg8jHLxGJqLqw== dependencies: - "@typescript-eslint/utils" "^5.43.0" + "@typescript-eslint/utils" "^5.58.0" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" @@ -8237,10 +8365,10 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== +eslint-scope@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" + integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -8250,10 +8378,10 @@ eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc" + integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== eslint-webpack-plugin@^3.1.1: version "3.2.0" @@ -8267,14 +8395,14 @@ eslint-webpack-plugin@^3.1.1: schema-utils "^4.0.0" eslint@^8.19.0, eslint@^8.3.0: - version "8.36.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" - integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== + version "8.39.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" + integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.1" - "@eslint/js" "8.36.0" + "@eslint/eslintrc" "^2.0.2" + "@eslint/js" "8.39.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -8284,9 +8412,9 @@ eslint@^8.19.0, eslint@^8.3.0: debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-visitor-keys "^3.3.0" - espree "^9.5.0" + eslint-scope "^7.2.0" + eslint-visitor-keys "^3.4.0" + espree "^9.5.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -8312,14 +8440,14 @@ eslint@^8.19.0, eslint@^8.3.0: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113" - integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw== +espree@^9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4" + integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.0" esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" @@ -8505,22 +8633,22 @@ expo-constants@~14.2.0, expo-constants@~14.2.1: uuid "^3.3.2" expo-dev-client@~2.1.1: - version "2.1.5" - resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-2.1.5.tgz#a0f0a7e319c09813a001c9df1935adef4eb378d5" - integrity sha512-Xcz+4cQhuUgbQ3krEGqjeC6rwVIZsCnOWLHQyuHuiKGtJLJ6CfKHyuCPY53b7c0DI7ThWafKMD3vc78E7ux3TQ== + version "2.1.6" + resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-2.1.6.tgz#b5f614dfcdd2793afda3d57e7fcadc7507ab8158" + integrity sha512-6XJS+giOUBA1onRFsT4rtaTkG96cw0tBrnn8LEW5lAM96mN/bl1IZsmyUmLgKfpE40lqvc9ZuYN3Uv2EwTGS/Q== dependencies: - expo-dev-launcher "2.1.5" - expo-dev-menu "2.1.3" + expo-dev-launcher "2.1.6" + expo-dev-menu "2.1.4" expo-dev-menu-interface "1.1.1" expo-manifests "~0.5.0" expo-updates-interface "~0.9.0" -expo-dev-launcher@2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-2.1.5.tgz#1ed3a407ac8a8f83cd92b0c06e7dcfdfc2dcaf1f" - integrity sha512-zwQ21JBEpL1FCTlJrPv3cOaDH9UN7MDPPx8k1j9i4ZxRMdLLYIDmgGiP/oz5dcLf4Z1yi3Ofur42eDYDkKgRlQ== +expo-dev-launcher@2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-2.1.6.tgz#4be192cfae397b2024947a437c5b65d154270c1b" + integrity sha512-fk2Vb7sJgk++CFfwxuL5A8yZXUghqTOZy0fXqpYBJlskSq2sQr8LPoOrqxEQhnA06/CEzS2OC6FTFo+aY9UkBQ== dependencies: - expo-dev-menu "2.1.3" + expo-dev-menu "2.1.4" resolve-from "^5.0.0" semver "^7.3.5" @@ -8529,10 +8657,10 @@ expo-dev-menu-interface@1.1.1: resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-1.1.1.tgz#8a0d979f62d9a192696f66a77f75d8fab79e604b" integrity sha512-doT+7WrSBnxCcTGZw9QIEZoL+43U4RywbG8XZwbhkcsFWGsh9scp0y/bv3ieFHxRtIdImxbxOoYh7fy1O6g28w== -expo-dev-menu@2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-2.1.3.tgz#e349d157b284e68c3eebec924c9bdc2f22174dbf" - integrity sha512-meQ3irhGNGyx6jKEpHy18WDS7on0iAJSmDnhT3+Jx55Ya+hdIvebF+aHDd4TrE/C5/Hlsn9/Fpm8bFAgmC1xpw== +expo-dev-menu@2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-2.1.4.tgz#8bf8ae605d75199a72b603d7ac246e853b8404ca" + integrity sha512-T9YPrfo3M+tf4kH61wp36QI2XU2FxeG7EMYg1bcF4BjYx4fUs6i/QvxJ32o5eB+96fXraG2bhiv0Q2QlYWU8Tg== dependencies: expo-dev-menu-interface "1.1.1" semver "^7.3.5" @@ -8868,9 +8996,9 @@ fast-text-encoding@^1.0.6: integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== fast-xml-parser@^4.0.12: - version "4.1.3" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.3.tgz#0254ad0d4d27f07e6b48254b068c0c137488dd97" - integrity sha512-LsNDahCiCcJPe8NO7HijcnukHB24tKbfDDA5IILx9dmW3Frb52lhbeX6MPNUSvyGNfav2VTYpJ/OqkRoVLrh2Q== + version "4.2.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.2.tgz#cb7310d1e9cf42d22c687b0fae41f3c926629368" + integrity sha512-DLzIPtQqmvmdq3VUKR7T6omPK/VCRNqgFlGtbESfyhcH2R4I8EzK1/K6E8PkRCK2EabWrUHK32NjYRbEFnnz0Q== dependencies: strnum "^1.0.5" @@ -9109,9 +9237,9 @@ flatted@^3.1.0: integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== flow-parser@0.*: - version "0.202.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.202.0.tgz#534178266d3ceec5368415e59990db97eece5bd0" - integrity sha512-ZiXxSIXK3zPmY3zrzCofFonM2T+/3Jz5QZKJyPVtUERQEJUnYkXBQ+0H3FzyqiyJs+VXqb/UNU6/K6sziVYdxw== + version "0.205.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.205.0.tgz#8756173b6488dedc31ab838e80c8f008d7a44e05" + integrity sha512-ZJ6VuLe/BoqeI4GsF+ZuzlpfGi3FCnBrb4xDYhgEJxRt7SAj3ibRuRSsuJSRcY+lQhPZRPNbNWiQqFMxramUzw== flow-parser@^0.185.0: version "0.185.2" @@ -9228,14 +9356,14 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" - integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== +fs-extra@^11.0.0: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" fs-extra@^8.1.0, fs-extra@~8.1.0: version "8.1.0" @@ -9293,7 +9421,7 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" -functions-have-names@^1.2.2: +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -9786,9 +9914,9 @@ html-to-text@7.1.1: minimist "^1.2.5" html-webpack-plugin@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50" - integrity sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw== + version "5.5.1" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz#826838e31b427f5f7f30971f8d8fa2422dfa6763" + integrity sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -9960,9 +10088,9 @@ immediate@~3.0.5: integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== immer@^9.0.7: - version "9.0.19" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b" - integrity sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ== + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== import-fresh@^2.0.0: version "2.0.0" @@ -10183,9 +10311,9 @@ is-ci@^2.0.0: ci-info "^2.0.0" is-core-module@^2.11.0, is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + version "2.12.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4" + integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== dependencies: has "^1.0.3" @@ -11518,10 +11646,15 @@ jimp-compact@0.16.1: resolved "https://registry.yarnpkg.com/jimp-compact/-/jimp-compact-0.16.1.tgz#9582aea06548a2c1e04dd148d7c3ab92075aefa3" integrity sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww== +jiti@^1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd" + integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg== + joi@^17.2.1: - version "17.8.4" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.8.4.tgz#f2d91ab8acd3cca4079ba70669c65891739234aa" - integrity sha512-jjdRHb5WtL+KgSHvOULQEPPv4kcl+ixd1ybOFQq3rWLgEEqc03QMmilodL0GVJE14U/SQDXkUhQUSZANGDH/AA== + version "17.9.2" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.2.tgz#8b2e4724188369f55451aebd1d0b1d9482470690" + integrity sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw== dependencies: "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" @@ -11557,9 +11690,9 @@ js-queue@2.0.2: easy-stack "^1.0.1" js-sdsl@^4.1.4: - version "4.3.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" - integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430" + integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== js-sha256@^0.9.0: version "0.9.0" @@ -11692,9 +11825,9 @@ jsesc@~0.5.0: integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== json-cycle@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/json-cycle/-/json-cycle-1.3.0.tgz#c4f6f7d926c2979012cba173b06f9cae9e866d3f" - integrity sha512-FD/SedD78LCdSvJaOUQAXseT8oQBb5z6IVYaQaCrVUlu9zOAr1BDdKyVYQaSD/GDsAMrXpKcOyBD4LIl8nfjHw== + version "1.5.0" + resolved "https://registry.yarnpkg.com/json-cycle/-/json-cycle-1.5.0.tgz#b1f1d976eee16cef51d5f3d3b3caece3e90ba23a" + integrity sha512-GOehvd5PO2FeZ5T4c+RxobeT5a1PiGpF4u9/3+UvrMU4bhnVqzJY7hm39wg8PDCqkU91fWGH8qjWR4bn+wgq9w== json-parse-better-errors@^1.0.1: version "1.0.2" @@ -11794,7 +11927,7 @@ jsonwebtoken@^8.5.1: ms "^2.1.1" semver "^5.6.0" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1, jsx-ast-utils@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== @@ -11925,7 +12058,7 @@ lie@3.1.1: dependencies: immediate "~3.0.5" -lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6: +lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== @@ -11943,9 +12076,9 @@ linkify-it@^4.0.1: uc.micro "^1.0.1" linkifyjs@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.0.tgz#0460bfcc37d3348fa80e078d92e7bbc82588db15" - integrity sha512-Ffv8VoY3+ixI1b3aZ3O+jM6x17cOsgwfB1Wq7pkytbo1WlyRp6ZO0YDMqiWT/gQPY/CmtiGuKfzDIVqxh1aCTA== + version "4.1.1" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.1.tgz#73d427e3bbaaf4ca8e71c589ad4ffda11a9a5fde" + integrity sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA== loader-runner@^4.2.0: version "4.3.0" @@ -12280,9 +12413,9 @@ media-typer@0.3.0: integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== memfs@^3.1.2, memfs@^3.4.3: - version "3.4.13" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345" - integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg== + version "3.5.1" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.5.1.tgz#f0cd1e2bfaef58f6fe09bfb9c2288f07fea099ec" + integrity sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA== dependencies: fs-monkey "^1.0.3" @@ -12416,7 +12549,7 @@ metro-minify-uglify@0.73.9: dependencies: uglify-es "^3.1.9" -metro-react-native-babel-preset@0.73.9: +metro-react-native-babel-preset@0.73.9, metro-react-native-babel-preset@^0.73.7: version "0.73.9" resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.9.tgz#ef54637dd20f025197beb49e71309a9c539e73e2" integrity sha512-AoD7v132iYDV4K78yN2OLgTPwtAKn0XlD2pOhzyBxiI8PeXzozhbKyPV7zUOJUPETj+pcEVfuYj5ZN/8+bhbCw== @@ -12460,50 +12593,6 @@ metro-react-native-babel-preset@0.73.9: "@babel/template" "^7.0.0" react-refresh "^0.4.0" -metro-react-native-babel-preset@^0.73.7: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.8.tgz#04908f264f5d99c944ae20b5b11f659431328431" - integrity sha512-spNrcQJTbQntEIqJnCA6yL4S+dzV9fXCk7U+Rm7yJasZ4o4Frn7jP23isu7FlZIp1Azx1+6SbP7SgQM+IP5JgQ== - dependencies: - "@babel/core" "^7.20.0" - "@babel/plugin-proposal-async-generator-functions" "^7.0.0" - "@babel/plugin-proposal-class-properties" "^7.0.0" - "@babel/plugin-proposal-export-default-from" "^7.0.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0" - "@babel/plugin-proposal-object-rest-spread" "^7.0.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" - "@babel/plugin-proposal-optional-chaining" "^7.0.0" - "@babel/plugin-syntax-dynamic-import" "^7.0.0" - "@babel/plugin-syntax-export-default-from" "^7.0.0" - "@babel/plugin-syntax-flow" "^7.18.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0" - "@babel/plugin-syntax-optional-chaining" "^7.0.0" - "@babel/plugin-transform-arrow-functions" "^7.0.0" - "@babel/plugin-transform-async-to-generator" "^7.0.0" - "@babel/plugin-transform-block-scoping" "^7.0.0" - "@babel/plugin-transform-classes" "^7.0.0" - "@babel/plugin-transform-computed-properties" "^7.0.0" - "@babel/plugin-transform-destructuring" "^7.0.0" - "@babel/plugin-transform-flow-strip-types" "^7.0.0" - "@babel/plugin-transform-function-name" "^7.0.0" - "@babel/plugin-transform-literals" "^7.0.0" - "@babel/plugin-transform-modules-commonjs" "^7.0.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0" - "@babel/plugin-transform-parameters" "^7.0.0" - "@babel/plugin-transform-react-display-name" "^7.0.0" - "@babel/plugin-transform-react-jsx" "^7.0.0" - "@babel/plugin-transform-react-jsx-self" "^7.0.0" - "@babel/plugin-transform-react-jsx-source" "^7.0.0" - "@babel/plugin-transform-runtime" "^7.0.0" - "@babel/plugin-transform-shorthand-properties" "^7.0.0" - "@babel/plugin-transform-spread" "^7.0.0" - "@babel/plugin-transform-sticky-regex" "^7.0.0" - "@babel/plugin-transform-template-literals" "^7.0.0" - "@babel/plugin-transform-typescript" "^7.5.0" - "@babel/plugin-transform-unicode-regex" "^7.0.0" - "@babel/template" "^7.0.0" - react-refresh "^0.4.0" - metro-react-native-babel-transformer@0.73.9: version "0.73.9" resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.9.tgz#4f4f0cfa5119bab8b53e722fabaf90687d0cbff0" @@ -12780,9 +12869,9 @@ minipass@^3.0.0, minipass@^3.1.1: yallist "^4.0.0" minipass@^4.0.0: - version "4.2.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.5.tgz#9e0e5256f1e3513f8c34691dd68549e85b2c8ceb" - integrity sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q== + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== minizlib@^2.1.1: version "2.1.2" @@ -12828,9 +12917,9 @@ mobx-utils@^6.0.6: integrity sha512-lzJtxOWgj3Dp2HeXviInV3ZRY4YhThzRHXuy90oKXDH2g+ymJGIts4bdjb7NQuSi34V25cMZoQX7TkHJQuKLOQ== mobx@^6.6.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.8.0.tgz#59051755fdb5c8a9f3f2e0a9b6abaf86bab7f843" - integrity sha512-+o/DrHa4zykFMSKfS8Z+CPSEg5LW9tSNGTuN8o6MF1GKxlfkSHSeJn5UtgxvPkGgaouplnrLXCF+duAsmm6FHQ== + version "6.9.0" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.9.0.tgz#8a894c26417c05bed2cf7499322e589ee9787397" + integrity sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ== moment@^2.19.3: version "2.29.4" @@ -12906,10 +12995,10 @@ nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" - integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== nanomatch@^1.2.9: version "1.2.13" @@ -12989,9 +13078,9 @@ nocache@^3.0.1: integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== node-abi@^3.3.0: - version "3.33.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.33.0.tgz#8b23a0cec84e1c5f5411836de6a9b84bccf26e7f" - integrity sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog== + version "3.40.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.40.0.tgz#51d8ed44534f70ff1357dfbc3a89717b1ceac1b4" + integrity sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA== dependencies: semver "^7.3.5" @@ -13026,6 +13115,11 @@ node-forge@^1, node-forge@^1.2.1, node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-gyp-build-optional-packages@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" + integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== + node-html-parser@^5.2.0: version "5.4.2" resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a" @@ -13039,7 +13133,7 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-ipc@^9.2.1: +node-ipc@9.2.1: version "9.2.1" resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.2.1.tgz#b32f66115f9d6ce841dc4ec2009d6a733f98bb6b" integrity sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ== @@ -13159,9 +13253,9 @@ number-is-nan@^1.0.0: integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== nwsapi@^2.2.0, nwsapi@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" - integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== + version "2.2.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5" + integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g== ob1@0.73.9: version "0.73.9" @@ -13246,14 +13340,15 @@ object.fromentries@^2.0.6: es-abstract "^1.20.4" object.getownpropertydescriptors@^2.1.0: - version "2.1.5" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz#db5a9002489b64eef903df81d6623c07e5b4b4d3" - integrity sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw== + version "2.1.6" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz#5e5c384dd209fa4efffead39e3a0512770ccc312" + integrity sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ== dependencies: array.prototype.reduce "^1.0.5" call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.21.2" + safe-array-concat "^1.0.0" object.hasown@^1.1.2: version "1.1.2" @@ -13808,9 +13903,9 @@ pino-http@^8.2.1, pino-http@^8.3.3: process-warning "^2.0.0" pino-std-serializers@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844" - integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g== + version "6.2.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz#169048c0df3f61352fce56aeb7fb962f1b66ab43" + integrity sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA== pino@^8.0.0, pino@^8.11.0, pino@^8.6.1: version "8.11.0" @@ -14043,10 +14138,10 @@ postcss-image-set-function@^4.0.7: dependencies: postcss-value-parser "^4.2.0" -postcss-import@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" - integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== dependencies: postcss-value-parser "^4.0.0" read-cache "^1.0.0" @@ -14057,7 +14152,7 @@ postcss-initial@^4.0.1: resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== -postcss-js@^4.0.0: +postcss-js@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== @@ -14072,13 +14167,13 @@ postcss-lab-function@^4.2.1: "@csstools/postcss-progressive-custom-properties" "^1.1.0" postcss-value-parser "^4.2.0" -postcss-load-config@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" - integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== +postcss-load-config@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" + integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA== dependencies: lilconfig "^2.0.5" - yaml "^1.10.2" + yaml "^2.1.1" postcss-loader@^6.2.1: version "6.2.1" @@ -14177,12 +14272,12 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735" - integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w== +postcss-nested@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" + integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== dependencies: - postcss-selector-parser "^6.0.10" + postcss-selector-parser "^6.0.11" postcss-nesting@^10.2.0: version "10.2.0" @@ -14386,9 +14481,9 @@ postcss-selector-not@^6.0.1: postcss-selector-parser "^6.0.10" postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== + version "6.0.12" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz#2efae5ffab3c8bfb2b7fbf0c426e3bca616c4abb" + integrity sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -14421,12 +14516,12 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.0.9, postcss@^8.3.5, postcss@^8.4.19, postcss@^8.4.4: - version "8.4.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== +postcss@^8.3.5, postcss@^8.4.19, postcss@^8.4.23, postcss@^8.4.4: + version "8.4.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" + integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== dependencies: - nanoid "^3.3.4" + nanoid "^3.3.6" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -14493,9 +14588,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^2.8.3: - version "2.8.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" - integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== pretty-bytes@5.6.0, pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" @@ -14554,9 +14649,9 @@ process-nextick-args@~2.0.0: integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== process-warning@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.1.0.tgz#1e60e3bfe8183033bbc1e702c2da74f099422d1a" - integrity sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg== + version "2.2.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626" + integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg== process@^0.11.10: version "0.11.10" @@ -14642,9 +14737,9 @@ prosemirror-commands@^1.0.0, prosemirror-commands@^1.3.1: prosemirror-transform "^1.0.0" prosemirror-dropcursor@^1.5.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.7.1.tgz#b6921ef866ca95b6f6c8b197767f60dc39598416" - integrity sha512-GmWk9bAwhfHwA8xmJhBFjPcebxUG9zAPYtqpIr7NTDigWZZEJCgUYyUQeqgyscLr8ZHoh9aeprX9kW7BihUT+w== + version "1.8.0" + resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.0.tgz#7bfa11925e0da41d1db869954fe51e1aa55158e4" + integrity sha512-TZMitR8nlp9Xh42pDYGcWopCoFPmJduoyGJ7FjYM2/7gZKnfD41TIaZN5Q1cQjm6Fm/P5vk/DpVYFhS8kDdigw== dependencies: prosemirror-state "^1.0.0" prosemirror-transform "^1.1.0" @@ -14661,12 +14756,13 @@ prosemirror-gapcursor@^1.3.1: prosemirror-view "^1.0.0" prosemirror-history@^1.0.0, prosemirror-history@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.0.tgz#bf5a1ff7759aca759ddf0c722c2fa5b14fb0ddc1" - integrity sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA== + version "1.3.1" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.1.tgz#d0dba9ed1cc2bce55a45ce9c7c8224e641f276b8" + integrity sha512-YMV/IWBZ+LZSfaNcBbPcaQUiAiJRYFyJW2aapuNzL8nhIRsI7fIO0ykJFSe802+mWeoTsVJ1jxvRWPYqaUqljQ== dependencies: prosemirror-state "^1.2.2" prosemirror-transform "^1.0.0" + prosemirror-view "^1.31.0" rope-sequence "^1.3.0" prosemirror-inputrules@^1.2.0: @@ -14747,13 +14843,13 @@ prosemirror-tables@^1.3.0: prosemirror-view "^1.13.3" prosemirror-trailing-node@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.3.tgz#213fc0e545a434ff3c37b5218a0de69561bf3892" - integrity sha512-lGrjMrn97KWkjQSW/FjdvnhJmqFACmQIyr6lKYApvHitDnKsCoZz6XzrHB7RZYHni/0NxQmZ01p/2vyK2SkvaA== + version "2.0.4" + resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.4.tgz#60febdeb947550ee93a224f2e56dbd5cb2cdd607" + integrity sha512-0Yl9w7IdHkaCdqR+NE3FOucePME4OmiGcybnF1iasarEILP5U8+4xTnl53yafULjmwcg1SrSG65Hg7Zk2H2v3g== dependencies: - "@babel/runtime" "^7.13.10" - "@remirror/core-constants" "^2.0.0" - "@remirror/core-helpers" "^2.0.1" + "@babel/runtime" "^7.21.0" + "@remirror/core-constants" "^2.0.1" + "@remirror/core-helpers" "^2.0.2" escape-string-regexp "^4.0.0" prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.0: @@ -14763,10 +14859,10 @@ prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transfor dependencies: prosemirror-model "^1.0.0" -prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2: - version "1.30.2" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.30.2.tgz#57a9d15c5baa454f0d0f4a3028ddbd9be1e8ed9b" - integrity sha512-nTNzZvalQf9kHeEyO407LiV6DoOs/pXsid88UqW9Vvybo4ozJW2PJhkfZUxCUF1hR/9vJLdhxX84wuw9P9HsXA== +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2, prosemirror-view@^1.31.0: + version "1.31.1" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.31.1.tgz#706611f134018a4dd832110911bdd908e3af92c1" + integrity sha512-9NKJdXnGV4+1qFRi16XFZxpnx6zNok9MEj/HElkqUJ1HtOyKOICffKxqoXUUCAdHrrP+yMDvdXc6wT7GGWBL3A== dependencies: prosemirror-model "^1.16.0" prosemirror-state "^1.0.0" @@ -14809,9 +14905,9 @@ punycode@^2.1.0, punycode@^2.1.1: integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== pure-rand@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.1.tgz#31207dddd15d43f299fdcdb2f572df65030c19af" - integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg== + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== q@^1.1.2: version "1.5.1" @@ -14855,11 +14951,6 @@ quick-format-unescaped@^4.0.3: resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - r2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/r2/-/r2-2.0.1.tgz#94cd802ecfce9a622549c8182032d8e4a2b2e612" @@ -14975,9 +15066,9 @@ react-dev-utils@^12.0.1: text-table "^0.2.0" react-devtools-core@^4.26.1: - version "4.27.2" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.2.tgz#d20fc57e258c656eedabafc2c851d38b33583148" - integrity sha512-8SzmIkpO87alD7Xr6gWIEa1jHkMjawOZ+6egjazlnjB4UUcbnzGDf/vBJ4BzGuWWEM+pzrxuzsPpcMqlQkYK2g== + version "4.27.6" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.6.tgz#e5a613014f7506801ed6c1a97bd0e6316cc9c48a" + integrity sha512-jeFNhEzcSwpiqmw+zix5IFibNEPmUodICN7ClrlRKGktzO/3FMteMb52l1NRUiz/ABSYt9hOZ9IPgVDrg5pyUw== dependencies: shell-quote "^1.6.1" ws "^7" @@ -15133,9 +15224,9 @@ react-native-root-siblings@^4.1.1: integrity sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA== react-native-safe-area-context@^4.4.1: - version "4.5.0" - resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz#9208313236e8f49e1920ac1e2a2c975f03aed284" - integrity sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ== + version "4.5.2" + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.2.tgz#38438c7a52ce2a6a05fc4de6cd3ee47f78a9366e" + integrity sha512-oH4/Dm7/PWOOZtFRiA4HE08lsfA948BRq8Fn7TEndYjoDXFoNdbjQRahXzCV8JGP/tv3qrVNeaDE8rmdRRUOlA== react-native-screens@^3.13.1: version "3.20.0" @@ -15471,14 +15562,14 @@ regex-parser@^2.2.11: resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== -regexp.prototype.flags@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== +regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" + integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" + define-properties "^1.2.0" + functions-have-names "^1.2.3" regexpu-core@^5.3.1: version "5.3.2" @@ -15580,9 +15671,9 @@ requires-port@^1.0.0: integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== reselect@^4.0.0, reselect@^4.1.7: - version "4.1.7" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42" - integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A== + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== resolve-cwd@^3.0.0: version "3.0.0" @@ -15628,16 +15719,16 @@ resolve.exports@^1.1.0: integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== resolve.exports@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.1.tgz#cee884cd4e3f355660e501fa3276b27d7ffe5a20" - integrity sha512-OEJWVeimw8mgQuj3HfkNl4KqRevH7lzeQNaWRPfx0PPse7Jk6ozcsG4FKVgtzDsC1KUF+YlTHh17NcgHOPykLw== + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.1.7, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== +resolve@^1.1.7, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.11.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -15740,9 +15831,9 @@ rn-fetch-blob@^0.12.0: glob "7.0.6" roarr@^7.0.4: - version "7.14.3" - resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.14.3.tgz#ff163bf9488222f327ee65cdee18018e790eb645" - integrity sha512-AvUQY27C6/biXEAyYUXc8ONBtP1cA3MQM88e24Fmsl3LAqtNR309nMaWFALYk7ORTqgGrgrjBJ1vE20DZAc5qA== + version "7.15.0" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.0.tgz#09b792f0cd31b4a7f91030bb1c47550ceec98ee4" + integrity sha512-CV9WefQfUXTX6wr8CrEMhfNef3sjIt9wNhE/5PNu4tNWsaoDNDXqq+OGn/RW9A1UPb0qc7FQlswXRaJJJsqn8A== dependencies: boolean "^3.1.4" fast-json-stringify "^2.7.10" @@ -15798,12 +15889,22 @@ rxjs@^6.4.0: tslib "^1.9.0" rxjs@^7.5.2: - version "7.8.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" + integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -15909,24 +16010,24 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" + integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== dependencies: "@types/json-schema" "^7.0.8" ajv "^6.12.5" ajv-keywords "^3.5.2" schema-utils@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + version "4.0.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.1.tgz#eb2d042df8b01f4b5c276a2dfd41ba0faab72e8d" + integrity sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ== dependencies: "@types/json-schema" "^7.0.9" - ajv "^8.8.0" + ajv "^8.9.0" ajv-formats "^2.1.1" - ajv-keywords "^5.0.0" + ajv-keywords "^5.1.0" select-hose@^2.0.0: version "2.0.0" @@ -15950,7 +16051,7 @@ semver@7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -semver@7.3.8, semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@~7.3.2: +semver@7.3.8, semver@~7.3.2: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -15967,6 +16068,13 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: + version "7.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" + integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0, send@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -16140,11 +16248,16 @@ shell-quote@1.7.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== -shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3: +shell-quote@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba" integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== +shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -16210,9 +16323,9 @@ slash@^4.0.0: integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== slash@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-5.0.0.tgz#8c18a871096b71ee0e002976a4fe3374991c3074" - integrity sha512-n6KkmvKS0623igEVj3FF0OZs1gYYJ0o0Hj939yc1fyxl2xt+xYpLnzJB6xBSqOfV9ZFLEWodBBN/heZJahuIJQ== + version "5.0.1" + resolved "https://registry.yarnpkg.com/slash/-/slash-5.0.1.tgz#c354c3a49c0d3b4da1cb0bbeb15a85c2a6defa71" + integrity sha512-ywNzUOiXwetmLvTUiCBZpLi+vxqN3i+zDqjs2HHfUSV3wN4UJxVVKWrS1JZDeiJIeBFNgB5pmioC2g0IUTL+rQ== slice-ansi@^2.0.0: version "2.1.0" @@ -16224,9 +16337,9 @@ slice-ansi@^2.0.0: is-fullwidth-code-point "^2.0.0" slugify@^1.3.4: - version "1.6.5" - resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8" - integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ== + version "1.6.6" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b" + integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw== snapdragon-node@^2.0.1: version "2.1.1" @@ -16268,9 +16381,9 @@ sockjs@^0.3.24: websocket-driver "^0.7.4" sonic-boom@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.1.tgz#972ceab831b5840a08a002fa95a672008bda1c38" - integrity sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A== + version "3.3.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c" + integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g== dependencies: atomic-sleep "^1.0.0" @@ -16393,9 +16506,9 @@ split-string@^3.0.1, split-string@^3.0.2: extend-shallow "^3.0.0" split2@^4.0.0, split2@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" - integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== split@^1.0.1: version "1.0.1" @@ -16721,11 +16834,12 @@ styleq@^0.1.2: resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.1.3.tgz#8efb2892debd51ce7b31dc09c227ad920decab71" integrity sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA== -sucrase@^3.20.0: - version "3.29.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.29.0.tgz#3207c5bc1b980fdae1e539df3f8a8a518236da7d" - integrity sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A== +sucrase@^3.20.0, sucrase@^3.32.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.32.0.tgz#c4a95e0f1e18b6847127258a75cf360bc568d4a7" + integrity sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ== dependencies: + "@jridgewell/gen-mapping" "^0.3.2" commander "^4.0.0" glob "7.1.6" lines-and-columns "^1.1.6" @@ -16825,33 +16939,33 @@ symbol-tree@^3.2.4: integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== tailwindcss@^3.0.2: - version "3.2.7" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.7.tgz#5936dd08c250b05180f0944500c01dce19188c07" - integrity sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ== + version "3.3.2" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.2.tgz#2f9e35d715fdf0bbf674d90147a0684d7054a2d3" + integrity sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w== dependencies: + "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" chokidar "^3.5.3" - color-name "^1.1.4" - detective "^5.2.1" didyoumean "^1.2.2" dlv "^1.1.3" fast-glob "^3.2.12" glob-parent "^6.0.2" is-glob "^4.0.3" - lilconfig "^2.0.6" + jiti "^1.18.2" + lilconfig "^2.1.0" micromatch "^4.0.5" normalize-path "^3.0.0" object-hash "^3.0.0" picocolors "^1.0.0" - postcss "^8.0.9" - postcss-import "^14.1.0" - postcss-js "^4.0.0" - postcss-load-config "^3.1.4" - postcss-nested "6.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" - quick-lru "^5.1.1" - resolve "^1.22.1" + resolve "^1.22.2" + sucrase "^3.32.0" tapable@^1.0.0: version "1.1.3" @@ -16974,7 +17088,7 @@ terminal-link@^2.0.0, terminal-link@^2.1.1: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.0: +terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.0, terser-webpack-plugin@^5.3.7: version "5.3.7" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7" integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== @@ -16986,9 +17100,9 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5, terser-webpack-plugi terser "^5.16.5" terser@^5.0.0, terser@^5.10.0, terser@^5.15.0, terser@^5.16.5: - version "5.16.6" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.6.tgz#f6c7a14a378ee0630fbe3ac8d1f41b4681109533" - integrity sha512-IBZ+ZQIA9sMaXmRZCUMDjNH0D5AQQfdn4WUjHL0+1lF4TP1IHRJbrhb6fNaXWikrYQTSkb7SLxkeXAiy1p7mbg== + version "5.17.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69" + integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -17315,15 +17429,15 @@ type-fest@^0.7.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== -type-fest@^2.0.0, type-fest@^2.3.3: +type-fest@^2.19.0, type-fest@^2.3.3: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== type-fest@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.6.1.tgz#cf8025edeebfd6cf48de73573a5e1423350b9993" - integrity sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA== + version "3.9.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.9.0.tgz#36a9e46e6583649f9e6098b267bc577275e9e4f4" + integrity sha512-hR8JP2e8UiH7SME5JZjsobBlEiatFoxpzCP+R3ZeCo7kAaG1jXQE5X/buLzogM6GJu8le9Y4OcfNuIQX0rZskA== type-is@~1.6.18: version "1.6.18" @@ -17361,12 +17475,7 @@ typescript@^4.4.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -ua-parser-js@^0.7.30: - version "0.7.34" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.34.tgz#afb439e2e3e394bdc90080acb661a39c685b67d7" - integrity sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ== - -ua-parser-js@^0.7.33: +ua-parser-js@^0.7.30, ua-parser-js@^0.7.33: version "0.7.35" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== @@ -17521,9 +17630,9 @@ upath@^1.2.0: integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -17571,9 +17680,9 @@ url-parse@^1.5.3, url-parse@^1.5.9: requires-port "^1.0.0" use-latest-callback@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51" - integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ== + version "0.1.6" + resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.6.tgz#3fa6e7babbb5f9bfa24b5094b22939e1e92ebcf6" + integrity sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg== use-sync-external-store@^1.0.0: version "1.2.0" @@ -17776,16 +17885,16 @@ webidl-conversions@^7.0.0: integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== webpack-cli@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.0.1.tgz#95fc0495ac4065e9423a722dec9175560b6f2d9a" - integrity sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A== + version "5.0.2" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.0.2.tgz#2954c10ecb61c5d4dad6f68ee2d77f051741946c" + integrity sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ== dependencies: "@discoveryjs/json-ext" "^0.5.0" "@webpack-cli/configtest" "^2.0.1" "@webpack-cli/info" "^2.0.1" - "@webpack-cli/serve" "^2.0.1" + "@webpack-cli/serve" "^2.0.2" colorette "^2.0.14" - commander "^9.4.1" + commander "^10.0.1" cross-spawn "^7.0.3" envinfo "^7.7.3" fastest-levenshtein "^1.0.12" @@ -17806,9 +17915,9 @@ webpack-dev-middleware@^5.3.1: schema-utils "^4.0.0" webpack-dev-server@^4.11.1, webpack-dev-server@^4.6.0: - version "4.13.1" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.13.1.tgz#6417a9b5d2f528e7644b68d6ed335e392dccffe8" - integrity sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA== + version "4.13.3" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz#9feb740b8b56b886260bae1360286818a221bae8" + integrity sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug== dependencies: "@types/bonjour" "^3.5.9" "@types/connect-history-api-fallback" "^1.3.5" @@ -17879,21 +17988,21 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4, webpack@^5.75.0: - version "5.76.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.2.tgz#6f80d1c1d1e3bf704db571b2504a0461fac80230" - integrity sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w== + version "5.81.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.81.0.tgz#27a2e8466c8b4820d800a8d90f06ef98294f9956" + integrity sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q== dependencies: "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" acorn "^8.7.1" acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.13.0" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" @@ -17902,9 +18011,9 @@ webpack@^5.64.4, webpack@^5.75.0: loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.1.2" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" + terser-webpack-plugin "^5.3.7" watchpack "^2.4.0" webpack-sources "^3.2.3" @@ -18016,9 +18125,9 @@ which-collection@^1.0.1: is-weakset "^2.0.1" which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== which-typed-array@^1.1.9: version "1.1.9" @@ -18054,19 +18163,19 @@ wide-align@^1.1.0: string-width "^1.0.2 || 2 || 3 || 4" wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== wonka@^4.0.14: version "4.0.15" resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.15.tgz#9aa42046efa424565ab8f8f451fcca955bf80b89" integrity sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg== -wonka@^6.1.2: - version "6.2.5" - resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.2.5.tgz#26e54a6827b96a6164b845106f4d925ede4089bb" - integrity sha512-adhGYKm5xWIZYXRkzEqHbRbRl2gXHqOudjQJMXpRgSyboFmaKOjGm3RIThBk4tZdiZx1DXuKK0H9wKBgXHhzZg== +wonka@^6.3.2: + version "6.3.2" + resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.2.tgz#6f32992b332251d7b696b038990f4dc284b3b33d" + integrity sha512-2xXbQ1LnwNS7egVm1HPhW2FyKrekolzhpM3mCwXdQr55gO+tAiY76rhb32OL9kKsW8taj++iP7C6hxlVzbnvrw== word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" @@ -18309,7 +18418,7 @@ ws@^7, ws@^7.0.0, ws@^7.4.6, ws@^7.5.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.11.0, ws@^8.12.1, ws@^8.13.0: +ws@^8.11.0, ws@^8.12.0, ws@^8.12.1, ws@^8.13.0: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== @@ -18372,7 +18481,7 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -18407,6 +18516,11 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.1.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" + integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -18415,12 +18529,12 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2, yargs-parser@^20.2.9: +yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.1.1: +yargs-parser@^21.0.0, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -18452,7 +18566,7 @@ yargs@^15.1.0: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.3, yargs@^16.2.0: +yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -18465,10 +18579,10 @@ yargs@^16.0.3, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.3.1, yargs@^17.5.1: - version "17.7.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" - integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== +yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1" From 74fbb4797922cfba654ad597803dfdd57598ae8d Mon Sep 17 00:00:00 2001 From: Ollie H Date: Mon, 1 May 2023 21:29:13 -0700 Subject: [PATCH 059/374] Strip whitespaces from tokens (#558) --- src/view/com/auth/login/Login.tsx | 3 ++- src/view/com/modals/DeleteAccount.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index 37558fb546..cec08192c2 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -704,8 +704,9 @@ const SetNewPasswordForm = ({ try { const agent = new BskyAgent({service: serviceUrl}) + const token = resetCode.replace(/\s/g, '') await agent.com.atproto.server.resetPassword({ - token: resetCode, + token, password, }) onPasswordSet() diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index f1febc2eab..724cefee8a 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -42,11 +42,13 @@ export function Component({}: {}) { const onPressConfirmDelete = async () => { setError('') setIsProcessing(true) + const token = confirmCode.replace(/\s/g, '') + try { await store.agent.com.atproto.server.deleteAccount({ did: store.me.did, password, - token: confirmCode, + token, }) Toast.show('Your account has been deleted') resetToTab('HomeTab') From bd80db619b5a8c35171a8c70bdbab33d53a81371 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Tue, 2 May 2023 14:27:00 -0700 Subject: [PATCH 060/374] Pre-web image changes refactor (#560) * Pre-web image changes refactor * Remove unneeded async behavior --- src/lib/media/alt-text.ts | 20 ++++++-------------- src/state/models/media/image.ts | 13 ++----------- src/state/models/ui/shell.ts | 4 ++-- src/view/com/composer/photos/Gallery.tsx | 7 +++++-- src/view/com/modals/AltImage.tsx | 13 +++++++------ 5 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/lib/media/alt-text.ts b/src/lib/media/alt-text.ts index 77b0be4461..4109f667a0 100644 --- a/src/lib/media/alt-text.ts +++ b/src/lib/media/alt-text.ts @@ -1,20 +1,12 @@ import {RootStoreModel} from 'state/index' +import {ImageModel} from 'state/models/media/image' export async function openAltTextModal( store: RootStoreModel, - prevAltText: string, -): Promise { - return new Promise((resolve, reject) => { - store.shell.openModal({ - name: 'alt-text-image', - prevAltText, - onAltTextSet: (altText?: string) => { - if (altText) { - resolve(altText) - } else { - reject(new Error('Canceled')) - } - }, - }) + image: ImageModel, +) { + store.shell.openModal({ + name: 'alt-text-image', + image, }) } diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index d989380d17..dcd47665c3 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -5,7 +5,6 @@ import {makeAutoObservable, runInAction} from 'mobx' import {openCropper} from 'lib/media/picker' import {POST_IMG_MAX} from 'lib/constants' import {scaleDownDimensions} from 'lib/media/util' -import {openAltTextModal} from 'lib/media/alt-text' // TODO: EXIF embed // Cases to consider: ExternalEmbed @@ -43,16 +42,8 @@ export class ImageModel implements RNImage { this.scaledHeight = height } - async setAltText() { - try { - const altText = await openAltTextModal(this.rootStore, this.altText) - - runInAction(() => { - this.altText = altText - }) - } catch (err) { - this.rootStore.log.error('Failed to set alt text', err) - } + async setAltText(altText: string) { + this.altText = altText } async crop() { diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 98e98ef8eb..5942ec1003 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -4,6 +4,7 @@ import {makeAutoObservable} from 'mobx' import {ProfileModel} from '../content/profile' import {isObj, hasProp} from 'lib/type-guards' import {Image as RNImage} from 'react-native-image-crop-picker' +import {ImageModel} from '../media/image' export interface ConfirmModal { name: 'confirm' @@ -43,8 +44,7 @@ export interface CropImageModal { export interface AltTextImageModal { name: 'alt-text-image' - prevAltText: string - onAltTextSet: (altText?: string) => void + image: ImageModel } export interface AltTextImageReadModal { diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index e2d95b2a4a..1aa0aef7a6 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -9,12 +9,15 @@ import {ImageModel} from 'state/models/media/image' import {Image} from 'expo-image' import {Text} from 'view/com/util/text/Text' import {isDesktopWeb} from 'platform/detection' +import {openAltTextModal} from 'lib/media/alt-text' +import {useStores} from 'state/index' interface Props { gallery: GalleryModel } export const Gallery = observer(function ({gallery}: Props) { + const store = useStores() const getImageStyle = useCallback(() => { let side: number @@ -34,9 +37,9 @@ export const Gallery = observer(function ({gallery}: Props) { const handleAddImageAltText = useCallback( (image: ImageModel) => { Keyboard.dismiss() - gallery.setAltText(image) + openAltTextModal(store, image) }, - [gallery], + [store], ) const handleRemovePhoto = useCallback( (image: ImageModel) => { diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index ba05a7d624..ce0a675a90 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -11,24 +11,25 @@ import {TouchableOpacity} from 'react-native-gesture-handler' import LinearGradient from 'react-native-linear-gradient' import {useStores} from 'state/index' import {isDesktopWeb} from 'platform/detection' +import {ImageModel} from 'state/models/media/image' export const snapPoints = ['80%'] interface Props { - prevAltText: string - onAltTextSet: (altText?: string | undefined) => void + image: ImageModel } -export function Component({prevAltText, onAltTextSet}: Props) { +export function Component({image}: Props) { const pal = usePalette('default') const store = useStores() const theme = useTheme() - const [altText, setAltText] = useState(prevAltText) + const [altText, setAltText] = useState(image.altText) const onPressSave = useCallback(() => { - onAltTextSet(altText) + setAltText(altText) + image.setAltText(altText) store.shell.closeModal() - }, [store, altText, onAltTextSet]) + }, [store, image, altText]) const onPressCancel = () => { store.shell.closeModal() From ddb8ebb4125e8fff784ad9c162645ed869817d3a Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 2 May 2023 20:00:22 -0500 Subject: [PATCH 061/374] Fix image sharing on iOS (#561) --- src/lib/media/manip.ts | 48 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 3b3ff8c6d8..4491010e85 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -1,13 +1,13 @@ import RNFetchBlob from 'rn-fetch-blob' import ImageResizer from '@bam.tech/react-native-image-resizer' -import {Image as RNImage} from 'react-native' +import {Image as RNImage, Share as RNShare} from 'react-native' import {Image} from 'react-native-image-crop-picker' -import RNFS from 'react-native-fs' +import * as RNFS from 'react-native-fs' import uuid from 'react-native-uuid' import * as Sharing from 'expo-sharing' import {Dimensions} from './types' import {POST_IMG_MAX} from 'lib/constants' -import {isAndroid} from 'platform/detection' +import {isAndroid, isIOS} from 'platform/detection' export async function compressAndResizeImageForPost( image: Image, @@ -128,11 +128,26 @@ export async function saveImageModal({uri}: {uri: string}) { fileCache: true, }).fetch('GET', uri) + // NOTE + // assuming PNG + // we're currently relying on the fact our CDN only serves pngs + // -prf + let imagePath = downloadResponse.path() - await Sharing.shareAsync(normalizePath(imagePath, true), { - mimeType: 'image/png', - UTI: 'public.png', - }) + imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) + + // NOTE + // for some reason expo-sharing refuses to work on iOS + // ...and visa versa + // -prf + if (isIOS) { + await RNShare.share({url: imagePath}) + } else { + await Sharing.shareAsync(imagePath, { + mimeType: 'image/png', + UTI: 'image/png', + }) + } RNFS.unlink(imagePath) } @@ -187,7 +202,7 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise { ) } -async function moveToPermanentPath(path: string): Promise { +async function moveToPermanentPath(path: string, ext = ''): Promise { /* Since this package stores images in a temp directory, we need to move the file to a permanent location. Relevant: IOS bug when trying to open a second time: @@ -195,11 +210,26 @@ async function moveToPermanentPath(path: string): Promise { */ const filename = uuid.v4() - const destinationPath = `${RNFS.TemporaryDirectoryPath}/${filename}` + const destinationPath = joinPath( + RNFS.TemporaryDirectoryPath, + `${filename}${ext}`, + ) await RNFS.moveFile(path, destinationPath) return normalizePath(destinationPath) } +function joinPath(a: string, b: string) { + if (a.endsWith('/')) { + if (b.startsWith('/')) { + return a.slice(0, -1) + b + } + return a + b + } else if (b.startsWith('/')) { + return a + b + } + return a + '/' + b +} + function normalizePath(str: string, allPlatforms = false): string { if (isAndroid || allPlatforms) { if (!str.startsWith('file://')) { From d225e857b5eadca46b75947e8ec3606129312de7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 2 May 2023 20:03:01 -0500 Subject: [PATCH 062/374] [APP-610] Make the language filter more lenient (#562) * Tune the language filter to accept posts when a determination cant be made * use j instead of i since i has been declared in upper scope * use j instead of i since i has been declared in upper scope * Pass the j man --------- Co-authored-by: Ansh Nanda --- src/lib/api/feed-manip.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 2429419d88..341e8727df 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -203,11 +203,25 @@ export class FeedTuner { typeof item.post.record.text === 'string' ) { const res = lande(item.post.record.text) - const contentLangCode3 = res[0][0] - if (langsCode3.includes(contentLangCode3)) { + + // require at least 70% confidence; otherwise, roll with it + if (res[0][1] <= 0.7) { hasPreferredLang = true break } + + // if the user's languages are in the top 5 guesses, roll with it + for (let j = 0; j < 5 && j < res.length; j++) { + hasPreferredLang = + hasPreferredLang || langsCode3.includes(res[i][0]) + } + if (hasPreferredLang) { + break + } + } else { + // no text? roll with it + hasPreferredLang = true + break } } if (!hasPreferredLang) { From 8c675248d45c01db255a63cd98dc58868ec329fa Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 2 May 2023 21:51:25 -0500 Subject: [PATCH 063/374] Fix replies with <2 likes showing in the following feed --- src/lib/api/feed-manip.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 341e8727df..60e7550488 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -1,4 +1,4 @@ -import {AppBskyFeedDefs} from '@atproto/api' +import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' import lande from 'lande' import {hasProp} from 'lib/type-guards' import {LANGUAGES_MAP_CODE2} from '../../locale/languages' @@ -48,6 +48,13 @@ export class FeedViewPostsSlice { return this.items[0] } + get isReply() { + return ( + AppBskyFeedPost.isRecord(this.rootItem.post.record) && + !!this.rootItem.post.record.reply + ) + } + containsUri(uri: string) { return !!this.items.find(item => item.post.uri === uri) } @@ -176,9 +183,10 @@ export class FeedTuner { ): FeedViewPostsSlice[] { // remove any replies without at least 2 likes for (let i = slices.length - 1; i >= 0; i--) { - if (slices[i].isFullThread || !slices[i].rootItem.reply) { + if (slices[i].isFullThread || !slices[i].isReply) { continue } + const item = slices[i].rootItem const isRepost = Boolean(item.reason) if (!isRepost && (item.post.likeCount || 0) < 2) { From af905947bc4835cfff6f748851c95ac75cb7fb23 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 2 May 2023 22:52:58 -0500 Subject: [PATCH 064/374] Fix confirm profile sizing for blocks (#564) --- src/view/com/modals/Confirm.tsx | 2 +- src/view/com/profile/ProfileHeader.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index f0c905d044..c508c2d3a5 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -13,7 +13,7 @@ import {cleanError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' import {isDesktopWeb} from 'platform/detection' -export const snapPoints = [300] +export const snapPoints = ['50%'] export function Component({ title, diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index d8c4b9d8fc..10f648e3d0 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -180,7 +180,7 @@ const ProfileHeaderLoaded = observer( name: 'confirm', title: 'Block Account', message: - 'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours.', + 'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.', onPressConfirm: async () => { try { await view.blockAccount() @@ -200,7 +200,7 @@ const ProfileHeaderLoaded = observer( name: 'confirm', title: 'Unblock Account', message: - 'The account will be able to interact with you after unblocking. (You can always block again in the future.)', + 'The account will be able to interact with you after unblocking.', onPressConfirm: async () => { try { await view.unblockAccount() From 95f8360d19938d00aaf9036a76616a7b82093703 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Tue, 2 May 2023 21:00:18 -0700 Subject: [PATCH 065/374] Add keyboard shortcuts: new, escape, and hard break (#552) * Add keyboard shortcuts: new, escape, and hard break * Add preferences modal * Remove code accidentally re-added due to rebase * Fix incorrect copy and lint * Put stuff back so diffs are clearer * Re-add invite codes to settings * Address comments * Tune the copy --------- Co-authored-by: Paul Frazee --- package.json | 2 ++ src/state/models/ui/shell.ts | 3 ++- src/view/com/composer/Composer.tsx | 24 +++++++++++++++++ .../com/composer/text-input/TextInput.web.tsx | 2 ++ src/view/com/modals/AltImageRead.tsx | 4 +-- src/view/com/modals/Confirm.tsx | 27 ++++++++++++++++--- src/view/com/util/forms/ToggleButton.tsx | 10 ++++--- src/view/screens/Settings.tsx | 22 +++++---------- yarn.lock | 10 +++++++ 9 files changed, 78 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 92077f4a1b..e34c52e1ca 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@sentry/react-native": "4.13.0", "@tiptap/core": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220", + "@tiptap/extension-hard-break": "^2.0.3", "@tiptap/extension-history": "^2.0.3", "@tiptap/extension-link": "^2.0.0-beta.220", "@tiptap/extension-mention": "^2.0.0-beta.220", @@ -58,6 +59,7 @@ "@tiptap/pm": "^2.0.0-beta.220", "@tiptap/react": "^2.0.0-beta.220", "@tiptap/suggestion": "^2.0.0-beta.220", + "@types/node": "^18.16.2", "@zxing/text-encoding": "^0.9.0", "await-lock": "^2.2.2", "base64-js": "^1.5.1", diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 5942ec1003..0b0da0001c 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -11,6 +11,7 @@ export interface ConfirmModal { title: string message: string | (() => JSX.Element) onPressConfirm: () => void | Promise + onPressCancel?: () => void | Promise } export interface EditProfileModal { @@ -86,10 +87,10 @@ export interface ContentFilteringSettingsModal { export type Modal = // Account + | AddAppPasswordModal | ChangeHandleModal | DeleteAccountModal | EditProfileModal - | AddAppPasswordModal // Curation | ContentFilteringSettingsModal diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 45e67d7cb3..5c7594d61f 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -88,6 +88,30 @@ export const ComposePost = observer(function ComposePost({ autocompleteView.setup() }, [autocompleteView]) + const onEscape = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + store.shell.openModal({ + name: 'confirm', + title: 'Cancel draft', + onPressConfirm: onClose, + onPressCancel: () => { + store.shell.closeModal() + }, + message: "Are you sure you'd like to cancel this draft?", + }) + } + }, + [store.shell, onClose], + ) + + useEffect(() => { + if (isDesktopWeb) { + window.addEventListener('keydown', onEscape) + return () => window.removeEventListener('keydown', onEscape) + } + }, [onEscape]) + const onPressAddLinkCard = useCallback( (uri: string) => { setExtLink({uri, isLoading: true}) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 4abedb3e23..b56d306e26 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -4,6 +4,7 @@ import {RichText} from '@atproto/api' import {useEditor, EditorContent, JSONContent} from '@tiptap/react' import {Document} from '@tiptap/extension-document' import History from '@tiptap/extension-history' +import Hardbreak from '@tiptap/extension-hard-break' import {Link} from '@tiptap/extension-link' import {Mention} from '@tiptap/extension-mention' import {Paragraph} from '@tiptap/extension-paragraph' @@ -72,6 +73,7 @@ export const TextInput = React.forwardRef( }), Text, History, + Hardbreak, ], editorProps: { attributes: { diff --git a/src/view/com/modals/AltImageRead.tsx b/src/view/com/modals/AltImageRead.tsx index 4dde8f58b4..985477287e 100644 --- a/src/view/com/modals/AltImageRead.tsx +++ b/src/view/com/modals/AltImageRead.tsx @@ -34,8 +34,8 @@ export function Component({altText}: Props) { testID="altTextImageSaveBtn" onPress={onPress} accessibilityRole="button" - accessibilityLabel="Save" - accessibilityHint="Save alt text"> + accessibilityLabel="Done" + accessibilityHint="Closes alt text modal"> JSX.Element) onPressConfirm: () => void | Promise + onPressCancel?: () => void | Promise }) { const pal = usePalette('default') const store = useStores() @@ -69,12 +71,23 @@ export function Component({ style={[styles.btn]} accessibilityRole="button" accessibilityLabel="Confirm" - // TODO: This needs to be updated so that modal roles are clear; - // Currently there is only one usage for the confirm modal: post deletion - accessibilityHint="Confirms a potentially destructive action"> + accessibilityHint=""> Confirm )} + {onPressCancel === undefined ? null : ( + + + Cancel + + + )} ) } @@ -104,4 +117,12 @@ const styles = StyleSheet.create({ marginHorizontal: 44, backgroundColor: colors.blue3, }, + btnCancel: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 32, + padding: 14, + marginHorizontal: 20, + }, }) diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx index a6e0ba3fec..804d414b3a 100644 --- a/src/view/com/util/forms/ToggleButton.tsx +++ b/src/view/com/util/forms/ToggleButton.tsx @@ -142,9 +142,11 @@ export function ToggleButton({ ]} /> - - {label} - + {label === '' ? null : ( + + {label} + + )} ) @@ -154,6 +156,7 @@ const styles = StyleSheet.create({ outer: { flexDirection: 'row', alignItems: 'center', + gap: 10, }, circle: { width: 42, @@ -161,7 +164,6 @@ const styles = StyleSheet.create({ borderRadius: 15, padding: 4, borderWidth: 1, - marginRight: 10, }, circleFill: { width: 16, diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 4d21f8e2c0..705c37b308 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -34,8 +34,8 @@ import {useCustomPalette} from 'lib/hooks/useCustomPalette' import {AccountData} from 'state/models/session' import {useAnalytics} from 'lib/analytics' import {NavigationProp} from 'lib/routes/types' -import {pluralize} from 'lib/strings/helpers' import {isDesktopWeb} from 'platform/detection' +import {pluralize} from 'lib/strings/helpers' type Props = NativeStackScreenProps export const SettingsScreen = withAuthRequired( @@ -54,6 +54,7 @@ export const SettingsScreen = withAuthRequired( light: {color: colors.blue3}, dark: {color: colors.blue2}, }) + const dangerBg = useCustomPalette({ light: {backgroundColor: colors.red1}, dark: {backgroundColor: colors.red7}, @@ -140,13 +141,12 @@ export const SettingsScreen = withAuthRequired( }, [store]) return ( - + - Signed in as @@ -161,9 +161,7 @@ export const SettingsScreen = withAuthRequired( + noFeedback> @@ -231,9 +229,7 @@ export const SettingsScreen = withAuthRequired( Add account - - Invite a friend @@ -301,9 +297,7 @@ export const SettingsScreen = withAuthRequired( Blocked accounts - - Advanced @@ -338,9 +332,7 @@ export const SettingsScreen = withAuthRequired( Change my handle - - Danger zone @@ -355,16 +347,14 @@ export const SettingsScreen = withAuthRequired( Delete my account - - Developer tools diff --git a/yarn.lock b/yarn.lock index 994ec7fb74..3351f6b1dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4433,6 +4433,11 @@ dependencies: tippy.js "^6.3.7" +"@tiptap/extension-hard-break@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.3.tgz#aa7805d825e5244bdccc508da18c781e231b2859" + integrity sha512-RCln6ARn16jvKTjhkcAD5KzYXYS0xRMc0/LrHeV8TKdCd4Yd0YYHe0PU4F9gAgAfPQn7Dgt4uTVJLN11ICl8sQ== + "@tiptap/extension-history@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.3.tgz#8936c15aa46f2ddeada1c3d9abe2888d58d08c30" @@ -4820,6 +4825,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01" integrity sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q== +"@types/node@^18.16.2": + version "18.16.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.2.tgz#2f610ea71034b3971c312192377f8a7178eb57f1" + integrity sha512-GQW/JL/5Fz/0I8RpeBG9lKp0+aNcXEaVL71c0D2Q0QHDTFvlYKT7an0onCUXj85anv7b4/WesqdfchLc0jtsCg== + "@types/object.omit@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.0.tgz#0d31e1208eac8fe2ad5c9499a1016a8273bbfafc" From 6f1c4ec9a9b5b7d0b4b67f3bc7c3af6810da5001 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 2 May 2023 23:06:55 -0500 Subject: [PATCH 066/374] [APP-549] Language controls for Whats Hot (#563) * Add a content-language preference control * Update whats hot to only show the selected languages and to refresh on lang pref changes * Fix lint * Fix tests * Add missing accessibility role --- __mocks__/expo-localization.js | 1 + src/lib/api/feed-manip.ts | 10 +- src/locale/languages.ts | 2 +- src/state/models/feeds/posts.ts | 12 ++ src/state/models/ui/preferences.ts | 39 +++-- src/state/models/ui/shell.ts | 5 + .../com/modals/ContentFilteringSettings.tsx | 2 +- .../com/modals/ContentLanguagesSettings.tsx | 143 ++++++++++++++++ src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/Modal.web.tsx | 3 + src/view/com/posts/FollowingEmptyState.tsx | 1 - src/view/com/posts/WhatsHotEmptyState.tsx | 76 +++++++++ src/view/screens/Home.tsx | 152 ++++++++++-------- src/view/screens/Settings.tsx | 24 ++- 14 files changed, 381 insertions(+), 93 deletions(-) create mode 100644 __mocks__/expo-localization.js create mode 100644 src/view/com/modals/ContentLanguagesSettings.tsx create mode 100644 src/view/com/posts/WhatsHotEmptyState.tsx diff --git a/__mocks__/expo-localization.js b/__mocks__/expo-localization.js new file mode 100644 index 0000000000..8bd537cf6c --- /dev/null +++ b/__mocks__/expo-localization.js @@ -0,0 +1 @@ +export const getLocales = jest.fn().mockResolvedValue([]) diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 60e7550488..96534d1ba5 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -202,7 +202,9 @@ export class FeedTuner { tuner: FeedTuner, slices: FeedViewPostsSlice[], ): FeedViewPostsSlice[] => { - const origSlices = slices.concat() + if (!langsCode2.length) { + return slices + } for (let i = slices.length - 1; i >= 0; i--) { let hasPreferredLang = false for (const item of slices[i].items) { @@ -236,11 +238,7 @@ export class FeedTuner { slices.splice(i, 1) } } - if (slices.length) { - return slices - } - // fallback: give everything if the language filter left nothing - return origSlices + return slices } } } diff --git a/src/locale/languages.ts b/src/locale/languages.ts index 31c1a9f70a..269e2fa9ab 100644 --- a/src/locale/languages.ts +++ b/src/locale/languages.ts @@ -23,7 +23,7 @@ export const LANGUAGES: Language[] = [ {code3: 'alt', code2: '', name: 'Southern Altai'}, {code3: 'amh', code2: 'am', name: 'Amharic'}, {code3: 'ang', code2: '', name: 'English, Old (ca.450-1100)'}, - {code3: 'anp ', code2: 'Angika', name: 'angika'}, + {code3: 'anp ', code2: 'Angika', name: 'Angika'}, {code3: 'apa', code2: '', name: 'Apache languages'}, {code3: 'ara', code2: 'ar', name: 'Arabic'}, { diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 62047acbab..44cec3af7f 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -297,6 +297,9 @@ export class PostsFeedModel { // used to linearize async modifications to state lock = new AwaitLock() + // used to track if what's hot is coming up empty + emptyFetches = 0 + // data slices: PostsFeedSliceModel[] = [] @@ -603,6 +606,9 @@ export class PostsFeedModel { ) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor + if (replace) { + this.emptyFetches = 0 + } this.rootStore.me.follows.hydrateProfiles( res.data.feed.map(item => item.post.author), @@ -625,6 +631,12 @@ export class PostsFeedModel { } else { this.slices = this.slices.concat(toAppend) } + if (toAppend.length === 0) { + this.emptyFetches++ + if (this.emptyFetches >= 10) { + this.hasMore = false + } + } }) } diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index ae3f712c43..f6b29169d4 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -2,12 +2,9 @@ import {makeAutoObservable} from 'mobx' import {getLocales} from 'expo-localization' import {isObj, hasProp} from 'lib/type-guards' import {ComAtprotoLabelDefs} from '@atproto/api' +import {LabelValGroup} from 'lib/labeling/types' import {getLabelValueGroup} from 'lib/labeling/helpers' -import { - LabelValGroup, - UNKNOWN_LABEL_GROUP, - ILLEGAL_LABEL_GROUP, -} from 'lib/labeling/const' +import {UNKNOWN_LABEL_GROUP, ILLEGAL_LABEL_GROUP} from 'lib/labeling/const' const deviceLocales = getLocales() @@ -28,24 +25,17 @@ export class LabelPreferencesModel { } export class PreferencesModel { - _contentLanguages: string[] | undefined + contentLanguages: string[] = + deviceLocales?.map?.(locale => locale.languageCode) || [] contentLabels = new LabelPreferencesModel() constructor() { makeAutoObservable(this, {}, {autoBind: true}) } - // gives an array of BCP 47 language tags without region codes - get contentLanguages() { - if (this._contentLanguages) { - return this._contentLanguages - } - return deviceLocales.map(locale => locale.languageCode) - } - serialize() { return { - contentLanguages: this._contentLanguages, + contentLanguages: this.contentLanguages, contentLabels: this.contentLabels, } } @@ -57,14 +47,31 @@ export class PreferencesModel { Array.isArray(v.contentLanguages) && typeof v.contentLanguages.every(item => typeof item === 'string') ) { - this._contentLanguages = v.contentLanguages + this.contentLanguages = v.contentLanguages } if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') { Object.assign(this.contentLabels, v.contentLabels) + } else { + // default to the device languages + this.contentLanguages = deviceLocales.map(locale => locale.languageCode) } } } + hasContentLanguage(code2: string) { + return this.contentLanguages.includes(code2) + } + + toggleContentLanguage(code2: string) { + if (this.hasContentLanguage(code2)) { + this.contentLanguages = this.contentLanguages.filter( + lang => lang !== code2, + ) + } else { + this.contentLanguages = this.contentLanguages.concat([code2]) + } + } + setContentLabelPref( key: keyof LabelPreferencesModel, value: LabelPreference, diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 0b0da0001c..dea220c55e 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -85,6 +85,10 @@ export interface ContentFilteringSettingsModal { name: 'content-filtering-settings' } +export interface ContentLanguagesSettingsModal { + name: 'content-languages-settings' +} + export type Modal = // Account | AddAppPasswordModal @@ -94,6 +98,7 @@ export type Modal = // Curation | ContentFilteringSettingsModal + | ContentLanguagesSettingsModal // Reporting | ReportAccountModal diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index c683e43f8f..cfba2575a0 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -21,7 +21,7 @@ export function Component({}: {}) { }, [store]) return ( - + Content Moderation diff --git a/src/view/com/modals/ContentLanguagesSettings.tsx b/src/view/com/modals/ContentLanguagesSettings.tsx new file mode 100644 index 0000000000..0c750fe0ea --- /dev/null +++ b/src/view/com/modals/ContentLanguagesSettings.tsx @@ -0,0 +1,143 @@ +import React from 'react' +import {StyleSheet, Pressable, View} from 'react-native' +import LinearGradient from 'react-native-linear-gradient' +import {observer} from 'mobx-react-lite' +import {ScrollView} from './util' +import {useStores} from 'state/index' +import {ToggleButton} from '../util/forms/ToggleButton' +import {s, colors, gradients} from 'lib/styles' +import {Text} from '../util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' +import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../locale/languages' + +export const snapPoints = ['100%'] + +export function Component({}: {}) { + const store = useStores() + const pal = usePalette('default') + const onPressDone = React.useCallback(() => { + store.shell.closeModal() + }, [store]) + + const languages = React.useMemo(() => { + const langs = LANGUAGES.filter( + lang => + !!lang.code2.trim() && + LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3, + ) + // sort so that selected languages are on top, then alphabetically + langs.sort((a, b) => { + const hasA = store.preferences.hasContentLanguage(a.code2) + const hasB = store.preferences.hasContentLanguage(b.code2) + if (hasA === hasB) return a.name.localeCompare(b.name) + if (hasA) return -1 + return 1 + }) + return langs + }, [store]) + + return ( + + Content Languages + + Which languages would you like to see in the What's Hot feed? (Leave + them all unchecked to see any language.) + + + {languages.map(lang => ( + + ))} + + + + + + Done + + + + + ) +} + +const LanguageToggle = observer( + ({code2, name}: {code2: string; name: string}) => { + const store = useStores() + const pal = usePalette('default') + + const onPress = React.useCallback(() => { + store.preferences.toggleContentLanguage(code2) + }, [store, code2]) + + return ( + + ) + }, +) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingTop: 20, + }, + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + marginBottom: 12, + }, + description: { + textAlign: 'center', + paddingHorizontal: 16, + marginBottom: 10, + }, + scrollContainer: { + flex: 1, + paddingHorizontal: 10, + }, + bottomSpacer: { + height: isDesktopWeb ? 0 : 60, + }, + btnContainer: { + paddingTop: 10, + paddingHorizontal: 10, + paddingBottom: isDesktopWeb ? 0 : 40, + borderTopWidth: isDesktopWeb ? 0 : 1, + }, + + languageToggle: { + borderTopWidth: 1, + borderRadius: 0, + paddingHorizontal: 0, + paddingVertical: 12, + }, + + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + borderRadius: 32, + padding: 14, + backgroundColor: colors.gray1, + }, +}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 2e053e3add..b5d71a116b 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -21,6 +21,7 @@ import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' +import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings' const DEFAULT_SNAPPOINTS = ['90%'] @@ -93,6 +94,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'content-filtering-settings') { snapPoints = ContentFilteringSettingsModal.snapPoints element = + } else if (activeModal?.name === 'content-languages-settings') { + snapPoints = ContentLanguagesSettingsModal.snapPoints + element = } else { return null } diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index e850c9f21d..50487e3eb4 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -21,6 +21,7 @@ import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' +import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings' export const ModalsContainer = observer(function ModalsContainer() { const store = useStores() @@ -84,6 +85,8 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'content-filtering-settings') { element = + } else if (modal.name === 'content-languages-settings') { + element = } else if (modal.name === 'alt-text-image') { element = } else if (modal.name === 'alt-text-image-read') { diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx index acd035f21d..b372981793 100644 --- a/src/view/com/posts/FollowingEmptyState.tsx +++ b/src/view/com/posts/FollowingEmptyState.tsx @@ -48,7 +48,6 @@ export function FollowingEmptyState() { } const styles = StyleSheet.create({ emptyContainer: { - // flex: 1, height: '100%', paddingVertical: 40, paddingHorizontal: 30, diff --git a/src/view/com/posts/WhatsHotEmptyState.tsx b/src/view/com/posts/WhatsHotEmptyState.tsx new file mode 100644 index 0000000000..ade94ca3f3 --- /dev/null +++ b/src/view/com/posts/WhatsHotEmptyState.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {MagnifyingGlassIcon} from 'lib/icons' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' + +export function WhatsHotEmptyState() { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const store = useStores() + + const onPressSettings = React.useCallback(() => { + store.shell.openModal({name: 'content-languages-settings'}) + }, [store]) + + return ( + + + + + + Your What's Hot feed is empty! This is because there aren't enough users + posting in your selected language. + + + + ) +} +const styles = StyleSheet.create({ + emptyContainer: { + height: '100%', + paddingVertical: 40, + paddingHorizontal: 30, + }, + emptyIconContainer: { + marginBottom: 16, + }, + emptyIcon: { + marginLeft: 'auto', + marginRight: 'auto', + }, + emptyBtn: { + marginVertical: 20, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 18, + paddingHorizontal: 24, + borderRadius: 30, + }, + + feedsTip: { + position: 'absolute', + left: 22, + }, + feedsTipArrow: { + marginLeft: 32, + marginTop: 8, + }, +}) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index ba9b05c438..2b102ae314 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -9,6 +9,7 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect' import {Feed} from '../com/posts/Feed' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' +import {WhatsHotEmptyState} from 'view/com/posts/WhatsHotEmptyState' import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' import {FeedsTabBar} from '../com/pager/FeedsTabBar' import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager' @@ -24,80 +25,97 @@ const HEADER_OFFSET = isDesktopWeb ? 50 : 40 const POLL_FREQ = 30e3 // 30sec type Props = NativeStackScreenProps -export const HomeScreen = withAuthRequired((_opts: Props) => { - const store = useStores() - const [selectedPage, setSelectedPage] = React.useState(0) +export const HomeScreen = withAuthRequired( + observer((_opts: Props) => { + const store = useStores() + const [selectedPage, setSelectedPage] = React.useState(0) + const [initialLanguages] = React.useState( + store.preferences.contentLanguages, + ) - const algoFeed = React.useMemo(() => { - const feed = new PostsFeedModel(store, 'goodstuff', {}) - feed.setup() - return feed - }, [store]) + const algoFeed: PostsFeedModel = React.useMemo(() => { + const feed = new PostsFeedModel(store, 'goodstuff', {}) + feed.setup() + return feed + }, [store]) - useFocusEffect( - React.useCallback(() => { - store.shell.setMinimalShellMode(false) - store.shell.setIsDrawerSwipeDisabled(selectedPage > 0) - return () => { - store.shell.setIsDrawerSwipeDisabled(false) + React.useEffect(() => { + // refresh whats hot when lang preferences change + if (initialLanguages !== store.preferences.contentLanguages) { + algoFeed.refresh() } - }, [store, selectedPage]), - ) + }, [initialLanguages, store.preferences.contentLanguages, algoFeed]) - const onPageSelected = React.useCallback( - (index: number) => { - store.shell.setMinimalShellMode(false) - setSelectedPage(index) - store.shell.setIsDrawerSwipeDisabled(index > 0) - }, - [store], - ) + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + store.shell.setIsDrawerSwipeDisabled(selectedPage > 0) + return () => { + store.shell.setIsDrawerSwipeDisabled(false) + } + }, [store, selectedPage]), + ) - const onPressSelected = React.useCallback(() => { - store.emitScreenSoftReset() - }, [store]) + const onPageSelected = React.useCallback( + (index: number) => { + store.shell.setMinimalShellMode(false) + setSelectedPage(index) + store.shell.setIsDrawerSwipeDisabled(index > 0) + }, + [store], + ) - const renderTabBar = React.useCallback( - (props: RenderTabBarFnProps) => { - return ( - { + store.emitScreenSoftReset() + }, [store]) + + const renderTabBar = React.useCallback( + (props: RenderTabBarFnProps) => { + return ( + + ) + }, + [onPressSelected], + ) + + const renderFollowingEmptyState = React.useCallback(() => { + return + }, []) + + const renderWhatsHotEmptyState = React.useCallback(() => { + return + }, []) + + const initialPage = store.me.followsCount === 0 ? 1 : 0 + return ( + + - ) - }, - [onPressSelected], - ) - - const renderFollowingEmptyState = React.useCallback(() => { - return - }, []) - - const initialPage = store.me.followsCount === 0 ? 1 : 0 - return ( - - - - - ) -}) + + + ) + }), +) const FeedPage = observer( ({ diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 705c37b308..7c48ce96b4 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -131,6 +131,11 @@ export const SettingsScreen = withAuthRequired( store.shell.openModal({name: 'content-filtering-settings'}) }, [track, store]) + const onPressContentLanguages = React.useCallback(() => { + track('Settings:ContentlanguagesButtonClicked') + store.shell.openModal({name: 'content-languages-settings'}) + }, [track, store]) + const onPressSignout = React.useCallback(() => { track('Settings:SignOutButtonClicked') store.session.logout() @@ -312,9 +317,26 @@ export const SettingsScreen = withAuthRequired( /> - App Passwords + App passwords + + + + + + Content languages + + Date: Tue, 2 May 2023 23:29:16 -0500 Subject: [PATCH 067/374] [APP-611] Add nice date to expanded post view (#567) * Add nice date to expanded post view * Fix styles --- src/lib/strings/time.ts | 12 ++++++++++++ src/lib/styles.ts | 3 +++ src/view/com/post-thread/PostThreadItem.tsx | 7 +++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts index 8357d3c314..6cd70498ec 100644 --- a/src/lib/strings/time.ts +++ b/src/lib/strings/time.ts @@ -27,3 +27,15 @@ export function ago(date: number | string | Date): string { return new Date(ts).toLocaleDateString() } } + +export function niceDate(date: number | string | Date) { + const d = new Date(date) + return `${d.toLocaleDateString('en-us', { + year: 'numeric', + month: 'short', + day: 'numeric', + })} at ${d.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + })}` +} diff --git a/src/lib/styles.ts b/src/lib/styles.ts index 1ff2d520d2..00a8638f9d 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -122,12 +122,15 @@ export const s = StyleSheet.create({ ml2: {marginLeft: 2}, ml5: {marginLeft: 5}, ml10: {marginLeft: 10}, + ml20: {marginLeft: 20}, mt2: {marginTop: 2}, mt5: {marginTop: 5}, mt10: {marginTop: 10}, + mt20: {marginTop: 20}, mb2: {marginBottom: 2}, mb5: {marginBottom: 5}, mb10: {marginBottom: 10}, + mb20: {marginBottom: 20}, // paddings p2: {padding: 2}, diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 1911511935..953e67b182 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -15,7 +15,7 @@ import {PostDropdownBtn} from '../util/forms/DropdownButton' import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' -import {ago} from 'lib/strings/time' +import {ago, niceDate} from 'lib/strings/time' import {sanitizeDisplayName} from 'lib/strings/display-names' import {pluralize} from 'lib/strings/helpers' import {useStores} from 'state/index' @@ -235,7 +235,10 @@ export const PostThreadItem = observer(function PostThreadItem({ ) : undefined} - {item._isHighlightedPost && hasEngagement ? ( + + {niceDate(item.post.indexedAt)} + + {hasEngagement ? ( {item.post.repostCount ? ( Date: Tue, 2 May 2023 23:32:16 -0500 Subject: [PATCH 068/374] [APP-601] Add muted accounts list (#565) * Add muted accounts list * Fix icon for muted accounts --- bskyweb/cmd/bskyweb/server.go | 1 + src/Navigation.tsx | 2 + src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/state/models/lists/muted-accounts.ts | 106 ++++++++++++++ src/view/com/profile/ProfileCard.tsx | 3 +- src/view/com/util/Link.tsx | 10 ++ src/view/screens/MutedAccounts.tsx | 168 +++++++++++++++++++++++ src/view/screens/Settings.tsx | 14 ++ web/index.html | 3 + 10 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 src/state/models/lists/muted-accounts.ts create mode 100644 src/view/screens/MutedAccounts.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index b901e226ce..07804e7cee 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -93,6 +93,7 @@ func serve(cctx *cli.Context) error { e.GET("/notifications", server.WebGeneric) e.GET("/settings", server.WebGeneric) e.GET("/settings/app-passwords", server.WebGeneric) + e.GET("/settings/muted-accounts", server.WebGeneric) e.GET("/settings/blocked-accounts", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 412c63f338..9a163fc43b 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -49,6 +49,7 @@ import {TermsOfServiceScreen} from './view/screens/TermsOfService' import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' import {AppPasswords} from 'view/screens/AppPasswords' +import {MutedAccounts} from 'view/screens/MutedAccounts' import {BlockedAccounts} from 'view/screens/BlockedAccounts' import {getRoutingInstrumentation} from 'lib/sentry' @@ -90,6 +91,7 @@ function commonScreens(Stack: typeof HomeTab) { /> + ) diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 3aff821174..34e6e6a468 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -20,6 +20,7 @@ export type CommonNavigatorParams = { CommunityGuidelines: undefined CopyrightPolicy: undefined AppPasswords: undefined + MutedAccounts: undefined BlockedAccounts: undefined } diff --git a/src/routes.ts b/src/routes.ts index 15595775e2..43d31ee099 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -14,6 +14,7 @@ export const router = new Router({ Debug: '/sys/debug', Log: '/sys/log', AppPasswords: '/settings/app-passwords', + MutedAccounts: '/settings/muted-accounts', BlockedAccounts: '/settings/blocked-accounts', Support: '/support', PrivacyPolicy: '/support/privacy', diff --git a/src/state/models/lists/muted-accounts.ts b/src/state/models/lists/muted-accounts.ts new file mode 100644 index 0000000000..9c3e1157b6 --- /dev/null +++ b/src/state/models/lists/muted-accounts.ts @@ -0,0 +1,106 @@ +import {makeAutoObservable} from 'mobx' +import { + AppBskyGraphGetMutes as GetMutes, + AppBskyActorDefs as ActorDefs, +} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' + +const PAGE_SIZE = 30 + +export class MutedAccountsModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + hasMore = true + loadMoreCursor?: string + + // data + mutes: ActorDefs.ProfileView[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.mutes.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async refresh() { + return this.loadMore(true) + } + + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return + } + this._xLoading(replace) + try { + const res = await this.rootStore.agent.app.bsky.graph.getMutes({ + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + // state transitions + // = + + _xLoading(isRefreshing = false) { + this.isLoading = true + this.isRefreshing = isRefreshing + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch user followers', err) + } + } + + // helper functions + // = + + _replaceAll(res: GetMutes.Response) { + this.mutes = [] + this._appendAll(res) + } + + _appendAll(res: GetMutes.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.mutes = this.mutes.concat(res.data.mutes) + } +} diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 66c1721413..12d6318337 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -60,7 +60,8 @@ export const ProfileCard = observer( ]} href={`/profile/${profile.handle}`} title={profile.handle} - asAnchor> + asAnchor + anchorNoUnderline> { children?: React.ReactNode noFeedback?: boolean asAnchor?: boolean + anchorNoUnderline?: boolean } export const Link = observer(function Link({ @@ -48,6 +49,7 @@ export const Link = observer(function Link({ noFeedback, asAnchor, accessible, + anchorNoUnderline, ...props }: Props) { const store = useStores() @@ -78,6 +80,14 @@ export const Link = observer(function Link({ ) } + + if (anchorNoUnderline) { + // @ts-ignore web only -prf + props.dataSet = props.dataSet || {} + // @ts-ignore web only -prf + props.dataSet.noUnderline = 1 + } + return ( +export const MutedAccounts = withAuthRequired( + observer(({}: Props) => { + const pal = usePalette('default') + const store = useStores() + const {screen} = useAnalytics() + const mutedAccounts = useMemo(() => new MutedAccountsModel(store), [store]) + + useFocusEffect( + React.useCallback(() => { + screen('MutedAccounts') + store.shell.setMinimalShellMode(false) + mutedAccounts.refresh() + }, [screen, store, mutedAccounts]), + ) + + const onRefresh = React.useCallback(() => { + mutedAccounts.refresh() + }, [mutedAccounts]) + const onEndReached = React.useCallback(() => { + mutedAccounts + .loadMore() + .catch(err => + store.log.error('Failed to load more muted accounts', err), + ) + }, [mutedAccounts, store]) + + const renderItem = ({ + item, + index, + }: { + item: ActorDefs.ProfileView + index: number + }) => ( + + ) + return ( + + + + Muted accounts have their posts removed from your feed and from your + notifications. Mutes are completely private. + + {!mutedAccounts.hasContent ? ( + + + + You have not muted any accounts yet. To mute an account, go to + their profile and selected "Mute account" from the menu on their + account. + + + + ) : ( + item.did} + refreshControl={ + + } + onEndReached={onEndReached} + renderItem={renderItem} + initialNumToRender={15} + ListFooterComponent={() => ( + + {mutedAccounts.isLoading && } + + )} + extraData={mutedAccounts.isLoading} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + )} + + ) + }), +) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 100, + }, + containerDesktop: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + title: { + textAlign: 'center', + marginTop: 12, + marginBottom: 12, + }, + description: { + textAlign: 'center', + paddingHorizontal: 30, + marginBottom: 14, + }, + descriptionDesktop: { + marginTop: 14, + }, + + flex1: { + flex: 1, + }, + empty: { + paddingHorizontal: 20, + paddingVertical: 20, + borderRadius: 16, + marginHorizontal: 24, + marginTop: 10, + }, + emptyText: { + textAlign: 'center', + }, + + footer: { + height: 200, + paddingTop: 20, + }, +}) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 7c48ce96b4..35c7f45520 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -288,6 +288,20 @@ export const SettingsScreen = withAuthRequired( Content moderation + + + + + + Muted accounts + + Date: Tue, 2 May 2023 23:32:29 -0500 Subject: [PATCH 069/374] Remove some confusing horizontal lines in threads (#566) --- src/state/models/content/post-thread.ts | 4 ++-- src/view/com/post-thread/PostThreadItem.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index 18a42732cd..a0f75493ac 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -125,7 +125,7 @@ export class PostThreadItemModel { parentModel._depth = this._depth - 1 parentModel._showChildReplyLine = true if (v.parent.parent) { - parentModel._showParentReplyLine = true //parentModel.uri !== higlightedPostUri + parentModel._showParentReplyLine = true parentModel.assignTreeModels(v.parent, higlightedPostUri, true, false) } this.parent = parentModel @@ -143,7 +143,7 @@ export class PostThreadItemModel { const itemModel = new PostThreadItemModel(this.rootStore, item) itemModel._depth = this._depth + 1 itemModel._showParentReplyLine = - itemModel.parentUri !== higlightedPostUri + itemModel.parentUri !== higlightedPostUri && replies.length === 0 if (item.replies?.length) { itemModel._showChildReplyLine = true itemModel.assignTreeModels(item, higlightedPostUri, false, true) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 953e67b182..ddb2cb7bbd 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -310,7 +310,12 @@ export const PostThreadItem = observer(function PostThreadItem({ {item._showParentReplyLine && ( Date: Tue, 2 May 2023 23:56:01 -0500 Subject: [PATCH 070/374] 1.26 --- app.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index f2ac0eb506..2f57689823 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Bluesky", "slug": "bluesky", "owner": "blueskysocial", - "version": "1.25.0", + "version": "1.26.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -13,7 +13,7 @@ "backgroundColor": "#ffffff" }, "ios": { - "buildNumber": "2", + "buildNumber": "1", "supportsTablet": false, "bundleIdentifier": "xyz.blueskyweb.app", "config": { diff --git a/package.json b/package.json index e34c52e1ca..a545ba2512 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.25.0", + "version": "1.26.0", "private": true, "scripts": { "postinstall": "patch-package", From 14e9719bccbe180f2da16ec4b4a981960ffe27f7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 3 May 2023 00:45:19 -0500 Subject: [PATCH 071/374] Increment android versioncode --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index 2f57689823..c18fc4a72e 100644 --- a/app.json +++ b/app.json @@ -38,7 +38,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 10, + "versionCode": 11, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" From 204c6729e755804ac017a19ff08f99e7f10a9db2 Mon Sep 17 00:00:00 2001 From: Jake Gold <52801504+Jacob2161@users.noreply.github.com> Date: Wed, 3 May 2023 07:14:29 -0700 Subject: [PATCH 072/374] add required security HTTP headers (#568) --- bskyweb/cmd/bskyweb/server.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 07804e7cee..5e934c6b0e 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -73,6 +73,15 @@ func serve(cctx *cli.Context) error { e := echo.New() e.HideBanner = true + // SECURITY: Do not modify without due consideration. + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + HSTSMaxAge: 31536000, // 365 days + // TODO: + // ContentSecurityPolicy + // XSSProtection + })) e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ // Don't log requests for static content. Skipper: func(c echo.Context) bool { From 906b906eb1e32788d829544ef7e8071be9cd60ff Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 3 May 2023 11:49:46 -0500 Subject: [PATCH 073/374] Remove the attempts to make the language filter more lenient (#569) --- src/lib/api/feed-manip.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 96534d1ba5..7f31fb292e 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -214,20 +214,10 @@ export class FeedTuner { ) { const res = lande(item.post.record.text) - // require at least 70% confidence; otherwise, roll with it - if (res[0][1] <= 0.7) { + if (langsCode3.includes(res[0][0])) { hasPreferredLang = true break } - - // if the user's languages are in the top 5 guesses, roll with it - for (let j = 0; j < 5 && j < res.length; j++) { - hasPreferredLang = - hasPreferredLang || langsCode3.includes(res[i][0]) - } - if (hasPreferredLang) { - break - } } else { // no text? roll with it hasPreferredLang = true From 13586ec6ef46b73836c5b384a73c09bcd2808751 Mon Sep 17 00:00:00 2001 From: renahlee Date: Wed, 3 May 2023 20:16:49 -0700 Subject: [PATCH 074/374] Update README with Go instructions --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a0a8da1961..46e81f3cf0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,29 @@ - For instance, the localhosted dev-wallet will need `adb reverse tcp:3001 tcp:3001` - For some reason, the typescript compiler chokes on platform-specific files (e.g. `foo.native.ts`) but only when compiling for Web thus far. Therefore we always have one version of the file which doesn't use a platform specifier, and that should bee the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).) +## Build instructions (with Go) + +### Prerequisites + +- [Go](https://go.dev/) +- [Yarn](https://yarnpkg.com/) + +### Steps + +To run the build with Go, use staging credentials, your own, or any other account you create. + +``` +cd social-app +yarn && yarn build-web +cp ./web-build/static/js/*.* bskyweb/static/js/ +cd bskyweb/ +go mod tidy +go build -v -tags timetzdata -o bskyweb ./cmd/bskyweb +./bskyweb serve --pds-host= --handle= --password= +``` + +On build success, access the application at [http://localhost:8100/](http://localhost:8100/). Subsequent changes require re-running the above steps in order to be reflected. + ## Various notes ### Debugging @@ -37,7 +60,9 @@ - Note that since 0.70, debugging using the old debugger (which shows up using CMD+D) doesn't work anymore. Follow the instructions below to debug the code: https://reactnative.dev/docs/next/hermes#debugging-js-on-hermes-using-google-chromes-devtools ### Developer Menu + To open the [Developer Menu](https://docs.expo.dev/debugging/tools/#developer-menu) on an `expo-dev-client` app you can do the following: + - Android Device: Shake the device vertically, or if your device is connected via USB, run adb shell input keyevent 82 in your terminal - Android Emulator: Either press Cmd ⌘ + m or Ctrl + m or run adb shell input keyevent 82 in your terminal - iOS Device: Shake the device, or touch 3 fingers to the screen @@ -56,26 +81,26 @@ To open the [Developer Menu](https://docs.expo.dev/debugging/tools/#developer-me - TextEncoder / TextDecoder - ### Sentry sourcemaps + Sourcemaps should automatically be updated when a signed build is created using `eas build` and published using `eas submit` due to the postPublish hook setup in `app.json`. However, if an update is created and published OTA using `eas update`, we need to the take the following steps to upload sourcemaps to Sentry: + - Run eas update. This will generate a dist folder in your project root, which contains your JavaScript bundles and source maps. This command will also output the 'Android update ID' and 'iOS update ID' that we'll need in the next step. - Copy or rename the bundle names in the `dist/bundles` folder to match `index.android.bundle` (Android) or `main.jsbundle` (iOS). - Next, you can use the Sentry CLI to upload your bundles and source maps: - release name should be set to `${bundleIdentifier}@${version}+${buildNumber}` (iOS) or `${androidPackage}@${version}+${versionCode}` (Android), so for example `com.domain.myapp@1.0.0+1`. - `dist` should be set to the Update ID that `eas update` generated. -- Command for Android: -`node_modules/@sentry/cli/bin/sentry-cli releases \ - files \ - upload-sourcemaps \ - --dist \ - --rewrite \ - dist/bundles/index.android.bundle dist/bundles/android-.map` +- Command for Android: + `node_modules/@sentry/cli/bin/sentry-cli releases \ +files \ +upload-sourcemaps \ +--dist \ +--rewrite \ +dist/bundles/index.android.bundle dist/bundles/android-.map` - Command for iOS: - `node_modules/@sentry/cli/bin/sentry-cli releases \ - files \ - upload-sourcemaps \ - --dist \ - --rewrite \ - dist/bundles/main.jsbundle dist/bundles/ios-.map` - + `node_modules/@sentry/cli/bin/sentry-cli releases \ +files \ +upload-sourcemaps \ +--dist \ +--rewrite \ +dist/bundles/main.jsbundle dist/bundles/ios-.map` From aed54822a3511640761cb63180783b55caf46735 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Wed, 3 May 2023 20:39:49 -0700 Subject: [PATCH 075/374] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 46e81f3cf0..b392579d95 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ cp ./web-build/static/js/*.* bskyweb/static/js/ cd bskyweb/ go mod tidy go build -v -tags timetzdata -o bskyweb ./cmd/bskyweb -./bskyweb serve --pds-host= --handle= --password= +./bskyweb serve --pds-host=https://staging.bsky.dev --handle= --password= ``` On build success, access the application at [http://localhost:8100/](http://localhost:8100/). Subsequent changes require re-running the above steps in order to be reflected. From 7f88845c9b6a524d6c1b396f2387d043911ba56c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 4 May 2023 00:22:54 -0500 Subject: [PATCH 076/374] Add icon-intolerant and behavior-intolerant to the political hategroup category (#579) --- src/lib/labeling/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index 6670e5413f..f219cdb792 100644 --- a/src/lib/labeling/const.ts +++ b/src/lib/labeling/const.ts @@ -57,7 +57,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< id: 'hate', title: 'Political Hate-Groups', warning: 'Hate', - values: ['icon-kkk', 'icon-nazi'], + values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'], imagesOnly: false, }, spam: { From 0f68e6a6ffca94239630801eaaed9a9531d5cef2 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 4 May 2023 00:24:14 -0500 Subject: [PATCH 077/374] Add mock data to test unknown labels (#578) --- __e2e__/mock-server.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts index 858ac5e086..3577990d35 100644 --- a/__e2e__/mock-server.ts +++ b/__e2e__/mock-server.ts @@ -81,6 +81,9 @@ async function main() { 'nudity-account', 'nudity-profile', 'nudity-posts', + 'unknown-account', + 'unknown-profile', + 'unknown-posts', 'muted-account', ]) { await server.mocker.createUser(user) @@ -164,6 +167,35 @@ async function main() { ), ) + await server.mocker.labelAccount( + 'not-a-real-label', + 'unknown-account', + ) + await server.mocker.labelProfile( + 'not-a-real-label', + 'unknown-profile', + ) + await server.mocker.labelPost( + 'not-a-real-label', + await server.mocker.createPost('unknown-posts', 'unknown post'), + ) + await server.mocker.labelPost( + 'not-a-real-label', + await server.mocker.createQuotePost( + 'unknown-posts', + 'unknown quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'not-a-real-label', + await server.mocker.createReply( + 'unknown-posts', + 'unknown reply', + anchorPost, + ), + ) + await server.mocker.users.alice.agent.mute('muted-account.test') await server.mocker.createPost('muted-account', 'muted post') await server.mocker.createQuotePost( From 011baa78c10cc38d98795fb87a0e54c14a657791 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Wed, 3 May 2023 22:53:49 -0700 Subject: [PATCH 078/374] a11y label cleanup (#576) --- src/view/com/auth/create/CreateAccount.tsx | 8 ++------ src/view/com/auth/login/Login.tsx | 24 ++++------------------ src/view/com/lightbox/Lightbox.web.tsx | 8 ++++---- src/view/com/post-thread/PostThread.tsx | 8 ++++---- src/view/com/profile/ProfileHeader.tsx | 6 +++--- src/view/com/search/HeaderWithInput.tsx | 7 +++++-- src/view/com/util/ViewHeader.tsx | 8 ++------ src/view/screens/Home.tsx | 4 ++-- src/view/shell/Drawer.tsx | 18 +++++++--------- src/view/shell/bottom-bar/BottomBar.tsx | 21 ++++++++++++++----- src/view/shell/desktop/LeftNav.tsx | 9 ++++---- src/view/shell/desktop/Search.tsx | 2 ++ 12 files changed, 56 insertions(+), 67 deletions(-) diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx index ac03081dff..26bf033864 100644 --- a/src/view/com/auth/create/CreateAccount.tsx +++ b/src/view/com/auth/create/CreateAccount.tsx @@ -75,9 +75,7 @@ export const CreateAccount = observer( + accessibilityRole="button"> Back @@ -87,9 +85,7 @@ export const CreateAccount = observer( + accessibilityRole="button"> {model.isProcessing ? ( ) : ( diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index cec08192c2..87512287b2 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -241,11 +241,7 @@ const ChooseAccountForm = ({ - + Back @@ -454,11 +450,7 @@ const LoginForm = ({ ) : undefined} - + Back @@ -632,11 +624,7 @@ const ForgotPasswordForm = ({ ) : undefined} - + Back @@ -794,11 +782,7 @@ const SetNewPasswordForm = ({ ) : undefined} - + Back diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index 1d4a9c2153..3388b54b20 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -106,8 +106,8 @@ function LightboxInner({ onPress={onPressLeft} style={[styles.btn, styles.leftBtn]} accessibilityRole="button" - accessibilityLabel="Go back" - accessibilityHint="Navigates to previous image in viewer"> + accessibilityLabel="Previous image" + accessibilityHint=""> + accessibilityLabel="Next image" + accessibilityHint=""> + accessibilityLabel="Back" + accessibilityHint=""> + accessibilityLabel="Back" + accessibilityHint=""> + accessibilityLabel="Back" + accessibilityHint=""> @@ -472,7 +472,7 @@ const ProfileHeaderLoaded = observer( onPress={onPressAvi} accessibilityRole="image" accessibilityLabel={`View ${view.handle}'s avatar`} - accessibilityHint={`Opens ${view.handle}'s avatar in an image viewer`}> + accessibilityHint=""> + accessibilityRole="button" + accessibilityLabel="Back" + accessibilityHint=""> {query ? ( + accessibilityLabel={canGoBack ? 'Back' : 'Menu'} + accessibilityHint=""> {canGoBack ? ( } accessibilityRole="button" - accessibilityLabel="Compose" - accessibilityHint="Opens post composer" + accessibilityLabel="Compose post" + accessibilityHint="" /> ) diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 404374b95c..1b8983e83a 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -171,7 +171,7 @@ export const DrawerContent = observer(() => { } label="Search" accessibilityLabel="Search" - accessibilityHint="Search through users and posts" + accessibilityHint="" bold={isAtSearch} onPress={onPressSearch} /> @@ -193,7 +193,7 @@ export const DrawerContent = observer(() => { } label="Home" accessibilityLabel="Home" - accessibilityHint="Navigates to default feed" + accessibilityHint="" bold={isAtHome} onPress={onPressHome} /> @@ -214,12 +214,8 @@ export const DrawerContent = observer(() => { ) } label="Notifications" - accessibilityLabel={ - notifications.unreadCountLabel === '1' - ? 'Notifications: 1 unread notification' - : `Notifications: ${notifications.unreadCountLabel} unread notifications` - } - accessibilityHint="Opens notification feed" + accessibilityLabel="Notifications" + accessibilityHint={`${store.me.notifications.unreadCountLabel} unread`} count={notifications.unreadCountLabel} bold={isAtNotifications} onPress={onPressNotifications} @@ -242,7 +238,7 @@ export const DrawerContent = observer(() => { } label="Profile" accessibilityLabel="Profile" - accessibilityHint="See profile display name, avatar, description, and other profile items" + accessibilityHint="" onPress={onPressProfile} /> { } label="Settings" accessibilityLabel="Settings" - accessibilityHint="Manage settings for your account, like handle, content moderation, and app passwords" + accessibilityHint="" onPress={onPressSettings} /> @@ -332,7 +328,7 @@ function MenuItem({ testID={`menuItemButton-${label}`} style={styles.menuItem} onPress={onPress} - accessibilityRole="menuitem" + accessibilityRole="tab" accessibilityLabel={accessibilityLabel} accessibilityHint=""> diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index b32072d5a6..c11a0128c6 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -94,8 +94,9 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { ) } onPress={onPressHome} - accessibilityLabel="Go home" - accessibilityHint="Navigates to feed home" + accessibilityRole="tab" + accessibilityLabel="Home" + accessibilityHint="" /> { } onPress={onPressSearch} accessibilityRole="search" + accessibilityLabel="Search" + accessibilityHint="" /> { } onPress={onPressNotifications} notificationCount={store.me.notifications.unreadCountLabel} + accessible={true} + accessibilityRole="tab" accessibilityLabel="Notifications" - accessibilityHint="Navigates to notifications" + accessibilityHint={`${store.me.notifications.unreadCountLabel} unread`} /> { } onPress={onPressProfile} + accessibilityRole="tab" accessibilityLabel="Profile" - accessibilityHint="Navigates to profile" + accessibilityHint="" /> ) @@ -169,7 +175,10 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { interface BtnProps extends Pick< ComponentProps, - 'accessibilityRole' | 'accessibilityHint' | 'accessibilityLabel' + | 'accessible' + | 'accessibilityRole' + | 'accessibilityHint' + | 'accessibilityLabel' > { testID?: string icon: JSX.Element @@ -184,6 +193,7 @@ function Btn({ notificationCount, onPress, onLongPress, + accessible, accessibilityHint, accessibilityLabel, }: BtnProps) { @@ -194,6 +204,7 @@ function Btn({ onPress={onLongPress ? onPress : undefined} onPressIn={onLongPress ? undefined : onPress} onLongPress={onLongPress} + accessible={accessible} accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint}> {notificationCount ? ( diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 86f1a3ef37..17d078dc51 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -66,7 +66,7 @@ function BackBtn() { style={styles.backBtn} accessibilityRole="button" accessibilityLabel="Go back" - accessibilityHint="Navigates to the previous screen"> + accessibilityHint=""> + accessibilityHint=""> {isCurrent ? iconFilled : icon} {typeof count === 'string' && count ? ( @@ -129,8 +130,8 @@ function ComposeBtn() { style={[styles.newPostBtn]} onPress={onPressCompose} accessibilityRole="button" - accessibilityLabel="New post" - accessibilityHint="Opens post composer"> + accessibilityLabel="Compose post" + accessibilityHint=""> {query ? ( From 4ef853ef6cd649af12f0810b8c38911cd639e033 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Wed, 3 May 2023 22:54:22 -0700 Subject: [PATCH 079/374] Remove text underline from lists (#574) --- bskyweb/templates/base.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index d3d76ad0a9..dfeae02c73 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -69,6 +69,9 @@ a[role="link"]:hover { text-decoration: underline; } + a[role="link"][data-no-underline="1"]:hover { + text-decoration: none; + } /* Styling hacks */ *[data-word-wrap] { From d97e75c62f39b874af97b40ebc7211841d9ce1b7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 4 May 2023 00:54:35 -0500 Subject: [PATCH 080/374] [APP-539] Rework lightbox and alt-image behaviors (#573) * Replace the long press on the lightbox with footer controls * Remove long-press from images in the feed * Tune the lightbox footer control ui * Replace the AltImageRead modal with the ability to view all alt text in the lightbox footer * Tune lightbox footer for iOS * Add alt text to the web lightbox * Fix lint * a11y slight changes --------- Co-authored-by: renahlee --- src/state/models/ui/shell.ts | 17 ++-- src/view/com/lightbox/Lightbox.tsx | 106 +++++++++++++++++++----- src/view/com/lightbox/Lightbox.web.tsx | 16 +++- src/view/com/modals/AltImageRead.tsx | 80 ------------------ src/view/com/modals/Modal.tsx | 4 - src/view/com/modals/Modal.web.tsx | 3 - src/view/com/util/images/Gallery.tsx | 33 ++------ src/view/com/util/post-embeds/index.tsx | 47 +++-------- 8 files changed, 127 insertions(+), 179 deletions(-) delete mode 100644 src/view/com/modals/AltImageRead.tsx diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index dea220c55e..4a55c23ad2 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -48,11 +48,6 @@ export interface AltTextImageModal { image: ImageModel } -export interface AltTextImageReadModal { - name: 'alt-text-image-read' - altText: string -} - export interface DeleteAccountModal { name: 'delete-account' } @@ -106,7 +101,6 @@ export type Modal = // Posts | AltTextImageModal - | AltTextImageReadModal | CropImageModal | ServerInputModal | RepostModal @@ -127,9 +121,14 @@ export class ProfileImageLightbox implements LightboxModel { } } +interface ImagesLightboxItem { + uri: string + alt?: string +} + export class ImagesLightbox implements LightboxModel { name = 'images' - constructor(public uris: string[], public index: number) { + constructor(public images: ImagesLightboxItem[], public index: number) { makeAutoObservable(this) } setIndex(index: number) { @@ -173,7 +172,7 @@ export class ShellUiModel { isModalActive = false activeModals: Modal[] = [] isLightboxActive = false - activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined + activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null isComposerActive = false composerOpts: ComposerOpts | undefined @@ -262,7 +261,7 @@ export class ShellUiModel { closeLightbox() { this.isLightboxActive = false - this.activeLightbox = undefined + this.activeLightbox = null } openComposer(opts: ComposerOpts) { diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 06b48143b1..c4bc88cf12 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,31 +1,75 @@ import React from 'react' +import {Pressable, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import ImageView from './ImageViewing' import {useStores} from 'state/index' import * as models from 'state/models/ui/shell' import {saveImageModal} from 'lib/media/manip' -import {ImageSource} from './ImageViewing/@types' +import {Text} from '../util/text/Text' +import {s, colors} from 'lib/styles' +import {Button} from '../util/forms/Button' +import {isIOS} from 'platform/detection' export const Lightbox = observer(function Lightbox() { const store = useStores() - if (!store.shell.isLightboxActive) { - return null - } + const [isAltExpanded, setAltExpanded] = React.useState(false) - const onClose = () => { + const onClose = React.useCallback(() => { store.shell.closeLightbox() - } - const onLongPress = (image: ImageSource) => { - if ( - typeof image === 'object' && - 'uri' in image && - typeof image.uri === 'string' - ) { - saveImageModal({uri: image.uri}) - } - } + }, [store]) - if (store.shell.activeLightbox?.name === 'profile-image') { + const LightboxFooter = React.useCallback( + ({imageIndex}: {imageIndex: number}) => { + const lightbox = store.shell.activeLightbox + if (!lightbox) { + return null + } + + let altText = '' + let uri + if (lightbox.name === 'images') { + const opts = store.shell.activeLightbox as models.ImagesLightbox + uri = opts.images[imageIndex].uri + altText = opts.images[imageIndex].alt + } else if (store.shell.activeLightbox.name === 'profile-image') { + const opts = store.shell.activeLightbox as models.ProfileImageLightbox + uri = opts.profileView.avatar + } + + return ( + + {altText ? ( + setAltExpanded(!isAltExpanded)} + accessibilityRole="button"> + + {altText} + + + ) : null} + + + + + ) + }, + [store.shell.activeLightbox, isAltExpanded, setAltExpanded], + ) + + if (!store.shell.activeLightbox) { + return null + } else if (store.shell.activeLightbox.name === 'profile-image') { const opts = store.shell.activeLightbox as models.ProfileImageLightbox return ( ) - } else if (store.shell.activeLightbox?.name === 'images') { + } else if (store.shell.activeLightbox.name === 'images') { const opts = store.shell.activeLightbox as models.ImagesLightbox return ( ({uri}))} + images={opts.images.map(({uri}) => ({uri}))} imageIndex={opts.index} visible onRequestClose={onClose} - onLongPress={onLongPress} + FooterComponent={LightboxFooter} /> ) } else { return null } }) + +const styles = StyleSheet.create({ + footer: { + paddingTop: 16, + paddingBottom: isIOS ? 40 : 24, + paddingHorizontal: 24, + backgroundColor: '#000d', + }, + footerText: { + paddingBottom: isIOS ? 20 : 16, + }, + footerBtns: { + flexDirection: 'row', + justifyContent: 'center', + }, + footerBtn: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: 'transparent', + borderColor: colors.white, + }, +}) diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index 3388b54b20..eff9af2d22 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -10,11 +10,13 @@ import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useStores} from 'state/index' import * as models from 'state/models/ui/shell' -import {colors} from 'lib/styles' +import {colors, s} from 'lib/styles' import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader' +import {Text} from '../util/text/Text' interface Img { uri: string + alt?: string } export const Lightbox = observer(function Lightbox() { @@ -37,7 +39,7 @@ export const Lightbox = observer(function Lightbox() { } } else if (activeLightbox instanceof models.ImagesLightbox) { const opts = activeLightbox - imgs = opts.uris.map(uri => ({uri})) + imgs = opts.images } if (!imgs) { @@ -131,6 +133,11 @@ function LightboxInner({ )} + {imgs[index].alt ? ( + + {imgs[index].alt} + + ) : null} @@ -183,4 +190,9 @@ const styles = StyleSheet.create({ right: 30, top: '50%', }, + footer: { + paddingHorizontal: 32, + paddingVertical: 24, + backgroundColor: colors.black, + }, }) diff --git a/src/view/com/modals/AltImageRead.tsx b/src/view/com/modals/AltImageRead.tsx deleted file mode 100644 index 985477287e..0000000000 --- a/src/view/com/modals/AltImageRead.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, {useCallback} from 'react' -import {StyleSheet, View} from 'react-native' -import {usePalette} from 'lib/hooks/usePalette' -import {gradients, s} from 'lib/styles' -import {Text} from '../util/text/Text' -import {TouchableOpacity} from 'react-native-gesture-handler' -import LinearGradient from 'react-native-linear-gradient' -import {useStores} from 'state/index' -import {isDesktopWeb} from 'platform/detection' - -export const snapPoints = ['70%'] - -interface Props { - altText: string -} - -export function Component({altText}: Props) { - const pal = usePalette('default') - const store = useStores() - - const onPress = useCallback(() => { - store.shell.closeModal() - }, [store]) - - return ( - - Image description - - {altText} - - - - - Done - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - gap: 18, - paddingVertical: isDesktopWeb ? 0 : 18, - paddingHorizontal: isDesktopWeb ? 0 : 12, - height: '100%', - width: '100%', - }, - title: { - textAlign: 'center', - fontWeight: 'bold', - fontSize: 24, - }, - text: { - borderRadius: 5, - marginVertical: 18, - paddingHorizontal: 18, - paddingVertical: 16, - }, - button: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 10, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index b5d71a116b..18b7ae4c4d 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -13,7 +13,6 @@ import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' import * as RepostModal from './Repost' import * as AltImageModal from './AltImage' -import * as AltImageReadModal from './AltImageRead' import * as ReportAccountModal from './ReportAccount' import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' @@ -76,9 +75,6 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'alt-text-image') { snapPoints = AltImageModal.snapPoints element = - } else if (activeModal?.name === 'alt-text-image-read') { - snapPoints = AltImageReadModal.snapPoints - element = } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 50487e3eb4..9dcc8fa7e6 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -15,7 +15,6 @@ import * as DeleteAccountModal from './DeleteAccount' import * as RepostModal from './Repost' import * as CropImageModal from './crop-image/CropImage.web' import * as AltTextImageModal from './AltImage' -import * as AltTextImageReadModal from './AltImageRead' import * as ChangeHandleModal from './ChangeHandle' import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' @@ -89,8 +88,6 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'alt-text-image') { element = - } else if (modal.name === 'alt-text-image-read') { - element = } else { return null } diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 5b6c3384d0..1a29b45306 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -1,8 +1,7 @@ import {AppBskyEmbedImages} from '@atproto/api' -import React, {ComponentProps, FC, useCallback} from 'react' -import {Pressable, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import React, {ComponentProps, FC} from 'react' +import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {Image} from 'expo-image' -import {useStores} from 'state/index' type EventFunction = (index: number) => void @@ -26,22 +25,14 @@ export const GalleryItem: FC = ({ onLongPress, }) => { const image = images[index] - const store = useStores() - - const onPressAltText = useCallback(() => { - store.shell.openModal({ - name: 'alt-text-image-read', - altText: image.alt, - }) - }, [image.alt, store.shell]) return ( onPress?.(index)} - onPressIn={() => onPressIn?.(index)} - onLongPress={() => onLongPress?.(index)} + onPress={onPress ? () => onPress(index) : undefined} + onPressIn={onPressIn ? () => onPressIn(index) : undefined} + onLongPress={onLongPress ? () => onLongPress(index) : undefined} accessibilityRole="button" accessibilityLabel="View image" accessibilityHint=""> @@ -54,15 +45,7 @@ export const GalleryItem: FC = ({ accessibilityIgnoresInvertColors /> - {image.alt === '' ? null : ( - - ALT - - )} + {image.alt === '' ? null : ALT} ) } @@ -78,8 +61,8 @@ const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 3, position: 'absolute', - left: 10, - top: -26, + left: 6, + bottom: 6, width: 46, }, }) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 929c85adcb..2dda9069ee 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -1,11 +1,10 @@ -import React, {useCallback} from 'react' +import React from 'react' import { StyleSheet, StyleProp, View, ViewStyle, Image as RNImage, - Pressable, Text, } from 'react-native' import { @@ -20,7 +19,6 @@ import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImagesLightbox} from 'state/models/ui/shell' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {saveImageModal} from 'lib/media/manip' import {YoutubeEmbed} from './YoutubeEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {getYoutubeVideoId} from 'lib/strings/url-helpers' @@ -44,16 +42,6 @@ export function PostEmbeds({ const pal = usePalette('default') const store = useStores() - const onPressAltText = useCallback( - (alt: string) => { - store.shell.openModal({ - name: 'alt-text-image-read', - altText: alt, - }) - }, - [store.shell], - ) - if ( AppBskyEmbedRecordWithMedia.isView(embed) && AppBskyEmbedRecord.isViewRecord(embed.record.record) && @@ -103,20 +91,17 @@ export function PostEmbeds({ const {images} = embed if (images.length > 0) { - const uris = embed.images.map(img => img.fullsize) + const items = embed.images.map(img => ({uri: img.fullsize, alt: img.alt})) const openLightbox = (index: number) => { - store.shell.openLightbox(new ImagesLightbox(uris, index)) - } - const onLongPress = (index: number) => { - saveImageModal({uri: uris[index]}) + store.shell.openLightbox(new ImagesLightbox(items, index)) } const onPressIn = (index: number) => { - const firstImageToShow = uris[index] + const firstImageToShow = items[index].uri RNImage.prefetch(firstImageToShow) - uris.forEach(uri => { - if (firstImageToShow !== uri) { + items.forEach(item => { + if (firstImageToShow !== item.uri) { // First image already prefeched above - RNImage.prefetch(uri) + RNImage.prefetch(item.uri) } }) } @@ -129,20 +114,9 @@ export function PostEmbeds({ alt={alt} uri={thumb} onPress={() => openLightbox(0)} - onLongPress={() => onLongPress(0)} onPressIn={() => onPressIn(0)} style={styles.singleImage}> - {alt === '' ? null : ( - { - onPressAltText(alt) - }} - accessibilityRole="button" - accessibilityLabel="View alt text" - accessibilityHint="Opens modal with alt text"> - ALT - - )} + {alt === '' ? null : ALT} ) @@ -153,7 +127,6 @@ export function PostEmbeds({ @@ -209,8 +182,8 @@ const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 3, position: 'absolute', - left: 10, - top: -26, + left: 6, + bottom: 6, width: 46, }, }) From 33bf9c38695c24ede28d68810d1f6b2e68f86f5f Mon Sep 17 00:00:00 2001 From: Ollie H Date: Wed, 3 May 2023 22:54:59 -0700 Subject: [PATCH 081/374] Remove focus outline on composer (#572) --- bskyweb/templates/base.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index dfeae02c73..866664e509 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -59,6 +59,9 @@ /* OLLIE: TODO -- this is not accessible */ /* Remove focus state on inputs */ + .ProseMirror-focused { + outline: 0; + } input:focus { outline: 0; } From ab3074fdee44103424a58577f5c81c9d2436fc68 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 4 May 2023 00:55:33 -0500 Subject: [PATCH 082/374] Add the !filter and !warn imperative labels (#580) --- __e2e__/mock-server.ts | 58 ++++++++++++++++++++++++++++++ src/lib/labeling/const.ts | 18 +++++++++- src/lib/labeling/helpers.ts | 8 +++++ src/lib/labeling/types.ts | 7 +++- src/state/models/ui/preferences.ts | 11 +++++- 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts index 3577990d35..6744f697ff 100644 --- a/__e2e__/mock-server.ts +++ b/__e2e__/mock-server.ts @@ -84,6 +84,12 @@ async function main() { 'unknown-account', 'unknown-profile', 'unknown-posts', + 'always-filter-account', + 'always-filter-profile', + 'always-filter-posts', + 'always-warn-account', + 'always-warn-profile', + 'always-warn-posts', 'muted-account', ]) { await server.mocker.createUser(user) @@ -196,6 +202,58 @@ async function main() { ), ) + await server.mocker.labelAccount('!filter', 'always-filter-account') + await server.mocker.labelProfile('!filter', 'always-filter-profile') + await server.mocker.labelPost( + '!filter', + await server.mocker.createPost( + 'always-filter-posts', + 'always-filter post', + ), + ) + await server.mocker.labelPost( + '!filter', + await server.mocker.createQuotePost( + 'always-filter-posts', + 'always-filter quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + '!filter', + await server.mocker.createReply( + 'always-filter-posts', + 'always-filter reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount('!warn', 'always-warn-account') + await server.mocker.labelProfile('!warn', 'always-warn-profile') + await server.mocker.labelPost( + '!warn', + await server.mocker.createPost( + 'always-warn-posts', + 'always-warn post', + ), + ) + await server.mocker.labelPost( + '!warn', + await server.mocker.createQuotePost( + 'always-warn-posts', + 'always-warn quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + '!warn', + await server.mocker.createReply( + 'always-warn-posts', + 'always-warn reply', + anchorPost, + ), + ) + await server.mocker.users.alice.agent.mute('muted-account.test') await server.mocker.createPost('muted-account', 'muted post') await server.mocker.createQuotePost( diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index f219cdb792..54cc732b92 100644 --- a/src/lib/labeling/const.ts +++ b/src/lib/labeling/const.ts @@ -6,7 +6,23 @@ export const ILLEGAL_LABEL_GROUP: LabelValGroup = { title: 'Illegal Content', warning: 'Illegal Content', values: ['csam', 'dmca-violation', 'nudity-nonconsentual'], - imagesOnly: false, // not applicable + imagesOnly: false, +} + +export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = { + id: 'always-filter', + title: 'Content Warning', + warning: 'Content Warning', + values: ['!filter'], + imagesOnly: false, +} + +export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = { + id: 'always-warn', + title: 'Content Warning', + warning: 'Content Warning', + values: ['!warn'], + imagesOnly: false, } export const UNKNOWN_LABEL_GROUP: LabelValGroup = { diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index 5ec591cfb5..71ea43c087 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -8,6 +8,8 @@ import { import { CONFIGURABLE_LABEL_GROUPS, ILLEGAL_LABEL_GROUP, + ALWAYS_FILTER_LABEL_GROUP, + ALWAYS_WARN_LABEL_GROUP, UNKNOWN_LABEL_GROUP, } from './const' import { @@ -34,6 +36,12 @@ export function getLabelValueGroup(labelVal: string): LabelValGroup { if (ILLEGAL_LABEL_GROUP.values.includes(labelVal)) { return ILLEGAL_LABEL_GROUP } + if (ALWAYS_FILTER_LABEL_GROUP.values.includes(labelVal)) { + return ALWAYS_FILTER_LABEL_GROUP + } + if (ALWAYS_WARN_LABEL_GROUP.values.includes(labelVal)) { + return ALWAYS_WARN_LABEL_GROUP + } if (CONFIGURABLE_LABEL_GROUPS[id].values.includes(labelVal)) { return CONFIGURABLE_LABEL_GROUPS[id] } diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts index 20ecaa5b58..123c5d1f38 100644 --- a/src/lib/labeling/types.ts +++ b/src/lib/labeling/types.ts @@ -4,7 +4,12 @@ import {LabelPreferencesModel} from 'state/models/ui/preferences' export type Label = ComAtprotoLabelDefs.Label export interface LabelValGroup { - id: keyof LabelPreferencesModel | 'illegal' | 'unknown' + id: + | keyof LabelPreferencesModel + | 'illegal' + | 'always-filter' + | 'always-warn' + | 'unknown' title: string imagesOnly: boolean subtitle?: string diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index f6b29169d4..7b41fa7466 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -4,7 +4,12 @@ import {isObj, hasProp} from 'lib/type-guards' import {ComAtprotoLabelDefs} from '@atproto/api' import {LabelValGroup} from 'lib/labeling/types' import {getLabelValueGroup} from 'lib/labeling/helpers' -import {UNKNOWN_LABEL_GROUP, ILLEGAL_LABEL_GROUP} from 'lib/labeling/const' +import { + UNKNOWN_LABEL_GROUP, + ILLEGAL_LABEL_GROUP, + ALWAYS_FILTER_LABEL_GROUP, + ALWAYS_WARN_LABEL_GROUP, +} from 'lib/labeling/const' const deviceLocales = getLocales() @@ -94,6 +99,10 @@ export class PreferencesModel { const group = getLabelValueGroup(label.val) if (group.id === 'illegal') { return {pref: 'hide', desc: ILLEGAL_LABEL_GROUP} + } else if (group.id === 'always-filter') { + return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP} + } else if (group.id === 'always-warn') { + return {pref: 'warn', desc: ALWAYS_WARN_LABEL_GROUP} } else if (group.id === 'unknown') { continue } From 2749b8e3713f853aed9f3449dd76d6bd50e81cc9 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 4 May 2023 00:55:57 -0500 Subject: [PATCH 083/374] Rework alt image modal to be fullscreen due to android bugs with the bottomsheet and keyboard (#577) --- src/view/com/modals/AltImage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index ce0a675a90..0359359ccc 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -1,19 +1,17 @@ import React, {useCallback, useState} from 'react' -import {StyleSheet, View} from 'react-native' +import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native' import {usePalette} from 'lib/hooks/usePalette' -import {TextInput} from './util' import {gradients, s} from 'lib/styles' import {enforceLen} from 'lib/strings/helpers' import {MAX_ALT_TEXT} from 'lib/constants' import {useTheme} from 'lib/ThemeContext' import {Text} from '../util/text/Text' -import {TouchableOpacity} from 'react-native-gesture-handler' import LinearGradient from 'react-native-linear-gradient' import {useStores} from 'state/index' import {isDesktopWeb} from 'platform/detection' import {ImageModel} from 'state/models/media/image' -export const snapPoints = ['80%'] +export const snapPoints = ['fullscreen'] interface Props { image: ImageModel From 49f9df635561f5de21c8d7318a94355249375bc1 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 4 May 2023 01:20:23 -0500 Subject: [PATCH 084/374] [APP-633] Improve some behaviors around desktop leftnav (#581) * Make leftnav elements act as anchor tags (bonus feature in this pr) * Add screen reset behavior to the desktop left nav * Move the leftnav link into the text --- src/view/shell/desktop/LeftNav.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 17d078dc51..621c7926ce 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -86,6 +86,7 @@ interface NavItemProps { const NavItem = observer( ({count, href, icon, iconFilled, label}: NavItemProps) => { const pal = usePalette('default') + const store = useStores() const [pathName] = React.useMemo(() => router.matchPath(href), [href]) const currentRouteName = useNavigationState(state => { if (!state) { @@ -96,12 +97,23 @@ const NavItem = observer( const isCurrent = isTab(currentRouteName, pathName) const {onPress} = useLinkProps({to: href}) + const onPressWrapped = React.useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + if (isCurrent) { + store.emitScreenSoftReset() + } else { + onPress() + } + }, + [onPress, isCurrent, store], + ) return ( @@ -113,7 +125,11 @@ const NavItem = observer( ) : null} - + {label} From 7a008c987ca40326b9a2c6195d3f5f705c2f6c3f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 4 May 2023 01:21:08 -0500 Subject: [PATCH 085/374] 1.27 --- app.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index c18fc4a72e..c9c48b0763 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Bluesky", "slug": "bluesky", "owner": "blueskysocial", - "version": "1.26.0", + "version": "1.27.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -38,7 +38,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 11, + "versionCode": 12, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" diff --git a/package.json b/package.json index a545ba2512..2e7113aaf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.26.0", + "version": "1.27.0", "private": true, "scripts": { "postinstall": "patch-package", From c8af784328177bbf8b8e9df5c2ae0e1087057b81 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 4 May 2023 11:56:17 -0700 Subject: [PATCH 086/374] Updates indigo golang packages post-lex-refactor (!), which fixes XRPC calls (#582) * bskyweb: update modules * bskyweb: fix XRPC string type * gitignore: yarn web build output in bskyweb --- bskyweb/.gitignore | 6 ++++ bskyweb/cmd/bskyweb/server.go | 2 +- bskyweb/go.mod | 28 +++++++++---------- bskyweb/go.sum | 52 +++++++++++++++++++---------------- 4 files changed, 49 insertions(+), 39 deletions(-) diff --git a/bskyweb/.gitignore b/bskyweb/.gitignore index b2a31beb38..1d945e1dab 100644 --- a/bskyweb/.gitignore +++ b/bskyweb/.gitignore @@ -4,5 +4,11 @@ test-coverage.out # Don't check in the binary. /bskyweb +# Don't accidentally commit JS-generated code +static/js/*.js +static/js/*.map +static/js/*.js.LICENSE.txt +templates/scripts.html + # Don't ignore this file !.gitignore diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 5e934c6b0e..25ae5cc3ff 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -47,7 +47,7 @@ func serve(cctx *cli.Context) error { } auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ - Identifier: &xrpcc.Auth.Handle, + Identifier: xrpcc.Auth.Handle, Password: atpPassword, }) if err != nil { diff --git a/bskyweb/go.mod b/bskyweb/go.mod index 9014fa1b55..b2d49a92bb 100644 --- a/bskyweb/go.mod +++ b/bskyweb/go.mod @@ -3,18 +3,18 @@ module github.com/bluesky-social/social-app/bskyweb go 1.20 require ( - github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8 + github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 github.com/flosch/pongo2/v6 v6.0.0 github.com/ipfs/go-log v1.0.5 github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.10.2 - github.com/urfave/cli/v2 v2.25.1 + github.com/urfave/cli/v2 v2.25.3 ) require ( github.com/benbjohnson/clock v1.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.2 // indirect @@ -26,7 +26,7 @@ require ( github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/ipfs/bbloom v0.0.4 // indirect github.com/ipfs/go-block-format v0.1.2 // indirect - github.com/ipfs/go-cid v0.4.0 // indirect + github.com/ipfs/go-cid v0.4.1 // indirect github.com/ipfs/go-datastore v0.6.0 // indirect github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect @@ -68,22 +68,22 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70 // indirect + github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 // indirect github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - go.opentelemetry.io/otel v1.14.0 // indirect - go.opentelemetry.io/otel/trace v1.14.0 // indirect - go.uber.org/atomic v1.10.0 // indirect + go.opentelemetry.io/otel v1.15.1 // indirect + go.opentelemetry.io/otel/trace v1.15.1 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/crypto v0.8.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gorm.io/driver/postgres v1.5.0 // indirect - gorm.io/driver/sqlite v1.4.4 // indirect - gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 // indirect + gorm.io/driver/sqlite v1.5.0 // indirect + gorm.io/gorm v1.25.0 // indirect lukechampine.com/blake3 v1.1.7 // indirect ) diff --git a/bskyweb/go.sum b/bskyweb/go.sum index 9155dc34d4..fa15187c39 100644 --- a/bskyweb/go.sum +++ b/bskyweb/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8 h1:6A8D48CksjnlmJb8r5WaFAB4J2osllkmQoQhhiREMHw= -github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8/go.mod h1:9dcnKLtEnDPBdWm5/BLJHvbaPndCdKzAaBK7sbGt3S4= +github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 h1:VCNXRXpgyK3xkaQ8fzL5WzswerwLycke4B9ggLs1uOA= +github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319/go.mod h1:Hc09SUJXAIujaAvq7JXxi8ZQQI887grzPkHgn4JyE1Q= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -12,8 +12,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -55,8 +56,8 @@ github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUP github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= -github.com/ipfs/go-cid v0.4.0 h1:a4pdZq0sx6ZSxbCizebnKiMCx/xI/aBBFlB73IgH4rA= -github.com/ipfs/go-cid v0.4.0/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk= github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= @@ -96,7 +97,6 @@ github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -209,8 +209,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= -github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= +github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -218,8 +218,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= -github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70 h1:iNBzUKTsJc9RqStEVX2VYgVHATTU39IuB7g0e8OPWXU= -github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= +github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 h1:XYEgH2nJgsrcrj32p+SAbx6T3s/6QknOXezXtz7kzbg= +github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 h1:EO/9z3yDvx1van1/0esdcqhalZZQGRj3I1BPTWr5k3A= github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220/go.mod h1:qPtRyexGM5XMHFIfjH+EiA/A/1n2JakWEdMPC53pJAE= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= @@ -228,14 +228,14 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= +go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= +go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= +go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -255,8 +255,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -273,8 +274,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -297,8 +299,9 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -307,8 +310,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -343,11 +347,11 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= -gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= -gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= -gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g= +gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= +gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= +gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= From d3e8bd3e9a1f71b1177c0493634515379b0d1de8 Mon Sep 17 00:00:00 2001 From: Ansh Date: Thu, 4 May 2023 14:18:27 -0700 Subject: [PATCH 087/374] [APP-547] Universal links & deeplinking (#555) * added ios scheme and intentFilters for deep linking * added intentFilters for android deep linking * add .env files to .gitignore * add autoVerify for android deep links --- .gitignore | 6 +++++- app.json | 19 +++++++++++++++++-- eas.json | 1 + src/Navigation.tsx | 4 +++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index bab37d2ad4..2fa850bf76 100644 --- a/.gitignore +++ b/.gitignore @@ -92,4 +92,8 @@ web-build/ # Android & iOS folders android/ -ios/ \ No newline at end of file +ios/ + +# environment variables +.env +.env.* \ No newline at end of file diff --git a/app.json b/app.json index c9c48b0763..cd0173881a 100644 --- a/app.json +++ b/app.json @@ -2,6 +2,7 @@ "expo": { "name": "Bluesky", "slug": "bluesky", + "scheme": "bluesky", "owner": "blueskysocial", "version": "1.27.0", "orientation": "portrait", @@ -31,7 +32,8 @@ "NSMicrophoneUsageDescription": "Used for posts and other kinds of content.", "NSPhotoLibraryAddUsageDescription": "Used to save images to your library.", "NSPhotoLibraryUsageDescription": "Used for profile pictures, posts, and other kinds of content" - } + }, + "associatedDomains": ["applinks:bsky.app", "applinks:staging.bsky.app"] }, "androidStatusBar": { "barStyle": "dark-content", @@ -43,7 +45,20 @@ "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" }, - "package": "xyz.blueskyweb.app" + "package": "xyz.blueskyweb.app", + "intentFilters": [ + { + "action": "VIEW", + "autoVerify": true, + "data": [ + { + "scheme": "https", + "host": "bsky.app" + } + ], + "category": ["BROWSABLE", "DEFAULT"] + } + ] }, "web": { "favicon": "./assets/favicon.png" diff --git a/eas.json b/eas.json index 37671c0868..32d5b13d9f 100644 --- a/eas.json +++ b/eas.json @@ -8,6 +8,7 @@ "developmentClient": true, "distribution": "internal", "ios": { + "simulator": true, "resourceClass": "medium" }, "channel": "development", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 9a163fc43b..afc7b39b87 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -299,7 +299,9 @@ function navigate( function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') { if (navigationRef.isReady()) { navigate(tabName) - navigationRef.dispatch(StackActions.popToTop()) + if (navigationRef.canGoBack()) { + navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it + } } } From 8d78e8581c7d24e2c3f4d96c5217914297542b7b Mon Sep 17 00:00:00 2001 From: Ollie H Date: Thu, 4 May 2023 22:25:52 -0700 Subject: [PATCH 088/374] Move href back to link (#590) * Move href back to link * Fix cmd/ctrl click on left nav --------- Co-authored-by: Paul Frazee --- src/view/shell/desktop/LeftNav.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 621c7926ce..ca63303041 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -99,6 +99,9 @@ const NavItem = observer( const {onPress} = useLinkProps({to: href}) const onPressWrapped = React.useCallback( (e: React.MouseEvent) => { + if (e.ctrlKey || e.metaKey || e.altKey) { + return + } e.preventDefault() if (isCurrent) { store.emitScreenSoftReset() @@ -114,6 +117,9 @@ const NavItem = observer( style={styles.navItemWrapper} hoverStyle={pal.viewLight} onPress={onPressWrapped} + // @ts-ignore web only -prf + href={href} + dataSet={{noUnderline: 1}} accessibilityRole="tab" accessibilityLabel={label} accessibilityHint=""> @@ -125,11 +131,7 @@ const NavItem = observer( ) : null} - + {label} From f28405f9283fe326abb1403f1eb0f3b1dfb61211 Mon Sep 17 00:00:00 2001 From: Ansh Date: Thu, 4 May 2023 22:27:05 -0700 Subject: [PATCH 089/374] sanitize app passwords name (#589) --- src/view/com/modals/AddAppPasswords.tsx | 42 ++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx index 58b53586b7..2af9967a79 100644 --- a/src/view/com/modals/AddAppPasswords.tsx +++ b/src/view/com/modals/AddAppPasswords.tsx @@ -72,6 +72,19 @@ export function Component({}: {}) { }, [store]) const createAppPassword = async () => { + // if name is all whitespace, we don't allow it + if (!name || !name.trim()) { + Toast.show( + 'Please enter a name for your app password. All spaces is not allowed.', + ) + return + } + // if name is too short (under 4 chars), we don't allow it + if (name.length < 4) { + Toast.show('App Password names must be at least 4 characters long.') + return + } + try { const newPassword = await store.me.createAppPassword(name) if (newPassword) { @@ -86,13 +99,27 @@ export function Component({}: {}) { } } + const _onChangeText = (text: string) => { + // sanitize input + // we only all alphanumeric characters, spaces, dashes, and underscores + // if the user enters anything else, we ignore it and shake the input container + // also, it cannot start with a space + if (text.match(/^[a-zA-Z0-9-_ ]*$/)) { + setName(text) + } else { + Toast.show( + 'App Password names can only contain letters, numbers, spaces, dashes, and underscores.', + ) + } + } + return ( {!appPassword ? ( - Please enter a unique name for this App Password. We have generated - a random name for you. + Please enter a unique name for this App Password or use our randomly + generated one. ) : ( @@ -106,7 +133,7 @@ export function Component({}: {}) { - ) : null} + ) : ( + + Only contain letters, numbers, spaces, dashes, and underscores + allowed. Must be at least 4 characters long, but no more than 32 + characters long. + + )} - )) - ) : ( -
No result
- )} + + {items.length > 0 ? ( + items.map((item, index) => { + const displayName = getDisplayedName( + item.displayName ?? item.handle, + ) + const isSelected = selectedIndex === index + + return ( + + + + + {displayName} + + + + {item.handle} + + + ) + }) + ) : ( + + No result + + )} + ) }, ) + +const styles = StyleSheet.create({ + container: { + width: 500, + borderRadius: 6, + borderWidth: 1, + borderStyle: 'solid', + padding: 4, + }, + mentionContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'row', + paddingHorizontal: 12, + paddingVertical: 8, + gap: 4, + }, + firstMention: { + borderTopLeftRadius: 2, + borderTopRightRadius: 2, + }, + lastMention: { + borderBottomLeftRadius: 2, + borderBottomRightRadius: 2, + }, + avatarAndDisplayName: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + noResult: { + paddingHorizontal: 12, + paddingVertical: 8, + }, +}) diff --git a/web/index.html b/web/index.html index f88fd727b3..f518665ca4 100644 --- a/web/index.html +++ b/web/index.html @@ -110,27 +110,7 @@ outline: 0; } .tippy-content .items { - border-radius: 6px; - background: #F3F3F8; - border: 1px solid #e0d9d9; - padding: 3px 3px; - } - .tippy-content .items .item { - display: block; - background: transparent; - color: #8a8c9a; - border: 0; - font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - padding: 7px 10px 8px; - width: 100%; - text-align: left; - box-sizing: border-box; - letter-spacing: 0.2px; - } - .tippy-content .items .item.is-selected { - background: #fff; - border-radius: 4px; - color: #333; + width: fit-content; } From 7a176b3fdff7d27651b306e7550010b344dfa922 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 8 May 2023 17:25:57 -0500 Subject: [PATCH 100/374] [APP-615] COPPA-compliant signup (#570) * Rework account creation to be COPPA compliant * Fix lint * Switch android datepicker to use the spinner mode * Fix type signatures & usages --- package.json | 1 + src/lib/strings/time.ts | 10 +++ src/state/models/ui/create-account.ts | 19 +++-- src/view/com/auth/create/Step2.tsx | 62 ++++----------- src/view/com/util/forms/Button.tsx | 11 ++- src/view/com/util/forms/DateInput.tsx | 96 +++++++++++++++++++++++ src/view/com/util/forms/DateInput.web.tsx | 92 ++++++++++++++++++++++ src/view/index.ts | 2 + src/view/screens/Settings.tsx | 6 +- yarn.lock | 7 ++ 10 files changed, 254 insertions(+), 52 deletions(-) create mode 100644 src/view/com/util/forms/DateInput.tsx create mode 100644 src/view/com/util/forms/DateInput.web.tsx diff --git a/package.json b/package.json index f6ad2ebad4..56b0366d42 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@react-native-camera-roll/camera-roll": "^5.2.2", "@react-native-clipboard/clipboard": "^1.10.0", "@react-native-community/blur": "^4.3.0", + "@react-native-community/datetimepicker": "6.7.3", "@react-navigation/bottom-tabs": "^6.5.7", "@react-navigation/drawer": "^6.6.2", "@react-navigation/native": "^6.1.6", diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts index 6cd70498ec..588b844598 100644 --- a/src/lib/strings/time.ts +++ b/src/lib/strings/time.ts @@ -39,3 +39,13 @@ export function niceDate(date: number | string | Date) { minute: '2-digit', })}` } + +export function getAge(birthDate: Date): number { + var today = new Date() + var age = today.getFullYear() - birthDate.getFullYear() + var m = today.getMonth() - birthDate.getMonth() + if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { + age-- + } + return age +} diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index e661cb59dc..3f83dd6a72 100644 --- a/src/state/models/ui/create-account.ts +++ b/src/state/models/ui/create-account.ts @@ -6,6 +6,9 @@ import {ComAtprotoServerCreateAccount} from '@atproto/api' import * as EmailValidator from 'email-validator' import {createFullHandle} from 'lib/strings/handles' import {cleanError} from 'lib/strings/errors' +import {getAge} from 'lib/strings/time' + +const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago export class CreateAccountModel { step: number = 1 @@ -21,7 +24,7 @@ export class CreateAccountModel { email = '' password = '' handle = '' - is13 = false + birthDate = DEFAULT_DATE constructor(public rootStore: RootStoreModel) { makeAutoObservable(this, {}, {autoBind: true}) @@ -32,6 +35,13 @@ export class CreateAccountModel { next() { this.error = '' + if (this.step === 2) { + if (getAge(this.birthDate) < 13) { + this.error = + 'Unfortunately, you do not meet the requirements to create an account.' + return + } + } this.step++ } @@ -124,8 +134,7 @@ export class CreateAccountModel { return ( (!this.isInviteCodeRequired || this.inviteCode) && !!this.email && - !!this.password && - this.is13 + !!this.password ) } return !!this.handle @@ -186,7 +195,7 @@ export class CreateAccountModel { this.handle = v } - setIs13(v: boolean) { - this.is13 = v + setBirthDate(v: Date) { + this.birthDate = v } } diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index eceee50d35..1e014f18e5 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -1,14 +1,9 @@ import React from 'react' -import { - StyleSheet, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from 'react-native' +import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {CreateAccountModel} from 'state/models/ui/create-account' import {Text} from 'view/com/util/text/Text' +import {DateInput} from 'view/com/util/forms/DateInput' import {StepHeader} from './StepHeader' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -104,26 +99,20 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => { - Legal check + nativeID="birthDate"> + Your birth date - model.setIs13(!model.is13)} - accessibilityRole="checkbox" - accessibilityLabel="Verify age" - accessibilityHint="Verifies that I am at least 13 years of age" - accessibilityLabelledBy="legalCheck"> - - {model.is13 && ( - - )} - - - I am 13 years old or older - - +
{model.serviceDescription && ( @@ -144,26 +133,9 @@ const styles = StyleSheet.create({ marginTop: 10, }, - toggleBtn: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', + dateInputButton: { borderWidth: 1, - paddingHorizontal: 10, - paddingVertical: 10, borderRadius: 6, - }, - toggleBtnLabel: { - flex: 1, - paddingHorizontal: 10, - }, - - checkbox: { - borderWidth: 1, - borderRadius: 2, - width: 24, - height: 24, - alignItems: 'center', - justifyContent: 'center', + paddingVertical: 14, }, }) diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx index 3b5b00284a..1c9b1cf516 100644 --- a/src/view/com/util/forms/Button.tsx +++ b/src/view/com/util/forms/Button.tsx @@ -35,6 +35,9 @@ export function Button({ onPress, children, testID, + accessibilityLabel, + accessibilityHint, + accessibilityLabelledBy, }: React.PropsWithChildren<{ type?: ButtonType label?: string @@ -42,6 +45,9 @@ export function Button({ labelStyle?: StyleProp onPress?: () => void testID?: string + accessibilityLabel?: string + accessibilityHint?: string + accessibilityLabelledBy?: string }>) { const theme = useTheme() const typeOuterStyle = choose>( @@ -133,7 +139,10 @@ export function Button({ style={[typeOuterStyle, styles.outer, style]} onPress={onPressWrapped} testID={testID} - accessibilityRole="button"> + accessibilityRole="button" + accessibilityLabel={accessibilityLabel} + accessibilityHint={accessibilityHint} + accessibilityLabelledBy={accessibilityLabelledBy}> {label ? ( {label} diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx new file mode 100644 index 0000000000..4aa5cb6106 --- /dev/null +++ b/src/view/com/util/forms/DateInput.tsx @@ -0,0 +1,96 @@ +import React, {useState, useCallback} from 'react' +import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' +import DateTimePicker, { + DateTimePickerEvent, +} from '@react-native-community/datetimepicker' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {isIOS, isAndroid} from 'platform/detection' +import {Button, ButtonType} from './Button' +import {Text} from '../text/Text' +import {TypographyVariant} from 'lib/ThemeContext' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' + +interface Props { + testID?: string + value: Date + onChange: (date: Date) => void + buttonType?: ButtonType + buttonStyle?: StyleProp + buttonLabelType?: TypographyVariant + buttonLabelStyle?: StyleProp + accessibilityLabel: string + accessibilityHint: string + accessibilityLabelledBy?: string +} + +export function DateInput(props: Props) { + const [show, setShow] = useState(false) + const theme = useTheme() + const pal = usePalette('default') + + const onChangeInternal = useCallback( + (event: DateTimePickerEvent, date: Date | undefined) => { + setShow(false) + if (date) { + props.onChange(date) + } + }, + [setShow, props], + ) + + const onPress = useCallback(() => { + setShow(true) + }, [setShow]) + + return ( + + {isAndroid && ( + + )} + {(isIOS || show) && ( + + )} + + ) +} + +const styles = StyleSheet.create({ + button: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, +}) diff --git a/src/view/com/util/forms/DateInput.web.tsx b/src/view/com/util/forms/DateInput.web.tsx new file mode 100644 index 0000000000..89dff5510c --- /dev/null +++ b/src/view/com/util/forms/DateInput.web.tsx @@ -0,0 +1,92 @@ +import React, {useState, useCallback} from 'react' +import { + StyleProp, + StyleSheet, + TextInput as RNTextInput, + TextStyle, + View, + ViewStyle, +} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' + +interface Props { + testID?: string + value: Date + onChange: (date: Date) => void + buttonType?: string + buttonStyle?: StyleProp + buttonLabelType?: string + buttonLabelStyle?: StyleProp + accessibilityLabel: string + accessibilityHint: string + accessibilityLabelledBy?: string +} + +export function DateInput(props: Props) { + const theme = useTheme() + const pal = usePalette('default') + const palError = usePalette('error') + const [value, setValue] = useState(props.value.toLocaleDateString()) + const [isValid, setIsValid] = useState(true) + + const onChangeInternal = useCallback( + (v: string) => { + setValue(v) + const d = new Date(v) + if (!isNaN(Number(d))) { + setIsValid(true) + props.onChange(d) + } else { + setIsValid(false) + } + }, + [setValue, setIsValid, props], + ) + + return ( + + + onChangeInternal(v)} + value={value} + accessibilityLabel={props.accessibilityLabel} + accessibilityHint={props.accessibilityHint} + accessibilityLabelledBy={props.accessibilityLabelledBy} + /> + + ) +} + +const styles = StyleSheet.create({ + container: { + borderWidth: 1, + borderRadius: 6, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 4, + }, + icon: { + marginLeft: 10, + }, + textInput: { + flex: 1, + width: '100%', + paddingVertical: 10, + paddingHorizontal: 10, + fontSize: 17, + letterSpacing: 0.25, + fontWeight: '400', + borderRadius: 10, + }, +}) diff --git a/src/view/index.ts b/src/view/index.ts index 8de0358683..dd8a585d66 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -20,6 +20,7 @@ import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark' import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark' +import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar' import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera' import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' import {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck' @@ -97,6 +98,7 @@ export function setup() { farBell, faBookmark, farBookmark, + farCalendar, faCamera, faCheck, faCircleCheck, diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 5559f036dc..f98cdc0c8a 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -440,6 +440,7 @@ export const SettingsScreen = withAuthRequired( function AccountDropdownBtn({handle}: {handle: string}) { const store = useStores() + const pal = usePalette('default') const items = [ { label: 'Remove account', @@ -452,7 +453,10 @@ function AccountDropdownBtn({handle}: {handle: string}) { return ( - + ) diff --git a/yarn.lock b/yarn.lock index 3351f6b1dc..bf556972e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2949,6 +2949,13 @@ prompts "^2.4.0" semver "^6.3.0" +"@react-native-community/datetimepicker@6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-6.7.3.tgz#e6d75a42729265d8404d1d668c86926564abca2f" + integrity sha512-fXWbEdHMLW/e8cts3snEsbOTbnFXfUHeO2pkiDFX3fWpFoDtUrRWvn50xbY13IJUUKHDhoJ+mj24nMRVIXfX1A== + dependencies: + invariant "^2.2.4" + "@react-native-community/eslint-config@^3.0.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@react-native-community/eslint-config/-/eslint-config-3.2.0.tgz#42f677d5fff385bccf1be1d3b8faa8c086cf998d" From b756a2795807ad85051d572f82957356f9b5f44b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 9 May 2023 00:43:20 -0500 Subject: [PATCH 101/374] [APP-639] Improve nsfw handling & force hidden on iOS (#605) * Identify adult content labels and handle them more specifically * Change adult content defaults to more conservative settings * Add an adultcontentenabled override that prohibits access on iOS * Improve usability of the content hider * Fix lint --- src/lib/labeling/const.ts | 15 +- src/lib/labeling/helpers.ts | 21 ++- src/lib/labeling/types.ts | 2 +- src/state/models/ui/preferences.ts | 15 +- .../com/modals/ContentFilteringSettings.tsx | 44 ++++-- src/view/com/post-thread/PostThreadItem.tsx | 9 +- src/view/com/post/Post.tsx | 5 +- src/view/com/posts/FeedItem.tsx | 5 +- src/view/com/util/moderation/ContentHider.tsx | 41 +++--- src/view/com/util/moderation/ImageHider.tsx | 128 ++++++++++++++++++ 10 files changed, 223 insertions(+), 62 deletions(-) create mode 100644 src/view/com/util/moderation/ImageHider.tsx diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index 54cc732b92..2a9b921dbc 100644 --- a/src/lib/labeling/const.ts +++ b/src/lib/labeling/const.ts @@ -6,7 +6,6 @@ export const ILLEGAL_LABEL_GROUP: LabelValGroup = { title: 'Illegal Content', warning: 'Illegal Content', values: ['csam', 'dmca-violation', 'nudity-nonconsentual'], - imagesOnly: false, } export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = { @@ -14,7 +13,6 @@ export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = { title: 'Content Warning', warning: 'Content Warning', values: ['!filter'], - imagesOnly: false, } export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = { @@ -22,7 +20,6 @@ export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = { title: 'Content Warning', warning: 'Content Warning', values: ['!warn'], - imagesOnly: false, } export const UNKNOWN_LABEL_GROUP: LabelValGroup = { @@ -30,7 +27,6 @@ export const UNKNOWN_LABEL_GROUP: LabelValGroup = { title: 'Unknown Label', warning: 'Content Warning', values: [], - imagesOnly: false, } export const CONFIGURABLE_LABEL_GROUPS: Record< @@ -43,7 +39,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'i.e. Pornography', warning: 'Sexually Explicit', values: ['porn'], - imagesOnly: false, // apply to whole thing + isAdultImagery: true, }, nudity: { id: 'nudity', @@ -51,7 +47,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Including non-sexual and artistic', warning: 'Nudity', values: ['nudity'], - imagesOnly: true, + isAdultImagery: true, }, suggestive: { id: 'suggestive', @@ -59,7 +55,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Does not include nudity', warning: 'Sexually Suggestive', values: ['sexual'], - imagesOnly: true, + isAdultImagery: true, }, gore: { id: 'gore', @@ -67,14 +63,13 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Gore, self-harm, torture', warning: 'Violence', values: ['gore', 'self-harm', 'torture'], - imagesOnly: true, + isAdultImagery: true, }, hate: { id: 'hate', title: 'Political Hate-Groups', warning: 'Hate', values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'], - imagesOnly: false, }, spam: { id: 'spam', @@ -82,7 +77,6 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Excessive low-quality posts', warning: 'Spam', values: ['spam'], - imagesOnly: false, }, impersonation: { id: 'impersonation', @@ -90,6 +84,5 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Accounts falsely claiming to be people or orgs', warning: 'Impersonation', values: ['impersonation'], - imagesOnly: false, }, } diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index 71ea43c087..baac0ed5a6 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -137,12 +137,12 @@ export function getPostModeration( // warning cases if (postPref.pref === 'warn') { - if (postPref.desc.imagesOnly) { + if (postPref.desc.isAdultImagery) { return { avatar, - list: warnContent(postPref.desc.warning), // TODO make warnImages when there's time - thread: warnContent(postPref.desc.warning), // TODO make warnImages when there's time - view: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + list: warnImages(postPref.desc.warning), + thread: warnImages(postPref.desc.warning), + view: warnImages(postPref.desc.warning), } } return { @@ -401,10 +401,9 @@ function warnContent(reason: string) { } } -// TODO -// function warnImages(reason: string) { -// return { -// behavior: ModerationBehaviorCode.WarnImages, -// reason, -// } -// } +function warnImages(reason: string) { + return { + behavior: ModerationBehaviorCode.WarnImages, + reason, + } +} diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts index 123c5d1f38..078043076e 100644 --- a/src/lib/labeling/types.ts +++ b/src/lib/labeling/types.ts @@ -11,7 +11,7 @@ export interface LabelValGroup { | 'always-warn' | 'unknown' title: string - imagesOnly: boolean + isAdultImagery?: boolean subtitle?: string warning: string values: string[] diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 7b41fa7466..fcd33af8e9 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -10,15 +10,16 @@ import { ALWAYS_FILTER_LABEL_GROUP, ALWAYS_WARN_LABEL_GROUP, } from 'lib/labeling/const' +import {isIOS} from 'platform/detection' const deviceLocales = getLocales() export type LabelPreference = 'show' | 'warn' | 'hide' export class LabelPreferencesModel { - nsfw: LabelPreference = 'warn' - nudity: LabelPreference = 'show' - suggestive: LabelPreference = 'show' + nsfw: LabelPreference = 'hide' + nudity: LabelPreference = 'warn' + suggestive: LabelPreference = 'warn' gore: LabelPreference = 'warn' hate: LabelPreference = 'hide' spam: LabelPreference = 'hide' @@ -30,6 +31,7 @@ export class LabelPreferencesModel { } export class PreferencesModel { + adultContentEnabled = !isIOS contentLanguages: string[] = deviceLocales?.map?.(locale => locale.languageCode) || [] contentLabels = new LabelPreferencesModel() @@ -102,7 +104,9 @@ export class PreferencesModel { } else if (group.id === 'always-filter') { return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP} } else if (group.id === 'always-warn') { - return {pref: 'warn', desc: ALWAYS_WARN_LABEL_GROUP} + res.pref = 'warn' + res.desc = ALWAYS_WARN_LABEL_GROUP + continue } else if (group.id === 'unknown') { continue } @@ -115,6 +119,9 @@ export class PreferencesModel { res.desc = group } } + if (res.desc.isAdultImagery && !this.adultContentEnabled) { + res.pref = 'hide' + } return res } } diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index cfba2575a0..30b465562c 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -24,10 +24,22 @@ export function Component({}: {}) { Content Moderation - - - - + + + + @@ -55,7 +67,13 @@ export function Component({}: {}) { // TODO: Refactor this component to pass labels down to each tab const ContentLabelPref = observer( - ({group}: {group: keyof typeof CONFIGURABLE_LABEL_GROUPS}) => { + ({ + group, + disabled, + }: { + group: keyof typeof CONFIGURABLE_LABEL_GROUPS + disabled?: boolean + }) => { const store = useStores() const pal = usePalette('default') return ( @@ -70,11 +88,17 @@ const ContentLabelPref = observer( )}
- store.preferences.setContentLabelPref(group, v)} - group={group} - /> + {disabled ? ( + + Hide + + ) : ( + store.preferences.setContentLabelPref(group, v)} + group={group} + /> + )}
) }, diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index d657c92c38..563a3ead6f 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -24,6 +24,7 @@ import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/PostCtrls' import {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' +import {ImageHider} from '../util/moderation/ImageHider' import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' import {formatCount} from '../util/numeric/format' @@ -234,7 +235,9 @@ export const PostThreadItem = observer(function PostThreadItem({ />
) : undefined} - + + + {niceDate(item.post.indexedAt)} @@ -366,7 +369,9 @@ export const PostThreadItem = observer(function PostThreadItem({ /> ) : undefined} - + + + ) : undefined} - + + + ) : undefined} - + + + ) { const pal = usePalette('default') const [override, setOverride] = React.useState(false) + const onPressShow = React.useCallback(() => { + setOverride(true) + }, [setOverride]) + const onPressHide = React.useCallback(() => { + setOverride(false) + }, [setOverride]) if ( moderation.behavior === ModerationBehaviorCode.Show || @@ -44,7 +44,15 @@ export function ContentHider({ return ( -
- setOverride(v => !v)} - accessibilityLabel={override ? 'Hide post' : 'Show post'} - // TODO: The text labelling should be split up so controls have unique roles - accessibilityHint={ - override - ? 'Re-hide post' - : 'Shows post hidden based on your moderation settings' - }> - + + {override ? 'Hide' : 'Show'} - - + +
{override && ( diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx new file mode 100644 index 0000000000..b42c6397da --- /dev/null +++ b/src/view/com/util/moderation/ImageHider.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {Text} from '../text/Text' +import {BlurView} from '../BlurView' +import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types' +import {isAndroid} from 'platform/detection' + +export function ImageHider({ + testID, + moderation, + style, + containerStyle, + children, +}: React.PropsWithChildren<{ + testID?: string + moderation: ModerationBehavior + style?: StyleProp + containerStyle?: StyleProp +}>) { + const pal = usePalette('default') + const [override, setOverride] = React.useState(false) + const onPressShow = React.useCallback(() => { + setOverride(true) + }, [setOverride]) + const onPressHide = React.useCallback(() => { + setOverride(false) + }, [setOverride]) + + if (moderation.behavior !== ModerationBehaviorCode.WarnImages) { + return ( + + {children} + + ) + } + + if (moderation.behavior === ModerationBehaviorCode.Hide) { + return null + } + + return ( + + + {children} + + {override ? ( + + + Hide + + + ) : ( + <> + {isAndroid ? ( + /* android has an issue that breaks the blurview */ + /* see https://github.com/Kureev/react-native-blur/issues/486 */ + + ) : ( + + )} + + + + {moderation.reason || 'Content warning'} + + + Show + + + + + )} + + ) +} + +const styles = StyleSheet.create({ + container: { + position: 'relative', + marginBottom: 10, + }, + overlay: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + blurView: { + borderRadius: 8, + }, + coverView: { + borderRadius: 8, + }, + info: { + justifyContent: 'center', + alignItems: 'center', + }, + showBtn: { + flexDirection: 'row', + gap: 8, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 24, + }, + hideBtn: { + position: 'absolute', + left: 8, + bottom: 20, + paddingHorizontal: 8, + paddingVertical: 6, + borderRadius: 8, + }, +}) From d0990e9b499f35d7bc89eac1932f4e26b42873b6 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 9 May 2023 00:45:33 -0500 Subject: [PATCH 102/374] Bump ios build number --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index c428284a5c..c1dc592cc2 100644 --- a/app.json +++ b/app.json @@ -14,7 +14,7 @@ "backgroundColor": "#ffffff" }, "ios": { - "buildNumber": "1", + "buildNumber": "2", "supportsTablet": false, "bundleIdentifier": "xyz.blueskyweb.app", "config": { From cfdfd8f39514a3d8edea373ad7d170c71ec2652b Mon Sep 17 00:00:00 2001 From: Ollie H Date: Tue, 9 May 2023 10:00:54 -0700 Subject: [PATCH 103/374] Add text wrapping to profile header (#603) --- src/view/com/profile/ProfileHeader.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index d69cf4a122..dee788aff5 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -30,7 +30,7 @@ import {ProfileHeaderWarnings} from '../util/moderation/ProfileHeaderWarnings' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' import {NavigationProp} from 'lib/routes/types' -import {isDesktopWeb} from 'platform/detection' +import {isDesktopWeb, isNative} from 'platform/detection' import {FollowState} from 'state/models/cache/my-follows' import {shareUrl} from 'lib/sharing' import {formatCount} from '../util/numeric/format' @@ -367,7 +367,7 @@ const ProfileHeaderLoaded = observer( ) : undefined} - @{view.handle} + @{view.handle} {!blockHide && ( <> @@ -553,6 +553,15 @@ const styles = StyleSheet.create({ }, title: {lineHeight: 38}, + // Word wrapping appears fine on + // mobile but overflows on desktop + handle: isNative + ? undefined + : { + // eslint-disable-next-line + wordBreak: 'break-all', + }, + handleLine: { flexDirection: 'row', marginBottom: 8, From 28f7ff76a4d7cb756d63f1cb6224965ce26cd6f8 Mon Sep 17 00:00:00 2001 From: Ansh Date: Tue, 9 May 2023 13:01:42 -0400 Subject: [PATCH 104/374] add target="_blank" prop to LinkText for safari (#606) --- src/lib/strings/url-helpers.ts | 4 ++++ src/view/com/util/Link.tsx | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 17a49fb265..549587f743 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -66,6 +66,10 @@ export function isBskyAppUrl(url: string): boolean { return url.startsWith('https://bsky.app/') } +export function isExternalUrl(url: string): boolean { + return !isBskyAppUrl(url) && url.startsWith('http') +} + export function isBskyPostUrl(url: string): boolean { if (isBskyAppUrl(url)) { try { diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 253f80bdc1..f753f01cc3 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -1,4 +1,4 @@ -import React, {ComponentProps} from 'react' +import React, {ComponentProps, useMemo} from 'react' import {observer} from 'mobx-react-lite' import { Linking, @@ -21,7 +21,7 @@ import {TypographyVariant} from 'lib/ThemeContext' import {NavigationProp} from 'lib/routes/types' import {router} from '../../../routes' import {useStores, RootStoreModel} from 'state/index' -import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers' +import {convertBskyAppUrlIfNeeded, isExternalUrl} from 'lib/strings/url-helpers' import {isDesktopWeb} from 'platform/detection' import {sanitizeUrl} from '@braintree/sanitize-url' @@ -132,6 +132,16 @@ export const TextLink = observer(function TextLink({ }, [store, navigation, href], ) + const hrefAttrs = useMemo(() => { + const isExternal = isExternalUrl(href) + if (isExternal) { + return { + target: '_blank', + // rel: 'noopener noreferrer', + } + } + return {} + }, [href]) return ( {text} From bf3ea67442857b62d29c74ba0739f31a79b0f7b7 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Tue, 9 May 2023 10:02:55 -0700 Subject: [PATCH 105/374] Add time to app password and fix related text wrapping (#604) --- src/view/screens/AppPasswords.tsx | 33 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx index a4bea68f7c..cb1896b4aa 100644 --- a/src/view/screens/AppPasswords.tsx +++ b/src/view/screens/AppPasswords.tsx @@ -180,21 +180,35 @@ function AppPassword({ ) }, [store, name]) + const {contentLanguages} = store.preferences + + const primaryLocale = + contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' + return ( - - {name} - - - - {new Date(createdAt).toDateString()} - + accessibilityLabel="Delete app password" + accessibilityHint=""> + + + {name} + + + Created{' '} + {Intl.DateTimeFormat(primaryLocale, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(new Date(createdAt))} + + ) @@ -246,6 +260,7 @@ const styles = StyleSheet.create({ item: { flexDirection: 'row', alignItems: 'center', + justifyContent: 'space-between', borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14, From 9a91b0c538e3c24a25285ae2bdf1d0dfd2ba53a4 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Tue, 9 May 2023 10:03:42 -0700 Subject: [PATCH 106/374] bskyweb: middleware to remove trailing / (#598) --- bskyweb/cmd/bskyweb/server.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 25ae5cc3ff..5ba1dbc803 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -92,6 +92,12 @@ func serve(cctx *cli.Context) error { e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) e.HTTPErrorHandler = customHTTPErrorHandler + // redirect trailing slash to non-trailing slash. + // all of our current endpoints have no trailing slash. + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ + RedirectCode: http.StatusFound, + })) + // configure routes e.GET("/robots.txt", echo.WrapHandler(staticHandler)) e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) From 8f6b5d3df9b5a5bb61514497f3f25289513ef119 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Tue, 9 May 2023 10:13:23 -0700 Subject: [PATCH 107/374] Add avatar to mobile autocomplete and create grapheme hook (#602) * Add avatar to mobile autocomplete and create grapheme hook * Remove comment, update filename, cut out redundant logic --- .../composer/text-input/hooks/useGrapheme.tsx | 36 ++++++ .../text-input/mobile/Autocomplete.tsx | 110 +++++++++++------- .../composer/text-input/web/Autocomplete.tsx | 30 +---- 3 files changed, 110 insertions(+), 66 deletions(-) create mode 100644 src/view/com/composer/text-input/hooks/useGrapheme.tsx diff --git a/src/view/com/composer/text-input/hooks/useGrapheme.tsx b/src/view/com/composer/text-input/hooks/useGrapheme.tsx new file mode 100644 index 0000000000..25947c3ec7 --- /dev/null +++ b/src/view/com/composer/text-input/hooks/useGrapheme.tsx @@ -0,0 +1,36 @@ +import Graphemer from 'graphemer' +import {useCallback, useMemo} from 'react' + +export const useGrapheme = () => { + const splitter = useMemo(() => new Graphemer(), []) + + const getGraphemeString = useCallback( + (name: string, length: number) => { + let remainingCharacters = 0 + + if (name.length > length) { + const graphemes = splitter.splitGraphemes(name) + + if (graphemes.length > length) { + remainingCharacters = 0 + name = `${graphemes.slice(0, length).join('')}...` + } else { + remainingCharacters = length - graphemes.length + name = graphemes.join('') + } + } else { + remainingCharacters = length - name.length + } + + return { + name, + remainingCharacters, + } + }, + [splitter], + ) + + return { + getGraphemeString, + } +} diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index 7806241f13..c9b8b84b18 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -5,6 +5,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {usePalette} from 'lib/hooks/usePalette' import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {useGrapheme} from '../hooks/useGrapheme' export const Autocomplete = observer( ({ @@ -16,6 +18,7 @@ export const Autocomplete = observer( }) => { const pal = usePalette('default') const positionInterp = useAnimatedValue(0) + const {getGraphemeString} = useGrapheme() useEffect(() => { Animated.timing(positionInterp, { @@ -35,58 +38,83 @@ export const Autocomplete = observer( }, ], } + return ( - - - {view.suggestions.slice(0, 5).map(item => ( - onSelect(item.handle)} - accessibilityLabel={`Select ${item.handle}`} - accessibilityHint={`Autocompletes to ${item.handle}`}> - - {item.displayName || item.handle} - -  @{item.handle} - + + {view.isActive ? ( + + {view.suggestions.length > 0 ? ( + view.suggestions.slice(0, 5).map(item => { + // Eventually use an average length + const MAX_CHARS = 40 + const MAX_HANDLE_CHARS = 20 + + // Using this approach because styling is not respecting + // bounding box wrapping (before converting to ellipsis) + const {name: displayHandle, remainingCharacters} = + getGraphemeString(item.handle, MAX_HANDLE_CHARS) + + const {name: displayName} = getGraphemeString( + item.displayName ?? item.handle, + MAX_CHARS - + MAX_HANDLE_CHARS + + (remainingCharacters > 0 ? remainingCharacters : 0), + ) + + return ( + onSelect(item.handle)} + accessibilityLabel={`Select ${item.handle}`} + accessibilityHint=""> + + + + {displayName} + + + + @{displayHandle} + + + ) + }) + ) : ( + + No result - - ))} - - + )} + + ) : null} + ) }, ) const styles = StyleSheet.create({ container: { - display: 'none', - height: 250, - }, - animatedContainer: { - display: 'none', - position: 'absolute', - left: -64, - right: 0, - top: 0, + marginLeft: -54, + top: 10, borderTopWidth: 1, }, - visible: { - display: 'flex', - }, item: { borderBottomWidth: 1, - paddingVertical: 16, - paddingHorizontal: 16, - height: 50, + paddingVertical: 12, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 6, + }, + avatarAndHandle: { + display: 'flex', + flexDirection: 'row', + gap: 6, + alignItems: 'center', + }, + noResults: { + paddingVertical: 12, }, }) diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 20dbbbbe8f..475ec119b2 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -1,9 +1,7 @@ import React, { forwardRef, - useCallback, useEffect, useImperativeHandle, - useMemo, useState, } from 'react' import {StyleSheet, View} from 'react-native' @@ -16,9 +14,9 @@ import { } from '@tiptap/suggestion' import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {usePalette} from 'lib/hooks/usePalette' -import Graphemer from 'graphemer' import {Text} from 'view/com/util/text/Text' import {UserAvatar} from 'view/com/util/UserAvatar' +import {useGrapheme} from '../hooks/useGrapheme' interface MentionListRef { onKeyDown: (props: SuggestionKeyDownProps) => boolean @@ -99,7 +97,7 @@ const MentionList = forwardRef( (props: SuggestionProps, ref) => { const [selectedIndex, setSelectedIndex] = useState(0) const pal = usePalette('default') - const splitter = useMemo(() => new Graphemer(), []) + const {getGraphemeString} = useGrapheme() const selectItem = (index: number) => { const item = props.items[index] @@ -148,32 +146,14 @@ const MentionList = forwardRef( const {items} = props - const getDisplayedName = useCallback( - (name: string) => { - // Heuristic value based on max display name and handle lengths - const DISPLAY_LIMIT = 30 - if (name.length > DISPLAY_LIMIT) { - const graphemes = splitter.splitGraphemes(name) - - if (graphemes.length > DISPLAY_LIMIT) { - return graphemes.length > DISPLAY_LIMIT - ? `${graphemes.slice(0, DISPLAY_LIMIT).join('')}...` - : name.substring(0, DISPLAY_LIMIT) - } - } - - return name - }, - [splitter], - ) - return (
{items.length > 0 ? ( items.map((item, index) => { - const displayName = getDisplayedName( + const {name: displayName} = getGraphemeString( item.displayName ?? item.handle, + 30, // Heuristic value; can be modified ) const isSelected = selectedIndex === index @@ -197,7 +177,7 @@ const MentionList = forwardRef( - {item.handle} + @{item.handle} ) From b0ebb6c9d17f9f6f78bf13fd2a0ba89d83a7c2a8 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Tue, 9 May 2023 12:55:44 -0700 Subject: [PATCH 108/374] Update web image editor (#588) * Update web image editor * Delete type-assertions.ts * Re-add getKeys * Uncomment rotation code * Revert "Uncomment rotation code" This reverts commit 6269f3b928c2e5cacaf5d0ff5323fe975ee48eab. * Shuffle dependencies and update mobile resolution * Update ImageEditor modal layout for mobile * Avoid accidental closes of the EditImage modal --------- Co-authored-by: Paul Frazee --- package.json | 1 + src/lib/type-assertions.ts | 3 + src/state/models/media/gallery.ts | 30 +- src/state/models/media/image.ts | 177 +++++++++- src/state/models/ui/shell.ts | 8 + src/view/com/composer/photos/Gallery.tsx | 8 +- src/view/com/modals/AltImage.tsx | 1 - src/view/com/modals/EditImage.tsx | 418 +++++++++++++++++++++++ src/view/com/modals/Modal.web.tsx | 5 +- yarn.lock | 7 + 10 files changed, 642 insertions(+), 16 deletions(-) create mode 100644 src/lib/type-assertions.ts create mode 100644 src/view/com/modals/EditImage.tsx diff --git a/package.json b/package.json index 56b0366d42..3f35f9bd74 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "expo-dev-client": "~2.1.1", "expo-device": "~5.2.1", "expo-image": "^1.2.1", + "expo-image-manipulator": "^11.1.1", "expo-image-picker": "~14.1.1", "expo-localization": "~14.1.1", "expo-media-library": "~15.2.3", diff --git a/src/lib/type-assertions.ts b/src/lib/type-assertions.ts new file mode 100644 index 0000000000..6b5db51247 --- /dev/null +++ b/src/lib/type-assertions.ts @@ -0,0 +1,3 @@ +export const getKeys = Object.keys as ( + obj: T, +) => Array diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts index 97b1ac1d87..86bf8a314b 100644 --- a/src/state/models/media/gallery.ts +++ b/src/state/models/media/gallery.ts @@ -5,6 +5,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker' import {openPicker} from 'lib/media/picker' import {getImageDim} from 'lib/media/manip' import {getDataUriSize} from 'lib/media/util' +import {isNative} from 'platform/detection' export class GalleryModel { images: ImageModel[] = [] @@ -37,7 +38,12 @@ export class GalleryModel { // Temporarily enforce uniqueness but can eventually also use index if (!this.images.some(i => i.path === image_.path)) { const image = new ImageModel(this.rootStore, image_) - await image.compress() + + if (!isNative) { + await image.manipulate({}) + } else { + await image.compress() + } runInAction(() => { this.images.push(image) @@ -45,6 +51,20 @@ export class GalleryModel { } } + async edit(image: ImageModel) { + if (!isNative) { + this.rootStore.shell.openModal({ + name: 'edit-image', + image, + gallery: this, + }) + + return + } else { + this.crop(image) + } + } + async paste(uri: string) { if (this.size >= 4) { return @@ -65,8 +85,8 @@ export class GalleryModel { }) } - setAltText(image: ImageModel) { - image.setAltText() + setAltText(image: ImageModel, altText: string) { + image.setAltText(altText) } crop(image: ImageModel) { @@ -78,6 +98,10 @@ export class GalleryModel { this.images.splice(index, 1) } + async previous(image: ImageModel) { + image.previous() + } + async pick() { const images = await openPicker(this.rootStore, { multiple: true, diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index dcd47665c3..ff464a5a9a 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -1,13 +1,26 @@ import {Image as RNImage} from 'react-native-image-crop-picker' import {RootStoreModel} from 'state/index' -import {compressAndResizeImageForPost} from 'lib/media/manip' import {makeAutoObservable, runInAction} from 'mobx' -import {openCropper} from 'lib/media/picker' import {POST_IMG_MAX} from 'lib/constants' -import {scaleDownDimensions} from 'lib/media/util' +import * as ImageManipulator from 'expo-image-manipulator' +import {getDataUriSize, scaleDownDimensions} from 'lib/media/util' +import {openCropper} from 'lib/media/picker' +import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator' +import {Position} from 'react-avatar-editor' +import {compressAndResizeImageForPost} from 'lib/media/manip' // TODO: EXIF embed // Cases to consider: ExternalEmbed + +export interface ImageManipulationAttributes { + rotate?: number + scale?: number + position?: Position + flipHorizontal?: boolean + flipVertical?: boolean + aspectRatio?: '4:3' | '1:1' | '3:4' | 'None' +} + export class ImageModel implements RNImage { path: string mime = 'image/jpeg' @@ -20,6 +33,17 @@ export class ImageModel implements RNImage { scaledWidth: number = POST_IMG_MAX.width scaledHeight: number = POST_IMG_MAX.height + // Web manipulation + aspectRatio?: ImageManipulationAttributes['aspectRatio'] + position?: Position = undefined + prev?: RNImage = undefined + rotation?: number = 0 + scale?: number = 1 + flipHorizontal?: boolean = false + flipVertical?: boolean = false + + prevAttributes: ImageManipulationAttributes = {} + constructor(public rootStore: RootStoreModel, image: RNImage) { makeAutoObservable(this, { rootStore: false, @@ -32,12 +56,55 @@ export class ImageModel implements RNImage { this.calcScaledDimensions() } + // TODO: Revisit compression factor due to updated sizing with zoom + // get compressionFactor() { + // const MAX_IMAGE_SIZE_IN_BYTES = 976560 + + // return this.size < MAX_IMAGE_SIZE_IN_BYTES + // ? 1 + // : MAX_IMAGE_SIZE_IN_BYTES / this.size + // } + + get ratioMultipliers() { + return { + '4:3': 4 / 3, + '1:1': 1, + '3:4': 3 / 4, + None: this.width / this.height, + } + } + + getDisplayDimensions( + as: ImageManipulationAttributes['aspectRatio'] = '1:1', + maxSide: number, + ) { + const ratioMultiplier = this.ratioMultipliers[as] + + if (ratioMultiplier === 1) { + return { + height: maxSide, + width: maxSide, + } + } + + if (ratioMultiplier < 1) { + return { + width: maxSide * ratioMultiplier, + height: maxSide, + } + } + + return { + width: maxSide, + height: maxSide / ratioMultiplier, + } + } + calcScaledDimensions() { const {width, height} = scaleDownDimensions( {width: this.width, height: this.height}, POST_IMG_MAX, ) - this.scaledWidth = width this.scaledHeight = height } @@ -46,6 +113,7 @@ export class ImageModel implements RNImage { this.altText = altText } + // Only for mobile async crop() { try { const cropped = await openCropper(this.rootStore, { @@ -55,15 +123,13 @@ export class ImageModel implements RNImage { width: this.scaledWidth, height: this.scaledHeight, }) - runInAction(() => { this.cropped = cropped + this.compress() }) } catch (err) { this.rootStore.log.error('Failed to crop photo', err) } - - this.compress() } async compress() { @@ -74,6 +140,8 @@ export class ImageModel implements RNImage { : {width: this.width, height: this.height}, POST_IMG_MAX, ) + + // TODO: Revisit this - currently iOS uses this as well const compressed = await compressAndResizeImageForPost({ ...(this.cropped === undefined ? this : this.cropped), width, @@ -87,4 +155,99 @@ export class ImageModel implements RNImage { this.rootStore.log.error('Failed to compress photo', err) } } + + // Web manipulation + async manipulate( + attributes: { + crop?: ActionCrop['crop'] + } & ImageManipulationAttributes, + ) { + const {aspectRatio, crop, flipHorizontal, flipVertical, rotate, scale} = + attributes + const modifiers = [] + + if (flipHorizontal !== undefined) { + this.flipHorizontal = flipHorizontal + } + + if (flipVertical !== undefined) { + this.flipVertical = flipVertical + } + + if (this.flipHorizontal) { + modifiers.push({flip: FlipType.Horizontal}) + } + + if (this.flipVertical) { + modifiers.push({flip: FlipType.Vertical}) + } + + // TODO: Fix rotation -- currently not functional + if (rotate !== undefined) { + this.rotation = rotate + } + + if (this.rotation !== undefined) { + modifiers.push({rotate: this.rotation}) + } + + if (crop !== undefined) { + modifiers.push({ + crop: { + originX: crop.originX * this.width, + originY: crop.originY * this.height, + height: crop.height * this.height, + width: crop.width * this.width, + }, + }) + } + + if (scale !== undefined) { + this.scale = scale + } + + if (aspectRatio !== undefined) { + this.aspectRatio = aspectRatio + } + + const ratioMultiplier = this.ratioMultipliers[this.aspectRatio ?? '1:1'] + + // TODO: Ollie - should support up to 2000 but smaller images that scale + // up need an updated compression factor calculation. Use 1000 for now. + const MAX_SIDE = 1000 + + const result = await ImageManipulator.manipulateAsync( + this.path, + [ + ...modifiers, + {resize: ratioMultiplier > 1 ? {width: MAX_SIDE} : {height: MAX_SIDE}}, + ], + { + compress: 0.7, // TODO: revisit compression calculation + format: SaveFormat.JPEG, + }, + ) + + runInAction(() => { + this.compressed = { + mime: 'image/jpeg', + path: result.uri, + size: getDataUriSize(result.uri), + ...result, + } + }) + } + + previous() { + this.compressed = this.prev + + const {flipHorizontal, flipVertical, rotate, position, scale} = + this.prevAttributes + + this.scale = scale + this.rotation = rotate + this.flipHorizontal = flipHorizontal + this.flipVertical = flipVertical + this.position = position + } } diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 4a55c23ad2..67f8e16d49 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -5,6 +5,7 @@ import {ProfileModel} from '../content/profile' import {isObj, hasProp} from 'lib/type-guards' import {Image as RNImage} from 'react-native-image-crop-picker' import {ImageModel} from '../media/image' +import {GalleryModel} from '../media/gallery' export interface ConfirmModal { name: 'confirm' @@ -37,6 +38,12 @@ export interface ReportAccountModal { did: string } +export interface EditImageModal { + name: 'edit-image' + image: ImageModel + gallery: GalleryModel +} + export interface CropImageModal { name: 'crop-image' uri: string @@ -102,6 +109,7 @@ export type Modal = // Posts | AltTextImageModal | CropImageModal + | EditImageModal | ServerInputModal | RepostModal diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 1aa0aef7a6..accd968035 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -50,7 +50,7 @@ export const Gallery = observer(function ({gallery}: Props) { const handleEditPhoto = useCallback( (image: ImageModel) => { - gallery.crop(image) + gallery.edit(image) }, [gallery], ) @@ -121,10 +121,10 @@ export const Gallery = observer(function ({gallery}: Props) { { handleEditPhoto(image) }} diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index 0359359ccc..07270d5574 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -24,7 +24,6 @@ export function Component({image}: Props) { const [altText, setAltText] = useState(image.altText) const onPressSave = useCallback(() => { - setAltText(altText) image.setAltText(altText) store.shell.closeModal() }, [store, image, altText]) diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx new file mode 100644 index 0000000000..4a5d9bfded --- /dev/null +++ b/src/view/com/modals/EditImage.tsx @@ -0,0 +1,418 @@ +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' +import {Pressable, StyleSheet, View} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {useWindowDimensions} from 'react-native' +import {gradients, s} from 'lib/styles' +import {useTheme} from 'lib/ThemeContext' +import {Text} from '../util/text/Text' +import LinearGradient from 'react-native-linear-gradient' +import {useStores} from 'state/index' +import ImageEditor, {Position} from 'react-avatar-editor' +import {TextInput} from './util' +import {enforceLen} from 'lib/strings/helpers' +import {MAX_ALT_TEXT} from 'lib/constants' +import {GalleryModel} from 'state/models/media/gallery' +import {ImageModel} from 'state/models/media/image' +import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons' +import {Slider} from '@miblanchard/react-native-slider' +import {MaterialIcons} from '@expo/vector-icons' +import {observer} from 'mobx-react-lite' +import {getKeys} from 'lib/type-assertions' + +export const snapPoints = ['80%'] + +interface Props { + image: ImageModel + gallery: GalleryModel +} + +// This is only used for desktop web +export const Component = observer(function ({image, gallery}: Props) { + const pal = usePalette('default') + const store = useStores() + const {shell} = store + const theme = useTheme() + const winDim = useWindowDimensions() + + const [altText, setAltText] = useState(image.altText) + const [aspectRatio, setAspectRatio] = useState( + image.aspectRatio ?? 'None', + ) + const [flipHorizontal, setFlipHorizontal] = useState( + image.flipHorizontal ?? false, + ) + const [flipVertical, setFlipVertical] = useState( + image.flipVertical ?? false, + ) + + // TODO: doesn't seem to be working correctly with crop + // const [rotation, setRotation] = useState(image.rotation ?? 0) + const [scale, setScale] = useState(image.scale ?? 1) + const [position, setPosition] = useState() + const [isEditing, setIsEditing] = useState(false) + const editorRef = useRef(null) + + const imgEditorStyles = useMemo(() => { + const dim = Math.min(425, winDim.width - 24) + return {width: dim, height: dim} + }, [winDim.width]) + + const manipulationAttributes = useMemo( + () => ({ + // TODO: doesn't seem to be working correctly with crop + // ...(rotation !== undefined ? {rotate: rotation} : {}), + ...(flipHorizontal !== undefined ? {flipHorizontal} : {}), + ...(flipVertical !== undefined ? {flipVertical} : {}), + }), + [flipHorizontal, flipVertical], + ) + + useEffect(() => { + const manipulateImage = async () => { + await image.manipulate(manipulationAttributes) + } + + manipulateImage() + }, [image, manipulationAttributes]) + + const ratios = useMemo( + () => + ({ + '4:3': { + hint: 'Sets image aspect ratio to wide', + Icon: RectWideIcon, + }, + '1:1': { + hint: 'Sets image aspect ratio to square', + Icon: SquareIcon, + }, + '3:4': { + hint: 'Sets image aspect ratio to tall', + Icon: RectTallIcon, + }, + None: { + label: 'None', + hint: 'Sets image aspect ratio to tall', + Icon: MaterialIcons, + name: 'do-not-disturb-alt', + }, + } as const), + [], + ) + + type AspectRatio = keyof typeof ratios + + const onFlipHorizontal = useCallback(() => { + setFlipHorizontal(!flipHorizontal) + image.manipulate({flipHorizontal}) + }, [flipHorizontal, image]) + + const onFlipVertical = useCallback(() => { + setFlipVertical(!flipVertical) + image.manipulate({flipVertical}) + }, [flipVertical, image]) + + const adjustments = useMemo( + () => + [ + // { + // name: 'rotate-left', + // label: 'Rotate left', + // hint: 'Rotate image left', + // onPress: () => { + // const rotate = (rotation - 90) % 360 + // setRotation(rotate) + // image.manipulate({rotate}) + // }, + // }, + // { + // name: 'rotate-right', + // label: 'Rotate right', + // hint: 'Rotate image right', + // onPress: () => { + // const rotate = (rotation + 90) % 360 + // setRotation(rotate) + // image.manipulate({rotate}) + // }, + // }, + { + name: 'flip', + label: 'Flip horizontal', + hint: 'Flip image horizontally', + onPress: onFlipHorizontal, + }, + { + name: 'flip', + label: 'Flip vertically', + hint: 'Flip image vertically', + onPress: onFlipVertical, + }, + ] as const, + [onFlipHorizontal, onFlipVertical], + ) + + useEffect(() => { + image.prev = image.compressed + setIsEditing(true) + }, [image]) + + const onCloseModal = useCallback(() => { + shell.closeModal() + setIsEditing(false) + }, [shell]) + + const onPressCancel = useCallback(async () => { + await gallery.previous(image) + onCloseModal() + }, [onCloseModal, gallery, image]) + + const onPressSave = useCallback(async () => { + image.setAltText(altText) + + const crop = editorRef.current?.getCroppingRect() + + await image.manipulate({ + ...(crop !== undefined + ? { + crop: { + originX: crop.x, + originY: crop.y, + width: crop.width, + height: crop.height, + }, + ...(scale !== 1 ? {scale} : {}), + ...(position !== undefined ? {position} : {}), + } + : {}), + ...manipulationAttributes, + aspectRatio, + }) + + image.prevAttributes = manipulationAttributes + onCloseModal() + }, [ + altText, + aspectRatio, + image, + manipulationAttributes, + position, + scale, + onCloseModal, + ]) + + const onPressRatio = useCallback((as: AspectRatio) => { + setAspectRatio(as) + }, []) + + const getLabelIconSize = useCallback((as: AspectRatio) => { + switch (as) { + case 'None': + return 22 + case '1:1': + return 32 + default: + return 26 + } + }, []) + + // Prevents preliminary flash when transformations are being applied + if (image.compressed === undefined) { + return null + } + + const {width, height} = image.getDisplayDimensions( + aspectRatio, + imgEditorStyles.width, + ) + + return ( + + Edit image + + + + + + setScale(Array.isArray(v) ? v[0] : v) + } + minimumValue={1} + maximumValue={3} + /> + + + {getKeys(ratios).map(ratio => { + const {hint, Icon, ...props} = ratios[ratio] + const labelIconSize = getLabelIconSize(ratio) + const isSelected = aspectRatio === ratio + + return ( + { + onPressRatio(ratio) + }} + accessibilityLabel={ratio} + accessibilityHint={hint}> + + + + {ratio} + + + ) + })} + + + + {adjustments.map(({label, hint, name, onPress}) => ( + + + + ))} + + + + + setAltText(enforceLen(text, MAX_ALT_TEXT))} + placeholder="Image description" + placeholderTextColor={pal.colors.textLight} + accessibilityLabel="Image alt text" + accessibilityHint="Sets image alt text for screenreaders" + accessibilityLabelledBy="imageAltText" + /> + + + + + Cancel + + + + + + Done + + + + + + ) +}) + +const styles = StyleSheet.create({ + container: { + gap: 18, + paddingVertical: 18, + paddingHorizontal: 12, + height: '100%', + width: '100%', + }, + gap18: { + gap: 18, + }, + + title: { + fontWeight: 'bold', + fontSize: 24, + }, + + textArea: { + borderWidth: 1, + borderRadius: 6, + paddingTop: 10, + paddingHorizontal: 12, + fontSize: 16, + height: 100, + textAlignVertical: 'top', + }, + + btns: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + btn: { + borderRadius: 4, + paddingVertical: 8, + paddingHorizontal: 24, + }, + + verticalSep: { + borderLeftWidth: 1, + }, + + imgControls: { + flexDirection: 'row', + gap: 5, + }, + imgControl: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: 40, + }, + flipVertical: { + transform: [{rotate: '90deg'}], + }, + flipBtn: { + paddingHorizontal: 4, + paddingVertical: 8, + }, + imgEditor: { + maxWidth: '100%', + }, + imgContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: 425, + width: 425, + borderWidth: 1, + borderRadius: 8, + borderStyle: 'solid', + overflow: 'hidden', + }, +}) diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 9dcc8fa7e6..c9f2c49521 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -15,6 +15,7 @@ import * as DeleteAccountModal from './DeleteAccount' import * as RepostModal from './Repost' import * as CropImageModal from './crop-image/CropImage.web' import * as AltTextImageModal from './AltImage' +import * as EditImageModal from './EditImage' import * as ChangeHandleModal from './ChangeHandle' import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' @@ -47,7 +48,7 @@ function Modal({modal}: {modal: ModalIface}) { } const onPressMask = () => { - if (modal.name === 'crop-image') { + if (modal.name === 'crop-image' || modal.name === 'edit-image') { return // dont close on mask presses during crop } store.shell.closeModal() @@ -88,6 +89,8 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'alt-text-image') { element = + } else if (modal.name === 'edit-image') { + element = } else { return null } diff --git a/yarn.lock b/yarn.lock index bf556972e4..8c9583e1ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8713,6 +8713,13 @@ expo-image-loader@~4.1.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.1.1.tgz#efadbb17de1861106864820194900f336dd641b6" integrity sha512-ciEHVokU0f6w0eTxdRxLCio6tskMsjxWIoV92+/ZD37qePUJYMfEphPhu1sruyvMBNR8/j5iyOvPFVGTfO8oxA== +expo-image-manipulator@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-11.1.1.tgz#bb54df80e98abc9798876e3f70596a5b880168c9" + integrity sha512-W9LfJK/IL7EhhkkC1JQnEX/1S9B09rcGasJiQjXc2s1bEsrQnqXvXEv7shUW8b/L8rE+ynf+XvvDE+YIDL7oFg== + dependencies: + expo-image-loader "~4.1.0" + expo-image-picker@~14.1.1: version "14.1.1" resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.1.1.tgz#181f1348ba6a43df7b87cee4a601d45c79b7c2d7" From c2fb4d4b4b584bb109fa83be750b11e6190f61d6 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 11 May 2023 08:37:26 -0700 Subject: [PATCH 109/374] small label updates (#612) * labels: fix 'consentual' typo This label has never been applied, so safe to just change it. * labels: new 'nsfl' label, under both porn and gore categories * labels: new account-security label (always warn) * labling: re-word spam subtitle --- src/lib/labeling/const.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index 2a9b921dbc..b26388123e 100644 --- a/src/lib/labeling/const.ts +++ b/src/lib/labeling/const.ts @@ -5,7 +5,7 @@ export const ILLEGAL_LABEL_GROUP: LabelValGroup = { id: 'illegal', title: 'Illegal Content', warning: 'Illegal Content', - values: ['csam', 'dmca-violation', 'nudity-nonconsentual'], + values: ['csam', 'dmca-violation', 'nudity-nonconsensual'], } export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = { @@ -19,7 +19,7 @@ export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = { id: 'always-warn', title: 'Content Warning', warning: 'Content Warning', - values: ['!warn'], + values: ['!warn', 'account-security'], } export const UNKNOWN_LABEL_GROUP: LabelValGroup = { @@ -38,7 +38,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< title: 'Explicit Sexual Images', subtitle: 'i.e. Pornography', warning: 'Sexually Explicit', - values: ['porn'], + values: ['porn', 'nsfl'], isAdultImagery: true, }, nudity: { @@ -62,7 +62,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< title: 'Violent / Bloody', subtitle: 'Gore, self-harm, torture', warning: 'Violence', - values: ['gore', 'self-harm', 'torture'], + values: ['gore', 'self-harm', 'torture', 'nsfl'], isAdultImagery: true, }, hate: { @@ -74,7 +74,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< spam: { id: 'spam', title: 'Spam', - subtitle: 'Excessive low-quality posts', + subtitle: 'Excessive unwanted interactions', warning: 'Spam', values: ['spam'], }, From 0192923ef3a13468d6a3cb86793c31af5e693335 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Thu, 11 May 2023 08:38:10 -0700 Subject: [PATCH 110/374] Only allow one close draft confirmation at a time (#611) * Only allow one close draft confirmation at a time * lint --- src/view/com/composer/Composer.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 5c7594d61f..3891fa268e 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -91,7 +91,13 @@ export const ComposePost = observer(function ComposePost({ const onEscape = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') { - store.shell.openModal({ + const {shell} = store + + if (shell.activeModals.some(modal => modal.name === 'confirm')) { + store.shell.closeModal() + } + + shell.openModal({ name: 'confirm', title: 'Cancel draft', onPressConfirm: onClose, @@ -102,7 +108,7 @@ export const ComposePost = observer(function ComposePost({ }) } }, - [store.shell, onClose], + [store, onClose], ) useEffect(() => { From 19d6ded631b3a22bc44e7763bf1f75efa704be4d Mon Sep 17 00:00:00 2001 From: Ollie H Date: Thu, 11 May 2023 08:38:54 -0700 Subject: [PATCH 111/374] Prevent reply to from cascading (#610) --- src/view/com/post/Post.tsx | 20 ++++++++++++-------- src/view/com/posts/FeedItem.tsx | 20 ++++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 90698ab312..0b49995fe1 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -235,15 +235,19 @@ const PostLoaded = observer( size={9} style={[pal.textLight, s.mr5]} /> - - Reply to - - + style={[pal.textLight, s.mr2]} + lineHeight={1.2} + numberOfLines={1}> + Reply to{' '} + + )} - - Reply to - - + style={[pal.textLight, s.mr2]} + lineHeight={1.2} + numberOfLines={1}> + Reply to{' '} + + )} Date: Thu, 11 May 2023 08:41:47 -0700 Subject: [PATCH 112/374] bskyweb: iterate on HTML card metadata (#609) Probably still not perfect, but better. - don't user avatar image. use banner for profile and post img, or nothing - most twitter metadata fields were redundant; twitter will parse out opengraph ("og:"), so don't duplicate those - add regular HTML description (for google, etc) - include URI - actually include text --- bskyweb/cmd/bskyweb/server.go | 10 +++++++- bskyweb/templates/home.html | 10 +++----- bskyweb/templates/post.html | 46 +++++++++++++++++++++++----------- bskyweb/templates/profile.html | 44 +++++++++++++++++++++----------- 4 files changed, 74 insertions(+), 36 deletions(-) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 5ba1dbc803..902d1ffc1a 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -185,7 +185,13 @@ func (srv *Server) WebPost(c echo.Context) error { if err != nil { log.Warnf("failed to fetch post: %s\t%v", uri, err) } else { - data["postView"] = tpv.Thread.FeedDefs_ThreadViewPost.Post + req := c.Request() + postView := tpv.Thread.FeedDefs_ThreadViewPost.Post + data["postView"] = postView + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) + if postView.Embed != nil && postView.Embed.EmbedImages_View != nil { + data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb + } } } @@ -203,7 +209,9 @@ func (srv *Server) WebProfile(c echo.Context) error { if err != nil { log.Warnf("failed to fetch handle: %s\t%v", handle, err) } else { + req := c.Request() data["profileView"] = pv + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) } } diff --git a/bskyweb/templates/home.html b/bskyweb/templates/home.html index 6625a7d96e..0677b91578 100644 --- a/bskyweb/templates/home.html +++ b/bskyweb/templates/home.html @@ -3,14 +3,12 @@ {% block head_title %}Bluesky{% endblock %} {% block html_head_extra -%} + + - - - - - - + + {%- endblock %} diff --git a/bskyweb/templates/post.html b/bskyweb/templates/post.html index 866a97396c..05d62a1c8f 100644 --- a/bskyweb/templates/post.html +++ b/bskyweb/templates/post.html @@ -1,23 +1,39 @@ {% extends "base.html" %} -{# TODO: link rel=canonical #} -{# TODO: "same as" #} +{% block head_title %} +{%- if postView -%} + @{{ postView.Author.Handle }} on Bluesky +{%- else -%} + Bluesky +{%- endif -%} +{% endblock %} + {% block html_head_extra -%} -{%- if postView %} - - - - - {%- if postView.Author.Avatar %} - - +{%- if postView -%} + + + {%- if requestURI %} + {% endif -%} - {%- if postView.Record.Text %} - - + {%- if postView.Author.DisplayName %} + + {% else %} + {% endif -%} - - + {%- if postView.Record.Val.Text %} + + + {% endif -%} + {%- if imgThumbUrl %} + + + {%- elif postView.Author.Avatar %} + {# Don't use avatar image in cards; usually looks bad #} + + {% endif %} + + + {% endif -%} {%- endblock %} diff --git a/bskyweb/templates/profile.html b/bskyweb/templates/profile.html index 0710d3280e..4d4f679466 100644 --- a/bskyweb/templates/profile.html +++ b/bskyweb/templates/profile.html @@ -1,24 +1,40 @@ {% extends "base.html" %} -{# TODO: "same as" indication with DID? #} -{# TODO: could work in profileView.DisplayName here, conditionally? #} +{% block head_title %} +{%- if profileView -%} + @{{ profileView.Handle }} on Bluesky +{%- else -%} + Bluesky +{%- endif -%} +{% endblock %} + {% block html_head_extra -%} {%- if profileView -%} - - - - + + + {%- if requestURI %} + + {% endif -%} + {%- if profileView.DisplayName %} + + {% else %} + + {% endif -%} {%- if profileView.Description %} - - + + {% endif -%} - {%- if profileView.Avatar %} - - - {% endif -%} - + {%- if profileView.Banner %} + + + {%- elif profileView.Avatar -%} + {# Don't use avatar image in cards; usually looks bad #} + + {% endif %} + -{%- endif -%} + +{% endif -%} {%- endblock %} {% block noscript_extra -%} From 34d8fa59916d87922c83a6cf93e3e288d43dadcc Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 11 May 2023 13:22:56 -0700 Subject: [PATCH 113/374] top-level Makefile (#597) * top-level Makefile The primary motivation here is the `build-web` command, which calls the yarn build and then also copies over JS files. The Dockerfile does this and I always forget when doing it manually. * build-web: cp bundles in yarn command, not Makefile+Dockerfile --- Dockerfile | 3 --- Makefile | 35 +++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 Makefile diff --git a/Dockerfile b/Dockerfile index 95f0ec02e8..fbd13bebf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,9 +35,6 @@ RUN \. "$NVM_DIR/nvm.sh" && \ # DEBUG RUN find ./bskyweb/static && find ./web-build/static -# Copy the bundle js files. -RUN cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/ - # # Generate the bksyweb Go binary. # diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..e93b6357a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ + +SHELL = /bin/bash +.SHELLFLAGS = -o pipefail -c + +.PHONY: help +help: ## Print info about all commands + @echo "Commands:" + @echo + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}' + +.PHONY: build-web +build-web: ## Compile web bundle, copy to bskyweb directory + yarn build-web + +.PHONY: test +test: ## Run all tests + yarn test + +.PHONY: lint +lint: ## Run style checks and verify syntax + yarn run lint + +#.PHONY: fmt +#fmt: ## Run syntax re-formatting +# yarn prettier + +.PHONY: deps +deps: ## Installs dependent libs using 'yarn install' + yarn install --frozen-lockfile + +.PHONY: nvm-setup +nvm-setup: ## Use NVM to install and activate node+yarn + nvm install 18 + nvm use 18 + npm install --global yarn diff --git a/package.json b/package.json index 3f35f9bd74..29d0449d89 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "build-web": "expo export:web && node ./scripts/post-web-build.js", + "build-web": "expo export:web && node ./scripts/post-web-build.js && cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/", "start": "expo start --dev-client", "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", "test": "jest --forceExit --testTimeout=20000 --bail", From ebcd6333863a2073278fad482981d9898c0f20ca Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 11 May 2023 16:08:21 -0500 Subject: [PATCH 114/374] [APP-635] Mutelists (#601) * Add lists and profilelist screens * Implement lists screen and lists-list in profiles * Add empty states to the lists screen * Switch (mostly) from blocklists to mutelists * Rework: create a new moderation screen and move everything related under it * Fix moderation screen on desktop web * Tune the empty state code * Change content moderation modal to content filtering * Add CreateMuteList modal * Implement mutelist creation * Add lists listings * Add the ability to create new mutelists * Add 'add to list' tool * Satisfy the hashtag hyphen haters * Add update/delete/subscribe/unsubscribe to lists * Show which list caused a mute * Add list un/subscribe * Add the mute override when viewing a profile's posts * Update to latest backend * Add simulation tests and tune some behaviors * Fix lint * Bump deps * Fix list refresh after creation * Mute list subscriptions -> Mute lists --- __e2e__/mock-server.ts | 24 +- __e2e__/tests/mute-lists.test.ts | 141 +++++++ bskyweb/cmd/bskyweb/server.go | 7 +- jest/test-pds.ts | 26 ++ package.json | 4 +- src/Navigation.tsx | 23 +- src/lib/labeling/helpers.ts | 33 +- src/lib/labeling/types.ts | 4 +- src/lib/routes/types.ts | 7 +- src/lib/strings/url-helpers.ts | 9 + src/routes.ts | 7 +- src/state/models/content/list-membership.ts | 112 +++++ src/state/models/content/list.ts | 257 ++++++++++++ src/state/models/content/post-thread.ts | 4 + src/state/models/content/profile.ts | 4 +- src/state/models/feeds/notifications.ts | 1 + src/state/models/feeds/posts.ts | 4 + src/state/models/lists/lists-list.ts | 214 ++++++++++ src/state/models/ui/profile.ts | 27 +- src/state/models/ui/shell.ts | 18 +- src/view/com/lists/ListCard.tsx | 155 +++++++ src/view/com/lists/ListItems.tsx | 387 ++++++++++++++++++ src/view/com/lists/ListsList.tsx | 240 +++++++++++ .../com/modals/ContentFilteringSettings.tsx | 6 +- src/view/com/modals/CreateOrEditMuteList.tsx | 273 ++++++++++++ src/view/com/modals/ListAddRemoveUser.tsx | 255 ++++++++++++ src/view/com/modals/Modal.tsx | 8 + src/view/com/modals/Modal.web.tsx | 6 + src/view/com/pager/TabBar.tsx | 19 +- src/view/com/posts/FeedItem.tsx | 18 +- src/view/com/posts/FeedSlice.tsx | 4 +- src/view/com/profile/ProfileCard.tsx | 4 +- src/view/com/profile/ProfileHeader.tsx | 56 ++- src/view/com/util/EmptyState.tsx | 4 +- src/view/com/util/EmptyStateWithButton.tsx | 88 ++++ src/view/com/util/ViewHeader.tsx | 19 +- src/view/index.ts | 10 + src/view/screens/Home.tsx | 2 +- src/view/screens/Moderation.tsx | 136 ++++++ ...unts.tsx => ModerationBlockedAccounts.tsx} | 7 +- src/view/screens/ModerationMuteLists.tsx | 122 ++++++ ...counts.tsx => ModerationMutedAccounts.tsx} | 7 +- src/view/screens/Profile.tsx | 95 +++-- src/view/screens/ProfileList.tsx | 175 ++++++++ src/view/screens/Settings.tsx | 55 +-- src/view/shell/Drawer.tsx | 19 + src/view/shell/desktop/LeftNav.tsx | 18 + yarn.lock | 21 +- 48 files changed, 2984 insertions(+), 151 deletions(-) create mode 100644 __e2e__/tests/mute-lists.test.ts create mode 100644 src/state/models/content/list-membership.ts create mode 100644 src/state/models/content/list.ts create mode 100644 src/state/models/lists/lists-list.ts create mode 100644 src/view/com/lists/ListCard.tsx create mode 100644 src/view/com/lists/ListItems.tsx create mode 100644 src/view/com/lists/ListsList.tsx create mode 100644 src/view/com/modals/CreateOrEditMuteList.tsx create mode 100644 src/view/com/modals/ListAddRemoveUser.tsx create mode 100644 src/view/com/util/EmptyStateWithButton.tsx create mode 100644 src/view/screens/Moderation.tsx rename src/view/screens/{BlockedAccounts.tsx => ModerationBlockedAccounts.tsx} (96%) create mode 100644 src/view/screens/ModerationMuteLists.tsx rename src/view/screens/{MutedAccounts.tsx => ModerationMutedAccounts.tsx} (96%) create mode 100644 src/view/screens/ProfileList.tsx diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts index 6744f697ff..6ddfe3ca02 100644 --- a/__e2e__/mock-server.ts +++ b/__e2e__/mock-server.ts @@ -91,6 +91,7 @@ async function main() { 'always-warn-profile', 'always-warn-posts', 'muted-account', + 'muted-by-list-account', ]) { await server.mocker.createUser(user) await server.mocker.follow('alice', user) @@ -258,11 +259,32 @@ async function main() { await server.mocker.createPost('muted-account', 'muted post') await server.mocker.createQuotePost( 'muted-account', - 'account quote post', + 'muted quote post', anchorPost, ) await server.mocker.createReply( 'muted-account', + 'muted reply', + anchorPost, + ) + + const list = await server.mocker.createMuteList( + 'alice', + 'Muted Users', + ) + await server.mocker.addToMuteList( + 'alice', + list, + server.mocker.users['muted-by-list-account'].did, + ) + await server.mocker.createPost('muted-by-list-account', 'muted post') + await server.mocker.createQuotePost( + 'muted-by-list-account', + 'account quote post', + anchorPost, + ) + await server.mocker.createReply( + 'muted-by-list-account', 'account reply', anchorPost, ) diff --git a/__e2e__/tests/mute-lists.test.ts b/__e2e__/tests/mute-lists.test.ts new file mode 100644 index 0000000000..e931625139 --- /dev/null +++ b/__e2e__/tests/mute-lists.test.ts @@ -0,0 +1,141 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer, sleep} from '../util' + +describe('Profile screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&follows&labels') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login and view my mutelists', async () => { + await expect(element(by.id('signInButton'))).toBeVisible() + await login(service, 'alice', 'hunter2') + await element(by.id('viewHeaderDrawerBtn')).tap() + await expect(element(by.id('drawer'))).toBeVisible() + await element(by.id('menuItemButton-Moderation')).tap() + await element(by.id('mutelistsBtn')).tap() + await expect(element(by.id('list-Muted Users'))).toBeVisible() + await element(by.id('list-Muted Users')).tap() + await expect( + element(by.id('user-muted-by-list-account.test')), + ).toBeVisible() + }) + + it('Toggle subscription', async () => { + await element(by.id('unsubscribeListBtn')).tap() + await element(by.id('subscribeListBtn')).tap() + }) + + it('Edit display name and description via the edit mutelist modal', async () => { + await element(by.id('editListBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() + await element(by.id('editNameInput')).clearText() + await element(by.id('editNameInput')).typeText('Bad Ppl') + await element(by.id('editDescriptionInput')).clearText() + await element(by.id('editDescriptionInput')).typeText('They bad') + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() + await expect(element(by.id('listName'))).toHaveText('Bad Ppl') + await expect(element(by.id('listDescription'))).toHaveText('They bad') + // have to wait for the toast to clear + await waitFor(element(by.id('editListBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Remove description via the edit mutelist modal', async () => { + await element(by.id('editListBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() + await element(by.id('editDescriptionInput')).clearText() + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() + await expect(element(by.id('listDescription'))).not.toBeVisible() + // have to wait for the toast to clear + await waitFor(element(by.id('editListBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Set avi via the edit mutelist modal', async () => { + await expect(element(by.id('userAvatarFallback'))).toExist() + await element(by.id('editListBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() + await element(by.id('changeAvatarBtn')).tap() + await element(by.id('changeAvatarLibraryBtn')).tap() + await sleep(3e3) + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() + await expect(element(by.id('userAvatarImage'))).toExist() + // have to wait for the toast to clear + await waitFor(element(by.id('editListBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Remove avi via the edit mutelist modal', async () => { + await expect(element(by.id('userAvatarImage'))).toExist() + await element(by.id('editListBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() + await element(by.id('changeAvatarBtn')).tap() + await element(by.id('changeAvatarRemoveBtn')).tap() + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() + await expect(element(by.id('userAvatarFallback'))).toExist() + // have to wait for the toast to clear + await waitFor(element(by.id('editListBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Delete the mutelist', async () => { + await element(by.id('deleteListBtn')).tap() + await element(by.id('confirmBtn')).tap() + await expect(element(by.id('emptyMuteLists'))).toBeVisible() + }) + + it('Create a new mutelist', async () => { + await element(by.id('emptyMuteLists-button')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() + await element(by.id('editNameInput')).typeText('Bad Ppl') + await element(by.id('editDescriptionInput')).typeText('They bad') + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() + await expect(element(by.id('listName'))).toHaveText('Bad Ppl') + await expect(element(by.id('listDescription'))).toHaveText('They bad') + // have to wait for the toast to clear + await waitFor(element(by.id('editListBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Shows the mutelist on my profile', async () => { + await element(by.id('bottomBarProfileBtn')).tap() + await element(by.id('selector-2')).tap() + await element(by.id('list-Bad Ppl')).tap() + }) + + it('Adds and removes users on mutelists', async () => { + await element(by.id('bottomBarSearchBtn')).tap() + await element(by.id('searchTextInput')).typeText('bob') + await element(by.id('searchAutoCompleteResult-bob.test')).tap() + await expect(element(by.id('profileView'))).toBeVisible() + + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownListAddRemoveBtn')).tap() + await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible() + await element(by.id('toggleBtn-Bad Ppl')).tap() + await element(by.id('saveBtn')).tap() + await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible() + + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownListAddRemoveBtn')).tap() + await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible() + await element(by.id('toggleBtn-Bad Ppl')).tap() + await element(by.id('saveBtn')).tap() + await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible() + }) +}) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 902d1ffc1a..7c230041e7 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -106,10 +106,12 @@ func serve(cctx *cli.Context) error { // generic routes e.GET("/search", server.WebGeneric) e.GET("/notifications", server.WebGeneric) + e.GET("/moderation", server.WebGeneric) + e.GET("/moderation/mute-lists", server.WebGeneric) + e.GET("/moderation/muted-accounts", server.WebGeneric) + e.GET("/moderation/blocked-accounts", server.WebGeneric) e.GET("/settings", server.WebGeneric) e.GET("/settings/app-passwords", server.WebGeneric) - e.GET("/settings/muted-accounts", server.WebGeneric) - e.GET("/settings/blocked-accounts", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) @@ -122,6 +124,7 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle", server.WebProfile) e.GET("/profile/:handle/follows", server.WebGeneric) e.GET("/profile/:handle/followers", server.WebGeneric) + e.GET("/profile/:handle/lists/:rkey", server.WebGeneric) // post endpoints; only first populates info e.GET("/profile/:handle/post/:rkey", server.WebPost) diff --git a/jest/test-pds.ts b/jest/test-pds.ts index 7f8d202323..a75a0034fe 100644 --- a/jest/test-pds.ts +++ b/jest/test-pds.ts @@ -337,6 +337,32 @@ class Mocker { ]) .execute() } + + async createMuteList(user: string, name: string): Promise { + const res = await this.users[user]?.agent.app.bsky.graph.list.create( + {repo: this.users[user]?.did}, + { + purpose: 'app.bsky.graph.defs#modlist', + name, + createdAt: new Date().toISOString(), + }, + ) + await this.users[user]?.agent.app.bsky.graph.muteActorList({ + list: res.uri, + }) + return res.uri + } + + async addToMuteList(owner: string, list: string, subject: string) { + await this.users[owner]?.agent.app.bsky.graph.listitem.create( + {repo: this.users[owner]?.did}, + { + list, + subject, + createdAt: new Date().toISOString(), + }, + ) + } } const checkAvailablePort = (port: number) => diff --git a/package.json b/package.json index 29d0449d89..77eae011af 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.2.11", + "@atproto/api": "0.3.1", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", @@ -140,7 +140,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.1.5", + "@atproto/pds": "^0.1.6", "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index afc7b39b87..4e0403be90 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -33,11 +33,14 @@ import {useStores} from './state' import {HomeScreen} from './view/screens/Home' import {SearchScreen} from './view/screens/Search' import {NotificationsScreen} from './view/screens/Notifications' +import {ModerationScreen} from './view/screens/Moderation' +import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' import {NotFoundScreen} from './view/screens/NotFound' import {SettingsScreen} from './view/screens/Settings' import {ProfileScreen} from './view/screens/Profile' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' import {ProfileFollowsScreen} from './view/screens/ProfileFollows' +import {ProfileListScreen} from './view/screens/ProfileList' import {PostThreadScreen} from './view/screens/PostThread' import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy' @@ -49,8 +52,8 @@ import {TermsOfServiceScreen} from './view/screens/TermsOfService' import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' import {AppPasswords} from 'view/screens/AppPasswords' -import {MutedAccounts} from 'view/screens/MutedAccounts' -import {BlockedAccounts} from 'view/screens/BlockedAccounts' +import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' +import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' import {getRoutingInstrumentation} from 'lib/sentry' const navigationRef = createNavigationContainerRef() @@ -70,6 +73,19 @@ function commonScreens(Stack: typeof HomeTab) { return ( <> + + + + + @@ -91,8 +108,6 @@ function commonScreens(Stack: typeof HomeTab) { /> - - ) } diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index baac0ed5a6..447b0a99ae 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -1,5 +1,6 @@ import { AppBskyActorDefs, + AppBskyGraphDefs, AppBskyEmbedRecordWithMedia, AppBskyEmbedRecord, AppBskyEmbedImages, @@ -16,6 +17,7 @@ import { Label, LabelValGroup, ModerationBehaviorCode, + ModerationBehavior, PostModeration, ProfileModeration, PostLabelInfo, @@ -127,11 +129,15 @@ export function getPostModeration( // muting if (postInfo.isMuted) { + let msg = 'Post from an account you muted.' + if (postInfo.mutedByList) { + msg = `Muted by ${postInfo.mutedByList.name}` + } return { avatar, - list: hide('Post from an account you muted.'), - thread: warn('Post from an account you muted.'), - view: warn('Post from an account you muted.'), + list: isMute(hide(msg)), + thread: isMute(warn(msg)), + view: isMute(warn(msg)), } } @@ -273,6 +279,7 @@ export function getProfileViewBasicLabelInfo( profileLabels: filterProfileLabels(profile.labels), isMuted: profile.viewer?.muted || false, isBlocking: !!profile.viewer?.blocking || false, + isBlockedBy: !!profile.viewer?.blockedBy || false, } } @@ -302,6 +309,21 @@ export function getEmbedMuted(embed?: Embed): boolean { return false } +export function getEmbedMutedByList( + embed?: Embed, +): AppBskyGraphDefs.ListViewBasic | undefined { + if (!embed) { + return undefined + } + if ( + AppBskyEmbedRecord.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record) + ) { + return embed.record.author.viewer?.mutedByList + } + return undefined +} + export function getEmbedBlocking(embed?: Embed): boolean { if (!embed) { return false @@ -401,6 +423,11 @@ function warnContent(reason: string) { } } +function isMute(behavior: ModerationBehavior): ModerationBehavior { + behavior.isMute = true + return behavior +} + function warnImages(reason: string) { return { behavior: ModerationBehaviorCode.WarnImages, diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts index 078043076e..1ee058024d 100644 --- a/src/lib/labeling/types.ts +++ b/src/lib/labeling/types.ts @@ -1,4 +1,4 @@ -import {ComAtprotoLabelDefs} from '@atproto/api' +import {ComAtprotoLabelDefs, AppBskyGraphDefs} from '@atproto/api' import {LabelPreferencesModel} from 'state/models/ui/preferences' export type Label = ComAtprotoLabelDefs.Label @@ -22,6 +22,7 @@ export interface PostLabelInfo { accountLabels: Label[] profileLabels: Label[] isMuted: boolean + mutedByList?: AppBskyGraphDefs.ListViewBasic isBlocking: boolean isBlockedBy: boolean } @@ -44,6 +45,7 @@ export enum ModerationBehaviorCode { export interface ModerationBehavior { behavior: ModerationBehaviorCode + isMute?: boolean noOverride?: boolean reason?: string } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 34e6e6a468..56775deee6 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -5,10 +5,15 @@ export type {NativeStackScreenProps} from '@react-navigation/native-stack' export type CommonNavigatorParams = { NotFound: undefined + Moderation: undefined + ModerationMuteLists: undefined + ModerationMutedAccounts: undefined + ModerationBlockedAccounts: undefined Settings: undefined Profile: {name: string; hideBackButton?: boolean} ProfileFollowers: {name: string} ProfileFollows: {name: string} + ProfileList: {name: string; rkey: string} PostThread: {name: string; rkey: string} PostLikedBy: {name: string; rkey: string} PostRepostedBy: {name: string; rkey: string} @@ -20,8 +25,6 @@ export type CommonNavigatorParams = { CommunityGuidelines: undefined CopyrightPolicy: undefined AppPasswords: undefined - MutedAccounts: undefined - BlockedAccounts: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 549587f743..a5412920e0 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -94,6 +94,15 @@ export function convertBskyAppUrlIfNeeded(url: string): string { return url } +export function listUriToHref(url: string): string { + try { + const {hostname, rkey} = new AtUri(url) + return `/profile/${hostname}/lists/${rkey}` + } catch { + return '' + } +} + export function getYoutubeVideoId(link: string): string | undefined { let url try { diff --git a/src/routes.ts b/src/routes.ts index 43d31ee099..571aca7ffa 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -5,17 +5,20 @@ export const router = new Router({ Search: '/search', Notifications: '/notifications', Settings: '/settings', + Moderation: '/moderation', + ModerationMuteLists: '/moderation/mute-lists', + ModerationMutedAccounts: '/moderation/muted-accounts', + ModerationBlockedAccounts: '/moderation/blocked-accounts', Profile: '/profile/:name', ProfileFollowers: '/profile/:name/followers', ProfileFollows: '/profile/:name/follows', + ProfileList: '/profile/:name/lists/:rkey', PostThread: '/profile/:name/post/:rkey', PostLikedBy: '/profile/:name/post/:rkey/liked-by', PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', Debug: '/sys/debug', Log: '/sys/log', AppPasswords: '/settings/app-passwords', - MutedAccounts: '/settings/muted-accounts', - BlockedAccounts: '/settings/blocked-accounts', Support: '/support', PrivacyPolicy: '/support/privacy', TermsOfService: '/support/tos', diff --git a/src/state/models/content/list-membership.ts b/src/state/models/content/list-membership.ts new file mode 100644 index 0000000000..b4af4472b3 --- /dev/null +++ b/src/state/models/content/list-membership.ts @@ -0,0 +1,112 @@ +import {makeAutoObservable} from 'mobx' +import {AtUri, AppBskyGraphListitem} from '@atproto/api' +import {runInAction} from 'mobx' +import {RootStoreModel} from '../root-store' + +const PAGE_SIZE = 100 +interface Membership { + uri: string + value: AppBskyGraphListitem.Record +} + +export class ListMembershipModel { + // data + memberships: Membership[] = [] + + constructor(public rootStore: RootStoreModel, public subject: string) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + // public api + // = + + async fetch() { + // NOTE + // this approach to determining list membership is too inefficient to work at any scale + // it needs to be replaced with server side list membership queries + // -prf + let cursor + let records = [] + for (let i = 0; i < 100; i++) { + const res = await this.rootStore.agent.app.bsky.graph.listitem.list({ + repo: this.rootStore.me.did, + cursor, + limit: PAGE_SIZE, + }) + records = records.concat( + res.records.filter(record => record.value.subject === this.subject), + ) + cursor = res.cursor + if (!cursor) { + break + } + } + runInAction(() => { + this.memberships = records + }) + } + + getMembership(listUri: string) { + return this.memberships.find(m => m.value.list === listUri) + } + + isMember(listUri: string) { + return !!this.getMembership(listUri) + } + + async add(listUri: string) { + if (this.isMember(listUri)) { + return + } + const res = await this.rootStore.agent.app.bsky.graph.listitem.create( + { + repo: this.rootStore.me.did, + }, + { + subject: this.subject, + list: listUri, + createdAt: new Date().toISOString(), + }, + ) + const {rkey} = new AtUri(res.uri) + const record = await this.rootStore.agent.app.bsky.graph.listitem.get({ + repo: this.rootStore.me.did, + rkey, + }) + runInAction(() => { + this.memberships = this.memberships.concat([record]) + }) + } + + async remove(listUri: string) { + const membership = this.getMembership(listUri) + if (!membership) { + return + } + const {rkey} = new AtUri(membership.uri) + await this.rootStore.agent.app.bsky.graph.listitem.delete({ + repo: this.rootStore.me.did, + rkey, + }) + runInAction(() => { + this.memberships = this.memberships.filter(m => m.value.list !== listUri) + }) + } + + async updateTo(uris: string) { + for (const uri of uris) { + await this.add(uri) + } + for (const membership of this.memberships) { + if (!uris.includes(membership.value.list)) { + await this.remove(membership.value.list) + } + } + } +} diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts new file mode 100644 index 0000000000..673ee9430d --- /dev/null +++ b/src/state/models/content/list.ts @@ -0,0 +1,257 @@ +import {makeAutoObservable} from 'mobx' +import { + AtUri, + AppBskyGraphGetList as GetList, + AppBskyGraphDefs as GraphDefs, + AppBskyGraphList, +} from '@atproto/api' +import {Image as RNImage} from 'react-native-image-crop-picker' +import {RootStoreModel} from '../root-store' +import * as apilib from 'lib/api/index' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' + +const PAGE_SIZE = 30 + +export class ListModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + loadMoreError = '' + hasMore = true + loadMoreCursor?: string + + // data + list: GraphDefs.ListView | null = null + items: GraphDefs.ListItemView[] = [] + + static async createModList( + rootStore: RootStoreModel, + { + name, + description, + avatar, + }: {name: string; description: string; avatar: RNImage | undefined}, + ) { + const record: AppBskyGraphList.Record = { + purpose: 'app.bsky.graph.defs#modlist', + name, + description, + avatar: undefined, + createdAt: new Date().toISOString(), + } + if (avatar) { + const blobRes = await apilib.uploadBlob( + rootStore, + avatar.path, + avatar.mime, + ) + record.avatar = blobRes.data.blob + } + const res = await rootStore.agent.app.bsky.graph.list.create( + { + repo: rootStore.me.did, + }, + record, + ) + await rootStore.agent.app.bsky.graph.muteActorList({list: res.uri}) + return res + } + + constructor(public rootStore: RootStoreModel, public uri: string) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.items.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + get isOwner() { + return this.list?.creator.did === this.rootStore.me.did + } + + // public api + // = + + async refresh() { + return this.loadMore(true) + } + + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return + } + this._xLoading(replace) + try { + const res = await this.rootStore.agent.app.bsky.graph.getList({ + list: this.uri, + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(replace ? e : undefined, !replace ? e : undefined) + } + }) + + async updateMetadata({ + name, + description, + avatar, + }: { + name: string + description: string + avatar: RNImage | null | undefined + }) { + if (!this.isOwner) { + throw new Error('Cannot edit this list') + } + + // get the current record + const {rkey} = new AtUri(this.uri) + const {value: record} = await this.rootStore.agent.app.bsky.graph.list.get({ + repo: this.rootStore.me.did, + rkey, + }) + + // update the fields + record.name = name + record.description = description + if (avatar) { + const blobRes = await apilib.uploadBlob( + this.rootStore, + avatar.path, + avatar.mime, + ) + record.avatar = blobRes.data.blob + } else if (avatar === null) { + record.avatar = undefined + } + return await this.rootStore.agent.com.atproto.repo.putRecord({ + repo: this.rootStore.me.did, + collection: 'app.bsky.graph.list', + rkey, + record, + }) + } + + async delete() { + // fetch all the listitem records that belong to this list + let cursor + let records = [] + for (let i = 0; i < 100; i++) { + const res = await this.rootStore.agent.app.bsky.graph.listitem.list({ + repo: this.rootStore.me.did, + cursor, + limit: PAGE_SIZE, + }) + records = records.concat( + res.records.filter(record => record.value.list === this.uri), + ) + cursor = res.cursor + if (!cursor) { + break + } + } + + // batch delete the list and listitem records + const createDel = (uri: string) => { + const urip = new AtUri(uri) + return { + $type: 'com.atproto.repo.applyWrites#delete', + collection: urip.collection, + rkey: urip.rkey, + } + } + await this.rootStore.agent.com.atproto.repo.applyWrites({ + repo: this.rootStore.me.did, + writes: [createDel(this.uri)].concat( + records.map(record => createDel(record.uri)), + ), + }) + } + + async subscribe() { + await this.rootStore.agent.app.bsky.graph.muteActorList({ + list: this.list.uri, + }) + await this.refresh() + } + + async unsubscribe() { + await this.rootStore.agent.app.bsky.graph.unmuteActorList({ + list: this.list.uri, + }) + await this.refresh() + } + + /** + * Attempt to load more again after a failure + */ + async retryLoadMore() { + this.loadMoreError = '' + this.hasMore = true + return this.loadMore() + } + + // state transitions + // = + + _xLoading(isRefreshing = false) { + this.isLoading = true + this.isRefreshing = isRefreshing + this.error = '' + } + + _xIdle(err?: any, loadMoreErr?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + this.loadMoreError = cleanError(loadMoreErr) + if (err) { + this.rootStore.log.error('Failed to fetch user items', err) + } + if (loadMoreErr) { + this.rootStore.log.error('Failed to fetch user items', loadMoreErr) + } + } + + // helper functions + // = + + _replaceAll(res: GetList.Response) { + this.items = [] + this._appendAll(res) + } + + _appendAll(res: GetList.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.list = res.data.list + this.items = this.items.concat( + res.data.items.map(item => ({...item, _reactKey: item.subject})), + ) + } +} diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index a0f75493ac..74a75d803c 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -14,6 +14,7 @@ import {PostLabelInfo, PostModeration} from 'lib/labeling/types' import { getEmbedLabels, getEmbedMuted, + getEmbedMutedByList, getEmbedBlocking, getEmbedBlockedBy, filterAccountLabels, @@ -70,6 +71,9 @@ export class PostThreadItemModel { this.post.author.viewer?.muted || getEmbedMuted(this.post.embed) || false, + mutedByList: + this.post.author.viewer?.mutedByList || + getEmbedMutedByList(this.post.embed), isBlocking: !!this.post.author.viewer?.blocking || getEmbedBlocking(this.post.embed) || diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index dddf488a3c..9d8378f795 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx' import { AtUri, ComAtprotoLabelDefs, + AppBskyGraphDefs, AppBskyActorGetProfile as GetProfile, AppBskyActorProfile, RichText, @@ -18,10 +19,9 @@ import { filterProfileLabels, } from 'lib/labeling/helpers' -export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser' - export class ProfileViewerModel { muted?: boolean + mutedByList?: AppBskyGraphDefs.ListViewBasic following?: string followedBy?: string blockedBy?: boolean diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 3ffd10b99e..73424f03ed 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -111,6 +111,7 @@ export class NotificationsFeedItemModel { addedInfo?.profileLabels || [], ), isMuted: this.author.viewer?.muted || addedInfo?.isMuted || false, + mutedByList: this.author.viewer?.mutedByList || addedInfo?.mutedByList, isBlocking: !!this.author.viewer?.blocking || addedInfo?.isBlocking || false, isBlockedBy: diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 44cec3af7f..b2dffdc694 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -24,6 +24,7 @@ import {PostLabelInfo, PostModeration} from 'lib/labeling/types' import { getEmbedLabels, getEmbedMuted, + getEmbedMutedByList, getEmbedBlocking, getEmbedBlockedBy, getPostModeration, @@ -105,6 +106,9 @@ export class PostsFeedItemModel { this.post.author.viewer?.muted || getEmbedMuted(this.post.embed) || false, + mutedByList: + this.post.author.viewer?.mutedByList || + getEmbedMutedByList(this.post.embed), isBlocking: !!this.post.author.viewer?.blocking || getEmbedBlocking(this.post.embed) || diff --git a/src/state/models/lists/lists-list.ts b/src/state/models/lists/lists-list.ts new file mode 100644 index 0000000000..309ab0e032 --- /dev/null +++ b/src/state/models/lists/lists-list.ts @@ -0,0 +1,214 @@ +import {makeAutoObservable} from 'mobx' +import { + AppBskyGraphGetLists as GetLists, + AppBskyGraphGetListMutes as GetListMutes, + AppBskyGraphDefs as GraphDefs, +} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {cleanError} from 'lib/strings/errors' +import {bundleAsync} from 'lib/async/bundle' + +const PAGE_SIZE = 30 + +export class ListsListModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + loadMoreError = '' + hasMore = true + loadMoreCursor?: string + + // data + lists: GraphDefs.ListView[] = [] + + constructor( + public rootStore: RootStoreModel, + public source: 'my-modlists' | string, + ) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.lists.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async refresh() { + return this.loadMore(true) + } + + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return + } + this._xLoading(replace) + try { + let res + if (this.source === 'my-modlists') { + res = { + success: true, + headers: {}, + data: { + subject: undefined, + lists: [], + }, + } + const [res1, res2] = await Promise.all([ + fetchAllUserLists(this.rootStore, this.rootStore.me.did), + fetchAllMyMuteLists(this.rootStore), + ]) + for (let list of res1.data.lists) { + if (list.purpose === 'app.bsky.graph.defs#modlist') { + res.data.lists.push(list) + } + } + for (let list of res2.data.lists) { + if ( + list.purpose === 'app.bsky.graph.defs#modlist' && + !res.data.lists.find(l => l.uri === list.uri) + ) { + res.data.lists.push(list) + } + } + } else { + res = await this.rootStore.agent.app.bsky.graph.getLists({ + actor: this.source, + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + } + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(replace ? e : undefined, !replace ? e : undefined) + } + }) + + /** + * Attempt to load more again after a failure + */ + async retryLoadMore() { + this.loadMoreError = '' + this.hasMore = true + return this.loadMore() + } + + // state transitions + // = + + _xLoading(isRefreshing = false) { + this.isLoading = true + this.isRefreshing = isRefreshing + this.error = '' + } + + _xIdle(err?: any, loadMoreErr?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + this.loadMoreError = cleanError(loadMoreErr) + if (err) { + this.rootStore.log.error('Failed to fetch user lists', err) + } + if (loadMoreErr) { + this.rootStore.log.error('Failed to fetch user lists', loadMoreErr) + } + } + + // helper functions + // = + + _replaceAll(res: GetLists.Response | GetListMutes.Response) { + this.lists = [] + this._appendAll(res) + } + + _appendAll(res: GetLists.Response | GetListMutes.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.lists = this.lists.concat( + res.data.lists.map(list => ({...list, _reactKey: list.uri})), + ) + } +} + +async function fetchAllUserLists( + store: RootStoreModel, + did: string, +): Promise { + let acc: GetLists.Response = { + success: true, + headers: {}, + data: { + subject: undefined, + lists: [], + }, + } + + let cursor + for (let i = 0; i < 100; i++) { + const res = await store.agent.app.bsky.graph.getLists({ + actor: did, + cursor, + limit: 50, + }) + cursor = res.data.cursor + acc.data.lists = acc.data.lists.concat(res.data.lists) + if (!cursor) { + break + } + } + + return acc +} + +async function fetchAllMyMuteLists( + store: RootStoreModel, +): Promise { + let acc: GetListMutes.Response = { + success: true, + headers: {}, + data: { + subject: undefined, + lists: [], + }, + } + + let cursor + for (let i = 0; i < 100; i++) { + const res = await store.agent.app.bsky.graph.getListMutes({ + cursor, + limit: 50, + }) + cursor = res.data.cursor + acc.data.lists = acc.data.lists.concat(res.data.lists) + if (!cursor) { + break + } + } + + return acc +} diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index d06a196f3d..861b3df0eb 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -2,13 +2,19 @@ import {makeAutoObservable} from 'mobx' import {RootStoreModel} from '../root-store' import {ProfileModel} from '../content/profile' import {PostsFeedModel} from '../feeds/posts' +import {ListsListModel} from '../lists/lists-list' export enum Sections { Posts = 'Posts', PostsWithReplies = 'Posts & replies', + Lists = 'Lists', } -const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.PostsWithReplies] +const USER_SELECTOR_ITEMS = [ + Sections.Posts, + Sections.PostsWithReplies, + Sections.Lists, +] export interface ProfileUiParams { user: string @@ -22,6 +28,7 @@ export class ProfileUiModel { // data profile: ProfileModel feed: PostsFeedModel + lists: ListsListModel // ui state selectedViewIndex = 0 @@ -43,14 +50,17 @@ export class ProfileUiModel { actor: params.user, limit: 10, }) + this.lists = new ListsListModel(rootStore, params.user) } - get currentView(): PostsFeedModel { + get currentView(): PostsFeedModel | ListsListModel { if ( this.selectedView === Sections.Posts || this.selectedView === Sections.PostsWithReplies ) { return this.feed + } else if (this.selectedView === Sections.Lists) { + return this.lists } throw new Error(`Invalid selector value: ${this.selectedViewIndex}`) } @@ -100,6 +110,12 @@ export class ProfileUiModel { } else if (this.feed.isEmpty) { arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) } + } else if (this.selectedView === Sections.Lists) { + if (this.lists.hasContent) { + arr = this.lists.lists + } else if (this.lists.isEmpty) { + arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) + } } else { arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) } @@ -113,6 +129,8 @@ export class ProfileUiModel { this.selectedView === Sections.PostsWithReplies ) { return this.feed.hasContent && this.feed.hasMore && this.feed.isLoading + } else if (this.selectedView === Sections.Lists) { + return this.lists.hasContent && this.lists.hasMore && this.lists.isLoading } return false } @@ -133,6 +151,11 @@ export class ProfileUiModel { .setup() .catch(err => this.rootStore.log.error('Failed to fetch feed', err)), ]) + // HACK: need to use the DID as a param, not the username -prf + this.lists.source = this.profile.did + this.lists + .loadMore() + .catch(err => this.rootStore.log.error('Failed to fetch lists', err)) } async update() { diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 67f8e16d49..9b9a176bed 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -5,6 +5,7 @@ import {ProfileModel} from '../content/profile' import {isObj, hasProp} from 'lib/type-guards' import {Image as RNImage} from 'react-native-image-crop-picker' import {ImageModel} from '../media/image' +import {ListModel} from '../content/list' import {GalleryModel} from '../media/gallery' export interface ConfirmModal { @@ -38,6 +39,19 @@ export interface ReportAccountModal { did: string } +export interface CreateOrEditMuteListModal { + name: 'create-or-edit-mute-list' + list?: ListModel + onSave?: (uri: string) => void +} + +export interface ListAddRemoveUserModal { + name: 'list-add-remove-user' + subject: string + displayName: string + onUpdate?: () => void +} + export interface EditImageModal { name: 'edit-image' image: ImageModel @@ -102,9 +116,11 @@ export type Modal = | ContentFilteringSettingsModal | ContentLanguagesSettingsModal - // Reporting + // Moderation | ReportAccountModal | ReportPostModal + | CreateMuteListModal + | ListAddRemoveUserModal // Posts | AltTextImageModal diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx new file mode 100644 index 0000000000..7cbdaaf648 --- /dev/null +++ b/src/view/com/lists/ListCard.tsx @@ -0,0 +1,155 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api' +import {Link} from '../util/Link' +import {Text} from '../util/text/Text' +import {RichText as RichTextCom} from '../util/text/RichText' +import {UserAvatar} from '../util/UserAvatar' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {sanitizeDisplayName} from 'lib/strings/display-names' + +export const ListCard = ({ + testID, + list, + noBg, + noBorder, + renderButton, +}: { + testID?: string + list: AppBskyGraphDefs.ListView + noBg?: boolean + noBorder?: boolean + renderButton?: () => JSX.Element +}) => { + const pal = usePalette('default') + const store = useStores() + + const rkey = React.useMemo(() => { + try { + const urip = new AtUri(list.uri) + return urip.rkey + } catch { + return '' + } + }, [list]) + + const descriptionRichText = React.useMemo(() => { + if (list.description) { + return new RichText({ + text: list.description, + facets: list.descriptionFacets, + }) + } + return undefined + }, [list]) + + return ( + + + + + + + + {sanitizeDisplayName(list.name)} + + + {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by{' '} + {list.creator.did === store.me.did + ? 'you' + : `@${list.creator.handle}`} + + {!!list.viewer?.muted && ( + + + + Subscribed + + + + )} + + {renderButton ? ( + {renderButton()} + ) : undefined} + + {descriptionRichText ? ( + + + + ) : undefined} + + ) +} + +const styles = StyleSheet.create({ + outer: { + borderTopWidth: 1, + paddingHorizontal: 6, + }, + outerNoBorder: { + borderTopWidth: 0, + }, + layout: { + flexDirection: 'row', + alignItems: 'center', + }, + layoutAvi: { + width: 54, + paddingLeft: 4, + paddingTop: 8, + paddingBottom: 10, + }, + avi: { + width: 40, + height: 40, + borderRadius: 20, + resizeMode: 'cover', + }, + layoutContent: { + flex: 1, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + }, + layoutButton: { + paddingRight: 10, + }, + details: { + paddingLeft: 54, + paddingRight: 10, + paddingBottom: 10, + }, + pill: { + borderRadius: 4, + paddingHorizontal: 6, + paddingVertical: 2, + }, + btn: { + paddingVertical: 7, + borderRadius: 50, + marginLeft: 6, + paddingHorizontal: 14, + }, +}) diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx new file mode 100644 index 0000000000..52b728cb99 --- /dev/null +++ b/src/view/com/lists/ListItems.tsx @@ -0,0 +1,387 @@ +import React, {MutableRefObject} from 'react' +import { + ActivityIndicator, + RefreshControl, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native' +import {AppBskyActorDefs, AppBskyGraphDefs, RichText} from '@atproto/api' +import {observer} from 'mobx-react-lite' +import {FlatList} from '../util/Views' +import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' +import {ProfileCard} from '../profile/ProfileCard' +import {Button} from '../util/forms/Button' +import {Text} from '../util/text/Text' +import {RichText as RichTextCom} from '../util/text/RichText' +import {UserAvatar} from '../util/UserAvatar' +import {TextLink} from '../util/Link' +import {ListModel} from 'state/models/content/list' +import {useAnalytics} from 'lib/analytics' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {s} from 'lib/styles' +import {isDesktopWeb} from 'platform/detection' + +const LOADING_ITEM = {_reactKey: '__loading__'} +const HEADER_ITEM = {_reactKey: '__header__'} +const EMPTY_ITEM = {_reactKey: '__empty__'} +const ERROR_ITEM = {_reactKey: '__error__'} +const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} + +export const ListItems = observer( + ({ + list, + style, + scrollElRef, + onPressTryAgain, + onToggleSubscribed, + onPressEditList, + onPressDeleteList, + renderEmptyState, + testID, + headerOffset = 0, + }: { + list: ListModel + style?: StyleProp + scrollElRef?: MutableRefObject | null> + onPressTryAgain?: () => void + onToggleSubscribed?: () => void + onPressEditList?: () => void + onPressDeleteList?: () => void + renderEmptyState?: () => JSX.Element + testID?: string + headerOffset?: number + }) => { + const pal = usePalette('default') + const store = useStores() + const {track} = useAnalytics() + const [isRefreshing, setIsRefreshing] = React.useState(false) + + const data = React.useMemo(() => { + let items: any[] = [HEADER_ITEM] + if (list.hasLoaded) { + if (list.hasError) { + items = items.concat([ERROR_ITEM]) + } + if (list.isEmpty) { + items = items.concat([EMPTY_ITEM]) + } else { + items = items.concat(list.items) + } + if (list.loadMoreError) { + items = items.concat([LOAD_MORE_ERROR_ITEM]) + } + } else if (list.isLoading) { + items = items.concat([LOADING_ITEM]) + } + return items + }, [ + list.hasError, + list.hasLoaded, + list.isLoading, + list.isEmpty, + list.items, + list.loadMoreError, + ]) + + // events + // = + + const onRefresh = React.useCallback(async () => { + track('Lists:onRefresh') + setIsRefreshing(true) + try { + await list.refresh() + } catch (err) { + list.rootStore.log.error('Failed to refresh lists', err) + } + setIsRefreshing(false) + }, [list, track, setIsRefreshing]) + + const onEndReached = React.useCallback(async () => { + track('Lists:onEndReached') + try { + await list.loadMore() + } catch (err) { + list.rootStore.log.error('Failed to load more lists', err) + } + }, [list, track]) + + const onPressRetryLoadMore = React.useCallback(() => { + list.retryLoadMore() + }, [list]) + + const onPressEditMembership = React.useCallback( + (profile: AppBskyActorDefs.ProfileViewBasic) => { + store.shell.openModal({ + name: 'list-add-remove-user', + subject: profile.did, + displayName: profile.displayName || profile.handle, + onUpdate() { + list.refresh() + }, + }) + }, + [store, list], + ) + + // rendering + // = + + const renderMemberButton = React.useCallback( + (profile: AppBskyActorDefs.ProfileViewBasic) => { + if (!list.isOwner) { + return null + } + return ( + + + ) +} + +const styles = StyleSheet.create({ + createNewContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 18, + paddingTop: 18, + paddingBottom: 16, + }, + createNewButton: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + feedFooter: {paddingTop: 20}, +}) diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index 30b465562c..5db0ef5a59 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -21,8 +21,8 @@ export function Component({}: {}) { }, [store]) return ( - - Content Moderation + + Content Filtering void + list?: ListModel +}) { + const store = useStores() + const [error, setError] = useState('') + const pal = usePalette('default') + const theme = useTheme() + const {track} = useAnalytics() + + const [isProcessing, setProcessing] = useState(false) + const [name, setName] = useState(list?.list.name || '') + const [description, setDescription] = useState( + list?.list.description || '', + ) + const [avatar, setAvatar] = useState(list?.list.avatar) + const [newAvatar, setNewAvatar] = useState() + + const onPressCancel = useCallback(() => { + store.shell.closeModal() + }, [store]) + + const onSelectNewAvatar = useCallback( + async (img: RNImage | null) => { + if (!img) { + setNewAvatar(null) + setAvatar(null) + return + } + track('CreateMuteList:AvatarSelected') + try { + const finalImg = await compressIfNeeded(img, 1000000) + setNewAvatar(finalImg) + setAvatar(finalImg.path) + } catch (e: any) { + setError(cleanError(e)) + } + }, + [track, setNewAvatar, setAvatar, setError], + ) + + const onPressSave = useCallback(async () => { + track('CreateMuteList:Save') + const nameTrimmed = name.trim() + if (!nameTrimmed) { + setError('Name is required') + return + } + setProcessing(true) + if (error) { + setError('') + } + try { + if (list) { + await list.updateMetadata({ + name: nameTrimmed, + description: description.trim(), + avatar: newAvatar, + }) + Toast.show('Mute list updated') + onSave?.(list.uri) + } else { + const res = await ListModel.createModList(store, { + name, + description, + avatar: newAvatar, + }) + Toast.show('Mute list created') + onSave?.(res.uri) + } + store.shell.closeModal() + } catch (e: any) { + if (isNetworkError(e)) { + setError( + 'Failed to create the mute list. Check your internet connection and try again.', + ) + } else { + setError(cleanError(e)) + } + } + setProcessing(false) + }, [ + track, + setProcessing, + setError, + error, + onSave, + store, + name, + description, + newAvatar, + list, + ]) + + return ( + + + + {list ? 'Edit Mute List' : 'New Mute List'} + + {error !== '' && ( + + + + )} + List Avatar + + + + + + List Name + setName(enforceLen(v, MAX_NAME))} + accessible={true} + accessibilityLabel="Name" + accessibilityHint="Set the list's name" + /> + + + Description + setDescription(enforceLen(v, MAX_DESCRIPTION))} + accessible={true} + accessibilityLabel="Description" + accessibilityHint="Edit your list's description" + /> + + {isProcessing ? ( + + + + ) : ( + + + Save + + + )} + + + Cancel + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: isDesktopWeb ? 0 : 16, + }, + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + marginBottom: 18, + }, + label: { + fontWeight: 'bold', + paddingHorizontal: 4, + paddingBottom: 4, + marginTop: 20, + }, + form: { + paddingHorizontal: 6, + }, + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 14, + paddingVertical: 10, + fontSize: 16, + }, + textArea: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 12, + paddingTop: 10, + fontSize: 16, + height: 100, + textAlignVertical: 'top', + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + borderRadius: 32, + padding: 10, + marginBottom: 10, + }, + avi: { + width: 84, + height: 84, + borderWidth: 2, + borderRadius: 42, + marginTop: 4, + }, + errorContainer: {marginTop: 20}, +}) diff --git a/src/view/com/modals/ListAddRemoveUser.tsx b/src/view/com/modals/ListAddRemoveUser.tsx new file mode 100644 index 0000000000..a2775df9fd --- /dev/null +++ b/src/view/com/modals/ListAddRemoveUser.tsx @@ -0,0 +1,255 @@ +import React, {useCallback} from 'react' +import {observer} from 'mobx-react-lite' +import {Pressable, StyleSheet, View} from 'react-native' +import {AppBskyGraphDefs as GraphDefs} from '@atproto/api' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {Text} from '../util/text/Text' +import {UserAvatar} from '../util/UserAvatar' +import {ListsList} from '../lists/ListsList' +import {ListsListModel} from 'state/models/lists/lists-list' +import {ListMembershipModel} from 'state/models/content/list-membership' +import {EmptyStateWithButton} from '../util/EmptyStateWithButton' +import {Button} from '../util/forms/Button' +import * as Toast from '../util/Toast' +import {useStores} from 'state/index' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb, isAndroid} from 'platform/detection' + +export const snapPoints = ['fullscreen'] + +export const Component = observer( + ({ + subject, + displayName, + onUpdate, + }: { + subject: string + displayName: string + onUpdate?: () => void + }) => { + const store = useStores() + const pal = usePalette('default') + const palPrimary = usePalette('primary') + const palInverted = usePalette('inverted') + const [selected, setSelected] = React.useState([]) + + const listsList: ListsListModel = React.useMemo( + () => new ListsListModel(store, store.me.did), + [store], + ) + const memberships: ListMembershipModel = React.useMemo( + () => new ListMembershipModel(store, subject), + [store, subject], + ) + React.useEffect(() => { + listsList.refresh() + memberships.fetch().then( + () => { + setSelected(memberships.memberships.map(m => m.value.list)) + }, + err => { + store.log.error('Failed to fetch memberships', {err}) + }, + ) + }, [memberships, listsList, store, setSelected]) + + const onPressCancel = useCallback(() => { + store.shell.closeModal() + }, [store]) + + const onPressSave = useCallback(async () => { + try { + await memberships.updateTo(selected) + } catch (err) { + store.log.error('Failed to update memberships', {err}) + return + } + Toast.show('Lists updated') + onUpdate?.() + store.shell.closeModal() + }, [store, selected, memberships, onUpdate]) + + const onPressNewMuteList = useCallback(() => { + store.shell.openModal({ + name: 'create-or-edit-mute-list', + onSave: (_uri: string) => { + listsList.refresh() + }, + }) + }, [store, listsList]) + + const onToggleSelected = useCallback( + (uri: string) => { + if (selected.includes(uri)) { + setSelected(selected.filter(uri2 => uri2 !== uri)) + } else { + setSelected([...selected, uri]) + } + }, + [selected, setSelected], + ) + + const renderItem = useCallback( + (list: GraphDefs.ListView) => { + const isSelected = selected.includes(list.uri) + return ( + onToggleSelected(list.uri)}> + + + + + + {sanitizeDisplayName(list.name)} + + + {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'}{' '} + by{' '} + {list.creator.did === store.me.did + ? 'you' + : `@${list.creator.handle}`} + + + + {isSelected && ( + + )} + + + ) + }, + [pal, palPrimary, palInverted, onToggleSelected, selected, store.me.did], + ) + + const renderEmptyState = React.useCallback(() => { + return ( + + ) + }, [onPressNewMuteList]) + + return ( + + Add {displayName} to lists + + + + + + ) +} +const styles = StyleSheet.create({ + container: { + height: '100%', + paddingVertical: 40, + paddingHorizontal: 30, + }, + iconContainer: { + marginBottom: 16, + }, + icon: { + marginLeft: 'auto', + marginRight: 'auto', + }, + btns: { + flexDirection: 'row', + justifyContent: 'center', + }, + btn: { + gap: 10, + marginVertical: 20, + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 24, + borderRadius: 30, + }, + notice: { + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 10, + marginHorizontal: 30, + }, +}) diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 7f5b5b7c21..97802394e8 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -20,11 +20,13 @@ export const ViewHeader = observer(function ({ canGoBack, hideOnScroll, showOnDesktop, + renderButton, }: { title: string canGoBack?: boolean hideOnScroll?: boolean showOnDesktop?: boolean + renderButton?: () => JSX.Element }) { const pal = usePalette('default') const store = useStores() @@ -46,7 +48,7 @@ export const ViewHeader = observer(function ({ if (isDesktopWeb) { if (showOnDesktop) { - return + return } return null } else { @@ -79,13 +81,23 @@ export const ViewHeader = observer(function ({ {title} - + {renderButton ? ( + renderButton() + ) : ( + + )} ) } }) -function DesktopWebHeader({title}: {title: string}) { +function DesktopWebHeader({ + title, + renderButton, +}: { + title: string + renderButton?: () => JSX.Element +}) { const pal = usePalette('default') return ( @@ -94,6 +106,7 @@ function DesktopWebHeader({title}: {title: string}) { {title} + {renderButton?.()} ) } diff --git a/src/view/index.ts b/src/view/index.ts index dd8a585d66..b8a13f7f86 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -38,6 +38,8 @@ import {faEye} from '@fortawesome/free-solid-svg-icons/faEye' import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' +import {faHand} from '@fortawesome/free-solid-svg-icons/faHand' +import {faHand as farHand} from '@fortawesome/free-regular-svg-icons/faHand' import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse' @@ -46,6 +48,7 @@ import {faImage} from '@fortawesome/free-solid-svg-icons/faImage' import {faInfo} from '@fortawesome/free-solid-svg-icons/faInfo' import {faLanguage} from '@fortawesome/free-solid-svg-icons/faLanguage' import {faLink} from '@fortawesome/free-solid-svg-icons/faLink' +import {faListUl} from '@fortawesome/free-solid-svg-icons/faListUl' import {faLock} from '@fortawesome/free-solid-svg-icons/faLock' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage' @@ -67,8 +70,10 @@ import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck' +import {faUserSlash} from '@fortawesome/free-solid-svg-icons/faUserSlash' import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus' import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' +import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faX} from '@fortawesome/free-solid-svg-icons/faX' @@ -116,6 +121,8 @@ export function setup() { farEyeSlash, faGear, faGlobe, + faHand, + farHand, faHeart, fasHeart, faHouse, @@ -124,6 +131,7 @@ export function setup() { faInfo, faLanguage, faLink, + faListUl, faLock, faMagnifyingGlass, faMessage, @@ -145,8 +153,10 @@ export function setup() { faUser, faUsers, faUserCheck, + faUserSlash, faUserPlus, faUserXmark, + faUsersSlash, faTicket, faTrashCan, faX, diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 18e4f2506b..0ead6b65cd 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -62,7 +62,7 @@ export const HomeScreen = withAuthRequired( setSelectedPage(index) store.shell.setIsDrawerSwipeDisabled(index > 0) }, - [store], + [store, setSelectedPage], ) const onPressSelected = React.useCallback(() => { diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx new file mode 100644 index 0000000000..29ef8b4b2e --- /dev/null +++ b/src/view/screens/Moderation.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {observer} from 'mobx-react-lite' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {useStores} from 'state/index' +import {s} from 'lib/styles' +import {CenteredView} from '../com/util/Views' +import {ViewHeader} from '../com/util/ViewHeader' +import {Link} from '../com/util/Link' +import {Text} from '../com/util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps +export const ModerationScreen = withAuthRequired( + observer(function Moderation({}: Props) { + const pal = usePalette('default') + const store = useStores() + const {screen, track} = useAnalytics() + + useFocusEffect( + React.useCallback(() => { + screen('Moderation') + store.shell.setMinimalShellMode(false) + }, [screen, store]), + ) + + const onPressContentFiltering = React.useCallback(() => { + track('Moderation:ContentfilteringButtonClicked') + store.shell.openModal({name: 'content-filtering-settings'}) + }, [track, store]) + + return ( + + + + + + + + + Content filtering + + + + + + + + Mute lists + + + + + + + + Muted accounts + + + + + + + + Blocked accounts + + + + ) + }), +) + +const styles = StyleSheet.create({ + desktopContainer: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + spacer: { + height: 6, + }, + linkCard: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 18, + marginBottom: 1, + }, + iconContainer: { + alignItems: 'center', + justifyContent: 'center', + width: 40, + height: 40, + borderRadius: 30, + marginRight: 12, + }, +}) diff --git a/src/view/screens/BlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx similarity index 96% rename from src/view/screens/BlockedAccounts.tsx rename to src/view/screens/ModerationBlockedAccounts.tsx index 1950685109..cd506d6305 100644 --- a/src/view/screens/BlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -22,8 +22,11 @@ import {ViewHeader} from '../com/util/ViewHeader' import {CenteredView} from 'view/com/util/Views' import {ProfileCard} from 'view/com/profile/ProfileCard' -type Props = NativeStackScreenProps -export const BlockedAccounts = withAuthRequired( +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'ModerationBlockedAccounts' +> +export const ModerationBlockedAccounts = withAuthRequired( observer(({}: Props) => { const pal = usePalette('default') const store = useStores() diff --git a/src/view/screens/ModerationMuteLists.tsx b/src/view/screens/ModerationMuteLists.tsx new file mode 100644 index 0000000000..0b81f432f6 --- /dev/null +++ b/src/view/screens/ModerationMuteLists.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import {StyleSheet} from 'react-native' +import {useFocusEffect, useNavigation} from '@react-navigation/native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {AtUri} from '@atproto/api' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {EmptyStateWithButton} from 'view/com/util/EmptyStateWithButton' +import {useStores} from 'state/index' +import {ListsListModel} from 'state/models/lists/lists-list' +import {ListsList} from 'view/com/lists/ListsList' +import {Button} from 'view/com/util/forms/Button' +import {NavigationProp} from 'lib/routes/types' +import {usePalette} from 'lib/hooks/usePalette' +import {CenteredView} from 'view/com/util/Views' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'ModerationMuteLists' +> +export const ModerationMuteListsScreen = withAuthRequired(({}: Props) => { + const pal = usePalette('default') + const store = useStores() + const navigation = useNavigation() + + const mutelists: ListsListModel = React.useMemo( + () => new ListsListModel(store, 'my-modlists'), + [store], + ) + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + mutelists.refresh() + }, [store, mutelists]), + ) + + const onPressNewMuteList = React.useCallback(() => { + store.shell.openModal({ + name: 'create-or-edit-mute-list', + onSave: (uri: string) => { + try { + const urip = new AtUri(uri) + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }) + } catch {} + }, + }) + }, [store, navigation]) + + const renderEmptyState = React.useCallback(() => { + return ( + + ) + }, [onPressNewMuteList]) + + const renderHeaderButton = React.useCallback( + () => ( + + ), + [onPressNewMuteList, pal], + ) + + return ( + + + + + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 100, + }, + containerDesktop: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + createBtn: { + width: 40, + }, +}) diff --git a/src/view/screens/MutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx similarity index 96% rename from src/view/screens/MutedAccounts.tsx rename to src/view/screens/ModerationMutedAccounts.tsx index f7120051fc..ec732f682e 100644 --- a/src/view/screens/MutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -22,8 +22,11 @@ import {ViewHeader} from '../com/util/ViewHeader' import {CenteredView} from 'view/com/util/Views' import {ProfileCard} from 'view/com/profile/ProfileCard' -type Props = NativeStackScreenProps -export const MutedAccounts = withAuthRequired( +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'ModerationMutedAccounts' +> +export const ModerationMutedAccounts = withAuthRequired( observer(({}: Props) => { const pal = usePalette('default') const store = useStores() diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 5fb212554b..d239748591 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -7,12 +7,16 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewSelector} from '../com/util/ViewSelector' import {CenteredView} from '../com/util/Views' import {ScreenHider} from 'view/com/util/moderation/ScreenHider' -import {ProfileUiModel} from 'state/models/ui/profile' +import {ProfileUiModel, Sections} from 'state/models/ui/profile' import {useStores} from 'state/index' import {PostsFeedSliceModel} from 'state/models/feeds/posts' import {ProfileHeader} from '../com/profile/ProfileHeader' import {FeedSlice} from '../com/posts/FeedSlice' -import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder' +import {ListCard} from 'view/com/lists/ListCard' +import { + PostFeedLoadingPlaceholder, + ProfileCardFeedLoadingPlaceholder, +} from '../com/util/LoadingPlaceholder' import {ErrorScreen} from '../com/util/error/ErrorScreen' import {ErrorMessage} from '../com/util/error/ErrorMessage' import {EmptyState} from '../com/util/EmptyState' @@ -111,52 +115,81 @@ export const ProfileScreen = withAuthRequired( }, [uiState.showLoadingMoreFooter]) const renderItem = React.useCallback( (item: any) => { - if (item === ProfileUiModel.END_ITEM) { - return - end of feed - - } else if (item === ProfileUiModel.LOADING_ITEM) { - return - } else if (item._reactKey === '__error__') { - if (uiState.feed.isBlocking) { + if (uiState.selectedView === Sections.Lists) { + if (item === ProfileUiModel.LOADING_ITEM) { + return + } else if (item._reactKey === '__error__') { + return ( + + + + ) + } else if (item === ProfileUiModel.EMPTY_ITEM) { return ( ) + } else { + return } - if (uiState.feed.isBlockedBy) { + } else { + if (item === ProfileUiModel.END_ITEM) { + return - end of feed - + } else if (item === ProfileUiModel.LOADING_ITEM) { + return + } else if (item._reactKey === '__error__') { + if (uiState.feed.isBlocking) { + return ( + + ) + } + if (uiState.feed.isBlockedBy) { + return ( + + ) + } + return ( + + + + ) + } else if (item === ProfileUiModel.EMPTY_ITEM) { return ( ) + } else if (item instanceof PostsFeedSliceModel) { + return ( + + ) } - return ( - - - - ) - } else if (item === ProfileUiModel.EMPTY_ITEM) { - return ( - - ) - } else if (item instanceof PostsFeedSliceModel) { - return } return }, [ onPressTryAgain, + uiState.selectedView, uiState.profile.did, uiState.feed.isBlocking, uiState.feed.isBlockedBy, diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx new file mode 100644 index 0000000000..a78faaf62d --- /dev/null +++ b/src/view/screens/ProfileList.tsx @@ -0,0 +1,175 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {useNavigation} from '@react-navigation/native' +import {observer} from 'mobx-react-lite' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {CenteredView} from 'view/com/util/Views' +import {ListItems} from 'view/com/lists/ListItems' +import {EmptyState} from 'view/com/util/EmptyState' +import {Button} from 'view/com/util/forms/Button' +import * as Toast from 'view/com/util/Toast' +import {ListModel} from 'state/models/content/list' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {NavigationProp} from 'lib/routes/types' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps +export const ProfileListScreen = withAuthRequired( + observer(({route}: Props) => { + const store = useStores() + const navigation = useNavigation() + const pal = usePalette('default') + const {name, rkey} = route.params + + const list: ListModel = React.useMemo(() => { + const model = new ListModel( + store, + `at://${name}/app.bsky.graph.list/${rkey}`, + ) + return model + }, [store, name, rkey]) + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + list.loadMore(true) + }, [store, list]), + ) + + const onToggleSubscribed = React.useCallback(async () => { + try { + if (list.list?.viewer?.muted) { + await list.unsubscribe() + } else { + await list.subscribe() + } + } catch (err) { + Toast.show( + 'There was an an issue updating your subscription, please check your internet connection and try again.', + ) + store.log.error('Failed up update subscription', {err}) + } + }, [store, list]) + + const onPressEditList = React.useCallback(() => { + store.shell.openModal({ + name: 'create-or-edit-mute-list', + list, + onSave() { + list.refresh() + }, + }) + }, [store, list]) + + const onPressDeleteList = React.useCallback(() => { + store.shell.openModal({ + name: 'confirm', + title: 'Delete List', + message: 'Are you sure?', + async onPressConfirm() { + await list.delete() + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, + }) + }, [store, list, navigation]) + + const renderEmptyState = React.useCallback(() => { + return + }, []) + + const renderHeaderBtn = React.useCallback(() => { + return ( + + {list?.isOwner && ( + + + )} - {isDesktopWeb && ( - - - - )} ) - }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) + }, [ + store.me.did, + pal, + currentFeed, + onToggleLiked, + onToggleSaved, + name, + rkey, + ]) return ( @@ -207,10 +221,6 @@ export const CustomFeedScreen = withAuthRequired( ) const styles = StyleSheet.create({ - headerBtns: { - flexDirection: 'row', - gap: 8, - }, header: { flexDirection: 'row', gap: 12, @@ -219,6 +229,11 @@ const styles = StyleSheet.create({ paddingBottom: 16, borderTopWidth: 1, }, + headerBtns: { + flexDirection: 'row', + gap: 8, + marginTop: 10, + }, headerDetails: { paddingHorizontal: 16, paddingBottom: 16, From acea0e074d75ac549abb01dc4ac16573a43ad7fa Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 12:05:32 -0500 Subject: [PATCH 211/374] Tab bar fixes --- src/view/com/pager/FeedsTabBarMobile.tsx | 2 +- src/view/com/pager/TabBar.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index de3f125838..cb910ccb96 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -73,7 +73,7 @@ const styles = StyleSheet.create({ top: 0, flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 18, + paddingLeft: 18, borderBottomWidth: 1, }, tabBarAvi: { diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index fe76a08b65..f6c41ce7ca 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -10,7 +10,7 @@ import {StyleSheet, View, ScrollView} from 'react-native' import {Text} from '../util/text/Text' import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' -import {isDesktopWeb} from 'platform/detection' +import {isDesktopWeb, isWeb} from 'platform/detection' export interface TabBarProps { testID?: string @@ -120,9 +120,9 @@ const styles = isDesktopWeb }) : StyleSheet.create({ outer: { + flex: 1, flexDirection: 'row', paddingLeft: 14, - paddingRight: 24, }, item: { paddingTop: 8, From 571fc37a9920d3b7b13a9eed2c46513036f3a4f4 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Thu, 18 May 2023 10:34:34 -0700 Subject: [PATCH 212/374] fix error & empty state when rendering custom feeds on profile --- src/view/screens/Profile.tsx | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index bf312cd06d..5f31c89c9d 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -120,6 +120,7 @@ export const ProfileScreen = withAuthRequired( }, [uiState.showLoadingMoreFooter]) const renderItem = React.useCallback( (item: any) => { + // if section is lists if (uiState.selectedView === Sections.Lists) { if (item === ProfileUiModel.LOADING_ITEM) { return @@ -144,6 +145,32 @@ export const ProfileScreen = withAuthRequired( } else { return } + // if section is custom algorithms + } else if (uiState.selectedView === Sections.CustomAlgorithms) { + if (item === ProfileUiModel.LOADING_ITEM) { + return + } else if (item._reactKey === '__error__') { + return ( + + + + ) + } else if (item === ProfileUiModel.EMPTY_ITEM) { + return ( + + ) + } else if (item instanceof CustomFeedModel) { + return + } + // if section is posts or posts & replies } else { if (item === ProfileUiModel.END_ITEM) { return - end of feed - @@ -188,8 +215,6 @@ export const ProfileScreen = withAuthRequired( return ( ) - } else if (item instanceof CustomFeedModel) { - return } } return From f1d2166c2911456fc60c83eb3204e5d823dff475 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Thu, 18 May 2023 10:36:16 -0700 Subject: [PATCH 213/374] fix spacing when user has no feeds --- src/view/com/feeds/SavedFeeds.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index 1cb109a432..110a6e8940 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -104,6 +104,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 26, paddingVertical: 18, gap: 18, + marginTop: 8, }, empty: { paddingHorizontal: 18, From 211fce47ce566fb5703be0019ba587150ff043b3 Mon Sep 17 00:00:00 2001 From: Jake Gold <52801504+Jacob2161@users.noreply.github.com> Date: Thu, 18 May 2023 11:56:09 -0700 Subject: [PATCH 214/374] Fix /api/waitlist API (#726) * move /waitlist to /api/waitlist where its expected * parse waitlist API request as JSON, duh --- bskyweb/cmd/bskyweb/server.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 7c230041e7..0b9f34504c 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/json" "fmt" "io/fs" + "io/ioutil" "net/http" "os" "strings" @@ -132,11 +134,36 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) // Mailmodo - e.POST("/waitlist", func(c echo.Context) error { - email := strings.TrimSpace(c.FormValue("email")) - if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil { + e.POST("/api/waitlist", func(c echo.Context) error { + type jsonError struct { + Error string `json:"error"` + } + + // Read the API request. + type apiRequest struct { + Email string `json:"email"` + } + + bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024) + payload, err := ioutil.ReadAll(bodyReader) + if err != nil { return err } + var req apiRequest + if err := json.Unmarshal(payload, &req); err != nil { + return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"}) + } + + if req.Email == "" { + return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."}) + } + + if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, req.Email); err != nil { + log.Errorf("adding email to waitlist failed: %s", err) + return c.JSON(http.StatusBadRequest, jsonError{ + Error: "Storing email in waitlist failed. Please enter a valid email address.", + }) + } return c.JSON(http.StatusOK, map[string]bool{"success": true}) }) From 5537d19e555c39f5f9a0ec16735ea4c3860357c4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 14:39:04 -0500 Subject: [PATCH 215/374] Update saved feeds to use preferences --- src/state/models/feeds/custom-feed.ts | 20 ++------ src/state/models/media/image.ts | 2 +- src/state/models/ui/preferences.ts | 57 ++++++++++++++++----- src/state/models/ui/saved-feeds.ts | 25 ++++----- src/state/models/ui/shell.ts | 2 +- src/view/com/feeds/CustomFeed.tsx | 2 +- src/view/com/util/ViewHeader.tsx | 2 +- src/view/com/util/moderation/ImageHider.tsx | 8 +-- 8 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 5e550ec694..e457d2d1ee 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -38,7 +38,7 @@ export class CustomFeedModel { } get isSaved() { - return this.data.viewer?.saved + return this.rootStore.preferences.savedFeeds.includes(this.uri) } get isLiked() { @@ -49,23 +49,11 @@ export class CustomFeedModel { // = async save() { - await this.rootStore.agent.app.bsky.feed.saveFeed({ - feed: this.uri, - }) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.saved = true - }) + await this.rootStore.preferences.addSavedFeed(this.uri) } async unsave() { - await this.rootStore.agent.app.bsky.feed.unsaveFeed({ - feed: this.uri, - }) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.saved = false - }) + await this.rootStore.preferences.removeSavedFeed(this.uri) } async like() { @@ -82,7 +70,7 @@ export class CustomFeedModel { } async unlike() { - if (!this.data.viewer.like) { + if (!this.data.viewer?.like) { return } try { diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index ec93bf5b65..6edf88d9df 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -135,7 +135,7 @@ export class ImageModel implements RNImage { // Only for mobile async crop() { try { - const cropped = await openCropper({ + const cropped = await openCropper(this.rootStore, { mediaType: 'photo', path: this.path, freeStyleCropEnabled: true, diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 05a1eb128f..120b4adcc8 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -46,6 +46,7 @@ export class PreferencesModel { contentLanguages: string[] = deviceLocales?.map?.(locale => locale.languageCode) || [] contentLabels = new LabelPreferencesModel() + savedFeeds: string[] = [] pinnedFeeds: string[] = [] constructor(public rootStore: RootStoreModel) { @@ -56,6 +57,7 @@ export class PreferencesModel { return { contentLanguages: this.contentLanguages, contentLabels: this.contentLabels, + savedFeeds: this.savedFeeds, pinnedFeeds: this.pinnedFeeds, } } @@ -75,6 +77,13 @@ export class PreferencesModel { // default to the device languages this.contentLanguages = deviceLocales.map(locale => locale.languageCode) } + if ( + hasProp(v, 'savedFeeds') && + Array.isArray(v.savedFeeds) && + typeof v.savedFeeds.every(item => typeof item === 'string') + ) { + this.savedFeeds = v.savedFeeds + } if ( hasProp(v, 'pinnedFeeds') && Array.isArray(v.pinnedFeeds) && @@ -106,10 +115,11 @@ export class PreferencesModel { pref.visibility as LabelPreference } } else if ( - AppBskyActorDefs.isPinnedFeedsPref(pref) && - AppBskyActorDefs.validatePinnedFeedsPref(pref).success + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success ) { - this.pinnedFeeds = pref.feeds + this.savedFeeds = pref.saved + this.pinnedFeeds = pref.pinned } } }) @@ -220,38 +230,57 @@ export class PreferencesModel { return res } - async setPinnedFeeds(v: string[]) { - const old = this.pinnedFeeds - this.pinnedFeeds = v + async setSavedFeeds(saved: string[], pinned: string[]) { + const oldSaved = this.savedFeeds + const oldPinned = this.pinnedFeeds + this.savedFeeds = saved + this.pinnedFeeds = pinned try { await this.update((prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.find( pref => - AppBskyActorDefs.isPinnedFeedsPref(pref) && - AppBskyActorDefs.validatePinnedFeedsPref(pref).success, + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success, ) if (existing) { - existing.feeds = v + existing.saved = saved + existing.pinned = pinned } else { prefs.push({ - $type: 'app.bsky.actor.defs#pinnedFeedsPref', - feeds: v, + $type: 'app.bsky.actor.defs#savedFeedsPref', + saved, + pinned, }) } }) } catch (e) { runInAction(() => { - this.pinnedFeeds = old + this.savedFeeds = oldSaved + this.pinnedFeeds = oldPinned }) throw e } } + async addSavedFeed(v: string) { + return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds) + } + + async removeSavedFeed(v: string) { + return this.setSavedFeeds( + this.savedFeeds.filter(uri => uri !== v), + this.pinnedFeeds.filter(uri => uri !== v), + ) + } + async addPinnedFeed(v: string) { - return this.setPinnedFeeds([...this.pinnedFeeds, v]) + return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v]) } async removePinnedFeed(v: string) { - return this.setPinnedFeeds(this.pinnedFeeds.filter(uri => uri !== v)) + return this.setSavedFeeds( + this.savedFeeds, + this.pinnedFeeds.filter(uri => uri !== v), + ) } } diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index f500aef2e4..9de28e0284 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -5,8 +5,6 @@ import {bundleAsync} from 'lib/async/bundle' import {cleanError} from 'lib/strings/errors' import {CustomFeedModel} from '../feeds/custom-feed' -const PAGE_SIZE = 100 - export class SavedFeedsModel { // state isLoading = false @@ -69,16 +67,15 @@ export class SavedFeedsModel { try { let feeds: AppBskyFeedDefs.GeneratorView[] = [] let cursor - for (let i = 0; i < 100; i++) { - const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({ - limit: PAGE_SIZE, - cursor, + for ( + let i = 0; + i < this.rootStore.preferences.savedFeeds.length; + i += 25 + ) { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerators({ + feeds: this.rootStore.preferences.savedFeeds.slice(i, 25), }) feeds = feeds.concat(res.data.feeds) - cursor = res.data.cursor - if (!cursor) { - break - } } runInAction(() => { this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f)) @@ -127,7 +124,8 @@ export class SavedFeedsModel { } async reorderPinnedFeeds(feeds: CustomFeedModel[]) { - return this.rootStore.preferences.setPinnedFeeds( + return this.rootStore.preferences.setSavedFeeds( + this.rootStore.preferences.savedFeeds, feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri), ) } @@ -151,7 +149,10 @@ export class SavedFeedsModel { pinned[index] = pinned[index + 1] pinned[index + 1] = temp } - await this.rootStore.preferences.setPinnedFeeds(pinned) + await this.rootStore.preferences.setSavedFeeds( + this.rootStore.preferences.savedFeeds, + pinned, + ) } // state transitions diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 9b9a176bed..95b6662433 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -119,7 +119,7 @@ export type Modal = // Moderation | ReportAccountModal | ReportPostModal - | CreateMuteListModal + | CreateOrEditMuteListModal | ListAddRemoveUserModal // Posts diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index d4e843b677..9a71eb8468 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -40,7 +40,7 @@ export const CustomFeed = observer( const navigation = useNavigation() const onToggleSaved = React.useCallback(async () => { - if (item.data.viewer?.saved) { + if (item.isSaved) { store.shell.openModal({ name: 'confirm', title: 'Remove from my feeds', diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 7f13f18383..c17a65b140 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -121,7 +121,7 @@ const Container = observer( }: { children: React.ReactNode hideOnScroll: boolean - showBorder: boolean + showBorder?: boolean }) => { const store = useStores() const pal = usePalette('default') diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx index b42c6397da..40add5b67b 100644 --- a/src/view/com/util/moderation/ImageHider.tsx +++ b/src/view/com/util/moderation/ImageHider.tsx @@ -27,6 +27,10 @@ export function ImageHider({ setOverride(false) }, [setOverride]) + if (moderation.behavior === ModerationBehaviorCode.Hide) { + return null + } + if (moderation.behavior !== ModerationBehaviorCode.WarnImages) { return ( @@ -35,10 +39,6 @@ export function ImageHider({ ) } - if (moderation.behavior === ModerationBehaviorCode.Hide) { - return null - } - return ( From 2f4408582bf27a83ba8d22605077d067f8433d7c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 15:06:32 -0500 Subject: [PATCH 216/374] Set default feeds --- src/lib/constants.ts | 43 ++++++++++++++++++++++++++++++ src/state/models/ui/preferences.ts | 34 +++++++++++++++++++++-- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6d0d4797b2..88e429d83f 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, +) { + if (serviceUrl.includes('localhost')) { + 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')) { + return { + pinned: [ + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + ], + saved: [ + STAGING_DEFAULT_FEED('bsky-team'), + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + STAGING_DEFAULT_FEED('hot-classic'), + ], + } + } else { + return { + pinned: [ + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + ], + saved: [ + STAGING_DEFAULT_FEED('bsky-team'), + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + STAGING_DEFAULT_FEED('hot-classic'), + ], + } + } +} + export const POST_IMG_MAX = { width: 2000, height: 2000, diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 120b4adcc8..c85faf6588 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -11,6 +11,7 @@ import { ALWAYS_FILTER_LABEL_GROUP, ALWAYS_WARN_LABEL_GROUP, } from 'lib/labeling/const' +import {DEFAULT_FEEDS} from 'lib/constants' import {isIOS} from 'platform/detection' const deviceLocales = getLocales() @@ -95,6 +96,8 @@ export class PreferencesModel { } async sync() { + // fetch preferences + let hasSavedFeedsPref = false const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) runInAction(() => { for (const pref of res.data.preferences) { @@ -120,14 +123,41 @@ export class PreferencesModel { ) { this.savedFeeds = pref.saved this.pinnedFeeds = pref.pinned + hasSavedFeedsPref = true } } }) + + // set defaults on missing items + if (!hasSavedFeedsPref) { + const {saved, pinned} = await DEFAULT_FEEDS( + this.rootStore.agent.service.toString(), + (handle: string) => + this.rootStore.agent + .resolveHandle({handle}) + .then(({data}) => data.did), + ) + runInAction(() => { + this.savedFeeds = saved + this.pinnedFeeds = pinned + }) + res.data.preferences.push({ + $type: 'app.bsky.actor.defs#savedFeedsPref', + saved, + pinned, + }) + await this.rootStore.agent.app.bsky.actor.putPreferences({ + preferences: res.data.preferences, + }) + /* dont await */ this.rootStore.me.savedFeeds.refresh() + } } - async update(cb: (prefs: AppBskyActorDefs.Preferences) => void) { + async update(cb: (prefs: AppBskyActorDefs.Preferences) => boolean | void) { const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) - cb(res.data.preferences) + if (cb(res.data.preferences) === false) { + return + } await this.rootStore.agent.app.bsky.actor.putPreferences({ preferences: res.data.preferences, }) From 84990c509e9feb0cd44921a318aedcbad92b1da7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 15:12:18 -0500 Subject: [PATCH 217/374] Drop the hard-coded what's hot algo --- src/state/models/feeds/posts.ts | 56 +------------------ .../com/modals/ContentLanguagesSettings.tsx | 4 +- src/view/com/pager/FeedsTabBar.web.tsx | 7 +-- src/view/com/pager/FeedsTabBarMobile.tsx | 7 +-- ...mptyState.tsx => CustomFeedEmptyState.tsx} | 27 ++++++++- src/view/screens/Home.tsx | 35 ++---------- 6 files changed, 36 insertions(+), 100 deletions(-) rename src/view/com/posts/{WhatsHotEmptyState.tsx => CustomFeedEmptyState.tsx} (69%) diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index dfd92b35c3..5a5b287856 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -310,7 +310,7 @@ export class PostsFeedModel { constructor( public rootStore: RootStoreModel, - public feedType: 'home' | 'author' | 'suggested' | 'goodstuff' | 'custom', + public feedType: 'home' | 'author' | 'suggested' | 'custom', params: | GetTimeline.QueryParams | GetAuthorFeed.QueryParams @@ -391,10 +391,9 @@ export class PostsFeedModel { } get feedTuners() { - if (this.feedType === 'goodstuff') { + if (this.feedType === 'custom') { return [ FeedTuner.dedupReposts, - FeedTuner.likedRepliesOnly, FeedTuner.preferredLangOnly( this.rootStore.preferences.contentLanguages, ), @@ -701,15 +700,6 @@ export class PostsFeedModel { return this.rootStore.agent.app.bsky.feed.getFeed( params as GetCustomFeed.QueryParams, ) - } else if (this.feedType === 'goodstuff') { - const res = await getGoodStuff( - this.rootStore.session.currentSession?.accessJwt || '', - params as GetTimeline.QueryParams, - ) - res.data.feed = (res.data.feed || []).filter( - item => !item.post.author.viewer?.muted, - ) - return res } else { return this.rootStore.agent.getAuthorFeed( params as GetAuthorFeed.QueryParams, @@ -717,45 +707,3 @@ export class PostsFeedModel { } } } - -// HACK -// temporary off-spec route to get the good stuff -// -prf -async function getGoodStuff( - accessJwt: string, - params: GetTimeline.QueryParams, -): Promise { - const controller = new AbortController() - const to = setTimeout(() => controller.abort(), 15e3) - - const uri = new URL('https://bsky.social/xrpc/app.bsky.unspecced.getPopular') - let k: keyof GetTimeline.QueryParams - for (k in params) { - if (typeof params[k] !== 'undefined') { - uri.searchParams.set(k, String(params[k])) - } - } - - const res = await fetch(String(uri), { - method: 'get', - headers: { - accept: 'application/json', - authorization: `Bearer ${accessJwt}`, - }, - signal: controller.signal, - }) - - const resHeaders: Record = {} - res.headers.forEach((value: string, key: string) => { - resHeaders[key] = value - }) - let resBody = await res.json() - - clearTimeout(to) - - return { - success: res.status === 200, - headers: resHeaders, - data: jsonToLex(resBody), - } -} diff --git a/src/view/com/modals/ContentLanguagesSettings.tsx b/src/view/com/modals/ContentLanguagesSettings.tsx index 0c750fe0ea..700f1cbcb3 100644 --- a/src/view/com/modals/ContentLanguagesSettings.tsx +++ b/src/view/com/modals/ContentLanguagesSettings.tsx @@ -41,8 +41,8 @@ export function Component({}: {}) { Content Languages - Which languages would you like to see in the What's Hot feed? (Leave - them all unchecked to see any language.) + Which languages would you like to see in the your feed? (Leave them all + unchecked to see any language.) {languages.map(lang => ( diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 56ca6f2a1d..78937611b5 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -28,12 +28,7 @@ const FeedsTabBarDesktop = observer( ) => { const store = useStores() const items = useMemo( - () => [ - 'Following', - "What's hot", - ...store.me.savedFeeds.pinnedFeedNames, - 'My feeds', - ], + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My feeds'], [store.me.savedFeeds.pinnedFeedNames], ) const pal = usePalette('default') diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index cb910ccb96..a41f0ef327 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -33,12 +33,7 @@ export const FeedsTabBar = observer( }, [store]) const items = useMemo( - () => [ - 'Following', - "What's hot", - ...store.me.savedFeeds.pinnedFeedNames, - 'My feeds', - ], + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My feeds'], [store.me.savedFeeds.pinnedFeedNames], ) diff --git a/src/view/com/posts/WhatsHotEmptyState.tsx b/src/view/com/posts/CustomFeedEmptyState.tsx similarity index 69% rename from src/view/com/posts/WhatsHotEmptyState.tsx rename to src/view/com/posts/CustomFeedEmptyState.tsx index ade94ca3f3..69dd799022 100644 --- a/src/view/com/posts/WhatsHotEmptyState.tsx +++ b/src/view/com/posts/CustomFeedEmptyState.tsx @@ -1,5 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' +import {useNavigation} from '@react-navigation/native' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -7,14 +8,21 @@ import { import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {MagnifyingGlassIcon} from 'lib/icons' +import {NavigationProp} from 'lib/routes/types' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' -export function WhatsHotEmptyState() { +export function CustomFeedEmptyState() { const pal = usePalette('default') const palInverted = usePalette('inverted') const store = useStores() + const navigation = useNavigation() + + const onPressFindAccounts = React.useCallback(() => { + navigation.navigate('SearchTab') + navigation.popToTop() + }, [navigation]) const onPressSettings = React.useCallback(() => { store.shell.openModal({name: 'content-languages-settings'}) @@ -26,9 +34,22 @@ export function WhatsHotEmptyState() { - Your What's Hot feed is empty! This is because there aren't enough users - posting in your selected language. + This feed is empty! You may need to follow more users or tune your + language settings. + + )} @@ -202,6 +232,7 @@ export const CustomFeedScreen = withAuthRequired( currentFeed, onToggleLiked, onToggleSaved, + onPressShare, name, rkey, ]) From 3c89dd40f90deda00dd4d717bec0bb2f4217c1d1 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 16:54:17 -0500 Subject: [PATCH 219/374] Fix lint --- src/state/models/feeds/posts.ts | 1 - src/state/models/ui/saved-feeds.ts | 1 - src/view/com/pager/TabBar.tsx | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 5a5b287856..ac32044b46 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -6,7 +6,6 @@ import { AppBskyFeedGetAuthorFeed as GetAuthorFeed, AppBskyFeedGetFeed as GetCustomFeed, RichText, - jsonToLex, } from '@atproto/api' import AwaitLock from 'await-lock' import {bundleAsync} from 'lib/async/bundle' diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 9de28e0284..0d04f9c8d0 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -66,7 +66,6 @@ export class SavedFeedsModel { this._xLoading(!quietRefresh) try { let feeds: AppBskyFeedDefs.GeneratorView[] = [] - let cursor for ( let i = 0; i < this.rootStore.preferences.savedFeeds.length; diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index f6c41ce7ca..4852197306 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -10,7 +10,7 @@ import {StyleSheet, View, ScrollView} from 'react-native' import {Text} from '../util/text/Text' import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' -import {isDesktopWeb, isWeb} from 'platform/detection' +import {isDesktopWeb} from 'platform/detection' export interface TabBarProps { testID?: string From ad8778ab10d853b0de59aa25355fdf61ea6f2b57 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 16:55:50 -0500 Subject: [PATCH 220/374] Add server-side routes --- bskyweb/cmd/bskyweb/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 7c230041e7..07df85146f 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -112,6 +112,7 @@ func serve(cctx *cli.Context) error { e.GET("/moderation/blocked-accounts", server.WebGeneric) e.GET("/settings", server.WebGeneric) e.GET("/settings/app-passwords", server.WebGeneric) + e.GET("/settings/saved-feeds", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) @@ -125,6 +126,8 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/follows", server.WebGeneric) e.GET("/profile/:handle/followers", server.WebGeneric) e.GET("/profile/:handle/lists/:rkey", server.WebGeneric) + e.GET("/profile/:handle/feed/:rkey", server.WebGeneric) + e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric) // post endpoints; only first populates info e.GET("/profile/:handle/post/:rkey", server.WebPost) From 324c9209dc5777dcf3019926fb5847f6073fd2e4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 17:01:48 -0500 Subject: [PATCH 221/374] Only show algos and lists on profiles if there are items --- src/state/models/ui/profile.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index 54ee461b08..35831d1f77 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -12,13 +12,6 @@ export enum Sections { Lists = 'Lists', } -const USER_SELECTOR_ITEMS = [ - Sections.Posts, - Sections.PostsWithReplies, - Sections.CustomAlgorithms, - Sections.Lists, -] - export interface ProfileUiParams { user: string } @@ -83,7 +76,14 @@ export class ProfileUiModel { } get selectorItems() { - return USER_SELECTOR_ITEMS + const items = [Sections.Posts, Sections.PostsWithReplies] + if (this.algos.hasLoaded && !this.algos.isEmpty) { + items.push(Sections.CustomAlgorithms) + } + if (this.lists.hasLoaded && !this.lists.isEmpty) { + items.push(Sections.Lists) + } + return items } get selectedView() { @@ -166,6 +166,7 @@ export class ProfileUiModel { .setup() .catch(err => this.rootStore.log.error('Failed to fetch feed', err)), ]) + this.algos.refresh() // HACK: need to use the DID as a param, not the username -prf this.lists.source = this.profile.did this.lists From 46ed910cdaeaa675b858ccdad3d64425f8e63031 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 17:10:42 -0500 Subject: [PATCH 222/374] Add list-type avatar --- src/view/com/lists/ListCard.tsx | 2 +- src/view/com/lists/ListItems.tsx | 2 +- src/view/com/modals/CreateOrEditMuteList.tsx | 1 + src/view/com/util/UserAvatar.tsx | 29 ++++++++++++++++++-- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index 7cbdaaf648..0e13ca3335 100644 --- a/src/view/com/lists/ListCard.tsx +++ b/src/view/com/lists/ListCard.tsx @@ -60,7 +60,7 @@ export const ListCard = ({ anchorNoUnderline> - + - + diff --git a/src/view/com/modals/CreateOrEditMuteList.tsx b/src/view/com/modals/CreateOrEditMuteList.tsx index 0c13f243aa..736deae74b 100644 --- a/src/view/com/modals/CreateOrEditMuteList.tsx +++ b/src/view/com/modals/CreateOrEditMuteList.tsx @@ -143,6 +143,7 @@ export function Component({ List Avatar ) } + if (type === 'list') { + // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. + return ( + + + + + + ) + } return ( { - if (type === 'algo') { + if (type === 'algo' || type === 'list') { return { width: size, height: size, From 4fa4c67cc5ef856a9548bb4154f82aebf447e91d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 17:36:16 -0500 Subject: [PATCH 223/374] Some fixes --- src/view/com/posts/CustomFeedEmptyState.tsx | 16 ---------------- src/view/screens/Home.tsx | 1 - 2 files changed, 17 deletions(-) diff --git a/src/view/com/posts/CustomFeedEmptyState.tsx b/src/view/com/posts/CustomFeedEmptyState.tsx index 69dd799022..e51794e7ce 100644 --- a/src/view/com/posts/CustomFeedEmptyState.tsx +++ b/src/view/com/posts/CustomFeedEmptyState.tsx @@ -9,14 +9,12 @@ import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {MagnifyingGlassIcon} from 'lib/icons' import {NavigationProp} from 'lib/routes/types' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' export function CustomFeedEmptyState() { const pal = usePalette('default') const palInverted = usePalette('inverted') - const store = useStores() const navigation = useNavigation() const onPressFindAccounts = React.useCallback(() => { @@ -24,10 +22,6 @@ export function CustomFeedEmptyState() { navigation.popToTop() }, [navigation]) - const onPressSettings = React.useCallback(() => { - store.shell.openModal({name: 'content-languages-settings'}) - }, [store]) - return ( @@ -50,16 +44,6 @@ export function CustomFeedEmptyState() { size={14} /> - ) } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index d761994f33..f8a497028e 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -53,7 +53,6 @@ export const HomeScreen = withAuthRequired( model.setup() feeds.push(model) } - pagerRef.current?.setPage(0) setCustomFeeds(feeds) }, [store, store.me.savedFeeds.pinned, customFeeds, setCustomFeeds]) From da6fa2dede7e1fd14023623d33ebe0be5187193f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 18:04:32 -0500 Subject: [PATCH 224/374] Bump deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c8eb64997f..81cb54937d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.3", + "@atproto/api": "0.3.6", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", @@ -141,7 +141,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.1.8", + "@atproto/pds": "^0.1.9", "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", From 0b8dd95f2a653ba13ef84ad0e0940f112cb3ae91 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 18:22:34 -0500 Subject: [PATCH 225/374] Bump deps again --- package.json | 4 ++-- yarn.lock | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 81cb54937d..e589f22d20 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.6", + "@atproto/api": "0.3.7", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", @@ -141,7 +141,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.1.9", + "@atproto/pds": "^0.1.10", "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", diff --git a/yarn.lock b/yarn.lock index 81772e67ca..d42660edae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,7 +29,7 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@*", "@atproto/api@0.3.3": +"@atproto/api@*": version "0.3.3" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.3.tgz#8c8d41567beb7b37217f76d2aacf2c280e9fd07e" integrity sha512-BlgpYbdPO0KSBypg2KgqHM0kS2Pk82P3X0w2rJs/vrdcMl72d2WeI9kQ5PPFiz80p6C6XcLcpnzzKKtQeFvh4A== @@ -40,6 +40,17 @@ tlds "^1.234.0" typed-emitter "^2.1.0" +"@atproto/api@0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.7.tgz#5cc4b0ccc5c6690eb0e5a3ae138a84ce20697e2f" + integrity sha512-JHN3rHNGro4AaJWU64hsmpTUzd2+FbfMBiDkqyBmoKtj972ueBJeH8tz6WdnPcsIRfCj1kRthKFj2yJwgt6aSQ== + dependencies: + "@atproto/common-web" "*" + "@atproto/uri" "*" + "@atproto/xrpc" "*" + tlds "^1.234.0" + typed-emitter "^2.1.0" + "@atproto/common-web@*": version "0.1.0" resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.1.0.tgz#5529fa66f9533aa00cfd13f0a25757df7b26bd3d" @@ -137,10 +148,10 @@ resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4" integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw== -"@atproto/pds@^0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.8.tgz#cf1a9bab2301c3fe1120c63576153ac5a20bf70d" - integrity sha512-I493U+/NNU9D8L8tVbM/OpD6gQ6/Mv7uE+/i4a1vfBGO6NqYJ6jKw3qeCy4jq3NVbTxcs+lSSpK27hgApx4PtA== +"@atproto/pds@^0.1.10": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.10.tgz#cde4a06982ec0ba7166e978afed78f58abc5c6b1" + integrity sha512-Yxnpv3mQNrIcR5GFPUUoffSSDpZHzXXHuk36wtPXm7dDSg+ACgtILPcDSpkjr27JwE5OcfgD2UbQwXt7az7OLA== dependencies: "@atproto/api" "*" "@atproto/common" "*" From 37acc9e9304b594ff21443e1be896e1e576bb488 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 18:22:46 -0500 Subject: [PATCH 226/374] A few more UX tweaks --- src/lib/constants.ts | 14 +++------ src/view/com/feeds/SavedFeeds.tsx | 39 ++++++++++++++++++------ src/view/com/pager/FeedsTabBar.web.tsx | 2 +- src/view/com/pager/FeedsTabBarMobile.tsx | 2 +- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 88e429d83f..f4c6f5021a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -110,26 +110,20 @@ export async function DEFAULT_FEEDS( } } else if (serviceUrl.includes('staging')) { return { - pinned: [ - STAGING_DEFAULT_FEED('skyline'), - STAGING_DEFAULT_FEED('whats-hot'), - ], + pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ STAGING_DEFAULT_FEED('bsky-team'), - STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('with-friends'), STAGING_DEFAULT_FEED('whats-hot'), STAGING_DEFAULT_FEED('hot-classic'), ], } } else { return { - pinned: [ - STAGING_DEFAULT_FEED('skyline'), - STAGING_DEFAULT_FEED('whats-hot'), - ], + pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ STAGING_DEFAULT_FEED('bsky-team'), - STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('with-friends'), STAGING_DEFAULT_FEED('whats-hot'), STAGING_DEFAULT_FEED('hot-classic'), ], diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index 110a6e8940..e92e741dac 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -8,7 +8,7 @@ import {FlatList} from 'view/com/util/Views' import {Text} from 'view/com/util/text/Text' import {isDesktopWeb} from 'platform/detection' import {s} from 'lib/styles' -import {Link} from 'view/com/util/Link' +import {Link, TextLink} from 'view/com/util/Link' import {CustomFeed} from './CustomFeed' export const SavedFeeds = observer( @@ -52,14 +52,35 @@ export const SavedFeeds = observer( const renderListFooterComponent = useCallback(() => { return ( - - - - Settings - - + <> + + + + Change Order + + + + + Feeds are custom algorithms that users build with a little coding + expertise.{' '} + {' '} + for more information. + + + ) }, [pal]) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 78937611b5..fc04c3b2cc 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -28,7 +28,7 @@ const FeedsTabBarDesktop = observer( ) => { const store = useStores() const items = useMemo( - () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My feeds'], + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My Feeds'], [store.me.savedFeeds.pinnedFeedNames], ) const pal = usePalette('default') diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index a41f0ef327..5954e7f2e4 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -33,7 +33,7 @@ export const FeedsTabBar = observer( }, [store]) const items = useMemo( - () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My feeds'], + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My Feeds'], [store.me.savedFeeds.pinnedFeedNames], ) From 2a5ac1a6de6315a2e7d9af8c86d132667e3fec4c Mon Sep 17 00:00:00 2001 From: renahlee Date: Thu, 18 May 2023 17:29:46 -0700 Subject: [PATCH 227/374] Update labels for avatar --- src/view/com/pager/FeedsTabBarMobile.tsx | 4 ++-- src/view/com/search/HeaderWithInput.tsx | 4 ++-- src/view/com/util/ViewHeader.tsx | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 725c44603e..b42ffe7269 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -39,8 +39,8 @@ export const FeedsTabBar = observer( style={styles.tabBarAvi} onPress={onPressAvi} accessibilityRole="button" - accessibilityLabel="Open navigation" - accessibilityHint="Access profile and other navigation links"> + accessibilityLabel="Menu" + accessibilityHint="Access navigation links and settings"> + accessibilityLabel="Menu" + accessibilityHint="Access navigation links and settings"> + accessibilityHint={ + canGoBack ? '' : 'Access navigation links and settings' + }> {canGoBack ? ( Date: Thu, 18 May 2023 21:06:00 -0500 Subject: [PATCH 228/374] 1.29 testflight --- app.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index 8a2fe039a9..25290e20d3 100644 --- a/app.json +++ b/app.json @@ -4,7 +4,7 @@ "slug": "bluesky", "scheme": "bluesky", "owner": "blueskysocial", - "version": "1.28.0", + "version": "1.29.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -14,7 +14,7 @@ "backgroundColor": "#ffffff" }, "ios": { - "buildNumber": "3", + "buildNumber": "1", "supportsTablet": false, "bundleIdentifier": "xyz.blueskyweb.app", "config": { diff --git a/package.json b/package.json index e589f22d20..5c30a0cae4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.28.0", + "version": "1.29.0", "private": true, "scripts": { "postinstall": "patch-package", From 48a9e1b1dd98aa6f730c7d3ad88d393508f836e4 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 19 May 2023 18:27:13 -0700 Subject: [PATCH 229/374] fix refresh control color in ViewSelector.tsx --- src/view/com/util/ViewSelector.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index f9ef0945d4..c44d372f54 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react' -import {Pressable, StyleSheet, View} from 'react-native' +import {Pressable, RefreshControl, StyleSheet, View} from 'react-native' import {FlatList} from './Views' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' @@ -41,6 +41,7 @@ export function ViewSelector({ onRefresh?: () => void onEndReached?: (info: {distanceFromEnd: number}) => void }) { + const pal = usePalette('default') const [selectedIndex, setSelectedIndex] = useState(0) // events @@ -97,6 +98,13 @@ export function ViewSelector({ onScroll={onScroll} onRefresh={onRefresh} onEndReached={onEndReached} + refreshControl={ + + } onEndReachedThreshold={0.6} contentContainerStyle={s.contentContainer} removeClippedSubviews={true} From 8bcbbb869af22e482cceaaf6c754c5c126a7a1a6 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 19 May 2023 18:30:24 -0700 Subject: [PATCH 230/374] fix dark mode color for creator handle on CustomFeed screen --- src/view/screens/CustomFeed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index d2b9041f9f..7ff22f7f39 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -146,6 +146,7 @@ export const CustomFeedScreen = withAuthRequired( )} From 7cad7d12f1b6d97ae3395a2b3ce6ad0c102aea56 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 19 May 2023 18:32:21 -0700 Subject: [PATCH 231/374] add refreshControl to tab ViewSelector --- src/view/com/util/ViewSelector.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index c44d372f54..5b671d06cb 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -94,9 +94,7 @@ export function ViewSelector({ ListFooterComponent={ListFooterComponent} // NOTE sticky header disabled on android due to major performance issues -prf stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} - refreshing={refreshing} onScroll={onScroll} - onRefresh={onRefresh} onEndReached={onEndReached} refreshControl={ Date: Mon, 22 May 2023 16:12:05 -0700 Subject: [PATCH 232/374] fix prod default feeds not working --- src/lib/constants.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f4c6f5021a..e492dd61a0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -102,13 +102,13 @@ export async function DEFAULT_FEEDS( serviceUrl: string, resolveHandle: (name: string) => Promise, ) { - if (serviceUrl.includes('localhost')) { + 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')) { + } else if (serviceUrl.includes('staging')) { // staging return { pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ @@ -118,14 +118,14 @@ export async function DEFAULT_FEEDS( STAGING_DEFAULT_FEED('hot-classic'), ], } - } else { + } else { // production return { - pinned: [STAGING_DEFAULT_FEED('whats-hot')], + pinned: [PROD_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'), + PROD_DEFAULT_FEED('bsky-team'), + PROD_DEFAULT_FEED('with-friends'), + PROD_DEFAULT_FEED('whats-hot'), + PROD_DEFAULT_FEED('hot-classic'), ], } } From 64e303d911d351a2f492a23ae97207e5c6035b6e Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 16:35:37 -0700 Subject: [PATCH 233/374] optimistic updates for liking custom feeds --- src/lib/async/revertible.ts | 16 ++++++++++++ src/state/models/feeds/custom-feed.ts | 36 ++++++++++++++++++--------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts index 3c8e3e8f9e..43383b61e8 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() +/** + * 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, U, diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index e457d2d1ee..9ac69ac28b 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -2,6 +2,7 @@ import {AppBskyFeedDefs} from '@atproto/api' import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from 'state/models/root-store' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {updateDataOptimistically} from 'lib/async/revertible' export class CustomFeedModel { // data @@ -58,12 +59,19 @@ export class CustomFeedModel { async like() { try { - const res = await this.rootStore.agent.like(this.data.uri, this.data.cid) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.like = res.uri - this.data.likeCount = (this.data.likeCount || 0) + 1 - }) + await updateDataOptimistically( + this.data, + () => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = 'pending' + this.data.likeCount = (this.data.likeCount || 0) + 1 + }, + () => this.rootStore.agent.like(this.data.uri, this.data.cid), + res => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = res.uri + }, + ) } catch (e: any) { this.rootStore.log.error('Failed to like feed', e) } @@ -74,12 +82,16 @@ export class CustomFeedModel { return } try { - await this.rootStore.agent.deleteLike(this.data.viewer.like!) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.like = undefined - this.data.likeCount = (this.data.likeCount || 1) - 1 - }) + const likeUri = this.data.viewer.like + await updateDataOptimistically( + this.data, + () => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = undefined + this.data.likeCount = (this.data.likeCount || 1) - 1 + }, + () => this.rootStore.agent.deleteLike(likeUri), + ) } catch (e: any) { this.rootStore.log.error('Failed to unlike feed', e) } From dfcdd37087c0be4055c92a0f88431b32646ced6f Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 18:46:36 -0700 Subject: [PATCH 234/374] add haptics to save, like, and pin actions on feed --- src/lib/haptics.ts | 24 ++++++++++++++++++++++ src/view/com/util/post-ctrls/PostCtrls.tsx | 15 +++++++------- src/view/screens/CustomFeed.tsx | 5 +++++ src/view/screens/SavedFeeds.tsx | 16 +++++++-------- 4 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 src/lib/haptics.ts diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts new file mode 100644 index 0000000000..23a3217965 --- /dev/null +++ b/src/lib/haptics.ts @@ -0,0 +1,24 @@ +import { isIOS } 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 = () => ReactNativeHapticFeedback.trigger(hapticImpact) + static impact = (type: HapticFeedbackTypes = hapticImpact) => ReactNativeHapticFeedback.trigger(type) + static selection = () => ReactNativeHapticFeedback.trigger('selection') + static notification = (type: 'success' | 'warning' | 'error') => { + switch (type) { + case 'success': + return ReactNativeHapticFeedback.trigger('notificationSuccess') + case 'warning': + return ReactNativeHapticFeedback.trigger('notificationWarning') + case 'error': + return ReactNativeHapticFeedback.trigger('notificationError') + } + } +} \ No newline at end of file diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 9980e9de07..0d2f83ce7d 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -10,9 +10,6 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import ReactNativeHapticFeedback, { - HapticFeedbackTypes, -} from 'react-native-haptic-feedback' // DISABLED see #135 // import { // TriggerableAnimated, @@ -24,8 +21,9 @@ import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' import {s, colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {useStores} from 'state/index' -import {isIOS, isNative} from 'platform/detection' +import {isNative} from 'platform/detection' import {RepostButton} from './RepostButton' +import {Haptics} from 'lib/haptics' interface PostCtrlsOpts { itemUri: string @@ -58,7 +56,6 @@ interface PostCtrlsOpts { } const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} -const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537 // DISABLED see #135 /* @@ -112,7 +109,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { store.shell.closeModal() if (!opts.isReposted) { if (isNative) { - ReactNativeHapticFeedback.trigger(hapticImpact) + Haptics.default() } opts.onPressToggleRepost().catch(_e => undefined) // DISABLED see #135 @@ -141,7 +138,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { }) if (isNative) { - ReactNativeHapticFeedback.trigger(hapticImpact) + Haptics.default() } }, [ opts.author, @@ -154,7 +151,9 @@ export function PostCtrls(opts: PostCtrlsOpts) { const onPressToggleLikeWrapper = async () => { if (!opts.isLiked) { - ReactNativeHapticFeedback.trigger(hapticImpact) + if (isNative) { + Haptics.default() + } await opts.onPressToggleLike().catch(_e => undefined) // DISABLED see #135 // likeRef.current?.trigger( diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 7ff22f7f39..353995540d 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -24,6 +24,9 @@ import {isDesktopWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' +import { Haptics } from 'lib/haptics' + +const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -49,6 +52,7 @@ export const CustomFeedScreen = withAuthRequired( const onToggleSaved = React.useCallback(async () => { try { + Haptics.default() if (currentFeed?.isSaved) { await currentFeed?.unsave() } else { @@ -63,6 +67,7 @@ export const CustomFeedScreen = withAuthRequired( }, [store, currentFeed]) const onToggleLiked = React.useCallback(async () => { + Haptics.default() try { if (currentFeed?.isLiked) { await currentFeed?.unlike() diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 613e42fbf4..0213b36a9c 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -28,6 +28,7 @@ import {CustomFeed} from 'view/com/feeds/CustomFeed' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {CustomFeedModel} from 'state/models/feeds/custom-feed' import * as Toast from 'view/com/util/Toast' +import {Haptics} from 'lib/haptics' type Props = NativeStackScreenProps @@ -128,14 +129,13 @@ const ListItem = observer( const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) const isPinned = savedFeeds.isPinned(item) - const onTogglePinned = useCallback( - () => - savedFeeds.togglePinnedFeed(item).catch(e => { - Toast.show('There was an issue contacting the server') - store.log.error('Failed to toggle pinned feed', {e}) - }), - [savedFeeds, item, store], - ) + const onTogglePinned = useCallback(() => { + Haptics.default() + savedFeeds.togglePinnedFeed(item).catch(e => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to toggle pinned feed', {e}) + }) + }, [savedFeeds, item, store]) const onPressUp = useCallback( () => savedFeeds.movePinnedFeed(item, 'up').catch(e => { From 512c918c033abf974260ea6a87a85ef14cdabe38 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 19:10:03 -0700 Subject: [PATCH 235/374] decrease long press time required to reoreder pinned feed --- src/view/screens/SavedFeeds.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 0213b36a9c..4a060c2fd6 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -159,6 +159,7 @@ const ListItem = observer( {isPinned && isWeb ? ( From 8a2349c55ffcff4f833c015f9b10296aa9d77738 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 19:14:10 -0700 Subject: [PATCH 236/374] increase pin button hitslop --- src/view/screens/SavedFeeds.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 4a060c2fd6..2f9165b374 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -198,6 +198,7 @@ const ListItem = observer( /> Date: Mon, 22 May 2023 20:07:40 -0700 Subject: [PATCH 237/374] update pinned feed from custom feed view --- src/state/models/ui/saved-feeds.ts | 10 ++++-- src/view/screens/CustomFeed.tsx | 52 +++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 0d04f9c8d0..244e758984 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -129,8 +129,14 @@ export class SavedFeedsModel { ) } - isPinned(feed: CustomFeedModel) { - return this.rootStore.preferences.pinnedFeeds.includes(feed.uri) + isPinned(feedOrUri: CustomFeedModel | string) { + let uri: string + if (typeof feedOrUri === 'string') { + uri = feedOrUri + } else { + uri = feedOrUri.uri + } + return this.rootStore.preferences.pinnedFeeds.includes(uri) } async movePinnedFeed(item: CustomFeedModel, direction: 'up' | 'down') { diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 353995540d..952461c9c8 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -24,7 +24,7 @@ import {isDesktopWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' -import { Haptics } from 'lib/haptics' +import {Haptics} from 'lib/haptics' const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} @@ -47,6 +47,7 @@ export const CustomFeedScreen = withAuthRequired( feed.setup() return feed }, [store, uri]) + const isPinned = store.me.savedFeeds.isPinned(uri) useSetTitle(currentFeed?.displayName) @@ -65,7 +66,6 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up update feeds', {err}) } }, [store, currentFeed]) - const onToggleLiked = React.useCallback(async () => { Haptics.default() try { @@ -81,6 +81,13 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up toggle like', {err}) } }, [store, currentFeed]) + const onTogglePinned = React.useCallback(async () => { + Haptics.default() + store.me.savedFeeds.togglePinnedFeed(currentFeed!).catch(e => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to toggle pinned feed', {e}) + }) + }, [store, currentFeed]) const onPressShare = React.useCallback(() => { const url = toShareUrl(`/profile/${name}/feed/${rkey}`) shareUrl(url) @@ -212,15 +219,30 @@ export const CustomFeedScreen = withAuthRequired( {currentFeed.data.description} ) : null} - + + + + ) @@ -275,6 +298,11 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingBottom: 16, }, + headerDetailsFooter: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, fakeSelector: { flexDirection: 'row', paddingHorizontal: isDesktopWeb ? 16 : 6, From b561a51ed9f798194c3c6a72eefab562a773f2c9 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 14:18:35 -0700 Subject: [PATCH 238/374] add button to reset preferences in dev mode --- src/state/models/ui/preferences.ts | 32 ++++++++++++++++++++++++++++++ src/view/screens/Settings.tsx | 15 ++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index c85faf6588..c4b6da0f69 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -63,6 +63,11 @@ export class PreferencesModel { } } + /** + * The function hydrates an object with properties related to content languages, labels, saved feeds, + * and pinned feeds that it gets from the parameter `v` (probably local storage) + * @param {unknown} v - the data object to hydrate from + */ hydrate(v: unknown) { if (isObj(v)) { if ( @@ -95,6 +100,9 @@ export class PreferencesModel { } } + /** + * This function fetches preferences and sets defaults for missing items. + */ async sync() { // fetch preferences let hasSavedFeedsPref = false @@ -153,6 +161,15 @@ export class PreferencesModel { } } + /** + * This function updates the preferences of a user and allows for a callback function to be executed + * before the update. + * @param cb - cb is a callback function that takes in a single parameter of type + * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to + * update the preferences of the user. The function is called with the current preferences as an + * argument and if the callback returns false, the preferences are not updated. + * @returns void + */ async update(cb: (prefs: AppBskyActorDefs.Preferences) => boolean | void) { const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) if (cb(res.data.preferences) === false) { @@ -163,6 +180,21 @@ export class PreferencesModel { }) } + /** + * This function resets the preferences to an empty array of no preferences. + */ + async reset() { + runInAction(() => { + this.contentLabels = new LabelPreferencesModel() + this.contentLanguages = deviceLocales.map(locale => locale.languageCode) + this.savedFeeds = [] + this.pinnedFeeds = [] + }) + await this.rootStore.agent.app.bsky.actor.putPreferences({ + preferences: [], + }) + } + hasContentLanguage(code2: string) { return this.contentLanguages.includes(code2) } diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 3ce41f8c07..ac4e5a9e0b 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -141,6 +141,11 @@ export const SettingsScreen = withAuthRequired( store.shell.openModal({name: 'delete-account'}) }, [store]) + const onPressResetPreferences = React.useCallback(async () => { + await store.preferences.reset() + Toast.show('Preferences reset') + }, [store]) + return ( @@ -393,6 +398,16 @@ export const SettingsScreen = withAuthRequired( Storybook + {__DEV__ ? ( + + + Reset preferences state + + + ) : null} Build version {AppInfo.appVersion} ({AppInfo.buildVersion}) From fc9e28ca72ce498df8d0902c8e51d226affefd83 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 15:28:46 -0700 Subject: [PATCH 239/374] slight performance improvements --- src/state/models/log.ts | 16 ++++++++++++++++ src/state/models/ui/preferences.ts | 8 ++++++-- src/state/models/ui/saved-feeds.ts | 4 ++++ src/view/screens/SavedFeeds.tsx | 9 +++++++-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/state/models/log.ts b/src/state/models/log.ts index d80617139f..7c9c37c0d9 100644 --- a/src/state/models/log.ts +++ b/src/state/models/log.ts @@ -27,6 +27,7 @@ function genId(): string { export class LogModel { entries: LogEntry[] = [] + timers = new Map() constructor() { makeAutoObservable(this) @@ -74,6 +75,21 @@ export class LogModel { ts: Date.now(), }) } + + time = (label = 'default') => { + this.timers.set(label, performance.now()) + } + + timeEnd = (label = 'default', warn = false) => { + const endTime = performance.now() + if (this.timers.has(label)) { + const elapsedTime = endTime - this.timers.get(label)! + console.log(`${label}: ${elapsedTime.toFixed(3)}ms`) + this.timers.delete(label) + } else { + warn && console.warn(`Timer with label '${label}' does not exist.`) + } + } } function detailsToStr(details?: any) { diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index c4b6da0f69..dcf6b9a7ad 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -292,11 +292,15 @@ export class PreferencesModel { return res } + setFeeds(saved: string[], pinned: string[]) { + this.savedFeeds = saved + this.pinnedFeeds = pinned + } + async setSavedFeeds(saved: string[], pinned: string[]) { const oldSaved = this.savedFeeds const oldPinned = this.pinnedFeeds - this.savedFeeds = saved - this.pinnedFeeds = pinned + this.setFeeds(saved, pinned) try { await this.update((prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.find( diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 244e758984..979fddf497 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -47,6 +47,10 @@ export class SavedFeedsModel { return this.feeds.filter(f => !this.isPinned(f)) } + get all() { + return this.pinned.concat(this.unpinned) + } + get pinnedFeedNames() { return this.pinned.map(f => f.displayName) } diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 2f9165b374..e305e63056 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -99,7 +99,7 @@ export const SavedFeeds = withAuthRequired( /> item.data.uri} refreshing={savedFeeds.isRefreshing} refreshControl={ @@ -111,6 +111,11 @@ export const SavedFeeds = withAuthRequired( /> } renderItem={({item, drag}) => } + getItemLayout={(data, index) => ({ + length: 77, + offset: 77 * index, + index, + })} initialNumToRender={10} ListFooterComponent={renderListFooterComponent} ListEmptyComponent={renderListEmptyComponent} @@ -198,7 +203,7 @@ const ListItem = observer( /> Date: Tue, 23 May 2023 15:33:27 -0700 Subject: [PATCH 240/374] refactor load latest btn --- src/view/com/util/load-latest/LoadLatestBtn.web.tsx | 4 ++-- src/view/com/util/load-latest/LoadLatestBtnMobile.tsx | 6 +++--- src/view/screens/Home.tsx | 2 +- src/view/screens/Notifications.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx index 839685029d..85fb5a0144 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx @@ -25,11 +25,11 @@ export const LoadLatestBtn = ({ onPress={onPress} hitSlop={HITSLOP} accessibilityRole="button" - accessibilityLabel={`Load new ${label}`} + accessibilityLabel={label} accessibilityHint=""> - Load new {label} + {label} ) diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 5279696a27..548d30d5a9 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -25,15 +25,15 @@ export const LoadLatestBtn = observer( onPress={onPress} hitSlop={HITSLOP} accessibilityRole="button" - accessibilityLabel={`Load new ${label}`} - accessibilityHint={`Loads new ${label}`}> + accessibilityLabel={label} + accessibilityHint={label}> - Load new {label} + {label} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index f8a497028e..4fe175fc1e 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -258,7 +258,7 @@ const FeedPage = observer( headerOffset={HEADER_OFFSET} /> {feed.hasNewLatest && !feed.isRefreshing && ( - + )} {store.me.notifications.hasNewLatest && !store.me.notifications.isRefreshing && ( - + )} ) From 858ec6438da9cc9bee765857ea925f77e074fde2 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 15:48:14 -0700 Subject: [PATCH 241/374] show scroll to top button when scrolling stops --- src/lib/hooks/useOnMainScroll.ts | 3 +++ src/view/com/posts/Feed.tsx | 8 +++++++- src/view/screens/CustomFeed.tsx | 20 ++++++++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 41b35dd4f6..994a357141 100644 --- a/src/lib/hooks/useOnMainScroll.ts +++ b/src/lib/hooks/useOnMainScroll.ts @@ -2,6 +2,9 @@ import {useState} from 'react' import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' import {RootStoreModel} from 'state/index' +export type onMomentumScrollEndCb = ( + event: NativeSyntheticEvent, +) => void export type OnScrollCb = ( event: NativeSyntheticEvent, ) => void diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 5b0110df83..50398e7065 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -14,7 +14,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {PostsFeedModel} from 'state/models/feeds/posts' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' @@ -31,6 +31,8 @@ export const Feed = observer(function Feed({ scrollElRef, onPressTryAgain, onScroll, + scrollEventThrottle, + onMomentumScrollEnd, renderEmptyState, testID, headerOffset = 0, @@ -43,6 +45,8 @@ export const Feed = observer(function Feed({ scrollElRef?: MutableRefObject | null> onPressTryAgain?: () => void onScroll?: OnScrollCb + scrollEventThrottle?: number + onMomentumScrollEnd?: onMomentumScrollEndCb renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number @@ -180,6 +184,8 @@ export const Feed = observer(function Feed({ contentContainerStyle={s.contentContainer} style={{paddingTop: headerOffset}} onScroll={onScroll} + scrollEventThrottle={scrollEventThrottle} + onMomentumScrollEnd={onMomentumScrollEnd} onEndReached={onEndReached} onEndReachedThreshold={0.6} removeClippedSubviews={true} diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 952461c9c8..2316d7f060 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef} from 'react' +import React, {useMemo, useRef, useState} from 'react' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' @@ -25,6 +25,8 @@ import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' +import { LoadLatestBtn } from 'view/com/util/load-latest/LoadLatestBtn' +import { onMomentumScrollEndCb } from 'lib/hooks/useOnMainScroll' const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} @@ -48,7 +50,7 @@ export const CustomFeedScreen = withAuthRequired( return feed }, [store, uri]) const isPinned = store.me.savedFeeds.isPinned(uri) - + const [allowScrollToTop, setAllowScrollToTop] = useState(false) useSetTitle(currentFeed?.displayName) const onToggleSaved = React.useCallback(async () => { @@ -266,15 +268,29 @@ export const CustomFeedScreen = withAuthRequired( isPinned, ]) + const onMomentumScrollEnd: onMomentumScrollEndCb = React.useCallback((event) => { + if (event.nativeEvent.contentOffset.y > 200) { + setAllowScrollToTop(true) + } else { + setAllowScrollToTop(false) + } + }, []) + return ( + {allowScrollToTop ? { + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) + }} + label='Scroll to top' + /> : null} ) }), From 58a0489ce3028c6e6d205557e3edcf03c85f6626 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 13:59:53 -0700 Subject: [PATCH 242/374] add isWeb check to disable haptics on web --- src/lib/haptics.ts | 30 +++++++++++++++++----- src/view/com/util/post-ctrls/PostCtrls.tsx | 16 +++--------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts index 23a3217965..516940c1ce 100644 --- a/src/lib/haptics.ts +++ b/src/lib/haptics.ts @@ -1,17 +1,33 @@ -import { isIOS } from 'platform/detection' +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 = () => ReactNativeHapticFeedback.trigger(hapticImpact) - static impact = (type: HapticFeedbackTypes = hapticImpact) => ReactNativeHapticFeedback.trigger(type) - static selection = () => ReactNativeHapticFeedback.trigger('selection') + 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') @@ -21,4 +37,4 @@ export class Haptics { return ReactNativeHapticFeedback.trigger('notificationError') } } -} \ No newline at end of file +} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 0d2f83ce7d..41d66641f4 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -21,7 +21,6 @@ import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' import {s, colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {useStores} from 'state/index' -import {isNative} from 'platform/detection' import {RepostButton} from './RepostButton' import {Haptics} from 'lib/haptics' @@ -108,9 +107,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { const onRepost = useCallback(() => { store.shell.closeModal() if (!opts.isReposted) { - if (isNative) { - Haptics.default() - } + Haptics.default() opts.onPressToggleRepost().catch(_e => undefined) // DISABLED see #135 // repostRef.current?.trigger( @@ -136,10 +133,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { indexedAt: opts.indexedAt, }, }) - - if (isNative) { - Haptics.default() - } + Haptics.default() }, [ opts.author, opts.indexedAt, @@ -151,9 +145,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { const onPressToggleLikeWrapper = async () => { if (!opts.isLiked) { - if (isNative) { - Haptics.default() - } + Haptics.default() await opts.onPressToggleLike().catch(_e => undefined) // DISABLED see #135 // likeRef.current?.trigger( @@ -200,7 +192,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { accessibilityRole="button" accessibilityLabel={opts.isLiked ? 'Unlike' : 'Like'} accessibilityHint={ - opts.isReposted ? `Removes like from the post` : `Like the post` + opts.isReposted ? 'Removes like from the post' : 'Like the post' }> {opts.isLiked ? ( Date: Wed, 24 May 2023 14:18:49 -0700 Subject: [PATCH 243/374] fix scrollToTop for web --- src/view/com/posts/Feed.tsx | 3 ++ src/view/screens/CustomFeed.tsx | 51 ++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 50398e7065..2726ff7d35 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -18,6 +18,7 @@ import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' +import {useTheme} from 'lib/ThemeContext' const LOADING_ITEM = {_reactKey: '__loading__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -54,6 +55,7 @@ export const Feed = observer(function Feed({ extraData?: any }) { const pal = usePalette('default') + const theme = useTheme() const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) @@ -186,6 +188,7 @@ export const Feed = observer(function Feed({ onScroll={onScroll} scrollEventThrottle={scrollEventThrottle} onMomentumScrollEnd={onMomentumScrollEnd} + indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} onEndReached={onEndReached} onEndReachedThreshold={0.6} removeClippedSubviews={true} diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 2316d7f060..dcb726873c 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -20,15 +20,13 @@ import {ViewHeader} from 'view/com/util/ViewHeader' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' -import {isDesktopWeb} from 'platform/detection' +import {isDesktopWeb, isWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' -import { LoadLatestBtn } from 'view/com/util/load-latest/LoadLatestBtn' -import { onMomentumScrollEndCb } from 'lib/hooks/useOnMainScroll' - -const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} +import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -257,22 +255,37 @@ export const CustomFeedScreen = withAuthRequired( ) }, [ - store.me.did, pal, currentFeed, - onToggleLiked, + store.me.did, onToggleSaved, + onToggleLiked, onPressShare, name, rkey, isPinned, + onTogglePinned, ]) - const onMomentumScrollEnd: onMomentumScrollEndCb = React.useCallback((event) => { - if (event.nativeEvent.contentOffset.y > 200) { - setAllowScrollToTop(true) - } else { - setAllowScrollToTop(false) + const onMomentumScrollEnd: onMomentumScrollEndCb = React.useCallback( + event => { + console.log('onMomentumScrollEnd') + if (event.nativeEvent.contentOffset.y > s.window.height * 3) { + setAllowScrollToTop(true) + } else { + setAllowScrollToTop(false) + } + }, + [], + ) + const onScroll: OnScrollCb = React.useCallback(event => { + // since onMomentumScrollEnd is not supported in react-native-web, we have to use onScroll which fires more often so is not desirable on mobile + if (isWeb) { + if (event.nativeEvent.contentOffset.y > s.window.height * 2) { + setAllowScrollToTop(true) + } else { + setAllowScrollToTop(false) + } } }, []) @@ -283,14 +296,18 @@ export const CustomFeedScreen = withAuthRequired( scrollElRef={scrollElRef} feed={algoFeed} onMomentumScrollEnd={onMomentumScrollEnd} + onScroll={onScroll} // same logic as onMomentumScrollEnd but for web ListHeaderComponent={renderListHeaderComponent} extraData={[uri, isPinned]} /> - {allowScrollToTop ? { - scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) - }} - label='Scroll to top' - /> : null} + {allowScrollToTop ? ( + { + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) + }} + label="Scroll to top" + /> + ) : null} ) }), From 04468eb1b7cccc6dcee127c7a89d001fa2ea352b Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 14:59:42 -0700 Subject: [PATCH 244/374] make prettier and eslint work together --- .eslintrc.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2d59d36dd5..19fcf23083 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,10 @@ module.exports = { root: true, - extends: ['@react-native-community', 'plugin:react-native-a11y/ios'], + extends: [ + '@react-native-community', + 'plugin:react-native-a11y/ios', + 'prettier', + ], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'detox'], ignorePatterns: [ From 7e555ecc1b04fed96192d3c68b87cf679993abfa Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 15:00:36 -0700 Subject: [PATCH 245/374] fix lint errors --- src/lib/constants.ts | 9 ++++++--- src/view/screens/Notifications.tsx | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e492dd61a0..c42e6f3a93 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -102,13 +102,15 @@ export async function DEFAULT_FEEDS( serviceUrl: string, resolveHandle: (name: string) => Promise, ) { - if (serviceUrl.includes('localhost')) { // local dev + 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 + } else if (serviceUrl.includes('staging')) { + // staging return { pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ @@ -118,7 +120,8 @@ export async function DEFAULT_FEEDS( STAGING_DEFAULT_FEED('hot-classic'), ], } - } else { // production + } else { + // production return { pinned: [PROD_DEFAULT_FEED('whats-hot')], saved: [ diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index df84b541ba..67507d009e 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -98,7 +98,10 @@ export const NotificationsScreen = withAuthRequired( /> {store.me.notifications.hasNewLatest && !store.me.notifications.isRefreshing && ( - + )} ) From 32c9dabb7467149baf39d8f5c2eb3d0b81236d92 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 15:04:30 -0700 Subject: [PATCH 246/374] make tab bar scroll view draggable on web --- src/lib/hooks/useDraggableScrollView.ts | 84 ++++++++++++++++++++++ src/lib/merge-refs.ts | 27 +++++++ src/view/com/pager/DraggableScrollView.tsx | 15 ++++ src/view/com/pager/FeedsTabBar.web.tsx | 2 +- src/view/com/pager/TabBar.tsx | 5 +- 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/lib/hooks/useDraggableScrollView.ts create mode 100644 src/lib/merge-refs.ts create mode 100644 src/view/com/pager/DraggableScrollView.tsx diff --git a/src/lib/hooks/useDraggableScrollView.ts b/src/lib/hooks/useDraggableScrollView.ts new file mode 100644 index 0000000000..b0f7465d79 --- /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 = { + cursor?: string + outerRef?: ForwardedRef +} + +export function useDraggableScroll({ + outerRef, + cursor = 'grab', +}: Props = {}) { + const ref = useRef(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/merge-refs.ts b/src/lib/merge-refs.ts new file mode 100644 index 0000000000..4617b5260d --- /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` or + * `React.LegacyRef`. These refs are used to store references to DOM elements or React components. + * The `mergeRefs` function takes in an array of these refs and returns a callback function that + * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and + * returns a ref callback function that can be used to merge multiple refs into a single ref. + */ +export function mergeRefs( + refs: Array | React.LegacyRef>, +): React.RefCallback { + return value => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value) + } else if (ref != null) { + ;(ref as React.MutableRefObject).current = value + } + }) + } +} diff --git a/src/view/com/pager/DraggableScrollView.tsx b/src/view/com/pager/DraggableScrollView.tsx new file mode 100644 index 0000000000..4b7396eaa9 --- /dev/null +++ b/src/view/com/pager/DraggableScrollView.tsx @@ -0,0 +1,15 @@ +import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView' +import React, {ComponentProps} from 'react' +import {ScrollView} from 'react-native' + +export const DraggableScrollView = React.forwardRef< + ScrollView, + ComponentProps +>(function DraggableScrollView(props, ref) { + const {refs} = useDraggableScroll({ + outerRef: ref, + cursor: 'grab', // optional, default + }) + + return +}) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index fc04c3b2cc..b51db1741f 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -53,8 +53,8 @@ const FeedsTabBarDesktop = observer( // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 4852197306..cebf58b480 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -11,6 +11,7 @@ import {Text} from '../util/text/Text' import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' import {isDesktopWeb} from 'platform/detection' +import {DraggableScrollView} from './DraggableScrollView' export interface TabBarProps { testID?: string @@ -75,7 +76,7 @@ export function TabBar({ return ( - ) })} - + ) } From 4e1876fe85ab3a70eba50466a62bff8a9d01c16c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 18:46:27 -0500 Subject: [PATCH 247/374] Refactor the scroll-to-top UX --- src/lib/hooks/useOnMainScroll.ts | 58 ++++++++++----- src/view/com/notifications/Feed.tsx | 1 + src/view/com/posts/Feed.tsx | 3 +- src/view/com/util/fab/FABInner.tsx | 2 +- .../util/load-latest/LoadLatestBtnMobile.tsx | 39 ++++------- src/view/screens/CustomFeed.tsx | 70 +++++++------------ src/view/screens/Home.tsx | 11 +-- src/view/screens/Notifications.tsx | 16 +++-- src/view/screens/SearchMobile.tsx | 2 +- 9 files changed, 102 insertions(+), 100 deletions(-) diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 994a357141..782c4704b6 100644 --- a/src/lib/hooks/useOnMainScroll.ts +++ b/src/lib/hooks/useOnMainScroll.ts @@ -1,28 +1,50 @@ -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' -export type onMomentumScrollEndCb = ( - event: NativeSyntheticEvent, -) => void export type OnScrollCb = ( event: NativeSyntheticEvent, ) => void +export type ResetCb = () => void -export function useOnMainScroll(store: RootStoreModel) { - let [lastY, setLastY] = useState(0) - let isMinimal = store.shell.minimalShellMode - return function onMainScroll(event: NativeSyntheticEvent) { - const y = event.nativeEvent.contentOffset.y - const dy = y - (lastY || 0) - setLastY(y) +export function useOnMainScroll( + store: RootStoreModel, +): [OnScrollCb, boolean, ResetCb] { + let lastY = useRef(0) + let [isScrolledDown, setIsScrolledDown] = useState(false) + return [ + useCallback( + (event: NativeSyntheticEvent) => { + const y = event.nativeEvent.contentOffset.y + const dy = y - (lastY.current || 0) + lastY.current = y - if (!isMinimal && y > 10 && dy > 10) { - store.shell.setMinimalShellMode(true) - isMinimal = true - } else if (isMinimal && (y <= 10 || dy < -10)) { + if (!store.shell.minimalShellMode && y > 10 && dy > 10) { + store.shell.setMinimalShellMode(true) + } else if (store.shell.minimalShellMode && (y <= 10 || dy < -10)) { + store.shell.setMinimalShellMode(false) + } + + 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/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 50bdc5dc93..d457d71362 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -154,6 +154,7 @@ export const Feed = observer(function Feed({ onEndReached={onEndReached} onEndReachedThreshold={0.6} onScroll={onScroll} + scrollEventThrottle={100} contentContainerStyle={s.contentContainer} /> ) : null} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 2726ff7d35..b90213472c 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -14,7 +14,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {PostsFeedModel} from 'state/models/feeds/posts' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' @@ -47,7 +47,6 @@ export const Feed = observer(function Feed({ onPressTryAgain?: () => void onScroll?: OnScrollCb scrollEventThrottle?: number - onMomentumScrollEnd?: onMomentumScrollEndCb renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 5eb4a65889..76824e575c 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -47,7 +47,7 @@ const styles = StyleSheet.create({ outer: { position: 'absolute', zIndex: 1, - right: 28, + right: 24, bottom: 94, width: 60, height: 60, diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 548d30d5a9..5e03e2285b 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -1,23 +1,25 @@ import React from 'react' import {StyleSheet, TouchableOpacity} from 'react-native' import {observer} from 'mobx-react-lite' -import LinearGradient from 'react-native-linear-gradient' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {Text} from '../text/Text' -import {colors, gradients} from 'lib/styles' import {clamp} from 'lodash' import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} export const LoadLatestBtn = observer( ({onPress, label}: {onPress: () => void; label: string}) => { const store = useStores() + const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() return ( - - - {label} - - + accessibilityHint=""> + ) }, @@ -44,19 +38,14 @@ export const LoadLatestBtn = observer( const styles = StyleSheet.create({ loadLatest: { position: 'absolute', - left: 20, + left: 18, bottom: 35, - shadowColor: '#000', - shadowOpacity: 0.3, - shadowOffset: {width: 0, height: 1}, - }, - loadLatestInner: { + borderWidth: 1, + width: 52, + height: 52, + borderRadius: 26, flexDirection: 'row', - paddingHorizontal: 14, - paddingVertical: 10, - borderRadius: 30, - }, - loadLatestText: { - color: colors.white, + alignItems: 'center', + justifyContent: 'center', }, }) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index dcb726873c..1409762d1c 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -20,13 +20,13 @@ import {ViewHeader} from 'view/com/util/ViewHeader' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' -import {isDesktopWeb, isWeb} from 'platform/detection' +import {isDesktopWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' -import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -48,7 +48,8 @@ export const CustomFeedScreen = withAuthRequired( return feed }, [store, uri]) const isPinned = store.me.savedFeeds.isPinned(uri) - const [allowScrollToTop, setAllowScrollToTop] = useState(false) + const [onMainScroll, isScrolledDown, resetMainScroll] = + useOnMainScroll(store) useSetTitle(currentFeed?.displayName) const onToggleSaved = React.useCallback(async () => { @@ -66,6 +67,7 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up update feeds', {err}) } }, [store, currentFeed]) + const onToggleLiked = React.useCallback(async () => { Haptics.default() try { @@ -81,6 +83,7 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up toggle like', {err}) } }, [store, currentFeed]) + const onTogglePinned = React.useCallback(async () => { Haptics.default() store.me.savedFeeds.togglePinnedFeed(currentFeed!).catch(e => { @@ -88,11 +91,17 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed to toggle pinned feed', {e}) }) }, [store, currentFeed]) + const onPressShare = React.useCallback(() => { const url = toShareUrl(`/profile/${name}/feed/${rkey}`) shareUrl(url) }, [name, rkey]) + const onScrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) + resetMainScroll() + }, [scrollElRef, resetMainScroll]) + const renderHeaderBtns = React.useCallback(() => { return ( @@ -220,15 +229,17 @@ export const CustomFeedScreen = withAuthRequired( ) : null} - + {currentFeed ? ( + + ) : null} ) }), diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx index f9b4864b29..c9d09373e6 100644 --- a/src/view/screens/SearchMobile.tsx +++ b/src/view/screens/SearchMobile.tsx @@ -35,7 +35,7 @@ export const SearchScreen = withAuthRequired( const store = useStores() const scrollViewRef = React.useRef(null) const flatListRef = React.useRef(null) - const onMainScroll = useOnMainScroll(store) + const [onMainScroll] = useOnMainScroll(store) const [isInputFocused, setIsInputFocused] = React.useState(false) const [query, setQuery] = React.useState('') const autocompleteView = React.useMemo( From 2ba4d9bfbf25b37473eb1d693cf5c9294e4b6d94 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 18:50:19 -0500 Subject: [PATCH 248/374] Add compose fab to custom feed screen --- src/view/screens/CustomFeed.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 1409762d1c..f406c43d5a 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef, useState} from 'react' +import React, {useMemo, useRef} from 'react' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' @@ -25,6 +25,8 @@ import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' +import {ComposeIcon2} from 'lib/icons' +import {FAB} from '../com/util/fab/FAB' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' @@ -102,6 +104,10 @@ export const CustomFeedScreen = withAuthRequired( resetMainScroll() }, [scrollElRef, resetMainScroll]) + const onPressCompose = React.useCallback(() => { + store.shell.openComposer({}) + }, [store]) + const renderHeaderBtns = React.useCallback(() => { return ( @@ -292,6 +298,14 @@ export const CustomFeedScreen = withAuthRequired( {isScrolledDown ? ( ) : null} + } + accessibilityRole="button" + accessibilityLabel="Compose post" + accessibilityHint="" + /> ) }), From 629ca24e905bb7653546ce8741ec6c472efea1f8 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 19:03:59 -0500 Subject: [PATCH 249/374] Tune the custom feed header --- src/view/com/util/forms/DropdownButton.tsx | 7 +- src/view/screens/CustomFeed.tsx | 95 ++++++++++++++++------ 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index 36ef1f4098..064b8211b5 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -136,7 +136,12 @@ export function DropdownButton({ } return ( - diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index f406c43d5a..0ade47c51a 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -28,6 +28,7 @@ import {Haptics} from 'lib/haptics' import {ComposeIcon2} from 'lib/icons' import {FAB} from '../com/util/fab/FAB' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps @@ -108,6 +109,22 @@ export const CustomFeedScreen = withAuthRequired( store.shell.openComposer({}) }, [store]) + const dropdownItems: DropdownItem[] = React.useMemo(() => { + let items: DropdownItem[] = [ + { + testID: 'feedHeaderDropdownRemoveBtn', + label: 'Remove from my feeds', + onPress: onToggleSaved, + }, + { + testID: 'feedHeaderDropdownShareBtn', + label: 'Share link', + onPress: onPressShare, + }, + ] + return items + }, [onToggleSaved, onPressShare]) + const renderHeaderBtns = React.useCallback(() => { return ( @@ -132,25 +149,46 @@ export const CustomFeedScreen = withAuthRequired( )} + onPress={onTogglePinned}> + + + {currentFeed?.isSaved ? ( + + + + ) : ( + + )} ) }, [ pal, currentFeed?.isSaved, currentFeed?.isLiked, + isPinned, onToggleSaved, + onTogglePinned, onToggleLiked, onPressShare, + dropdownItems, ]) const renderListHeaderComponent = React.useCallback(() => { @@ -195,6 +233,20 @@ export const CustomFeedScreen = withAuthRequired( : 'Add to My Feeds' } /> + ) : null} - @@ -286,7 +325,7 @@ export const CustomFeedScreen = withAuthRequired( return ( - + Date: Wed, 24 May 2023 19:27:04 -0500 Subject: [PATCH 250/374] Tune the custom feeds header a bit more --- src/view/screens/CustomFeed.tsx | 70 ++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 0ade47c51a..0690a17d81 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -129,53 +129,57 @@ export const CustomFeedScreen = withAuthRequired( return ( - - + {currentFeed?.isSaved ? ( + + ) : undefined} {currentFeed?.isSaved ? ( - + ) : ( )} ) @@ -187,7 +191,6 @@ export const CustomFeedScreen = withAuthRequired( onToggleSaved, onTogglePinned, onToggleLiked, - onPressShare, dropdownItems, ]) @@ -361,8 +364,13 @@ const styles = StyleSheet.create({ }, headerBtns: { flexDirection: 'row', - gap: 12, - marginTop: 10, + alignItems: 'center', + }, + headerAddBtn: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + paddingLeft: 4, }, headerDetails: { paddingHorizontal: 16, From dfb39e7c4fcaff3effcc82b412191177fdfdaf22 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 22:09:39 -0500 Subject: [PATCH 251/374] Add feed discovery page --- bskyweb/cmd/bskyweb/server.go | 1 + package.json | 2 +- src/Navigation.tsx | 6 ++ src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/state/models/discovery/feeds.ts | 97 +++++++++++++++++++++++++ src/view/com/feeds/SavedFeeds.tsx | 41 +++++++---- src/view/screens/CustomFeed.tsx | 6 +- src/view/screens/DiscoverFeeds.tsx | 109 ++++++++++++++++++++++++++++ yarn.lock | 8 +- 10 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 src/state/models/discovery/feeds.ts create mode 100644 src/view/screens/DiscoverFeeds.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 07df85146f..462740f546 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -105,6 +105,7 @@ func serve(cctx *cli.Context) error { // generic routes e.GET("/search", server.WebGeneric) + e.GET("/search/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) e.GET("/moderation", server.WebGeneric) e.GET("/moderation/mute-lists", server.WebGeneric) diff --git a/package.json b/package.json index 5c30a0cae4..253c3b7820 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.7", + "@atproto/api": "0.3.8", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index ff7a5f5c26..0664ac5265 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -35,6 +35,7 @@ import {SearchScreen} from './view/screens/Search' import {NotificationsScreen} from './view/screens/Notifications' import {ModerationScreen} from './view/screens/Moderation' import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' +import {DiscoverFeedsScreen} from 'view/screens/DiscoverFeeds' import {NotFoundScreen} from './view/screens/NotFound' import {SettingsScreen} from './view/screens/Settings' import {ProfileScreen} from './view/screens/Profile' @@ -103,6 +104,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { component={ModerationBlockedAccounts} options={{title: title('Blocked Accounts')}} /> + 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + refresh = bundleAsync(async () => { + this._xLoading() + try { + const res = + await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators( + {}, + ) + this._replaceAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + clear() { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = false + this.error = '' + this.feeds = [] + } + + // state transitions + // = + + _xLoading() { + this.isLoading = true + this.isRefreshing = true + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch popular feeds', err) + } + } + + // helper functions + // = + + _replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) { + this.feeds = [] + for (const f of res.data.feeds) { + this.feeds.push(new CustomFeedModel(this.rootStore, f)) + } + } +} diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index e92e741dac..610562c9d7 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -53,14 +53,28 @@ export const SavedFeeds = observer( const renderListFooterComponent = useCallback(() => { return ( <> - - - - Change Order - - + + + + + Discover new feeds + + + {!store.me.savedFeeds.isEmpty && ( + + + + Change Order + + + )} + ) - }, [pal]) + }, [pal, store.me.savedFeeds.isEmpty]) const renderItem = useCallback( ({item}) => , @@ -118,14 +132,16 @@ export const SavedFeeds = observer( ) const styles = StyleSheet.create({ + footerLinks: { + marginTop: 8, + borderBottomWidth: 1, + }, footerLink: { flexDirection: 'row', borderTopWidth: 1, - borderBottomWidth: 1, paddingHorizontal: 26, paddingVertical: 18, gap: 18, - marginTop: 8, }, empty: { paddingHorizontal: 18, @@ -134,7 +150,4 @@ const styles = StyleSheet.create({ marginHorizontal: 18, marginTop: 10, }, - feedItem: { - borderTopWidth: 1, - }, }) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 0690a17d81..49798d7583 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -220,7 +220,7 @@ export const CustomFeedScreen = withAuthRequired( )} {isDesktopWeb && ( - +