Compare commits

...

196 Commits

Author SHA1 Message Date
Eric Bailey 2dbc7419f6 Swap bell icon, improve alignment and size 2024-05-10 07:23:10 -05:00
Eric Bailey c88647c8d1 Fine tune 2024-05-10 07:23:10 -05:00
Eric Bailey 1be2d395f8 Ditch feeds, drop size 2024-05-10 07:23:10 -05:00
Eric Bailey d0afc91620 Replace icons in bottom bars 2024-05-10 07:23:10 -05:00
Eric Bailey 91a4c97c1a Replace icons in left nav 2024-05-10 07:23:10 -05:00
Eric Bailey 56efebb6ab Swap for chat icon 2024-05-09 20:47:20 -05:00
Eric Bailey 55fdbc7399 Add retries to all handlers (#3935) 2024-05-09 16:31:36 -05:00
Samuel Newman becc708c61 [🐴] Rich text in messages (#3926)
* add facets to message

* richtext messages

* undo richtexttag changes

* whoops, don't redetect facets

* dont set color directly

* shorten links and filter invalid facets

* fix link shortening

* pass in underline style
2024-05-09 21:08:56 +01:00
Samuel Newman 03b2796976 lowcase ALT text (#3934) 2024-05-09 20:58:22 +01:00
Hailey 17e3b946cb Handle push notifications for DMs (#3895)
* add some better handling for notifications

prep merge

move `useNotificationsListener` into shell

progress

better structure

only show messages notifications while using app if it is the current account

progress

only emit on native

current chat emitter

only show alerts for the current chat

type

add logs

setup handlers

* remove event emitter

* just needs cleanup

* oops

* remove unnecessary `queryClient` param

* few fixes

* cleanup

* nit

* remove folds

* remove comment

* simplify if

* add back invalidate

* comment out other navigations for now

* rename type

* handle various navigation cases

* push to conversation from notification

* update badge in all cases except `chat-message`

* ensure no duplicate notifications

* rm unused `animationOnReplace`

* revert to using `goBack` in the conversation header

* add todo comment
2024-05-09 10:04:05 -07:00
Eric Bailey 1341845537 Add gate, a:a swap onboarding state (#3930) 2024-05-09 11:34:36 -05:00
Hailey 2fe76333bc [🐴] Add hover context menu for convo list on web (#3923)
* remove some unnecessary props

* add hover trigger on web for convo list

* lint

* use `UserAvatar` to not affect accessibility

* remove extra wrapper

* add `label`

* always show on mobile

* adjust size of dots

* make the message trigger dots the same size

* 
2024-05-09 09:07:57 -07:00
Samuel Newman 55ac287d5e [🐴] Skeleton header (#3921) 2024-05-08 17:08:24 -07:00
Eric Bailey 3bac0182b5 [🐴] Integrate event bus (#3915)
* Integrate event bus

* Fixes

* Move events mgmt into Convo class

* Clean up poll interval updates

* Remove unused

* Remove annoying log
2024-05-08 18:01:07 -05:00
Eric Bailey ce2eddca8e [🐴] Refactor event bus (#3919)
* Refactor to singleton class outside react

* Fix retry, remove debug logs
2024-05-08 17:41:10 -05:00
dan 0c6bf276dd Replace updateCurrentAccount() with refreshSession() (#3910)
Replace updateCurrentAccount() with resumeSession()
2024-05-08 23:11:39 +01:00
Hailey f62b0458a7 [🐴] Adjust press and hold delay on messages (#3918)
* adjust press and hold delay on messages

* adjust hit slop
2024-05-08 13:56:36 -07:00
Samuel Newman 701e508a1a add sideborders prop (#3920) 2024-05-08 21:44:49 +01:00
Samuel Newman 750a5c899b actually optimistic mark as read (#3917) 2024-05-08 21:19:45 +01:00
Eric Bailey 7d06fb9432 Move to requestPollInterval handling (#3914) 2024-05-08 11:34:04 -05:00
bnewbold f3515e2673 use four-digit years for RSS pubDate (#3907) 2024-05-08 09:33:38 -07:00
Samuel Newman 38198fdf7f [🐴] Mark as read in convo menu (#3913)
* add mark as read option

* optimistic update + link up menu

* rm messageid
2024-05-08 15:48:05 +01:00
Eric Bailey 56f713077f [🐴] Reorg convo files (#3909)
* Remove unused prop

* Reorganize
2024-05-07 19:46:59 -07:00
dan 814ec2bd7f Add session reducer tests (#3860) 2024-05-08 03:34:35 +01:00
dan 0910525e2e [Session] Code cleanup (#3854)
* Split utils into files

* Move reducer to another file

* Write types explicitly

* Remove unnnecessary check

* Move things around a bit

* Move more stuff into agent factories

* Move more stuff into agent

* Fix gates await

* Clarify comments

* Enforce more via types

* Nit

* initSession -> resumeSession

* Protect against races

* Make agent opaque to reducer

* Check using plain condition
2024-05-08 03:30:55 +01:00
Samuel Newman 4fe5a869c3 [🐴] Unread messages badge (#3901)
* add badge

* move stringify logic to hook

* add mutation hooks

* optimistic mark convo as read

* don't count muted chats

* Integrate new context

* Integrate mark unread mutation

* Remove unused edit

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-05-07 21:23:09 -05:00
dan 0c41b3188a [Session] Remove global agent (#3852)
* Remove logs and outdated comments

* Move side effect upwards

* Pull refreshedAccount next to usage

* Simplify account refresh logic

* Extract setupPublicAgentState()

* Collapse setStates into one

* Ignore events from stale agents

* Use agent from state

* Remove clearCurrentAccount

* Move state to a reducer

* Remove global agent

* Fix stale agent reference in create flow

* Proceed to onboarding even if setting date fails

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-05-08 03:10:03 +01:00
Eric Bailey 31a8356aef Handle invalid service URLs (#3908) 2024-05-07 21:09:02 -05:00
Eric Bailey 165fdb7049 [🐴] Integrate global event bus (#3904)
* Conditionally run global event bus

* Add current convo id context, bundle providers
2024-05-07 20:25:58 -05:00
Hailey 37f22ca224 fix privacy api type (#3903) 2024-05-07 16:35:04 -07:00
Eric Bailey b2b1c6c040 Let React app replace preload (#3900)
* Let React app replace preload

* Fix html structure

* Fix indent
2024-05-07 18:29:21 -05:00
Eric Bailey 53d844495b Remove messages provider (#3902) 2024-05-07 18:29:06 -05:00
Eric Bailey f78126e01a [🐴] State transitions (#3880)
* Handle init/resume/suspend/background and polling

* Add debug and temp guard

* Make state transitions sync

* Make init sync also

* Checkpoint: confusing but working state machine

* Reducer-esque

* Remove poll events

* Guard fetchConvo

(cherry picked from commit 8385579d31500bb4bfb60afeecdc1eb3ddd7e747)

* Clean up polling, make sync

(cherry picked from commit 7f75cd04c3bf81c94662785748698640a84bef51)

* Update history handling

(cherry picked from commit b82b552ba4040adf7ead2377541132a386964ff8)

* Check for screen focus in app state listener

* Get rid of ad-hoc status checks
2024-05-07 17:54:52 -05:00
Eric Bailey 87cb4c105e [🐴] Global event mgmt (#3897)
* Add global event bus for messages logs

* Add rev to state

* Better handle error

* Clean up polling, add backgrounding

* Add trailConvo method

* Extend polling until we're ready for this
2024-05-07 17:54:34 -05:00
Paul Frazee 0625a914bd Run intl:extract 2024-05-07 14:15:34 -07:00
Hailey 4678c4353b typo in if/else (#3899) 2024-05-07 13:17:40 -07:00
Hailey 0658dd053d create new cache key on each run when saving previous commit hash (#3898)
* create new cache key on each run

* dynamic restore-key
2024-05-07 13:08:44 -07:00
Minseo Lee c6d30f671a Remove unnecessary Trans tags (#3888) 2024-05-07 20:06:39 +01:00
Samuel Newman 7d72dfb1cb [GIFs] Restore default alt text (#3893)
* restore default alt text

* factor out gif alt logic + enable require alt text setting

* rm console.log

* don't prefill input + esc handling

* typo

* Nits

* shorten user alt prefix

* Remove unnecessary condition, rename for clarity

* Add comment

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-05-07 20:05:40 +01:00
Samuel Newman 77e6c75a2c Fix color of new web splash logo (#3896) 2024-05-07 19:28:51 +01:00
Minseo Lee af77574b3c Update Feed.tsx (#3715) 2024-05-07 11:09:22 -07:00
Minseo Lee b1a1ca98e8 Update Feeds.tsx (#3719) 2024-05-07 11:07:57 -07:00
kodebanget 3cca5f04d6 Update Indonesian translation (#3891)
* Update Indonesian translation

* Update id/messages.po

---------

Co-authored-by: Indonesian <github-actions@github.com>
2024-05-07 10:33:59 -07:00
Minseo Lee 7f88ec7ca5 Update WhoCanReply.tsx (#3720) 2024-05-07 10:20:05 -07:00
Hailey 790a9c4fad fix bad bool check in action (#3885)
* fix bad bool check in action

* add `fetch-depth` so we can get the commit hash

* `.env` should be in `.easignore` 🙃
2024-05-06 20:13:12 -07:00
Jan-Olof Eriksson 92f6665f93 Finnish translation update (#3755)
* Update messages.po

Translated new strings (GIF's etc) and cleaned up deprecated ones.

* Update messages.po

resolve conflict

* Update messages.po

removed double quotes. Thanks @lapanti
2024-05-06 20:08:36 -07:00
Minseo Lee 2ea9e2c929 Improve localization marks (#3285)
* Update Lightbox.tsx

* Change strings for easier localization

* Update DeleteAccount.tsx

* Update LabelsOnMeDialog.tsx

* Update FeedCard.tsx

* Update index.tsx

* Update LabelsOnMeDialog.tsx

* Update index.tsx

* Update FeedCard.tsx

* Update SelfLabel.tsx

* Update Hashtag.tsx

* Update index.tsx

* Update Hashtag.tsx

* Update ChangeHandle.tsx

* Update index.web.tsx

* Update index.web.tsx

* Update index.tsx

* Remove unnecessary `<Trans>` tags

* Update Drawer.tsx
2024-05-06 19:58:04 -07:00
Paul Frazee 4fad18b2fa Implement FeedFeedback API (#3498)
* Implement onViewableItemsChanged on List.web.tsx

* Introduce onItemSeen to List API

* Add FeedFeedback tracker

* Add clickthrough interaction tracking

* Add engagement interaction tracking

* Reduce duplicate sends, introduce a flushAndReset to be triggered on refreshes, and modify the api design a bit

* Wire up SDK types and feedContext

* Avoid needless function allocations

* Fix schema usage

* Add show more / show less buttons

* Fix minor rendering issue on mobile menu

* Wire up sendInteractions()

* Fix logic error

* Fix: it's item not uri

* Update 'seen' to mean 3 seconds on-screen with some significant portion visible

* Fix non-reactive debounce

* Move methods out

* Use a WeakSet for deduping

* Reset timeout

* 3 -> 2 seconds

* Oopsie

* Throttle instead

* Fix divider

* Remove explicit flush calls

* Rm unused

---------

Co-authored-by: dan <dan.abramov@gmail.com>
2024-05-07 03:08:33 +01:00
Paul Frazee e264dfbb87 Merge branch 'TuxPenguin09-web-loading' into main 2024-05-06 18:06:44 -07:00
Paul Frazee 032f849092 Protect against missing element 2024-05-06 18:06:31 -07:00
Paul Frazee 0a8dc289ff Merge branch 'web-loading' of https://github.com/TuxPenguin09/bsky-social-app into TuxPenguin09-web-loading 2024-05-06 17:51:51 -07:00
Paul Frazee 901feba6db Replace pluralize by plural by @tkusano (#3882)
* Replace pluralize with plural or Plural
* Replace all pluralize (defined by src/lib/strings/helpers.ts) with plural or Plural (defined by @lingui/macro) to make some UI elements translatable.
* Delete pluralize() and related test.

* Import @formatjs polyfill libraries for plural on ios and android

- ios and andorid: import `@formtjs/intl-locale` and `@formatjs/intl-pluralrules` to polyfill `Intl.Locale` and `Intl.PluralRules` which are used in `plural()` and '<Plural />'.
- update `plural` use in notification messages for better translation.

* Rewrite to pass lint

* Add Catalan plural polyfill

* more replacement

* import zh plural data for zh-CN

* Refactor feed header components (#2964)

* Move home-related files to view/com/home

* Add HomeHeader in front of FeedTabBar

* Move isDekstop check outside FeedsTabBar

* Remove PWI logic from tabbar

* Separate platform-specific layout from shared logic

* Rename Home Feed Prefs to Following Feed Prefs (#2965)

* use `useOpenLink` hook for links in ALF (#2975)

* use `useOpenLink` hook for links in ALF

* web only for `outline`

* increase timeout to 15s (#2958)

* Normalize relative day (#2874)

* fix: normalize relative date

* chore: add comments

* refactor: skip flooring normalized diff

* refactor: let -> const

* fix: get own copy of date to prevent mutating

* refactor: rounding does the same trick

* Add handle validation to create account UI (#2959)

* show uiState errors in the box as well

simplify copy

update ui for only letters and numbers

add ui validation to handle selection

* simplify names

* Fix accidental text-node render

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Make dim theme dim (#2966)

* Make dim color scheme dim

* Tweaks

* Overall tweaks

* We have to go darker

* Tweak saturation of blues in dim

* Increase contrast on dark-dark mode

* adjust dim

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>

* Fix dim mode unread notif color

* use `showControls` to show/hide live text icon on ios (#2982)

* Update .po files

* fix reversed icons in validator 🤦 (#2991)

* Adjust `windowSize` on `PostThread` `FlatList` (#2989)

* adjust window size, cells batching period

* rm batching period change

* Pluralize 'follow(s)'

* Include a space between the msgid count and "follower(s)/following(s)" so the translator can adjust the translated count line to fit within the Drawer.

* pluralie '# following'

* Fix & Update

* Rewrite to use Plural

* rmeove unused import

* When commiting changes, disable 'simple-import-sort' plugin in .eslintrc.js to sync with bluesky-social:main

* Revert simple-import-sort/imports related changes

* Move ProfileHoverCard web to plural util

* Followings -> following

* Add plural following to hovercard

* Followings -> Following

---------

Co-authored-by: Takayuki KUSANO <kusano@tkusano.jp>
Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>
Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Mary <148872143+mary-ext@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2024-05-06 16:55:57 -07:00
Paul Frazee 2ca4b74955 Merge branch 'piotrpalek-fix-banner-cropper' into main 2024-05-06 15:49:52 -07:00
Paul Frazee fe82257801 Dont leave promise unresolved 2024-05-06 15:44:19 -07:00
Paul Frazee e1940983a3 Merge branch 'fix-banner-cropper' of https://github.com/piotrpalek/social-app into piotrpalek-fix-banner-cropper 2024-05-06 15:30:54 -07:00
Samuel Newman 136b0e12ab make gif alt text prompt selectable (#3879) 2024-05-06 22:14:00 +01:00
Hailey fdb5ffff53 remove trailing period from query term in useActorAutocompleteQuery (#3765)
* remove trailing period from query term

* Rm useMemo

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-05-06 22:04:47 +01:00
Hailey b4015d10d1 Add PrivacyManifest info to comply with new App Store guidelines (#3784)
* add necessary privacy reasons

* bump expo to support creation of the manifest
2024-05-06 21:47:55 +01:00
Hailey 99f3f10fe7 A few containWeb List nits (#3877)
* use getters for returned values

* pass ref

* add log to `onScroll` in tester

* improve expect error
2024-05-06 21:45:14 +01:00
Eric Bailey 0b6ace990e [Clipclops] All my clops gone (#3850)
* Handle two common errors, provide more clarity around error states

* Handle failed polling

* Remove unused error type

* format
2024-05-06 15:35:05 -05:00
Ivan Beà 2a1dbd2756 Update catalan messages.po (#3764)
Translate new lines, messages are coming! 
You can check it @jordimas @darccio @ferranrego @MiquelAdell @mvdan @johnnydement @Mejans @surfdude29
2024-05-06 13:27:08 -07:00
Minseo Lee a9feba4efd Update Korean localization (#3809)
* Update messages.po

* Update messages.po

* Update messages.po
2024-05-06 13:26:32 -07:00
Takayuki KUSANO 5cfe5d4f58 Update Japanese translations (#3810)
* Update Japanese translations

* Updated Japanese translation

* Updated Japanese translation

* Update Japanese translations

* Updated based on @Hima-Zinn's suggestion
2024-05-06 13:25:59 -07:00
Frudrax Cheng c90443b37a Update zh-TW translations (#3873)
* 修正及改進翻譯

* Epic zh-TW rebrand

* small fix

* remove reundant "設定" for "偏好"

* legacy for "殘存"

* Update Translator

---------

Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
2024-05-06 13:25:07 -07:00
Hailey b34d224207 remove hide_vertical_scrollbars test (#3855) 2024-05-06 20:59:36 +01:00
Hailey db0b030359 show spinner when preferences has not loaded yet (#3756) 2024-05-06 20:58:03 +01:00
Samuel Newman c33c3b7d1e Alt text for gifs (#3876)
* add alt text dialog

* multiline alt text input

* add pressable alt text badge

* rename `ALT: ` to `Alt text: ` to avoid including old bad ones

* reuse alt text reminder

* reuse alt text reminder in gallery

* add alt text reminder in the dialog itself

* autofocus text input

* reorder components to fix tab order

* fix close btn position
2024-05-06 17:28:38 +01:00
Hailey ae7626ce6e [🐴] Finalize web message screen (#3868)
* add `onStartReached` to web list

* fix `rootMargin`

* Add `contain`, handle scroll events

* improve types, fix typo

* simplify

* adjust `scrollToTop` and `scrollToOffset` to support `contain`, add `scrollToEnd`

* rename `handleWindowScroll` to `handleScroll`

* support basic `maintainVisibleContentPosition`

* rename `contain` to `containWeb`

* remove unnecessary `flex: 1`

* add missing props

* add root prop to `Visibility`

* add root prop to `Visibility`

* revert adding `maintainVisibleContentPosition`

* remove unnecessary wrapper

* add style

* oops

* maintain position for web

* always apply `flex: 1` to styles when contained

* add a contained list to storybook

* make `onScroll` a worklet in storybook

* revert test code

* remove unnecessary `flex: 1`
2024-05-06 08:48:08 -07:00
Hailey bc07019911 Further align web List with FlatList, add contain mode to web list implementation (#3867)
* add `onStartReached` to web list

* fix `rootMargin`

* Add `contain`, handle scroll events

* improve types, fix typo

* simplify

* adjust `scrollToTop` and `scrollToOffset` to support `contain`, add `scrollToEnd`

* rename `handleWindowScroll` to `handleScroll`

* support basic `maintainVisibleContentPosition`

* rename `contain` to `containWeb`

* remove unnecessary `flex: 1`

* add missing props

* add root prop to `Visibility`

* add root prop to `Visibility`

* revert adding `maintainVisibleContentPosition`

* oops

* always apply `flex: 1` to styles when contained

* add a contained list to storybook

* make `onScroll` a worklet in storybook

* revert test code

* add scrolling to storybook

* simplify getting scrollable node

* nit: extra whitespace

* nit: random comment

* foolproof the logic

* typecheck
2024-05-06 08:34:32 -07:00
Hailey 594b40c3ae Fix IntersectionObserver rootMargin in web List implementation, add onStartReached (#3866)
* add `onStartReached` to web list

* fix `rootMargin`
2024-05-05 04:24:01 -07:00
Hailey 6b615f3720 only use KeyboardProvider in conversation screen (#3869) 2024-05-05 03:14:14 -07:00
Hailey c4160c25a3 [Clipclops] Delete message in dialog (#3849)
* delete and copy

* add retry dialog if message send fails

* add layout animation

* fix `nextMessage` being incorrect

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-05-05 02:06:10 +01:00
Samuel Newman 7448c8f785 [🐴] Improve message pressing (#3864)
* allow tapping message when keyboard open

* dismiss keyboard on open msg menu

* show message info on press

* Revert "show message info on press"

This reverts commit 61d039a702.
2024-05-05 01:13:57 +01:00
Hailey 91f8202549 fix typo in app-info (#3863) 2024-05-04 16:45:59 -07:00
Samuel Newman 9b7818d358 [🐴] Dismiss keyboard before opening convo menu (#3862)
* Dismiss keyboard before opening convo menu

* call `props.onPress()` instead
2024-05-05 00:39:52 +01:00
Hailey 4862bc2ba8 add bundle identifiers to app-info (#3861)
* add bundle identifiers to `app-info`

* add them to the `.env.example`

* add environment variables for docker build

* add environment variables for native builds and bundles

* also include the hour in bundle date

* organize app info better in settings
2024-05-04 16:33:10 -07:00
Hailey eb55bdf172 [🐴] List Adjustments (#3857) 2024-05-04 13:22:14 -07:00
Samuel Newman c223bcdaf7 Assorted clipclop fixes (#3853)
* empty state for new chat dialog

* use terniary

* dark mode pending state

* copy message text option

* fix service url input (scrollview ftw)

* whoops, fix equality

* slightly reduce horizontal message padding
2024-05-04 01:49:49 +01:00
Hailey feff55a14a Clipclop tweaks (#3851)
* scroll on input resize

* add back the padding 🙊

* fix timestamps
2024-05-03 15:08:59 -07:00
Hailey 876816675e [Clipclops] Refactor message list (#3832)
* rework the list for accessibility

* Reverse reverse

* progress

* good to start testing

* memo `MessageItem`

* small hack

* use our custom `List` impl

* use `ScrollProvider` for `onScroll` event

* remove use of `runOnJS`

* actually, let's keep it

* add some comments

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-05-03 14:18:01 -07:00
Samuel Newman 6a4199febb [Clipclops] Pending message style with layout animation (#3844)
* decrease group gap to 3 mins

* pending style with layout animation

* make pending state lighter
2024-05-03 20:19:48 +01:00
Samuel Newman 55f3df5596 reenable tabs (#3847) 2024-05-03 20:14:10 +01:00
Hailey 19a6bdcc86 Version bump (#3848) 2024-05-03 11:21:02 -07:00
Samuel Newman ce02a41163 add focus refresh + polling (#3846) 2024-05-03 17:57:20 +01:00
dan 4a2d4253e5 [Session] Align state and global agent switchpoints (#3845)
* Adopt synced accounts unconditionally

* Remove try/catch around resuming session

* Move to login form on resume failure

* Restructure code flow for easier reading

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-05-03 17:57:09 +01:00
dan 85b34418ef Don't clear toasts when changing users (#3843)
* Move ThemeProvider to the top

* Move RootSiblingParent above the remounted part

* Move ToastContainer outside the remounted part

* Remove setTimeout around toasts

* Consistently handle dropped session
2024-05-03 16:36:58 +01:00
Samuel Newman aeed4786db make service url gate friendlier (#3841) 2024-05-03 16:32:20 +01:00
Samuel Newman 67b0cdf649 remove broken keyboard offset code (#3842) 2024-05-03 16:14:25 +01:00
Samuel Newman 997e621ce3 [Clipclops] Header tweaks (#3839)
* wrap text

* add dividers to convo menu
2024-05-03 15:55:35 +01:00
Samuel Newman a5511e3c22 update date logic to account for timezones (#3840) 2024-05-03 15:55:24 +01:00
Eric Bailey 051e897a2b [Clipclops] Moar error (#3837)
* Add history error

* Log error

* Add period
2024-05-03 09:39:40 -05:00
dan 1e484c6318 [Session] Reset global agent on expire (#3838) 2024-05-03 13:46:34 +01:00
dan cdf7a1957a [Session] Rely on agent session change event for persisting resumed session (#3836)
* Rely on agent session change handler for resumption

* Add a fast path for noop resumes
2024-05-03 03:47:11 +01:00
Eric Bailey c9cf608f78 [Clipclops] External store, suspend/resume (#3829)
* Initial working external store

* Clean up WIP, explore suspend/resume

* Clean up state, bindings, snapshots, add some logs

* Reduce snapshots, add better logic check

* Bump interval a smidge

* Remove unused type
2024-05-02 20:57:51 -05:00
dan c13685a0cf [Session] Fix stale emailAuthFactor and emailConfirmed on the client (#3835)
* Fix account refresh to not clobber falsy values

* Use ?? in more places
2024-05-03 02:52:37 +01:00
dan 0f827c3213 [Session] Track agent in state (#3833)
* Expand currentAccountDid into currentAgentState

* Inline all callsites of upsertAccount

* Inline all internal callsites of clearCurrentAccount

* Add agent to currentAgentState

* Collapse extra setStates

* Add TODOs
2024-05-03 02:04:36 +01:00
Samuel Newman a9af83692c visually align icon (#3831) 2024-05-03 01:28:03 +01:00
dan 6405ad7cba [Session] Call persist handler directly (#3828) 2024-05-02 23:05:53 +01:00
dan b86c3b486f Improve account switcher pending state (#3827)
* Protect against races

* Reduce UI jank when switching accounts

* Add pending state to selected account

* Disable presses while pending
2024-05-02 21:55:50 +01:00
Hailey 8ba1b10ce0 [Clipclops] Message actions for native and web (#3807)
* haptic on long press

* add animation to press and hold

* eslint disable for now

* adjust styles

* dont trigger if animation is cancelled

* organize

* add a delete menu

* reset scale automatically

* message actions dialog

cleanup

center the trigger

handle focus/unfocus better

make triggers accessible

weg dropdown menu

add a wep specific wrapper

decrease press delay

add report button

improve shrink logic

use `self_end` instead of `margin: auto`

rm extra `?`

move `MessageItem` to `components`

add delete button

* rm some padding

* update after merge

* fix merge

* web only types

* fix crash

* add an explanation

* fix web types

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-05-02 13:54:17 -07:00
Samuel Newman 6da18e3dcf Only run "disable min shell on foregrounding" behaviour if on the home screen (#3825)
* change useEffect to useFocusEffect

* v2 -> v3
2024-05-02 21:27:54 +01:00
Samuel Newman 96c5db3e69 create keyboardverticaloffset hook (#3823) 2024-05-02 21:05:11 +01:00
Samuel Newman aca55cb192 rename "MessagesList" to "Messages" (#3826) 2024-05-02 21:02:45 +01:00
Eric Bailey a20fe4c9a0 [Clipclops] Deletions (#3824)
* Handle message deletions

* Handle failures
2024-05-02 13:12:34 -05:00
dan 1a07e23192 [Session] Extract selectAccount out (#3812) 2024-05-02 18:25:09 +01:00
dan 5ec945b762 [Session] Extract resumeSession out (#3811) 2024-05-02 18:17:53 +01:00
Samuel Newman dadf27fd2f [Clipclops] Minor web tweaks (#3822)
* add a11y + onPress to web send button

* only adjust leading on web, due to overflow issue
2024-05-02 18:00:42 +01:00
Samuel Newman bff055f618 [Clipclop] Input polish (#3819)
* tweak input styles + add min-shell mode

* android tweaks + hitslop
2024-05-02 17:08:41 +01:00
Hailey e977c99c96 [Clipclops] rm <Trans> from header (#3818) 2024-05-02 08:54:12 -07:00
Eric Bailey c2062753c5 Allow unmuting and unblocking of curate lists, but not the opposite (#3817) 2024-05-02 10:17:17 -05:00
aliamanuba 1d8fdbcc34 Add Preload for Web - Update base.html 2024-05-02 19:38:33 +08:00
aliamanuba c303df5bd6 Added Preload for Web
- Bluesky logo in the center when a new visitor or shift+refresh by browser client
2024-05-02 19:24:35 +08:00
Hailey cc9727a844 [Clipclops] Remove viewability config (#3805)
* remove viewability config

* use `onScroll` for keeping track of content offset
2024-05-01 16:26:19 -07:00
Samuel Newman e19f882450 [Clipclops] Clop menu, leave clop, mute/unmute clop (#3804)
* convo menu

* memoize convomenu

* add convoId to useChat + memoize value

* leave convo

* Create mute-conversation.ts

* add mutes, remove changes to useChat and use chat.convo instead

* add todo comments

* leave convo confirm prompt

* remove dependency on useChat and pass in props instead

* show menu on long press

* optimistic update

* optimistic update leave + add error capture

* don't `popToTop` when unnecessary

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-05-02 00:15:10 +01:00
Paul Frazee d3fafdc066 Fix: put label defs provider above moderation opts provider (#3806) 2024-05-01 15:59:19 -07:00
Eric Bailey 5148b957b7 [Clipclops] Pending messages (#3803)
* Make pending message like normal message

* Remove getter

* Make props public
2024-05-01 16:29:34 -05:00
Samuel Newman e2556d9f40 [Clipclops] Replace 🐴 with real icon (#3802)
* replace 🐴 with paper plane icon

* replace envelope with plus
2024-05-01 21:49:10 +01:00
Eric Bailey fc0eab2d03 Retry clops (#3800)
* Add convo retries, sketch out tests

* Only append nextMessage to messages

* Remove debug code
2024-05-01 15:24:56 -05:00
Hailey 333ccdad39 [Clipclops] Dynamic input height (#3778)
* input max height/scrollability

* remove unused imports

* add a web-specific version

* enter and shift enter for web

* missing onSubmit for native

* missing attributes

* improve layout of input on web

* use the correct text color in the input

* trim messages

* remove `onSubmit`

* move prop up

* trim message on web

* remove extra function call

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-05-01 20:42:11 +01:00
Hailey 6f9993ca55 [Clipclops] Fix list, rework structure (#3799)
* proper min index

* move keyextractor out of react

* move onSendMessage out

* don't render the flatlist conditionally

* add loader

* rework structure

* remove some unneeded logic
2024-05-01 11:48:19 -07:00
Samuel Newman 8304ad91ac add header to empty chat list view (#3791) 2024-05-01 18:43:54 +01:00
Eric Bailey 538ca8dff1 [Clipclops] 2 Clipped 2 Clopped (#3796)
* Add new pkg

* copy queries over to new file

* useConvoQuery

* useListConvos

* Use useListConvos

* extract useConvoQuery

* useGetConvoForMembers

* Delete unused

* exract useListConvos

* Replace imports

* Messages/List/index.tsx

* extract getconvoformembers

* MessageItem

* delete chatLog and rename query.ts

* Update import

* Clipclop service (#3794)

* Add Chat service

* Better handle deletions

* Rollback unneeded changes

* Better insertion order

* Use clipclops

* don't show FAB if error

* clean up imports

* Update Convo service

* Remove temp files

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-05-01 10:14:41 -07:00
dan d61b366b26 [Session] Derive currentAccount from accounts + currentAccountDid (#3795)
* Derive currentAccount from accounts and currentAccountDid

* Add TODOs for divergence with __globalAgent
2024-05-01 17:35:27 +01:00
dan df9af92eb2 [Session] Use flag on state for persistence (#3793)
* Move isInitialLoad and isSwitchingAccounts out of main state

* Remove spreads, order object keys

* Track need to persist on state object

* Reoder state variables
2024-05-01 17:26:33 +01:00
Samuel Newman a6061489ff replace getDate with toIsoString().slice(0,10) (#3792) 2024-05-01 15:50:30 +01:00
Samuel Newman 80a1687dae exclude bskyweb bskyembed and web-build (#3790) 2024-05-01 15:29:30 +01:00
Samuel Newman 09f3b2ae14 [Clipclops] Improve message list behaviour (#3789)
* improve message list behaviour

* replace useAgent with useSession

* add explicit types to appease linter
2024-05-01 15:16:54 +01:00
Hailey b8d8bec388 sentry errors for captcha web views and registration attempts (#3761)
* sentry errors for captcha web views

* include handles with errors

* log all registration request failures

* rm

* use a better trigger for web captcha errors

* add another trigger for recording a possible signup error

* unknown error type

* don't needlessly log on href errors

* honestly i probably cant always do a captcha in 20 seconds

* rm log

* timeout on back

* remove unnecessary colons
2024-05-01 01:08:59 -07:00
Samuel Newman 81ae7e425d Add kawaii mode (#3773) 2024-05-01 08:59:40 +01:00
dan 181e61bedb Memoize context value (#3786) 2024-05-01 05:35:52 +01:00
Paul Frazee 31cb3e5422 Performance improvements: structural sharing & moderation opts context (#3785)
* Fix: correctly apply structural sharing to preferences object

* Move moderation opts into a context

* Fix import

* Remove log

* Pass userdid directly

* Pass moderationPrefs directly
2024-04-30 21:06:51 -07:00
dan 39807a8630 [Session] Refactor to prepare for V2 (#3781)
* Move types to another file

Co-authored-by: dan <dan.abramov@gmail.com>

* Move utilities out

Co-authored-by: dan <dan.abramov@gmail.com>

* Move PUBLIC_BSKY_AGENT

Co-authored-by: dan <dan.abramov@gmail.com>

* Move createPersistSessionHandler inline

Co-authored-by: dan <dan.abramov@gmail.com>

* Call configureModeration when clearing account too

This ensures that the app labelers get reset in a test environment.

Co-authored-by: dan <dan.abramov@gmail.com>

* Make guest configureModeration sync, non-guest async

* Extract isSessionExpired

Co-authored-by: dan <dan.abramov@gmail.com>

* Flip isSessionExpired condition

Co-authored-by: dan <dan.abramov@gmail.com>

* Extract agentToSessionAccount

Co-authored-by: dan <dan.abramov@gmail.com>

* Extract createAgent*

Co-authored-by: dan <dan.abramov@gmail.com>

* Simplify isSessionExpired

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-05-01 02:55:43 +01:00
dan 66ad5543f1 [Session] Move deactivated field off the session object (#3780) 2024-05-01 01:36:56 +01:00
dan 339f26567a [Session] Consistently copy SessionAgent fields (#3779)
Co-authored-by: Eric Bailey <git@esb.lol>
2024-05-01 01:13:55 +01:00
Samuel Newman 80bcf81dc5 [Clipclops] Separate messages if there's a time gap (#3777)
* separate msg groups by age

* clean up logic and add comments
2024-04-30 23:40:26 +01:00
Hailey db968b7610 [Clipclops] Header for chat (#3775)
* add temp `getchat` query

* properly get the other profile

* add basic header

* normalize layout on all devices

* remove unused imports, adjust style

* remove unnecessary log

* remove another log

* remove some more imports

* cleanup

* use `Button` instead in the header

* lint
2024-04-30 15:04:17 -07:00
Hailey 268e30d21a [Clipclops] adjust scroll position, keyboard color (#3771)
* use the correct keyboard appearance based on theme

* i guess the min index should be 1
2024-04-30 11:33:41 -07:00
Samuel Newman 611ff0c7e4 [Clipclops] Add clop sent time to clipclop (#3772)
* add message sent time to message

* fix last message in group logic
2024-04-30 19:31:30 +01:00
Samuel Newman 7b694fd860 [Clipclops] Use API data for clipclop list (#3769)
* use real API

* remove extra tab icon

* messages list web layout + style improvements

* use style's text color for input

* make new chat button way more obvious

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-04-30 18:15:48 +01:00
Samuel Newman bcd3678067 [Clipclops] New clipclop dialog (#3750)
* add new routes with placeholder screens

* add clops list

* add a clop input

* add some better padding to the clops

* some more adjustments

* add rnkc

* implement rnkc

* implement rnkc

* be a little less weird about it

* rename clop stuff

* rename more clop

* one more

* add codegenerated lexicon

* replace hailey's types

* use codegen'd types in components

* fix error + throw if fetch failed

* remove bad imports

* update messageslist and messageitem

* import useState

* replace hailey's types

* use codegen'd types in components

* add FAB

* new chat dialog

* error + default search term

* fix typo

* fix web styles

* optimistically set chat data

* use cursor instead of last rev

* [Clipclops] Temp codegenerated lexicon (#3749)

* add codegenerated lexicon

* replace hailey's types

* use codegen'd types in components

* fix error + throw if fetch failed

* remove bad imports

* update messageslist and messageitem

* import useState

* add clop service URL hook

* add dm service url storage

* use context

* use context for service url (temp)

* remove log

* cleanup merge

* fix merge error

* disable hack

* sender-based message styles

* temporary filter

* merge cleanup

* add `hideBackButton`

* rm unneeded return

* tried to be smart

* hide go back button

* use `searchActorTypeahead` instead

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-04-30 17:43:57 +01:00
dan 2b7d796ca9 Session fixes, pt. 1 (#3762)
* Update persisted schema for new source of truth, implement in existing session

(cherry picked from commit b1e5f12bae)

* Improve toasts, log caught error, during switch account

(cherry picked from commit fe0d150706)

* Handle thrown errors from initSession during login

(cherry picked from commit 2c85c04591)

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-04-30 17:38:05 +01:00
Samuel Newman 4de78fb69e use keyboardDismissMode rather than onScrollBeginDrag (#3767) 2024-04-30 16:11:41 +01:00
Hailey eb8bfd11d1 [Clipclops] Add screen to view and send clip clops (#3754)
* add new routes with placeholder screens

* add clops list

* add a clop input

* add some better padding to the clops

* some more adjustments

* add rnkc

* implement rnkc

* implement rnkc

* be a little less weird about it

* rename clop stuff

* rename more clop

* one more

* [Clipclops] Temp codegenerated lexicon (#3749)

* add codegenerated lexicon

* replace hailey's types

* use codegen'd types in components

* fix error + throw if fetch failed

* remove bad imports

* update messageslist and messageitem

* import useState

* add clop service URL hook

* add dm service url storage

* use context

* use context for service url (temp)

* remove log

* nits

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-04-29 23:34:26 -07:00
Hailey 5d19f27052 switch branch for installing react-native-bottom-sheet (#3760) 2024-04-29 16:13:41 -07:00
dan 42966fb49e Fix List onMomentumScrollEnd (#3759) 2024-04-29 23:38:03 +01:00
Paul Frazee 28fc98d942 Release 1.80 (#3757)
* Test fixes

* Intl extract
2024-04-29 15:00:25 -07:00
Frudrax Cheng 599dab1c7a Update zh-TW translations (#3678)
* consistent improvements

* update messages.po

* improve translation

* improve translation

* fix punctuation issues

* remove superseded strings

* GIFs to GIF

* modify according to review suggestions

* fix some misnomers

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* modify according to review suggestions

* modify according to review suggestions

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Update messages.po

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* modify according to review suggestions

* a small adjustment

---------

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>
2024-04-29 14:54:17 -07:00
Gabriella 6dd2e07ab9 Italian localization (#3684)
* Add files via upload

* Update messages.po

Changed the grama of string

* Update messages.po

Following the input of @niconick89.bsky.social I have added additional correction to some of the strings.
2024-04-29 14:53:45 -07:00
Ivan Beà 983d9e55b3 Update catalan messages.po (#3697)
* Update catalan messages.po

New lines translated
You can check it @jordimas @darccio @ferranrego @MiquelAdell @mvdan @johnnydement @Mejans @surfdude29

* Update messages.po

Remove extra space

* Update messages.po
2024-04-29 14:53:13 -07:00
Minseo Lee c8aa16e90e Update Korean localization (#3698)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po
2024-04-29 14:52:39 -07:00
Takayuki KUSANO c2fff6a861 Update Japanese translation (#3734)
* Remove old old onboarding related entries

cf. https://github.com/bluesky-social/social-app/pull/3674

* Update Japanese translation

(accessibilityLabel & accessibilityHint)
cf. https://github.com/bluesky-social/social-app/pull/3670

* Update Japanese translation

* Fix Japanase translation related to block
2024-04-29 14:52:15 -07:00
Frudrax Cheng f3c32f9996 Update zh-CN translations (#3716)
* fix punctuation issues

* improve translation

* remove superseded strings

* update messages.po

* fix typo

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* Update src/locale/locales/zh-CN/messages.po

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>

* update language-team

* Update messages.po

Co-authored-by: Leonid <kasumi@bangdream.moe>

* Update messages.po

Co-authored-by: Leonid <kasumi@bangdream.moe>

* Update messages.po

Co-authored-by: Leonid <kasumi@bangdream.moe>

* Update messages.po

Co-authored-by: Leonid <kasumi@bangdream.moe>

* Update messages.po

Co-authored-by: Leonid <kasumi@bangdream.moe>

* Update messages.po

Co-authored-by: Leonid <kasumi@bangdream.moe>

* Update messages.po

Co-authored-by: Leonid <kasumi@bangdream.moe>

---------

Co-authored-by: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com>
Co-authored-by: Leonid <kasumi@bangdream.moe>
2024-04-29 14:51:41 -07:00
Eric Bailey a4e34537ce Send Bluesky feeds and suggested follows more data (#3695)
* WIP

* Fix constructors

* Clean up

* Tweak

* Rm extra assignment

* Narrow down the argument

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-29 22:04:35 +01:00
Kisaragi Hiu d893fe005d android: fix various places still using default Material Teal (#3555)
These places include TextInput cursor, TextInput selection, and the
spinner (ActivityIndicator) --- the default Material Teal is out of
place wherever it shows up.

This sets Expo's primaryColor to #1083fe, which is the color that
defaultTheme.palette.default.brandText resolves to, then applies it as
the native accent color via a plugin because Expo doesn't apply the
accent color.
2024-04-29 11:36:05 -07:00
Hailey 2feea51ae3 clear search on tab press (#3753)
* clear search on tab press

* don't need to call `onPressCancelSearch`
2024-04-29 17:32:20 +01:00
dan 5d715ae1d0 Improve search screen perf (#3752)
* Extract SearchHistory to a component

* Extract AutocompleteResults to a component

* Extract SearchInputBox to a component

* Add a bunch of memoization

* Optimize switching by rendering both

* Remove subdomain matching

This is only ever useful if you type it exactly correct. Search now does a better job anyway.

* Give recent search decent hitslops
2024-04-29 16:52:24 +01:00
dan 3c2d73909b Remove layout animation from search (#3751) 2024-04-29 16:11:59 +01:00
Hailey 388c4f79cf Increase search TextInput hit area and improve the related UI (#3748)
* improve hit area of search text input

use text cursor on web

use a pressable instead

use a vertical padding of 9

oops

move vertical padding to `TextInput` to increase hit area

* Hide it from a11y tree, change cursor

* Hide clear on empty text

* Render either Clear or Cancel

* Remove Clear button

* Animate it

* Better animation

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-29 05:12:20 +01:00
dan dfce190cb6 [iOS] Fix selecting search input on focus (#3746)
* Select search text on focus

* Scope to iOS only
2024-04-28 22:52:40 +01:00
dan 361d255e95 Remove useEffect from TimeElapsed (#3741) 2024-04-28 22:37:00 +01:00
dan 256bb33de0 Remove test gating code (#3742) 2024-04-28 22:36:50 +01:00
dan 2a08931127 Fix dropdown immediately closing on Enter (#3745)
* Move dropdown content into separate component

* Fix dropdown with keyboard

* No-op is sufficient
2024-04-28 21:29:43 +01:00
dan 1dd3d6657c Account for momentum when hiding minimal shell (#3740)
* Add optional momentum events to scroll context

* If there is a velocity, don't snap until momentum end

* Don't show bar on scroll down

* Rm onMomentumBegin
2024-04-28 17:48:20 +01:00
dan 3b4848ba59 Reduce hovercard false positives, add them to display names and handles (#3739)
* Don't trigger hovercards on scroll

* Add display name / handle hovercards

* Increase hovercard delay
2024-04-28 13:59:18 +01:00
Mary 74cd1d4458 YouTube Music as supported player (#3736) 2024-04-27 22:11:08 -07:00
Piotr P bf7aee19b8 add missing cropImageUtil file 2024-04-27 14:59:00 +02:00
Piotr P beddddcb57 hide alternative ratio buttons when using custom ratio 2024-04-27 14:31:51 +02:00
Piotr P bc956803b8 allow for custom cropper aspect ration based on image 2024-04-27 14:31:45 +02:00
Piotr P ebd333b331 Fix cropper crashing after clicking cancel 2024-04-27 14:31:39 +02:00
Piotr P ae26963b45 eslint autofixes 2024-04-27 14:31:33 +02:00
dan 21e5a87fab Patch lexicon for perf (#3733) 2024-04-27 07:54:47 +01:00
Hailey 61d7e6014b ensure dialog is closed on unmount (#3650)
Revert "properly check if the ref is null"

This reverts commit 8f563808a5d39389b0bc47a31e73cd147d1e7e8b.

properly check if the ref is null

ensure dialog is closed on unmount
2024-04-27 07:42:49 +01:00
Hailey 7eb1444f2c remove precacheThreadPostProfiles (#3729)
* remove `precacheThreadPostProfiles`

* add `displayName` to `PreviewableUserAvatar`

* memo

* use `precacheProfile`

* pass `profile` directly to `PreviewableUserAvatar`

* update the `UserAvatar`'s props

* remove feed cache

* one more spot

* rm unused queryClient

* Don't call fn unnecessarily

* Preload for display name too

* try notification item

* add to feeditem

* and finally, precache for post threads

* timestamp

* Fix

* onBeforePress

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-27 06:31:07 +01:00
Samuel Newman ce85375c85 [Clipclops] New routes with placeholder screens (#3725)
* add new routes with placeholder screens

* gate content

* add filled envelope style

* swap filled state

* switch to `useAgent`
2024-04-27 05:54:18 +01:00
Samuel Newman 1af59ca8a7 Special-case bsky.app/download to open share sheet or copy to clipboard (#3710)
* special-case bsky.app/download to share

* Address feedback

* Improve detection

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-27 05:25:28 +01:00
Hailey b10c96f28d ver bump (#3731) 2024-04-27 04:38:51 +01:00
Hailey 5f9136479b Improve usability of search on web (#3663)
* dont select the text on web

* TODO REVERT THESE CHANGES

* use `usethrottledvalue` for autocomplete

* use `isFetching` from query

* rm setTimeout

* getting there

* improve functionality of cancel button

* rm todo

* add comment back

* encode `searchText` rather than `queryTerm`

* use "back" on web in some cases

* don't flash results in autocomplete

* remove unnecesary usestate

* rename everything to `query` temporarily

* revert accidental lint

* rm todo

* rm comment

* use `useFocusEffect` to update the query term on back navigation

* `searchText` is always defined here

* Fix race

* remove back functionality

* use `keepPreviousData` for query

* rename `q` to `queryParam`

* remove hack

* remove `q=` on cancel

* blur on submit

* use `setParams` instead of `replace`

* use `replace` on web still

* clear the search input when we clear `q` on native

* onPress dismiss attempt

* Adjustments

* Fix search history

* Always hide autocomplete

* Clear right pane search on select

* `blur` on autosuggestion press

* Rename to reduce diff

* Fixes

* Unify codepaths

* Fixes

* precache the autosuggestion

* do the cache in the link card

* Revert "precache the autosuggestion"

This reverts commit 79c433e984.

* use `throttledValue` and `keepPreviousData` in sidebar search

* show spinner when fetching pt 1

* show spinner when fetching pt 2

* show spinner properly for autocomplete

* Fix extra border

* Position fixed

* TS

* Revert "TS"

This reverts commit df187ea2d7.

* Revert "Position fixed"

This reverts commit 9c721c952b.

* Maybe fix iPad

* Revert "TODO REVERT THESE CHANGES"

This reverts commit 279f717f30.

* Rename var

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-27 04:34:53 +01:00
bnewbold d81a373d21 embedr: handle out-of-range maxwidth; change default (#3713) 2024-04-25 17:23:45 -07:00
dan f2797218f8 Move Notifications up on desktop web (#3696) 2024-04-25 23:30:07 +01:00
Eric Bailey 45d354cd0c [Session] Add useAgent hook and replace (#3706)
* Hook it up

* Memoize getAgent method

* Use one shared reference

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-25 22:29:05 +01:00
Eric Bailey d8c8e1e854 [Session] Drill getAgent into notifications handlers (#3705)
* Drill into notifications handlers

(cherry picked from commit 7ac9e500866732e1f2e205bbe96e70db331e5ffb)

* Thread getAgent instead

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-25 21:57:45 +01:00
Eric Bailey e5fdbfe294 [Session] Drill getAgent into Onboarding/util (#3704)
* Drill agent into Onboarding/util

(cherry picked from commit 2ba68eb5e446a694730b720f2a5b3307eb0914ef)

* Thread getAgent instead

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-25 21:53:27 +01:00
Eric Bailey 35005da59e [Session] Drill getAgent into notifications/utils (#3703)
* Drill into notifications/util

(cherry picked from commit 84b535ed54f4fe93debcd198809bb184519c3507)

* Thread getAgent instead

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-25 21:48:01 +01:00
Eric Bailey a69e567991 [Session] Drill into getAgent into whenAppViewReady (#3702)
* Drill into whenAppViewReady

(cherry picked from commit e290e5be3df509bdd9d0e626a164996c9dee3636)

* Drill getAgent instead

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-25 21:39:28 +01:00
Eric Bailey ec37696034 [Session] Drill getAgent into feed APIs (#3701)
* Update to desired post-feed usage

* Drill agent into feed apis

* Thread getAgent instead

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-25 21:29:06 +01:00
Kevin Scannell 282ad4b17d Irish translation back to 100% (#3683) 2024-04-25 09:08:56 -07:00
Eric Bailey 6aded4f257 [Session] Base (#3541)
* Add readLastActiveAccount to use accounts[] as source of truth

* Add public service constant, use
2024-04-25 10:35:21 -05:00
Hailey 5b82b15007 remove use of pointerEvents on iOS profile header (#3694) 2024-04-25 01:24:20 +01:00
Hailey c3fcd486b3 Cleanup files after each iteration of compression and downloading (#3599)
* delete image on each iteration of compression

* replace a few other instances of `unlink()`

* ensure that moving to the permanent path will succeed

* use `cacheDirectory`

* missing file extension?

* assert

* Remove extra .

* Extract safeDeleteAsync, fix normalization

* Normalize everywhere

* Use safeDeleteAsync in more places

* Delete .bin too

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-25 01:12:36 +01:00
dan 90c3ec8749 Ignore image responses on non-200 status (#3693)
* Ignore image responses on non-200 status

* Fix tests
2024-04-24 23:16:11 +01:00
Mary 15055cb8c4 Persist PDS URL for session resumption (#3620)
* fix: store PDS URL for session resumption

* fix: handle a few more cases

* fix: blocking resumption should also use pds url

* refactor: do it in the construct itself

* fix: revert ce96223
2024-04-24 21:57:16 +01:00
dan 05212ca9e3 Remove Moderation from sidebar (#3692) 2024-04-24 21:37:16 +01:00
dan e2a59449df Remove old Suggested Follows logic (#3689) 2024-04-24 17:45:11 +01:00
dan 8ec3d8c76e Rewrite the link detection (#3687)
* Rewrite the link detection

* Handle parens and colons
2024-04-24 17:30:44 +01:00
Hailey b3df0b177f Remove unnecessary .trimEnd() and fix facet detection (#3672)
* Fix link facet detection

Exclude the trailing period from the link, similar as done in the atproto package

* Fix JSON to text conversion

The trimEnd() call was only needed because we were always appending \n\n at the end. Fix that by only adding line breaks after non-final paragraphs.

* Detect paste reliably

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-04-24 15:10:29 +01:00
Eric Bailey 05beb1bbad Remove old old onboarding (#3674) 2024-04-23 19:30:49 -05:00
340 changed files with 35527 additions and 22882 deletions
+1 -2
View File
@@ -95,8 +95,7 @@ web-build/
/ios/
# environment variables
.env
.env.*
# Firebase (Android) Google services
# INCLUDED: google-services.json
# INCLUDED: google-services.json
+2
View File
@@ -4,3 +4,5 @@ SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
EXPO_PUBLIC_BUNDLE_DATE=0
@@ -1,9 +1,9 @@
name: build-and-push-bskyweb-aws
on:
workflow_dispatch:
push:
branches:
- main
- 3p-moderators
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
@@ -54,3 +54,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
env:
EXPO_PUBLIC_BUNDLE_IDENTIFIER: $(git rev-parse --short HEAD)
EXPO_PUBLIC_BUNDLE_DATE: $(date -u +"%y%m%d%H")
@@ -25,6 +25,8 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -57,6 +59,8 @@ jobs:
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
+4
View File
@@ -25,6 +25,8 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -65,6 +67,8 @@ jobs:
- name: ✏️ Write environment variables
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
+14 -2
View File
@@ -54,7 +54,9 @@ jobs:
uses: actions/cache@v4
with:
path: last-successful-commit-hash.txt
key: last-successful-deployment-commit-${{ github.ref_name }}
key: last-successful-deployment-commit-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
last-successful-deployment-commit-${{ github.ref_name }}-
- name: Add the last successful deployment commit to the output
id: last-successful-commit
@@ -68,7 +70,7 @@ jobs:
- name: 🕵️ Get the base commit
id: base-commit
run: |
if [ -z "${{ inputs.channel == 'production' }}" ]; then
if ${{ inputs.channel == 'production' }}; then
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
else
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
@@ -144,6 +146,8 @@ jobs:
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
@@ -182,6 +186,8 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -222,6 +228,8 @@ jobs:
- name: ✏️ Write environment variables
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
@@ -251,6 +259,8 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -283,6 +293,8 @@ jobs:
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
+4 -4
View File
@@ -20,8 +20,8 @@ jobs:
uses: actions/setup-go@v3
with:
go-version: '1.21'
- name: Dummy JS File
run: touch bskyweb/static/js/blah.js
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
- name: Check
run: cd bskyweb/ && make check
- name: Build (binary)
@@ -37,7 +37,7 @@ jobs:
uses: actions/setup-go@v3
with:
go-version: '1.21'
- name: Dummy JS File
run: touch bskyweb/static/js/blah.js
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
- name: Lint
run: cd bskyweb/ && make lint
+1
View File
@@ -40,6 +40,7 @@ RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bs
# hack around issue with empty directory and go:embed
RUN touch bskyweb/static/js/empty.txt
RUN touch bskyweb/static/media/empty.txt
#
# Generate the embedr Go binary.
+4 -2
View File
@@ -1,8 +1,9 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
import {createServer, loginAsAlice, loginAsBob, openApp, sleep} from '../util'
describe('Curate lists', () => {
beforeAll(async () => {
@@ -115,6 +116,7 @@ describe('Curate lists', () => {
await element(by.text('About')).tap()
await element(by.id('addUserBtn')).tap()
await expect(element(by.id('listAddUserModal'))).toBeVisible()
await element(by.id('searchInput')).typeText('b')
await waitFor(element(by.id('user-bob.test-addBtn')))
.toBeVisible()
.withTimeout(5000)
+27 -2
View File
@@ -1,9 +1,10 @@
import ImageResizer from '@bam.tech/react-native-image-resizer'
import RNFetchBlob from 'rn-fetch-blob'
import {
downloadAndResize,
DownloadAndResizeOpts,
} from '../../src/lib/media/manip'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import RNFetchBlob from 'rn-fetch-blob'
describe('downloadAndResize', () => {
const errorSpy = jest.spyOn(global.console, 'error')
@@ -30,6 +31,7 @@ describe('downloadAndResize', () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
info: jest.fn().mockReturnValue({status: 200}),
flush: jest.fn(),
})
@@ -84,6 +86,7 @@ describe('downloadAndResize', () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({
path: jest.fn().mockReturnValue('file://downloaded-image'),
info: jest.fn().mockReturnValue({status: 200}),
flush: jest.fn(),
})
@@ -118,4 +121,26 @@ describe('downloadAndResize', () => {
{mode: 'cover'},
)
})
it('should return undefined for non-200 response', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({
path: jest.fn().mockReturnValue('file://downloaded-image'),
info: jest.fn().mockReturnValue({status: 400}),
flush: jest.fn(),
})
const opts: DownloadAndResizeOpts = {
uri: 'https://example.com/image',
width: 100,
height: 100,
maxSize: 500000,
mode: 'cover',
timeout: 10000,
}
const result = await downloadAndResize(opts)
expect(errorSpy).not.toHaveBeenCalled()
expect(result).toBeUndefined()
})
})
+7 -30
View File
@@ -3,7 +3,7 @@ import {RichText} from '@atproto/api'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import {cleanError} from '../../src/lib/strings/errors'
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
import {enforceLen, pluralize} from '../../src/lib/strings/helpers'
import {enforceLen} from '../../src/lib/strings/helpers'
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {ago} from '../../src/lib/strings/time'
@@ -127,35 +127,6 @@ describe('detectLinkables', () => {
})
})
describe('pluralize', () => {
const inputs: [number, string, string?][] = [
[1, 'follower'],
[1, 'member'],
[100, 'post'],
[1000, 'repost'],
[10000, 'upvote'],
[100000, 'other'],
[2, 'man', 'men'],
]
const outputs = [
'follower',
'member',
'posts',
'reposts',
'upvotes',
'others',
'men',
]
it('correctly pluralizes a set of words', () => {
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i]
const output = pluralize(...input)
expect(output).toEqual(outputs[i])
}
})
})
describe('makeRecordUri', () => {
const inputs: [string, string, string][] = [
['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],
@@ -396,6 +367,7 @@ describe('parseEmbedPlayerFromUrl', () => {
'https://youtube.com/watch?v=videoId&feature=share',
'https://youtube.com/shorts/videoId',
'https://m.youtube.com/watch?v=videoId',
'https://music.youtube.com/watch?v=videoId',
'https://youtube.com/shorts/',
'https://youtube.com/',
@@ -504,6 +476,11 @@ describe('parseEmbedPlayerFromUrl', () => {
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
},
{
type: 'youtube_video',
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
},
undefined,
undefined,
+4 -3
View File
@@ -1,10 +1,10 @@
import {it, describe, expect} from '@jest/globals'
import {describe, expect, it} from '@jest/globals'
import {
linkRequiresWarning,
isPossiblyAUrl,
splitApexDomain,
isTrustedUrl,
linkRequiresWarning,
splitApexDomain,
} from '../../../src/lib/strings/url-helpers'
describe('linkRequiresWarning', () => {
@@ -170,6 +170,7 @@ describe('isTrustedUrl', () => {
['https://google.com', false],
['https://docs.google.com', false],
['https://google.com/#', false],
['https://blueskywebxzendesk.com', false],
]
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
+26
View File
@@ -64,6 +64,8 @@ module.exports = function (config) {
icon: './assets/icon.png',
userInterfaceStyle: 'automatic',
splash: SPLASH_CONFIG,
// hsl(211, 99%, 53%), same as palette.default.brandText
primaryColor: '#1083fe',
ios: {
supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app',
@@ -89,6 +91,29 @@ module.exports = function (config) {
entitlements: {
'com.apple.security.application-groups': 'group.app.bsky',
},
privacyManifests: {
NSPrivacyAccessedAPITypes: [
{
NSPrivacyAccessedAPIType:
'NSPrivacyAccessedAPICategoryFileTimestamp',
NSPrivacyAccessedAPITypeReasons: ['C617.1', '3B52.1', '0A2A.1'],
},
{
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryDiskSpace',
NSPrivacyAccessedAPITypeReasons: ['E174.1', '85F4.1'],
},
{
NSPrivacyAccessedAPIType:
'NSPrivacyAccessedAPICategorySystemBootTime',
NSPrivacyAccessedAPITypeReasons: ['35F9.1'],
},
{
NSPrivacyAccessedAPIType:
'NSPrivacyAccessedAPICategoryUserDefaults',
NSPrivacyAccessedAPITypeReasons: ['CA92.1'],
},
],
},
},
androidStatusBar: {
barStyle: 'light-content',
@@ -180,6 +205,7 @@ module.exports = function (config) {
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/shareExtension/withShareExtensions.js',
].filter(Boolean),
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.293 3.293A1 1 0 0 1 4 3h7.25a1 1 0 1 1 0 2H5v14h6.25a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4a1 1 0 0 1 .293-.707Zm11.5 3.5a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 402 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 2a7.307 7.307 0 0 0-7.298 6.943l-.19 3.798-1.321 2.641A1.809 1.809 0 0 0 4.809 18H7.1a5.002 5.002 0 0 0 9.8 0h2.291a1.809 1.809 0 0 0 1.618-2.618l-1.32-2.641-.19-3.798A7.308 7.308 0 0 0 12 2Zm0 18a3.001 3.001 0 0 1-2.83-2h5.66A3.001 3.001 0 0 1 12 20Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 398 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.702 8.943a7.307 7.307 0 0 1 14.596 0l.19 3.798 1.321 2.641A1.809 1.809 0 0 1 19.191 18H16.9a5.002 5.002 0 0 1-9.8 0H4.809a1.809 1.809 0 0 1-1.618-2.618l1.32-2.641.19-3.798ZM9.17 18a3.001 3.001 0 0 0 5.658 0H9.171ZM12 4a5.307 5.307 0 0 0-5.3 5.042l-.19 3.798a2 2 0 0 1-.21.795L5.119 16h13.764l-1.183-2.365a2 2 0 0 1-.208-.795l-.19-3.798A5.308 5.308 0 0 0 12 4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 505 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 2a7.853 7.853 0 0 0-7.784 6.815l-1.207 9.053A1 1 0 0 0 4 19h3.354c.904 1.748 2.607 3 4.646 3 2.039 0 3.742-1.252 4.646-3H20a1 1 0 0 0 .991-1.132l-1.207-9.053A7.853 7.853 0 0 0 12 2Zm2.222 17H9.778c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 387 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.216 8.815a7.853 7.853 0 0 1 15.568 0l1.207 9.053A1 1 0 0 1 20 19h-3.354c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM12 4a5.853 5.853 0 0 0-5.802 5.08L5.142 17h13.716l-1.056-7.92A5.853 5.853 0 0 0 12 4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 454 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2.002 7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2a4 4 0 0 1-4-4V7Zm4-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h6a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-12Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 381 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm0 10a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm10-1a1 1 0 1 0 0 2h7a1 1 0 1 0 0-2h-7Zm-1-9a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 302 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Zm-6 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 381 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M17.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-9 9A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l9-9ZM10 12.414V14h1.586l8-8L18 4.414l-8 8ZM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 393 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M17.657 6.343A8 8 0 1 0 6.343 17.657 8 8 0 0 0 17.657 6.343ZM4.929 4.93c3.905-3.905 10.237-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0-3.905-3.905-3.905-10.237 0-14.142Zm3.536 9.192a1 1 0 0 1 1.414 0 3 3 0 0 0 4.243 0 1 1 0 0 1 1.414 1.415 5 5 0 0 1-7.071 0 1 1 0 0 1 0-1.415Z M10.5 9.5c0 .828-.56 1.5-1.25 1.5S8 10.328 8 9.5 8.56 8 9.25 8s1.25.672 1.25 1.5ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 623 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 11.708 2.654 4.06A.998.998 0 0 1 3 4h18c.122 0 .238.022.346.061L12 11.708ZM2 19V6.11l9.367 7.664a1 1 0 0 0 1.266 0L22 6.11V19a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 303 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M9.186 2.512a1.5 1.5 0 0 0-1.674 1.302L7.176 6.5H4a1.5 1.5 0 1 0 0 3h2.8l-.624 5H4a1.5 1.5 0 0 0 0 3h1.8l-.288 2.314a1.5 1.5 0 1 0 2.976.372l.336-2.686h4.977l-.29 2.314a1.5 1.5 0 1 0 2.977.372l.336-2.686H20a1.5 1.5 0 0 0 0-3h-2.8l.624-5H20a1.5 1.5 0 0 0 0-3h-1.8l.288-2.314a1.5 1.5 0 1 0-2.976-.372L15.176 6.5h-4.977l.29-2.314a1.5 1.5 0 0 0-1.303-1.674ZM9.2 14.5l.625-5h4.977l-.625 5H9.199Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 533 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12.63 1.724a1 1 0 0 0-1.26 0l-8 6.5A1 1 0 0 0 3 9v11a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-6h4v6a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V9a1 1 0 0 0-.37-.776l-8-6.5Z"/></svg>

After

Width:  |  Height:  |  Size: 252 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.37 1.724a1 1 0 0 1 1.26 0l8 6.5A1 1 0 0 1 21 9v11a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-5h-2v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 .37-.776l8-6.5ZM5 9.476V19h4v-5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5h4V9.476l-7-5.688-7 5.688Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 11a6 6 0 1 1 12 0 6 6 0 0 1-12 0Zm6-8a8 8 0 1 0 4.906 14.32l3.387 3.387a1 1 0 0 0 1.414-1.414l-3.387-3.387A8 8 0 0 0 11 3Zm4 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 301 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 12a8 8 0 1 1 4.445 7.169 1 1 0 0 0-.629-.088l-3.537.662.7-3.415a1 1 0 0 0-.09-.66A7.961 7.961 0 0 1 4 12Zm8-10C6.477 2 2 6.477 2 12c0 1.523.341 2.968.951 4.262l-.93 4.537a1 1 0 0 0 1.163 1.184l4.68-.876A9.968 9.968 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2ZM7.5 13.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 567 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10a9.968 9.968 0 0 1-4.136-.893l-4.68.876a1 1 0 0 1-1.164-1.184l.931-4.537A9.965 9.965 0 0 1 2 12Zm4.25 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm4.5 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm5.75 1.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 453 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.374 3.22a1 1 0 0 1 1.073-.114l16 8a1 1 0 0 1 0 1.788l-16 8a1 1 0 0 1-1.417-1.136L4.97 12 3.03 4.243a1 1 0 0 1 .344-1.023ZM6.781 13l-1.284 5.133L17.764 12 5.497 5.867 6.781 11H9a1 1 0 1 1 0 2H6.78Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 342 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 6a1 1 0 0 1 1 1v4h4a1 1 0 1 1 0 2h-4v4a1 1 0 1 1-2 0v-4H7a1 1 0 1 1 0-2h4V7a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 237 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M9.996 2.869A1.951 1.951 0 0 1 11.62 2h.76c.653 0 1.262.326 1.624.869l1.141 1.712 1.749-.404a1.951 1.951 0 0 1 1.819.522l.588.589c.476.475.673 1.162.522 1.818l-.404 1.749 1.712 1.141c.543.362.869.971.869 1.624v.76c0 .653-.326 1.262-.869 1.624l-1.712 1.141.404 1.749a1.951 1.951 0 0 1-.522 1.819l-.588.588a1.951 1.951 0 0 1-1.819.522l-1.749-.404-1.141 1.712A1.951 1.951 0 0 1 12.38 22h-.76a1.951 1.951 0 0 1-1.624-.869L8.855 19.42l-1.749.404a1.951 1.951 0 0 1-1.818-.522l-.59-.588a1.951 1.951 0 0 1-.52-1.819l.403-1.749-1.712-1.141A1.951 1.951 0 0 1 2 12.38v-.76c0-.653.326-1.262.869-1.624L4.58 8.855l-.404-1.749A1.951 1.951 0 0 1 4.7 5.288l.589-.59a1.951 1.951 0 0 1 1.818-.52l1.749.403 1.141-1.712ZM8.5 12a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 886 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 3a1 1 0 0 1 1 1v1.126a4 4 0 0 1 0 7.748V20a1 1 0 1 1-2 0v-7.126a4 4 0 0 1 0-7.748V4a1 1 0 0 1 1-1Zm10 0a1 1 0 0 1 1 1v9.126a4 4 0 1 1-2 0V4a1 1 0 0 1 1-1ZM7 7a2 2 0 1 0 0 4 2 2 0 1 0 0-4Zm10 8a2 2 0 1 0 0 4 2 2 0 1 0 0-4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3-12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm-3 10a7.976 7.976 0 0 1-5.714-2.4C7.618 16.004 9.605 15 12 15c2.396 0 4.383 1.005 5.714 2.6A7.976 7.976 0 0 1 12 20Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 366 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 0 0-5.935 13.365C7.56 15.895 9.612 15 12 15c2.388 0 4.44.894 5.935 2.365A8 8 0 0 0 12 4Zm4.412 14.675C15.298 17.636 13.792 17 12 17c-1.791 0-3.298.636-4.412 1.675A7.96 7.96 0 0 0 12 20a7.96 7.96 0 0 0 4.412-1.325ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10a9.98 9.98 0 0 1-3.462 7.567A9.965 9.965 0 0 1 12 22a9.965 9.965 0 0 1-6.538-2.433A9.98 9.98 0 0 1 2 12Zm10-4a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 581 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

+2
View File
@@ -10,6 +10,8 @@ static/js/*.js
static/js/*.map
static/js/*.js.LICENSE.txt
static/js/empty.txt
static/media/*.png
static/media/empty.txt
templates/scripts.html
templates/*-embed.html
static/embed/*.html
+4 -2
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"strings"
"time"
appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax"
@@ -13,6 +12,9 @@ import (
"github.com/labstack/echo/v4"
)
// time.RFC822Z, but with four digit year. used for RSS pubData.
var FullYearRFC822Z = "02 Jan 2006 15:04 -0700"
type ItemGUID struct {
XMLName xml.Name `xml:"guid"`
Value string `xml:",chardata"`
@@ -107,7 +109,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
pubDate := ""
createdAt, err := syntax.ParseDatetimeLenient(rec.CreatedAt)
if nil == err {
pubDate = createdAt.Time().Format(time.RFC822Z)
pubDate = createdAt.Time().Format(FullYearRFC822Z)
}
posts = append(posts, Item{
Link: fmt.Sprintf("https://%s/profile/%s/post/%s", req.Host, pv.Handle, aturi.RecordKey().String()),
+3 -1
View File
@@ -158,7 +158,7 @@ func serve(cctx *cli.Context) error {
// Cache javascript and images files for 1 week, which works because
// they're always versioned (e.g. /static/js/main.64c14927.js)
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") {
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 7 * (60 * 60 * 24) // 1 week
}
@@ -200,6 +200,8 @@ func serve(cctx *cli.Context) error {
e.GET("/support/community-guidelines", server.WebGeneric)
e.GET("/support/copyright", server.WebGeneric)
e.GET("/intent/compose", server.WebGeneric)
e.GET("/messages", server.WebGeneric)
e.GET("/messages/:conversation", server.WebGeneric)
// profile endpoints; only first populates info
e.GET("/profile/:handleOrDID", server.WebProfile)
+10 -4
View File
@@ -122,14 +122,20 @@ func (srv *Server) WebOEmbed(c echo.Context) error {
}
// TODO: do we actually do something with width?
width := 550
width := 600
maxWidthParam := c.QueryParam("maxwidth")
if maxWidthParam != "" {
maxWidthInt, err := strconv.Atoi(maxWidthParam)
if err != nil || maxWidthInt < 220 || maxWidthInt > 550 {
return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer between 220 and 550)")
if err != nil {
return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer)")
}
if maxWidthInt < 220 {
width = 220
} else if maxWidthInt > 600 {
width = 600
} else {
width = maxWidthInt
}
width = maxWidthInt
}
// NOTE: maxheight ignored
View File
+21 -7
View File
@@ -47,6 +47,14 @@
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
}
#preload {
width: 100px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea {
font: inherit;
@@ -262,13 +270,19 @@
</head>
<body>
{%- block body_all %}
<div id="root"></div>
<noscript>
<h1 lang="en">JavaScript Required</h1>
<p lang="en">This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
<p lang="en">Learn more about Bluesky at <a href="https://bsky.social">bsky.social</a> and <a href="https://atproto.com">atproto.com</a>.
{% block noscript_extra %}{% endblock %}
</noscript>
<div id="root">
<div id="preload">
<!-- Bluesky SVG -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320"><path fill="#0085ff" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/></svg>
</div>
</div>
<noscript>
<h1 lang="en">JavaScript Required</h1>
<p lang="en">This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
<p lang="en">Learn more about Bluesky at <a href="https://bsky.social">bsky.social</a> and <a href="https://atproto.com">atproto.com</a>.
{% block noscript_extra %}{% endblock %}
</noscript>
{% endblock -%}
</body>
</html>
@@ -1,5 +1,6 @@
import {requireNativeViewManager} from 'expo-modules-core'
import * as React from 'react'
import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
+12 -5
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.79.0",
"version": "1.81.0",
"private": true,
"engines": {
"node": ">=18"
@@ -15,7 +15,7 @@
"web": "expo start --web",
"use-build-number": "./scripts/useBuildNumberEnv.sh",
"use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/ && cp -v ./web-build/static/media/*.png ./bskyweb/static/media/",
"build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
"build-ios": "yarn use-build-number-with-bump eas build -p ios",
"build-android": "yarn use-build-number-with-bump eas build -p android",
@@ -50,15 +50,18 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto-labs/api": "^0.12.8-clipclops.0",
"@atproto/api": "^0.12.5",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.4.2",
"@expo/webpack-config": "^19.0.0",
"@floating-ui/dom": "^1.6.3",
"@floating-ui/react-dom": "^2.0.8",
"@formatjs/intl-locale": "^3.4.3",
"@formatjs/intl-pluralrules": "^5.2.10",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
@@ -97,6 +100,7 @@
"@tiptap/react": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.220",
"@types/invariant": "^2.2.37",
"@types/lodash.throttle": "^4.1.9",
"@types/node": "^18.16.2",
"@zxing/text-encoding": "^0.9.0",
"array.prototype.findlast": "^1.2.3",
@@ -108,7 +112,7 @@
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1",
"expo": "^50.0.8",
"expo": "^50.0.17",
"expo-application": "^5.8.3",
"expo-build-properties": "^0.11.1",
"expo-camera": "~14.0.4",
@@ -148,6 +152,7 @@
"lodash.samplesize": "^4.2.0",
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
"mobx-utils": "^6.0.6",
@@ -168,6 +173,7 @@
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.11.7",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress",
@@ -183,6 +189,7 @@
"react-native-web-webview": "^1.0.2",
"react-native-webview": "13.6.4",
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
"rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1",
"statsig-react-native-expo": "^4.6.1",
@@ -283,7 +290,7 @@
"\\.[jt]sx?$": "babel-jest"
},
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)"
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|@discord|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)"
],
"modulePathIgnorePatterns": [
"__tests__/.*/__mocks__",
+28
View File
@@ -0,0 +1,28 @@
diff --git a/node_modules/@atproto/lexicon/dist/validators/complex.js b/node_modules/@atproto/lexicon/dist/validators/complex.js
index 32d7798..9d688b7 100644
--- a/node_modules/@atproto/lexicon/dist/validators/complex.js
+++ b/node_modules/@atproto/lexicon/dist/validators/complex.js
@@ -113,7 +113,22 @@ function object(lexicons, path, def, value) {
if (value[key] === null && nullableProps.has(key)) {
continue;
}
- const propDef = def.properties[key];
+ const propDef = def.properties[key]
+ if (typeof value[key] === 'undefined' && !requiredProps.has(key)) {
+ // Fast path for non-required undefined props.
+ if (
+ propDef.type === 'integer' ||
+ propDef.type === 'boolean' ||
+ propDef.type === 'string'
+ ) {
+ if (typeof propDef.default === 'undefined') {
+ continue
+ }
+ } else {
+ // Other types have no defaults.
+ continue
+ }
+ }
const propPath = `${path}/${key}`;
const validated = (0, util_1.validateOneOf)(lexicons, propPath, propDef, value[key]);
const propValue = validated.success ? validated.value : value[key];
@@ -0,0 +1,25 @@
/**
* @file Set accent color to primaryColor from app.config.js.
* This way we get a sane default color for spinners, text inputs, etc.
*/
const {withAndroidStyles, AndroidConfig} = require('@expo/config-plugins')
module.exports = function withAndroidStylesAccentColorPlugin(appConfig) {
return withAndroidStyles(appConfig, function (decoratedAppConfig) {
try {
decoratedAppConfig.modResults = AndroidConfig.Styles.assignStylesValue(
decoratedAppConfig.modResults,
{
add: true,
parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(),
name: 'colorAccent',
value: '@color/colorPrimary',
},
)
} catch (e) {
console.error(`withAndroidStylesAccentColorPlugin failed`, e)
}
return decoratedAppConfig
})
}
+56 -44
View File
@@ -12,14 +12,15 @@ import {
import * as SplashScreen from 'expo-splash-screen'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {MessagesProvider} from '#/state/messages'
import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
import {readLastActiveAccount} from '#/state/session/util'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {useNotificationsListener} from 'lib/notifications/notifications'
import {QueryProvider} from 'lib/react-query'
import {s} from 'lib/styles'
import {ThemeProvider} from 'lib/ThemeContext'
@@ -32,6 +33,7 @@ import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {
Provider as SessionProvider,
SessionAccount,
useSession,
useSessionApi,
} from 'state/session'
@@ -51,7 +53,8 @@ import {listenSessionDropped} from './state/events'
SplashScreen.preventAutoHideAsync()
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
@@ -60,57 +63,66 @@ function InnerApp() {
// init
useEffect(() => {
listenSessionDropped(() => {
async function onLaunch(account?: SessionAccount) {
try {
if (account) {
await resumeSession(account)
}
} catch (e) {
logger.error(`session: resume failed`, {message: e})
} finally {
setIsReady(true)
}
}
const account = readLastActiveAccount()
onLaunch(account)
}, [resumeSession])
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
})
const account = persisted.get('session').currentAccount
resumeSession(account)
}, [resumeSession, _])
}, [_])
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Alf theme={theme}>
<Splash isReady={!isInitialLoad}>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PushNotificationsListener>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</PushNotificationsListener>
</QueryProvider>
</React.Fragment>
</Splash>
<ThemeProvider theme={theme}>
<Splash isReady={isReady}>
<RootSiblingParent>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
</RootSiblingParent>
</Splash>
</ThemeProvider>
</Alf>
</SafeAreaProvider>
)
}
function PushNotificationsListener({children}: {children: React.ReactNode}) {
const queryClient = useQueryClient()
useNotificationsListener(queryClient)
return children
}
function App() {
const [isReady, setReady] = useState(false)
+61 -30
View File
@@ -4,11 +4,16 @@ import 'view/icons'
import React, {useEffect, useState} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {MessagesProvider} from '#/state/messages'
import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
import {readLastActiveAccount} from '#/state/session/util'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
import {ThemeProvider} from 'lib/ThemeContext'
@@ -21,61 +26,87 @@ import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {
Provider as SessionProvider,
SessionAccount,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import * as Toast from 'view/com/util/Toast'
import {ToastContainer} from 'view/com/util/Toast.web'
import {Shell} from 'view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal'
import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events'
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
useIntentHandler()
// init
useEffect(() => {
const account = persisted.get('session').currentAccount
resumeSession(account)
async function onLaunch(account?: SessionAccount) {
try {
if (account) {
await resumeSession(account)
}
} catch (e) {
logger.error(`session: resumeSession failed`, {message: e})
} finally {
setIsReady(true)
}
}
const account = readLastActiveAccount()
onLaunch(account)
}, [resumeSession])
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
})
}, [_])
// wait for session to resume
if (isInitialLoad) return null
if (!isReady) return null
return (
<Alf theme={theme}>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
<ThemeProvider theme={theme}>
<RootSiblingParent>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
<ToastContainer />
</RootSiblingParent>
</ThemeProvider>
</Alf>
)
}
+50
View File
@@ -25,6 +25,7 @@ import {
FeedsTabNavigatorParams,
FlatNavigatorParams,
HomeTabNavigatorParams,
MessagesTabNavigatorParams,
MyProfileTabNavigatorParams,
NotificationsTabNavigatorParams,
SearchTabNavigatorParams,
@@ -46,6 +47,9 @@ import {init as initAnalytics} from './lib/analytics/analytics'
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
import {router} from './routes'
import {MessagesConversationScreen} from './screens/Messages/Conversation'
import {MessagesScreen} from './screens/Messages/List'
import {MessagesSettingsScreen} from './screens/Messages/Settings'
import {useModalControls} from './state/modals'
import {useUnreadNotifications} from './state/queries/notifications/unread'
import {useSession} from './state/session'
@@ -92,6 +96,8 @@ const NotificationsTab =
createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
const MyProfileTab =
createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>()
const MessagesTab =
createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>()
const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
@@ -290,6 +296,16 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => HashtagScreen}
options={{title: title(msg`Hashtag`)}}
/>
<Stack.Screen
name="MessagesConversation"
getComponent={() => MessagesConversationScreen}
options={{title: title(msg`Chat`), requireAuth: true}}
/>
<Stack.Screen
name="MessagesSettings"
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Messaging settings`), requireAuth: true}}
/>
</>
)
}
@@ -323,6 +339,10 @@ function TabsNavigator() {
name="MyProfileTab"
getComponent={() => MyProfileTabNavigator}
/>
<Tab.Screen
name="MessagesTab"
getComponent={() => MessagesTabNavigator}
/>
</Tab.Navigator>
)
}
@@ -429,6 +449,28 @@ function MyProfileTabNavigator() {
)
}
function MessagesTabNavigator() {
const pal = usePalette('default')
return (
<MessagesTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<MessagesTab.Screen
name="Messages"
getComponent={() => MessagesScreen}
options={{requireAuth: true}}
/>
{commonScreens(MessagesTab as typeof HomeTab)}
</MessagesTab.Navigator>
)
}
/**
* The FlatNavigator is used by Web to represent the routes
* in a single ("flat") stack.
@@ -469,6 +511,11 @@ const FlatNavigator = () => {
getComponent={() => NotificationsScreen}
options={{title: title(msg`Notifications`), requireAuth: true}}
/>
<Flat.Screen
name="Messages"
getComponent={() => MessagesScreen}
options={{title: title(msg`Messages`), requireAuth: true}}
/>
{commonScreens(Flat as typeof HomeTab, numUnread)}
</Flat.Navigator>
)
@@ -522,6 +569,9 @@ const LINKING = {
if (name === 'Home') {
return buildStateObject('HomeTab', 'Home', params)
}
if (name === 'Messages') {
return buildStateObject('MessagesTab', 'Messages', params)
}
// if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work
return buildStateObject('HomeTab', name, params, [
{
+18
View File
@@ -840,4 +840,22 @@ export const atoms = {
mr_auto: {
marginRight: 'auto',
},
/*
* Pointer events
*/
pointer_events_none: {
pointerEvents: 'none',
},
pointer_events_auto: {
pointerEvents: 'auto',
},
/*
* Text decoration
*/
underline: {
textDecorationLine: 'underline',
},
strike_through: {
textDecorationLine: 'line-through',
},
} as const
+10 -5
View File
@@ -16,12 +16,14 @@ export function AccountList({
onSelectAccount,
onSelectOther,
otherLabel,
pendingDid,
}: {
onSelectAccount: (account: SessionAccount) => void
onSelectOther: () => void
otherLabel?: string
pendingDid: string | null
}) {
const {isSwitchingAccounts, currentAccount, accounts} = useSession()
const {currentAccount, accounts} = useSession()
const t = useTheme()
const {_} = useLingui()
@@ -31,6 +33,7 @@ export function AccountList({
return (
<View
pointerEvents={pendingDid ? 'none' : 'auto'}
style={[
a.rounded_md,
a.overflow_hidden,
@@ -43,6 +46,7 @@ export function AccountList({
account={account}
onSelect={onSelectAccount}
isCurrentAccount={account.did === currentAccount?.did}
isPendingAccount={account.did === pendingDid}
/>
<View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment>
@@ -50,7 +54,7 @@ export function AccountList({
<Button
testID="chooseAddAccountBtn"
style={[a.flex_1]}
onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
onPress={pendingDid ? undefined : onPressAddAccount}
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
@@ -59,8 +63,7 @@ export function AccountList({
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed || isSwitchingAccounts) &&
t.atoms.bg_contrast_25,
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<Text
style={[
@@ -84,10 +87,12 @@ function AccountItem({
account,
onSelect,
isCurrentAccount,
isPendingAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
isPendingAccount: boolean
}) {
const t = useTheme()
const {_} = useLingui()
@@ -115,7 +120,7 @@ function AccountItem({
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
(hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25,
]}>
<View style={a.p_md}>
<UserAvatar avatar={profile?.avatar} size={24} />
+1 -1
View File
@@ -64,7 +64,7 @@ type NonTextElements =
export type ButtonProps = Pick<
PressableProps,
'disabled' | 'onPress' | 'testID'
'disabled' | 'onPress' | 'testID' | 'onLongPress'
> &
AccessibilityProps &
VariantProps & {
+6
View File
@@ -152,6 +152,12 @@ export function Outer({
[open, close],
)
React.useEffect(() => {
return () => {
setDialogIsOpen(control.id, false)
}
}, [control.id, setDialogIsOpen])
const context = React.useMemo(() => ({close}), [close])
return (
+16 -12
View File
@@ -17,12 +17,14 @@ export function Error({
message,
onRetry,
onGoBack: onGoBackProp,
hideBackButton,
sideBorders = true,
}: {
title?: string
message?: string
onRetry?: () => unknown
onGoBack?: () => unknown
hideBackButton?: boolean
sideBorders?: boolean
}) {
const navigation = useNavigation<NavigationProp>()
@@ -70,7 +72,7 @@ export function Error({
a.text_center,
t.atoms.text_contrast_high,
{lineHeight: 1.4},
gtMobile && {width: 450},
gtMobile ? {width: 450} : [a.w_full, a.px_lg],
]}>
{message}
</Text>
@@ -89,17 +91,19 @@ export function Error({
</ButtonText>
</Button>
)}
<Button
variant="solid"
color={onRetry ? 'secondary' : 'primary'}
label={_(msg`Return to previous page`)}
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
<ButtonText>
<Trans>Go Back</Trans>
</ButtonText>
</Button>
{!hideBackButton && (
<Button
variant="solid"
color={onRetry ? 'secondary' : 'primary'}
label={_(msg`Return to previous page`)}
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
<ButtonText>
<Trans>Go Back</Trans>
</ButtonText>
</Button>
)}
</View>
</CenteredView>
)
+2 -5
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
@@ -13,7 +13,6 @@ import {RichText} from '#/components/RichText'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {sanitizeHandle} from '#/lib/strings/handles'
import {pluralize} from '#/lib/strings/helpers'
type LabelingServiceProps = {
labeler: AppBskyLabelerDefs.LabelerViewDetailed
@@ -69,9 +68,7 @@ export function LikeCount({count}: {count: number}) {
t.atoms.text_contrast_medium,
{fontWeight: '500'},
]}>
<Trans>
Liked by {count} {pluralize(count, 'user')}
</Trans>
<Plural value={count} one="Liked by # user" other="Liked by # users" />
</Text>
)
}
+5 -1
View File
@@ -3,10 +3,12 @@ import {GestureResponderEvent} from 'react-native'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {StackActions, useLinkProps} from '@react-navigation/native'
import {BSKY_DOWNLOAD_URL} from '#/lib/constants'
import {AllNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {
convertBskyAppUrlIfNeeded,
isBskyDownloadUrl,
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
@@ -125,7 +127,9 @@ export function useLink({
(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
const shouldOpenInNewTab = isMetaKey || isMiddleClick
if (
if (isBskyDownloadUrl(href)) {
shareUrl(BSKY_DOWNLOAD_URL)
} else if (
shouldOpenInNewTab ||
href.startsWith('http') ||
href.startsWith('mailto')
+4
View File
@@ -134,6 +134,7 @@ let ListMaybePlaceholder = ({
emptyType = 'page',
onRetry,
onGoBack,
hideBackButton,
sideBorders,
}: {
isLoading: boolean
@@ -146,6 +147,7 @@ let ListMaybePlaceholder = ({
emptyType?: 'page' | 'results'
onRetry?: () => Promise<unknown>
onGoBack?: () => void
hideBackButton?: boolean
sideBorders?: boolean
}): React.ReactNode => {
const t = useTheme()
@@ -179,6 +181,7 @@ let ListMaybePlaceholder = ({
onRetry={onRetry}
onGoBack={onGoBack}
sideBorders={sideBorders}
hideBackButton={hideBackButton}
/>
)
}
@@ -198,6 +201,7 @@ let ListMaybePlaceholder = ({
}
onRetry={onRetry}
onGoBack={onGoBack}
hideBackButton={hideBackButton}
sideBorders={sideBorders}
/>
)
+13 -11
View File
@@ -1,27 +1,29 @@
import React from 'react'
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import flattenReactChildren from 'react-keyed-flatten-children'
import {isNative} from 'platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Text} from '#/components/Typography'
import {Context} from '#/components/Menu/context'
import {
ContextType,
TriggerProps,
ItemProps,
GroupProps,
ItemTextProps,
ItemIconProps,
ItemProps,
ItemTextProps,
TriggerProps,
} from '#/components/Menu/types'
import {Button, ButtonText} from '#/components/Button'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from 'platform/detection'
import {Text} from '#/components/Typography'
export {useDialogControl as useMenuControl} from '#/components/Dialog'
export {
type DialogControlProps as MenuControlProps,
useDialogControl as useMenuControl,
} from '#/components/Dialog'
export function useMemoControlContext() {
return React.useContext(Context)
+24 -13
View File
@@ -1,27 +1,26 @@
/* eslint-disable react/prop-types */
import React from 'react'
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {atoms as a, useTheme, flatten, web} from '#/alf'
import {Text} from '#/components/Typography'
import {Context} from '#/components/Menu/context'
import {
ContextType,
TriggerProps,
ItemProps,
GroupProps,
ItemTextProps,
ItemIconProps,
ItemProps,
ItemTextProps,
RadixPassThroughTriggerProps,
TriggerProps,
} from '#/components/Menu/types'
import {Context} from '#/components/Menu/context'
import {Portal} from '#/components/Portal'
import {Text} from '#/components/Typography'
export function useMenuControl(): Dialog.DialogControlProps {
const id = React.useId()
@@ -135,10 +134,22 @@ export function Trigger({children, label}: TriggerProps) {
},
props: {
...props,
// disable on web, use `onPress`
onPointerDown: () => false,
onPress: () =>
control.isOpen ? control.close() : control.open(),
// No-op override to prevent false positive that interprets mobile scroll as a tap.
// This requires the custom onPress handler below to compensate.
// https://github.com/radix-ui/primitives/issues/1912
onPointerDown: undefined,
onPress: () => {
if (window.event instanceof KeyboardEvent) {
// The onPointerDown hack above is not relevant to this press, so don't do anything.
return
}
// Compensate for the disabled onPointerDown above by triggering it manually.
if (control.isOpen) {
control.close()
} else {
control.open()
}
},
onFocus: onFocus,
onBlur: onBlur,
onMouseEnter,
+10 -3
View File
@@ -34,10 +34,17 @@ export function createPortalGroup() {
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const contextValue = React.useMemo(
() => ({
outlet,
append,
remove,
}),
[outlet, append, remove],
)
return (
<Context.Provider value={{outlet, append, remove}}>
{props.children}
</Context.Provider>
<Context.Provider value={contextValue}>{props.children}</Context.Provider>
)
}
+37 -21
View File
@@ -2,14 +2,13 @@ import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
import {msg, Trans} from '@lingui/macro'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {pluralize} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/queries/preferences'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useProfileShadow} from 'state/cache/profile-shadow'
@@ -72,10 +71,10 @@ type Action =
| 'unhovered-long-enough'
| 'finished-animating-hide'
const SHOW_DELAY = 400
const SHOW_DELAY = 500
const SHOW_DURATION = 300
const HIDE_DELAY = 150
const HIDE_DURATION = 150
const HIDE_DURATION = 200
export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
const {refs, floatingStyles} = useFloating({
@@ -244,12 +243,20 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
}
}, [prefetchProfileQuery, props.did])
const onPointerEnterTarget = React.useCallback(() => {
const didFireHover = React.useRef(false)
const onPointerMoveTarget = React.useCallback(() => {
prefetchIfNeeded()
dispatch('hovered-target')
// Conceptually we want something like onPointerEnter,
// but we want to ignore entering only due to scrolling.
// So instead we hover on the first onPointerMove.
if (!didFireHover.current) {
didFireHover.current = true
dispatch('hovered-target')
}
}, [prefetchIfNeeded])
const onPointerLeaveTarget = React.useCallback(() => {
didFireHover.current = false
dispatch('unhovered-target')
}, [])
@@ -280,7 +287,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
return (
<div
ref={refs.setReference}
onPointerEnter={onPointerEnterTarget}
onPointerMove={onPointerMoveTarget}
onPointerLeave={onPointerLeaveTarget}
onMouseUp={onPress}
style={{
@@ -363,7 +370,14 @@ function Inner({
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
const following = formatCount(profile.followsCount || 0)
const followers = formatCount(profile.followersCount || 0)
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
const pluralizedFollowers = plural(profile.followersCount || 0, {
one: 'follower',
other: 'followers',
})
const pluralizedFollowings = plural(profile.followsCount || 0, {
one: 'following',
other: 'following',
})
const profileURL = makeProfileLink({
did: profile.did,
handle: profile.handle,
@@ -390,7 +404,9 @@ function Inner({
color={profileShadow.viewer?.following ? 'secondary' : 'primary'}
variant="solid"
label={
profileShadow.viewer?.following ? _('Following') : _('Follow')
profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)
}
style={[a.rounded_full]}
onPress={profileShadow.viewer?.following ? unfollow : follow}>
@@ -399,7 +415,9 @@ function Inner({
icon={profileShadow.viewer?.following ? Check : Plus}
/>
<ButtonText>
{profileShadow.viewer?.following ? _('Following') : _('Follow')}
{profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)}
</ButtonText>
</Button>
)}
@@ -426,22 +444,20 @@ function Inner({
label={`${followers} ${pluralizedFollowers}`}
style={[t.atoms.text]}
onPress={hide}>
<Trans>
<Text style={[a.text_md, a.font_bold]}>{followers} </Text>
<Text style={[t.atoms.text_contrast_medium]}>
{pluralizedFollowers}
</Text>
</Trans>
<Text style={[a.text_md, a.font_bold]}>{followers} </Text>
<Text style={[t.atoms.text_contrast_medium]}>
{pluralizedFollowers}
</Text>
</InlineLinkText>
<InlineLinkText
to={makeProfileLink(profile, 'follows')}
label={_(msg`${following} following`)}
style={[t.atoms.text]}
onPress={hide}>
<Trans>
<Text style={[a.text_md, a.font_bold]}>{following} </Text>
<Text style={[t.atoms.text_contrast_medium]}>following</Text>
</Trans>
<Text style={[a.text_md, a.font_bold]}>{following} </Text>
<Text style={[t.atoms.text_contrast_medium]}>
{pluralizedFollowings}
</Text>
</InlineLinkText>
</View>
+8 -2
View File
@@ -43,7 +43,9 @@ export function Outer({
<Dialog.ScrollableInner
accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId}
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
style={[
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
]}>
{children}
</Dialog.ScrollableInner>
</Context.Provider>
@@ -60,12 +62,16 @@ export function TitleText({children}: React.PropsWithChildren<{}>) {
)
}
export function DescriptionText({children}: React.PropsWithChildren<{}>) {
export function DescriptionText({
children,
selectable,
}: React.PropsWithChildren<{selectable?: boolean}>) {
const t = useTheme()
const {descriptionId} = React.useContext(Context)
return (
<Text
nativeID={descriptionId}
selectable={selectable}
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high, a.pb_lg]}>
{children}
</Text>
+3 -1
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {ReportOption} from '#/lib/moderation/useReportOptions'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, native, useTheme} from '#/alf'
@@ -35,6 +35,7 @@ export function SubmitView({
}) {
const t = useTheme()
const {_} = useLingui()
const {getAgent} = useAgent()
const [details, setDetails] = React.useState<string>('')
const [submitting, setSubmitting] = React.useState<boolean>(false)
const [selectedServices, setSelectedServices] = React.useState<string[]>([
@@ -90,6 +91,7 @@ export function SubmitView({
selectedServices,
onSubmitComplete,
setError,
getAgent,
])
return (
+19 -11
View File
@@ -1,4 +1,5 @@
import React from 'react'
import {TextStyle} from 'react-native'
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -26,6 +27,7 @@ export function RichText({
enableTags = false,
authorHandle,
onLinkPress,
interactiveStyle,
}: TextStyleProp &
Pick<TextProps, 'selectable'> & {
value: RichTextAPI | string
@@ -35,13 +37,22 @@ export function RichText({
enableTags?: boolean
authorHandle?: string
onLinkPress?: LinkProps['onPress']
interactiveStyle?: TextStyle
}) {
const richText = React.useMemo(
() =>
value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
[value],
)
const styles = [a.leading_snug, flatten(style)]
const flattenedStyle = flatten(style)
const plainStyles = [a.leading_snug, flattenedStyle]
const interactiveStyles = [
a.leading_snug,
a.pointer_events_auto,
flatten(interactiveStyle),
flattenedStyle,
]
const {text, facets} = richText
@@ -67,7 +78,7 @@ export function RichText({
<Text
selectable={selectable}
testID={testID}
style={styles}
style={plainStyles}
numberOfLines={numberOfLines}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
@@ -93,7 +104,7 @@ export function RichText({
<InlineLinkText
selectable={selectable}
to={`/profile/${mention.did}`}
style={[...styles, {pointerEvents: 'auto'}]}
style={interactiveStyles}
// @ts-ignore TODO
dataSet={WORD_WRAP}
onPress={onLinkPress}>
@@ -110,7 +121,7 @@ export function RichText({
selectable={selectable}
key={key}
to={link.uri}
style={[...styles, {pointerEvents: 'auto'}]}
style={interactiveStyles}
// @ts-ignore TODO
dataSet={WORD_WRAP}
shareOnLongPress
@@ -130,7 +141,7 @@ export function RichText({
key={key}
text={segment.text}
tag={tag.tag}
style={styles}
style={interactiveStyles}
selectable={selectable}
authorHandle={authorHandle}
/>,
@@ -145,7 +156,7 @@ export function RichText({
<Text
selectable={selectable}
testID={testID}
style={styles}
style={plainStyles}
numberOfLines={numberOfLines}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
@@ -219,19 +230,16 @@ function RichTextTag({
onFocus={onFocus}
onBlur={onBlur}
style={[
style,
{
pointerEvents: 'auto',
color: t.palette.primary_500,
},
web({
cursor: 'pointer',
}),
{color: t.palette.primary_500},
(hovered || focused || pressed) && {
...web({outline: 0}),
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
},
style,
]}>
{text}
</Text>
+2 -2
View File
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo, useRef, useState} from 'react'
import {Keyboard, TextInput, View} from 'react-native'
import {TextInput, View} from 'react-native'
import {Image} from 'expo-image'
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {msg, Trans} from '@lingui/macro'
@@ -216,7 +216,7 @@ function GifList({
keyExtractor={(item: Gif) => item.id}
// @ts-expect-error web only
style={isWeb && {minHeight: '100vh'}}
onScrollBeginDrag={() => Keyboard.dismiss()}
keyboardDismissMode="on-drag"
ListFooterComponent={
hasData ? (
<ListFooter
+2 -2
View File
@@ -37,12 +37,12 @@ export function MutedWordsDialog() {
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<MutedWordsInner control={control} />
<MutedWordsInner />
</Dialog.Outer>
)
}
function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
function MutedWordsInner() {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
+2 -1
View File
@@ -18,7 +18,7 @@ export function SwitchAccountDialog({
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
const {onPressSwitchAccount} = useAccountSwitcher()
const {onPressSwitchAccount, pendingDid} = useAccountSwitcher()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onSelectAccount = useCallback(
@@ -54,6 +54,7 @@ export function SwitchAccountDialog({
onSelectAccount={onSelectAccount}
onSelectOther={onPressAddAccount}
otherLabel={_(msg`Add account`)}
pendingDid={pendingDid}
/>
</View>
</Dialog.ScrollableInner>
+85
View File
@@ -0,0 +1,85 @@
import React from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import Animated, {
cancelAnimation,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {HITSLOP_10} from 'lib/constants'
import {useHaptics} from 'lib/haptics'
import {atoms as a} from '#/alf'
import {MessageMenu} from '#/components/dms/MessageMenu'
import {useMenuControl} from '#/components/Menu'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
export function ActionsWrapper({
message,
isFromSelf,
children,
}: {
message: ChatBskyConvoDefs.MessageView
isFromSelf: boolean
children: React.ReactNode
}) {
const playHaptic = useHaptics()
const menuControl = useMenuControl()
const scale = useSharedValue(1)
const animationDidComplete = useSharedValue(false)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
}))
// Reanimated's `runOnJS` doesn't like refs, so we can't use `runOnJS(menuControl.open)()`. Instead, we'll use this
// function
const open = React.useCallback(() => {
Keyboard.dismiss()
menuControl.open()
}, [menuControl])
const shrink = React.useCallback(() => {
'worklet'
cancelAnimation(scale)
scale.value = withTiming(1, {duration: 200}, () => {
animationDidComplete.value = false
})
}, [animationDidComplete, scale])
const grow = React.useCallback(() => {
'worklet'
scale.value = withTiming(1.05, {duration: 450}, finished => {
if (!finished) return
animationDidComplete.value = true
runOnJS(playHaptic)()
runOnJS(open)()
shrink()
})
}, [scale, animationDidComplete, playHaptic, shrink, open])
return (
<View
style={[
{
maxWidth: '65%',
},
isFromSelf ? a.self_end : a.self_start,
]}>
<AnimatedPressable
style={animatedStyle}
unstable_pressDelay={200}
onPressIn={grow}
onTouchEnd={shrink}
hitSlop={HITSLOP_10}>
{children}
</AnimatedPressable>
<MessageMenu message={message} control={menuControl} hideTrigger={true} />
</View>
)
}
+80
View File
@@ -0,0 +1,80 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {atoms as a} from '#/alf'
import {MessageMenu} from '#/components/dms/MessageMenu'
import {useMenuControl} from '#/components/Menu'
export function ActionsWrapper({
message,
isFromSelf,
children,
}: {
message: ChatBskyConvoDefs.MessageView
isFromSelf: boolean
children: React.ReactNode
}) {
const menuControl = useMenuControl()
const viewRef = React.useRef(null)
const [showActions, setShowActions] = React.useState(false)
const onMouseEnter = React.useCallback(() => {
setShowActions(true)
}, [])
const onMouseLeave = React.useCallback(() => {
setShowActions(false)
}, [])
// We need to handle the `onFocus` separately because we want to know if there is a related target (the element
// that is losing focus). If there isn't that means the focus is coming from a dropdown that is now closed.
const onFocus = React.useCallback<React.FocusEventHandler>(e => {
if (e.nativeEvent.relatedTarget == null) return
setShowActions(true)
}, [])
return (
<View
// @ts-expect-error web only
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onMouseLeave}
style={StyleSheet.flatten([a.flex_1, a.flex_row])}
ref={viewRef}>
{isFromSelf && (
<View
style={[
a.mr_xl,
a.justify_center,
{
marginLeft: 'auto',
},
]}>
<MessageMenu
message={message}
control={menuControl}
triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
/>
</View>
)}
<View
style={{
maxWidth: '65%',
}}>
{children}
</View>
{!isFromSelf && (
<View style={[a.flex_row, a.align_center, a.ml_xl]}>
<MessageMenu
message={message}
control={menuControl}
triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
/>
</View>
)}
</View>
)
}
+207
View File
@@ -0,0 +1,207 @@
import React, {useCallback} from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {
useMuteConvo,
useUnmuteConvo,
} from '#/state/queries/messages/mute-conversation'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
let ConvoMenu = ({
convo,
profile,
onUpdateConvo,
control,
currentScreen,
showMarkAsRead,
hideTrigger,
triggerOpacity,
}: {
convo: ChatBskyConvoDefs.ConvoView
profile: AppBskyActorDefs.ProfileViewBasic
onUpdateConvo?: (convo: ChatBskyConvoDefs.ConvoView) => void
control?: Menu.MenuControlProps
currentScreen: 'list' | 'conversation'
showMarkAsRead?: boolean
hideTrigger?: boolean
triggerOpacity?: number
}): React.ReactNode => {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const t = useTheme()
const leaveConvoControl = Prompt.usePromptControl()
const {mutate: markAsRead} = useMarkAsReadMutation()
const onNavigateToProfile = useCallback(() => {
navigation.navigate('Profile', {name: profile.did})
}, [navigation, profile.did])
const {mutate: muteConvo} = useMuteConvo(convo.id, {
onSuccess: data => {
onUpdateConvo?.(data.convo)
Toast.show(_(msg`Chat muted`))
},
onError: () => {
Toast.show(_(msg`Could not mute chat`))
},
})
const {mutate: unmuteConvo} = useUnmuteConvo(convo.id, {
onSuccess: data => {
onUpdateConvo?.(data.convo)
Toast.show(_(msg`Chat unmuted`))
},
onError: () => {
Toast.show(_(msg`Could not unmute chat`))
},
})
const {mutate: leaveConvo} = useLeaveConvo(convo.id, {
onSuccess: () => {
if (currentScreen === 'conversation') {
navigation.replace('Messages')
}
},
onError: () => {
Toast.show(_(msg`Could not leave chat`))
},
})
return (
<>
<Menu.Root control={control}>
{!hideTrigger && (
<View style={{opacity: triggerOpacity}}>
<Menu.Trigger label={_(msg`Chat settings`)}>
{({props, state}) => (
<Pressable
{...props}
onPress={() => {
Keyboard.dismiss()
// eslint-disable-next-line react/prop-types -- eslint is confused by the name `props`
props.onPress()
}}
style={[
a.p_sm,
a.rounded_full,
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
// make sure pfp is in the middle
{marginLeft: -10},
]}>
<DotsHorizontal size="md" style={t.atoms.text} />
</Pressable>
)}
</Menu.Trigger>
</View>
)}
<Menu.Outer>
<Menu.Group>
{showMarkAsRead && (
<Menu.Item
label={_(msg`Mark as read`)}
onPress={() =>
markAsRead({
convoId: convo.id,
})
}>
<Menu.ItemText>
<Trans>Mark as read</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Bubble} />
</Menu.Item>
)}
<Menu.Item
label={_(msg`Go to user's profile`)}
onPress={onNavigateToProfile}>
<Menu.ItemText>
<Trans>Go to profile</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Person} />
</Menu.Item>
<Menu.Item
label={_(msg`Mute notifications`)}
onPress={() => (convo?.muted ? unmuteConvo() : muteConvo())}>
<Menu.ItemText>
{convo?.muted ? (
<Trans>Unmute notifications</Trans>
) : (
<Trans>Mute notifications</Trans>
)}
</Menu.ItemText>
<Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
{/* TODO(samuel): implement these */}
<Menu.Group>
<Menu.Item
label={_(msg`Block account`)}
onPress={() => {}}
disabled>
<Menu.ItemText>
<Trans>Block account</Trans>
</Menu.ItemText>
<Menu.ItemIcon
icon={profile.viewer?.blocking ? PersonCheck : PersonX}
/>
</Menu.Item>
<Menu.Item
label={_(msg`Report account`)}
onPress={() => {}}
disabled>
<Menu.ItemText>
<Trans>Report account</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Flag} />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.Item
label={_(msg`Leave conversation`)}
onPress={leaveConvoControl.open}>
<Menu.ItemText>
<Trans>Leave conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeft} />
</Menu.Item>
</Menu.Group>
</Menu.Outer>
</Menu.Root>
<Prompt.Basic
control={leaveConvoControl}
title={_(msg`Leave conversation`)}
description={_(
msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for other participants.`,
)}
confirmButtonCta={_(msg`Leave`)}
confirmButtonColor="negative"
onConfirm={() => leaveConvo()}
/>
</>
)
}
ConvoMenu = React.memo(ConvoMenu)
export {ConvoMenu}
+206
View File
@@ -0,0 +1,206 @@
import React, {useCallback, useMemo, useRef} from 'react'
import {LayoutAnimation, StyleProp, TextStyle, View} from 'react-native'
import {RichText as RichTextAPI} from '@atproto/api'
import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {TimeElapsed} from 'view/com/util/TimeElapsed'
import {atoms as a, useTheme} from '#/alf'
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
import {Text} from '#/components/Typography'
import {RichText} from '../RichText'
let MessageItem = ({
item,
next,
pending,
}: {
item: ChatBskyConvoDefs.MessageView
next:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null
pending?: boolean
}): React.ReactNode => {
const t = useTheme()
const {currentAccount} = useSession()
const isFromSelf = item.sender?.did === currentAccount?.did
const isNextFromSelf =
ChatBskyConvoDefs.isMessageView(next) &&
next.sender?.did === currentAccount?.did
const isLastInGroup = useMemo(() => {
// TODO this means it's a placeholder. Let's figure out the right way to do this though!
if (item.id.length > 13) {
return false
}
// if the next message is from a different sender, then it's the last in the group
if (isFromSelf ? !isNextFromSelf : isNextFromSelf) {
return true
}
// or, if there's a 3 minute gap between this message and the next
if (ChatBskyConvoDefs.isMessageView(next)) {
const thisDate = new Date(item.sentAt)
const nextDate = new Date(next.sentAt)
const diff = nextDate.getTime() - thisDate.getTime()
// 3 minutes
return diff > 3 * 60 * 1000
}
return true
}, [item, next, isFromSelf, isNextFromSelf])
const lastInGroupRef = useRef(isLastInGroup)
if (lastInGroupRef.current !== isLastInGroup) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
lastInGroupRef.current = isLastInGroup
}
const pendingColor =
t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800
const rt = useMemo(() => {
return new RichTextAPI({text: item.text, facets: item.facets})
}, [item.text, item.facets])
return (
<View>
<ActionsWrapper isFromSelf={isFromSelf} message={item}>
<View
style={[
a.py_sm,
a.my_2xs,
a.rounded_md,
{
paddingLeft: 14,
paddingRight: 14,
backgroundColor: isFromSelf
? pending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
borderRadius: 17,
},
isFromSelf
? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
: {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
]}>
<RichText
value={rt}
style={[
a.text_md,
a.leading_snug,
isFromSelf && {color: t.palette.white},
pending && t.name !== 'light' && {color: t.palette.primary_300},
]}
interactiveStyle={a.underline}
enableTags
/>
</View>
</ActionsWrapper>
<MessageItemMetadata
message={item}
isLastInGroup={isLastInGroup}
style={isFromSelf ? a.text_right : a.text_left}
/>
</View>
)
}
MessageItem = React.memo(MessageItem)
export {MessageItem}
let MessageItemMetadata = ({
message,
isLastInGroup,
style,
}: {
message: ChatBskyConvoDefs.MessageView
isLastInGroup: boolean
style: StyleProp<TextStyle>
}): React.ReactNode => {
const t = useTheme()
const {_} = useLingui()
const relativeTimestamp = useCallback(
(timestamp: string) => {
const date = new Date(timestamp)
const now = new Date()
const time = new Intl.DateTimeFormat(undefined, {
hour: 'numeric',
minute: 'numeric',
hour12: true,
}).format(date)
const diff = now.getTime() - date.getTime()
// if under 1 minute
if (diff < 1000 * 60) {
return _(msg`Now`)
}
// if in the last day
if (localDateString(now) === localDateString(date)) {
return time
}
// if yesterday
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
if (localDateString(yesterday) === localDateString(date)) {
return _(msg`Yesterday, ${time}`)
}
return new Intl.DateTimeFormat(undefined, {
hour: 'numeric',
minute: 'numeric',
hour12: true,
day: 'numeric',
month: 'numeric',
year: 'numeric',
}).format(date)
},
[_],
)
if (!isLastInGroup) {
return null
}
return (
<TimeElapsed timestamp={message.sentAt} timeToString={relativeTimestamp}>
{({timeElapsed}) => (
<Text
style={[
t.atoms.text_contrast_medium,
a.text_xs,
a.mt_2xs,
a.mb_lg,
style,
]}>
{timeElapsed}
</Text>
)}
</TimeElapsed>
)
}
MessageItemMetadata = React.memo(MessageItemMetadata)
function localDateString(date: Date) {
// can't use toISOString because it should be in local time
const mm = date.getMonth()
const dd = date.getDate()
const yyyy = date.getFullYear()
// not padding with 0s because it's not necessary, it's just used for comparison
return `${yyyy}-${mm}-${dd}`
}
+140
View File
@@ -0,0 +1,140 @@
import React from 'react'
import {LayoutAnimation, Pressable, View} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isWeb} from 'platform/detection'
import {useConvo} from 'state/messages/convo'
import {ConvoStatus} from 'state/messages/convo/types'
import {useSession} from 'state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {usePromptControl} from '#/components/Prompt'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipboard'
export let MessageMenu = ({
message,
control,
triggerOpacity,
}: {
hideTrigger?: boolean
triggerOpacity?: number
message: ChatBskyConvoDefs.MessageView
control: Menu.MenuControlProps
}): React.ReactNode => {
const {_} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const convo = useConvo()
const deleteControl = usePromptControl()
const retryDeleteControl = usePromptControl()
const isFromSelf = message.sender?.did === currentAccount?.did
const onCopyPostText = React.useCallback(() => {
// use when we have rich text
// const str = richTextToString(richText, true)
Clipboard.setStringAsync(message.text)
Toast.show(_(msg`Copied to clipboard`))
}, [_, message.text])
const onDelete = React.useCallback(() => {
if (convo.status !== ConvoStatus.Ready) return
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
convo
.deleteMessage(message.id)
.then(() => Toast.show(_(msg`Message deleted`)))
.catch(() => retryDeleteControl.open())
}, [_, convo, message.id, retryDeleteControl])
const onReport = React.useCallback(() => {
// TODO report the message
}, [])
return (
<>
<Menu.Root control={control}>
{isWeb && (
<View style={{opacity: triggerOpacity}}>
<Menu.Trigger label={_(msg`Chat settings`)}>
{({props, state}) => (
<Pressable
{...props}
style={[
a.p_sm,
a.rounded_full,
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
]}>
<DotsHorizontal size="md" style={t.atoms.text} />
</Pressable>
)}
</Menu.Trigger>
</View>
)}
<Menu.Outer>
<Menu.Group>
<Menu.Item
testID="messageDropdownCopyBtn"
label={_(msg`Copy message text`)}
onPress={onCopyPostText}>
<Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.Item
testID="messageDropdownDeleteBtn"
label={_(msg`Delete message for me`)}
onPress={deleteControl.open}>
<Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
<Menu.ItemIcon icon={Trash} position="right" />
</Menu.Item>
{!isFromSelf && (
<Menu.Item
testID="messageDropdownReportBtn"
label={_(msg`Report message`)}
onPress={onReport}>
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
<Menu.ItemIcon icon={Warning} position="right" />
</Menu.Item>
)}
</Menu.Group>
</Menu.Outer>
</Menu.Root>
<Prompt.Basic
control={deleteControl}
title={_(msg`Delete message`)}
description={_(
msg`Are you sure you want to delete this message? The message will be deleted for you, but not for other participants.`,
)}
confirmButtonCta={_(msg`Delete`)}
confirmButtonColor="negative"
onConfirm={onDelete}
/>
<Prompt.Basic
control={retryDeleteControl}
title={_(msg`Failed to delete message`)}
description={_(
msg`An error occurred while trying to delete the message. Please try again.`,
)}
confirmButtonCta={_(msg`Retry`)}
confirmButtonColor="negative"
onConfirm={onDelete}
/>
</>
)
}
MessageMenu = React.memo(MessageMenu)
+255
View File
@@ -0,0 +1,255 @@
import React, {useCallback, useMemo, useRef, useState} from 'react'
import {Keyboard, View} from 'react-native'
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Button} from '../Button'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope'
import {ListMaybePlaceholder} from '../Lists'
import {Text} from '../Typography'
export function NewChat({
control,
onNewChat,
}: {
control: Dialog.DialogControlProps
onNewChat: (chatId: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onNewChat(data.convo.id)
},
onError: error => {
Toast.show(error.message)
},
})
const onCreateChat = useCallback(
(did: string) => {
control.close(() => createChat([did]))
},
[control, createChat],
)
return (
<>
<FAB
testID="newChatFAB"
onPress={control.open}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
accessibilityHint=""
/>
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<Dialog.Handle />
<SearchablePeopleList onCreateChat={onCreateChat} />
</Dialog.Outer>
</>
)
}
function SearchablePeopleList({
onCreateChat,
}: {
onCreateChat: (did: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const control = Dialog.useDialogContext()
const listRef = useRef<BottomSheetFlatListMethods>(null)
const [searchText, setSearchText] = useState('')
const {
data: actorAutocompleteData,
isFetching,
isError,
refetch,
} = useActorAutocompleteQuery(searchText, true)
const renderItem = useCallback(
({item: profile}: {item: AppBskyActorDefs.ProfileView}) => {
if (!moderationOpts) return null
const moderation = moderateProfile(profile, moderationOpts)
return (
<Button
label={profile.displayName || sanitizeHandle(profile.handle)}
onPress={() => onCreateChat(profile.did)}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.px_md,
a.py_sm,
a.gap_md,
a.align_center,
a.flex_row,
a.rounded_sm,
pressed
? t.atoms.bg_contrast_25
: hovered
? t.atoms.bg_contrast_50
: t.atoms.bg,
]}>
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
type={profile.associated?.labeler ? 'labeler' : 'user'}
/>
<View style={{flex: 1}}>
<Text
style={[t.atoms.text, a.font_bold, a.leading_snug]}
numberOfLines={1}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
</Text>
<Text style={t.atoms.text_contrast_high} numberOfLines={1}>
{sanitizeHandle(profile.handle, '@')}
</Text>
</View>
</View>
)}
</Button>
)
},
[
moderationOpts,
onCreateChat,
t.atoms.bg_contrast_25,
t.atoms.bg_contrast_50,
t.atoms.bg,
t.atoms.text,
t.atoms.text_contrast_high,
],
)
const listHeader = useMemo(() => {
return (
<View style={[a.relative, a.mb_lg]}>
{/* cover top corners */}
<View
style={[
a.absolute,
a.inset_0,
{
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
},
t.atoms.bg,
]}
/>
<Dialog.Close />
<Text
style={[
a.text_2xl,
a.font_bold,
a.leading_tight,
a.pb_lg,
web(a.pt_lg),
]}>
<Trans>Start a new chat</Trans>
</Text>
<TextField.Root>
<TextField.Icon icon={Search} />
<Dialog.Input
label={_(msg`Search profiles`)}
placeholder={_(msg`Search`)}
value={searchText}
onChangeText={text => {
setSearchText(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}}
returnKeyType="search"
clearButtonMode="while-editing"
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
control.close()
}
}}
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
autoFocus
/>
</TextField.Root>
</View>
)
}, [t.atoms.bg, _, control, searchText])
return (
<Dialog.InnerFlatList
ref={listRef}
data={actorAutocompleteData}
renderItem={renderItem}
ListHeaderComponent={
<>
{listHeader}
{searchText.length === 0 ? (
<View style={[a.pt_4xl, a.align_center, a.px_lg]}>
<Envelope width={64} fill={t.palette.contrast_200} />
<Text
style={[
a.text_lg,
a.text_center,
a.mt_md,
t.atoms.text_contrast_low,
]}>
<Trans>Search for someone to start a conversation with.</Trans>
</Text>
</View>
) : (
!actorAutocompleteData?.length && (
<ListMaybePlaceholder
isLoading={isFetching}
isError={isError}
onRetry={refetch}
hideBackButton={true}
emptyType="results"
sideBorders={false}
emptyMessage={
isError
? _(msg`No search results found for "${searchText}".`)
: _(msg`Could not load profiles. Please try again later.`)
}
/>
)
)}
</>
}
stickyHeaderIndices={[0]}
keyExtractor={(item: AppBskyActorDefs.ProfileView) => item.did}
// @ts-expect-error web only
style={isWeb && {minHeight: '100vh'}}
onScrollBeginDrag={() => Keyboard.dismiss()}
/>
)
}
+17
View File
@@ -0,0 +1,17 @@
import {useCallback, useRef} from 'react'
import {useFocusEffect} from '@react-navigation/native'
export function useRefreshOnFocus<T>(refetch: () => Promise<T>) {
const firstTimeRef = useRef(true)
useFocusEffect(
useCallback(() => {
if (firstTimeRef.current) {
firstTimeRef.current = false
return
}
refetch()
}, [refetch]),
)
}
+3 -2
View File
@@ -1,12 +1,13 @@
import React from 'react'
import {RichText as RichTextAPI} from '@atproto/api'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
export function useRichText(text: string): [RichTextAPI, boolean] {
const [prevText, setPrevText] = React.useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
const {getAgent} = useAgent()
if (text !== prevText) {
setPrevText(text)
setRawRT(new RichTextAPI({text}))
@@ -27,7 +28,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
return () => {
ignore = true
}
}, [text])
}, [text, getAgent])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
}
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ArrowBoxLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.293 3.293A1 1 0 0 1 4 3h7.25a1 1 0 1 1 0 2H5v14h6.25a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4a1 1 0 0 1 .293-.707Zm11.5 3.5a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Bell_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4.216 8.815a7.853 7.853 0 0 1 15.568 0l1.207 9.053A1 1 0 0 1 20 19h-3.354c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM12 4a5.853 5.853 0 0 0-5.802 5.08L5.142 17h13.716l-1.056-7.92A5.853 5.853 0 0 0 12 4Z',
})
export const Bell_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M12 2a7.853 7.853 0 0 0-7.784 6.815l-1.207 9.053A1 1 0 0 0 4 19h3.354c.904 1.748 2.607 3 4.646 3 2.039 0 3.742-1.252 4.646-3H20a1 1 0 0 0 .991-1.132l-1.207-9.053A7.853 7.853 0 0 0 12 2Zm2.222 17H9.778c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Bell2_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4.702 8.943a7.307 7.307 0 0 1 14.596 0l.19 3.798 1.321 2.641A1.809 1.809 0 0 1 19.191 18H16.9a5.002 5.002 0 0 1-9.8 0H4.809a1.809 1.809 0 0 1-1.618-2.618l1.32-2.641.19-3.798ZM9.17 18a3.001 3.001 0 0 0 5.658 0H9.171ZM12 4a5.307 5.307 0 0 0-5.3 5.042l-.19 3.798a2 2 0 0 1-.21.795L5.119 16h13.764l-1.183-2.365a2 2 0 0 1-.208-.795l-.19-3.798A5.308 5.308 0 0 0 12 4Z',
})
export const Bell2_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M12 2a7.307 7.307 0 0 0-7.298 6.943l-.19 3.798-1.321 2.641A1.809 1.809 0 0 0 4.809 18H7.1a5.002 5.002 0 0 0 9.8 0h2.291a1.809 1.809 0 0 0 1.618-2.618l-1.32-2.641-.19-3.798A7.308 7.308 0 0 0 12 2Zm0 18a3.001 3.001 0 0 1-2.83-2h5.66A3.001 3.001 0 0 1 12 20Z',
})
+8
View File
@@ -3,3 +3,11 @@ import {createSinglePathSVG} from './TEMPLATE'
export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
})
export const Bubble_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M2.002 6a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2h-1a3 3 0 0 1-3-3V6Zm3-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h7a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-14Z',
})
export const Bubble_Stroke2_Corner3_Rounded = createSinglePathSVG({
path: 'M2.002 7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2a4 4 0 0 1-4-4V7Zm4-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h6a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-12Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const BulletList_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Zm-6 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z',
})
export const BulletList_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm0 10a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm10-1a1 1 0 1 0 0 2h7a1 1 0 1 0 0-2h-7Zm-1-9a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const EditBig_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M17.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-9 9A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l9-9ZM10 12.414V14h1.586l8-8L18 4.414l-8 8ZM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Z',
})
+4
View File
@@ -4,6 +4,10 @@ export const EmojiSad_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6.343 6.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 6.343 6.343ZM19.071 4.93c-3.905-3.905-10.237-3.905-14.142 0-3.905 3.905-3.905 10.237 0 14.142 3.905 3.905 10.237 3.905 14.142 0 3.905-3.905 3.905-10.237 0-14.142Zm-3.537 9.535a5 5 0 0 0-7.07 0 1 1 0 1 0 1.413 1.415 3 3 0 0 1 4.243 0 1 1 0 0 0 1.414-1.415ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5ZM9.25 11c.69 0 1.25-.672 1.25-1.5S9.94 8 9.25 8 8 8.672 8 9.5 8.56 11 9.25 11Z',
})
export const EmojiSmile_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M17.657 6.343A8 8 0 1 0 6.343 17.657 8 8 0 0 0 17.657 6.343ZM4.929 4.93c3.905-3.905 10.237-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0-3.905-3.905-3.905-10.237 0-14.142Zm3.536 9.192a1 1 0 0 1 1.414 0 3 3 0 0 0 4.243 0 1 1 0 0 1 1.414 1.415 5 5 0 0 1-7.071 0 1 1 0 0 1 0-1.415ZM10.5 9.5c0 .828-.56 1.5-1.25 1.5S8 10.328 8 9.5 8.56 8 9.25 8s1.25.672 1.25 1.5ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5Z',
})
export const EmojiArc_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-5a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm-5.894 7.803a1 1 0 0 1 1.341-.447c1.719.859 3.387.859 5.106 0a1 1 0 1 1 .894 1.788c-2.281 1.141-4.613 1.141-6.894 0a1 1 0 0 1-.447-1.341Z',
})
+4
View File
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Envelope_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z',
})
export const Envelope_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 11.708 2.654 4.06A.998.998 0 0 1 3 4h18c.122 0 .238.022.346.061L12 11.708ZM2 19V6.11l9.367 7.664a1 1 0 0 0 1.266 0L22 6.11V19a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z',
})
+4
View File
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Hashtag_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M9.124 3.008a1 1 0 0 1 .868 1.116L9.632 7h5.985l.39-3.124a1 1 0 0 1 1.985.248L17.632 7H20a1 1 0 1 1 0 2h-2.617l-.75 6H20a1 1 0 1 1 0 2h-3.617l-.39 3.124a1 1 0 1 1-1.985-.248l.36-2.876H8.382l-.39 3.124a1 1 0 1 1-1.985-.248L6.368 17H4a1 1 0 1 1 0-2h2.617l.75-6H4a1 1 0 1 1 0-2h3.617l.39-3.124a1 1 0 0 1 1.117-.868ZM9.383 9l-.75 6h5.984l.75-6H9.383Z',
})
export const Hashtag_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M9.186 2.512a1.5 1.5 0 0 0-1.674 1.302L7.176 6.5H4a1.5 1.5 0 1 0 0 3h2.8l-.624 5H4a1.5 1.5 0 0 0 0 3h1.8l-.288 2.314a1.5 1.5 0 1 0 2.976.372l.336-2.686h4.977l-.29 2.314a1.5 1.5 0 1 0 2.977.372l.336-2.686H20a1.5 1.5 0 0 0 0-3h-2.8l.624-5H20a1.5 1.5 0 0 0 0-3h-1.8l.288-2.314a1.5 1.5 0 1 0-2.976-.372L15.176 6.5h-4.977l.29-2.314a1.5 1.5 0 0 0-1.303-1.674ZM9.2 14.5l.625-5h4.977l-.625 5H9.199Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const HomeOpen_Stoke2_Corner0_Rounded = createSinglePathSVG({
path: 'M11.37 1.724a1 1 0 0 1 1.26 0l8 6.5A1 1 0 0 1 21 9v11a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-5h-2v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 .37-.776l8-6.5ZM5 9.476V19h4v-5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5h4V9.476l-7-5.688-7 5.688Z',
})
export const HomeOpen_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M12.63 1.724a1 1 0 0 0-1.26 0l-8 6.5A1 1 0 0 0 3 9v11a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-6h4v6a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V9a1 1 0 0 0-.37-.776l-8-6.5Z',
})
+6
View File
@@ -0,0 +1,6 @@
import {createSinglePathSVG} from './TEMPLATE'
export const MagnifyingGlass_Filled_Stroke2_Corner0_Rounded =
createSinglePathSVG({
path: 'M5 11a6 6 0 1 1 12 0 6 6 0 0 1-12 0Zm6-8a8 8 0 1 0 4.906 14.32l3.387 3.387a1 1 0 0 0 1.414-1.414l-3.387-3.387A8 8 0 0 0 11 3Zm4 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Message_Stroke2_Corner0_Rounded_Filled = createSinglePathSVG({
path: 'M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10a9.968 9.968 0 0 1-4.136-.893l-4.68.876a1 1 0 0 1-1.164-1.184l.931-4.537A9.965 9.965 0 0 1 2 12Zm4.25 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm4.5 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm5.75 1.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
})
export const Message_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 12a8 8 0 1 1 4.445 7.169 1 1 0 0 0-.629-.088l-3.537.662.7-3.415a1 1 0 0 0-.09-.66A7.961 7.961 0 0 1 4 12Zm8-10C6.477 2 2 6.477 2 12c0 1.523.341 2.968.951 4.262l-.93 4.537a1 1 0 0 0 1.163 1.184l4.68-.876A9.968 9.968 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2ZM7.5 13.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const PaperPlane_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.374 3.22a1 1 0 0 1 1.073-.114l16 8a1 1 0 0 1 0 1.788l-16 8a1 1 0 0 1-1.417-1.136L4.97 12 3.03 4.243a1 1 0 0 1 .344-1.023ZM6.781 13l-1.284 5.133L17.764 12 5.497 5.867 6.781 11H9a1 1 0 1 1 0 2H6.78Z',
})
+4
View File
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const PlusLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z',
})
export const PlusSmall_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 6a1 1 0 0 1 1 1v4h4a1 1 0 1 1 0 2h-4v4a1 1 0 1 1-2 0v-4H7a1 1 0 1 1 0-2h4V7a1 1 0 0 1 1-1Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
})
export const SettingsGear2_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M9.996 2.869A1.951 1.951 0 0 1 11.62 2h.76c.653 0 1.262.326 1.624.869l1.141 1.712 1.749-.404a1.951 1.951 0 0 1 1.819.522l.588.589c.476.475.673 1.162.522 1.818l-.404 1.749 1.712 1.141c.543.362.869.971.869 1.624v.76c0 .653-.326 1.262-.869 1.624l-1.712 1.141.404 1.749a1.951 1.951 0 0 1-.522 1.819l-.588.588a1.951 1.951 0 0 1-1.819.522l-1.749-.404-1.141 1.712A1.951 1.951 0 0 1 12.38 22h-.76a1.951 1.951 0 0 1-1.624-.869L8.855 19.42l-1.749.404a1.951 1.951 0 0 1-1.818-.522l-.59-.588a1.951 1.951 0 0 1-.52-1.819l.403-1.749-1.712-1.141A1.951 1.951 0 0 1 2 12.38v-.76c0-.653.326-1.262.869-1.624L4.58 8.855l-.404-1.749A1.951 1.951 0 0 1 4.7 5.288l.589-.59a1.951 1.951 0 0 1 1.818-.52l1.749.403 1.141-1.712ZM8.5 12a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z',
})
+6
View File
@@ -0,0 +1,6 @@
import {createSinglePathSVG} from './TEMPLATE'
export const SettingsSliderVertical_Stroke2_Corner0_Rounded =
createSinglePathSVG({
path: 'M7 3a1 1 0 0 1 1 1v1.126a4 4 0 0 1 0 7.748V20a1 1 0 1 1-2 0v-7.126a4 4 0 0 1 0-7.748V4a1 1 0 0 1 1-1Zm10 0a1 1 0 0 1 1 1v9.126a4 4 0 1 1-2 0V4a1 1 0 0 1 1-1ZM7 7a2 2 0 1 0 0 4 2 2 0 1 0 0-4Zm10 8a2 2 0 1 0 0 4 2 2 0 1 0 0-4Z',
})
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react'
import Svg, {Path} from 'react-native-svg'
import {useCommonSVGProps, Props} from '#/components/icons/common'
import {Props, useCommonSVGProps} from '#/components/icons/common'
export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef(
function LogoImpl(props: Props, ref) {
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const UserCircle_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a8 8 0 0 0-5.935 13.365C7.56 15.895 9.612 15 12 15c2.388 0 4.44.894 5.935 2.365A8 8 0 0 0 12 4Zm4.412 14.675C15.298 17.636 13.792 17 12 17c-1.791 0-3.298.636-4.412 1.675A7.96 7.96 0 0 0 12 20a7.96 7.96 0 0 0 4.412-1.325ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10a9.98 9.98 0 0 1-3.462 7.567A9.965 9.965 0 0 1 12 22a9.965 9.965 0 0 1-6.538-2.433A9.98 9.98 0 0 1 2 12Zm10-4a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z',
})
export const UserCircle_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3-12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm-3 10a7.976 7.976 0 0 1-5.714-2.4C7.618 16.004 9.605 15 12 15c2.396 0 4.383 1.005 5.714 2.6A7.976 7.976 0 0 1 12 20Z',
})
+12 -6
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {msg, Plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
@@ -39,7 +39,6 @@ export function LabelsOnMe({
return null
}
const labelTarget = isAccount ? _(msg`account`) : _(msg`content`)
return (
<View style={[a.flex_row, style]}>
<LabelsOnMeDialog control={control} subject={details} labels={labels} />
@@ -54,11 +53,18 @@ export function LabelsOnMe({
}}>
<ButtonIcon position="left" icon={CircleInfo} />
<ButtonText style={[a.leading_snug]}>
{labels.length}{' '}
{labels.length === 1 ? (
<Trans>label has been placed on this {labelTarget}</Trans>
{isAccount ? (
<Plural
value={labels.length}
one="# label has been placed on this account"
other="# labels has been placed on this account"
/>
) : (
<Trans>labels have been placed on this {labelTarget}</Trans>
<Plural
value={labels.length}
one="# label has been placed on this content"
other="# labels has been placed on this content"
/>
)}
</ButtonText>
</Button>
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -173,6 +173,7 @@ function AppealForm({
const {gtMobile} = useBreakpoints()
const [details, setDetails] = React.useState('')
const isAccountReport = 'did' in subject
const {getAgent} = useAgent()
const onSubmit = async () => {
try {
@@ -189,7 +190,7 @@ function AppealForm({
},
reason: details,
})
Toast.show(_(msg`Appeal submitted.`))
Toast.show(_(msg`Appeal submitted`))
} finally {
control.close()
}
@@ -121,20 +121,20 @@ function ModerationDetailsDialogInner({
<>
<Divider />
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
<Trans>
This label was applied by{' '}
{modcause.source.type === 'user' ? (
<Trans>the author</Trans>
) : (
{modcause.source.type === 'user' ? (
<Trans>This label was applied by the author.</Trans>
) : (
<Trans>
This label was applied by{' '}
<InlineLinkText
to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()}
style={a.text_md}>
{desc.source}
</InlineLinkText>
)}
.
</Trans>
.
</Trans>
)}
</Text>
</>
)}
+16 -7
View File
@@ -1,25 +1,27 @@
import React, {ComponentProps} from 'react'
import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useQueryClient} from '@tanstack/react-query'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {addStyle} from 'lib/styles'
import {useTheme, atoms as a} from '#/alf'
import {precacheProfile} from 'state/queries/profile'
// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up
import {Link} from '#/view/com/util/Link'
import {atoms as a, useTheme} from '#/alf'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
import {Text} from '#/components/Typography'
// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up
import {Link} from '#/view/com/util/Link'
interface Props extends ComponentProps<typeof Link> {
iconSize: number
iconStyles: StyleProp<ViewStyle>
modui: ModerationUI
profile: AppBskyActorDefs.ProfileViewBasic
}
export function PostHider({
@@ -30,8 +32,10 @@ export function PostHider({
children,
iconSize,
iconStyles,
profile,
...props
}: Props) {
const queryClient = useQueryClient()
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
@@ -39,6 +43,10 @@ export function PostHider({
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur)
const onBeforePress = React.useCallback(() => {
precacheProfile(queryClient, profile)
}, [queryClient, profile])
if (!blur) {
return (
<Link
@@ -46,6 +54,7 @@ export function PostHider({
style={style}
href={href}
accessible={false}
onBeforePress={onBeforePress}
{...props}>
{children}
</Link>
+4 -1
View File
@@ -5,6 +5,7 @@ const ScrollContext = createContext<ScrollHandlers<any>>({
onBeginDrag: undefined,
onEndDrag: undefined,
onScroll: undefined,
onMomentumEnd: undefined,
})
export function useScrollHandlers(): ScrollHandlers<any> {
@@ -20,14 +21,16 @@ export function ScrollProvider({
onBeginDrag,
onEndDrag,
onScroll,
onMomentumEnd,
}: ProviderProps) {
const handlers = useMemo(
() => ({
onBeginDrag,
onEndDrag,
onScroll,
onMomentumEnd,
}),
[onBeginDrag, onEndDrag, onScroll],
[onBeginDrag, onEndDrag, onScroll, onMomentumEnd],
)
return (
<ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider>

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