From 6da0f22fdf1dee5b84c6501d7da3bb9440dcefab Mon Sep 17 00:00:00 2001 From: Debashish Patra Date: Sun, 9 Feb 2025 23:28:09 +0530 Subject: [PATCH] feat(bsky): replies and quotes (#218) feat: proxy service source code feat(bsky): support replying refactor: remove casing libs fix: crash on profile tab navigation fix(akkoma): updates inbox folder fix(akkoma): broken dimensions of media thumbs fix: casing function does not handle arrays fix: unable to add/remove to colleciton style: improved media thumbs in notifs chore: v1 notification pack fn chore(bsky): experiment with oauth chore(bsky): width/height can be null chore: add a settings button to edit my posts chore: preliminary quote support --- .github/ISSUE_TEMPLATE/bug_report.yml | 21 +- apps/mobile/app.config.ts | 5 +- apps/mobile/app/(tabs)/index/index.tsx | 4 +- .../app/(tabs)/profile/account/lists.tsx | 4 +- apps/mobile/app/(tabs)/profile/index.tsx | 6 +- .../{signin-bsky.tsx => add-bluesky.tsx} | 109 +++++- .../(tabs)/profile/onboard/add-mastodon.tsx | 9 +- .../(tabs)/profile/onboard/add-misskey.tsx | 9 +- .../(tabs)/profile/onboard/signin-atproto.tsx | 3 + .../app/(tabs)/profile/onboard/signin-md.tsx | 2 +- .../app/(tabs)/profile/onboard/signin-mk.tsx | 2 +- .../mobile/app/(tabs)/profile/pick-driver.tsx | 2 +- .../app/(tabs)/profile/settings/index.tsx | 6 +- .../common/media/NotificationMediaThumbs.tsx | 73 ---- .../common/status/TextContentView/index.tsx | 2 +- .../common/status/fragments/PostCreatedBy.tsx | 14 +- .../fragments/PostCreatedByIconOnly.tsx | 9 +- .../status/fragments/StatusInteraction.tsx | 32 +- .../common/status/fragments/StatusQuoted.tsx | 16 +- .../fragments/ComposerTopMenu.tsx | 17 +- .../post-composer/fragments/PostButton.tsx | 66 ++-- .../components/error-screen/AppNoAccount.tsx | 273 --------------- apps/mobile/components/lib/AppDialog.tsx | 217 ++++++------ apps/mobile/components/lib/Icon.tsx | 44 ++- .../fragments/NotificationPostPeek.tsx | 4 +- .../screens/profile/stack/MyLists.tsx | 16 - .../screens/profile/stack/SelectAccount.tsx | 25 +- .../onboard/fragments/EnterYourServer.tsx | 102 ------ .../stacks/MastodonServerSelection.tsx | 107 ------ .../stack/onboard/stacks/MisskeySignIn.tsx | 319 ------------------ .../screens/settings/stack/SelectProvider.tsx | 4 +- .../components/shared/mfm/MentionSegment.tsx | 2 +- .../database/entities/account-saved-user.ts | 8 +- .../features/composer/ComposerLandingPage.tsx | 4 +- .../presenters/BottomMenuPresenter.tsx | 2 +- .../presenters/ThreadGateDialogPresenter.tsx | 8 + .../composer/reducers/composer.reducer.ts | 3 + .../inbox/presenters/InboxPresenter.tsx | 4 +- .../presenters/MediaThumbListPresenter.tsx | 39 +++ .../features/inbox/view/MediaThumbView.tsx | 48 +++ .../presenters/MyAccountPresenter.tsx | 4 +- .../presenters/MyListsPresenter.tsx | 15 + .../components}/PleromaPasteToken.tsx | 4 +- .../onboarding/components}/PopularServers.tsx | 4 +- .../onboarding/components/ProtocolCards.tsx | 143 ++++++++ .../onboarding}/data/server-meta.ts | 0 .../presenters/AtProtoLoginPresenter.tsx | 55 +++ .../onboarding/interactors/useAtprotoLogin.ts | 54 +++ .../interactors/useMastoApiLogin.ts | 48 +++ .../onboarding/interactors/useMiauthLogin.ts | 121 +++++++ .../presenters/AddAccountPresenter.tsx | 164 +++++++++ .../presenters/MastoApiServerPresenter.tsx | 52 +++ .../onboarding/presenters/MastoApiSignIn.tsx} | 18 +- .../presenters/MiauthServerPresenter.tsx} | 24 +- .../onboarding/presenters/MiauthSignIn.tsx | 209 ++++++++++++ .../views/PopularMastoServersView.tsx | 39 +++ .../onboarding/views/ServerInputView.tsx | 103 ++++++ .../search/components/SearchResultFull.tsx | 3 +- .../interactors/SearchTabInteractor.tsx | 5 +- .../features/search/views/LandingPageView.tsx | 1 + .../timelines/view/TimelineErrorView.tsx | 16 +- .../presenters/UserProfilePresenter.tsx | 7 +- apps/mobile/hooks/api/useNotifications.ts | 36 +- apps/mobile/hooks/app/useImageDims.tsx | 77 +---- apps/mobile/i18n/locales/de/core.json | 24 +- apps/mobile/i18n/locales/en/core.json | 25 +- apps/mobile/i18n/locales/id/core.json | 26 +- apps/mobile/i18n/locales/jp/core.json | 24 +- apps/mobile/index.js | 7 +- apps/mobile/metro.config.cjs | 36 +- apps/mobile/package.json | 7 +- apps/mobile/services/activitypub.service.ts | 7 + .../services/atproto/atproto-compose.ts | 40 ++- .../atproto/atproto-session.service.ts | 32 ++ apps/mobile/services/driver.service.ts | 9 +- apps/mobile/services/masto-api.service.ts | 35 +- apps/mobile/services/suvamio.service.ts | 103 ++++++ apps/mobile/types/app-post.types.ts | 4 +- apps/mobile/utils/linking.utils.ts | 4 + apps/mobile/utils/route-list.ts | 1 + apps/proxy/.env | 5 + .../stack/MyBlocks.tsx => proxy/.gitignore} | 0 apps/proxy/README.md | 39 +++ apps/proxy/api/atproto-oauth-callack.ts | 42 +++ apps/proxy/api/atproto-oauth-generate.ts | 44 +++ apps/proxy/api/giphy.ts | 39 +++ apps/proxy/api/tenor.ts | 95 ++++++ apps/proxy/api/translate.ts | 106 ++++++ apps/proxy/package.json | 6 + apps/proxy/tsconfig.json | 27 ++ apps/proxy/utils/api-response.utils.ts | 42 +++ apps/proxy/utils/atproto.utils.ts | 28 ++ apps/push/README.md | 1 + bun.lock | 85 ++--- package.json | 2 +- packages/bridge/package.json | 8 +- .../src/adapters/_client/_router/_runner.ts | 1 - .../_client/_router/routes/statuses.ts | 10 +- .../_client/_router/utils/casing.utils.ts | 16 - .../src/adapters/_client/default/accounts.ts | 2 +- .../_client/mastodon/notifications.ts | 30 +- .../src/adapters/_client/pleroma/accounts.ts | 20 +- .../src/adapters/_client/pleroma/media.ts | 4 +- .../adapters/_client/pleroma/notifications.ts | 97 ++++-- .../src/adapters/_client/pleroma/search.ts | 5 +- .../src/adapters/_client/pleroma/statuses.ts | 30 +- .../src/adapters/_client/pleroma/timelines.ts | 31 +- .../bridge/src/custom-clients/custom-fetch.ts | 10 +- .../src/implementors/profile/_interface.ts | 4 +- .../src/implementors/status/_adapters.ts | 4 +- .../src/implementors/status/mastodon.ts | 4 +- packages/bridge/src/types/megalodon.types.ts | 4 +- packages/bridge/src/utiils/casing.utils.ts | 47 +++ 113 files changed, 2612 insertions(+), 1502 deletions(-) rename apps/mobile/app/(tabs)/profile/onboard/{signin-bsky.tsx => add-bluesky.tsx} (59%) create mode 100644 apps/mobile/app/(tabs)/profile/onboard/signin-atproto.tsx delete mode 100644 apps/mobile/components/common/media/NotificationMediaThumbs.tsx delete mode 100644 apps/mobile/components/error-screen/AppNoAccount.tsx delete mode 100644 apps/mobile/components/screens/profile/stack/MyLists.tsx delete mode 100644 apps/mobile/components/screens/profile/stack/onboard/fragments/EnterYourServer.tsx delete mode 100644 apps/mobile/components/screens/profile/stack/onboard/stacks/MastodonServerSelection.tsx delete mode 100644 apps/mobile/components/screens/profile/stack/onboard/stacks/MisskeySignIn.tsx create mode 100644 apps/mobile/features/composer/presenters/ThreadGateDialogPresenter.tsx create mode 100644 apps/mobile/features/inbox/presenters/MediaThumbListPresenter.tsx create mode 100644 apps/mobile/features/inbox/view/MediaThumbView.tsx create mode 100644 apps/mobile/features/my-account/presenters/MyListsPresenter.tsx rename apps/mobile/{components/screens/profile/stack/onboard/fragments => features/onboarding/components}/PleromaPasteToken.tsx (90%) rename apps/mobile/{components/screens/profile/stack/onboard/fragments => features/onboarding/components}/PopularServers.tsx (90%) create mode 100644 apps/mobile/features/onboarding/components/ProtocolCards.tsx rename apps/mobile/{components/screens/profile/stack/onboard => features/onboarding}/data/server-meta.ts (100%) create mode 100644 apps/mobile/features/onboarding/features/atproto/presenters/AtProtoLoginPresenter.tsx create mode 100644 apps/mobile/features/onboarding/interactors/useAtprotoLogin.ts create mode 100644 apps/mobile/features/onboarding/interactors/useMastoApiLogin.ts create mode 100644 apps/mobile/features/onboarding/interactors/useMiauthLogin.ts create mode 100644 apps/mobile/features/onboarding/presenters/AddAccountPresenter.tsx create mode 100644 apps/mobile/features/onboarding/presenters/MastoApiServerPresenter.tsx rename apps/mobile/{components/screens/profile/stack/onboard/stacks/MastodonSignIn.tsx => features/onboarding/presenters/MastoApiSignIn.tsx} (88%) rename apps/mobile/{components/screens/profile/stack/onboard/stacks/MisskeyServerSelection.tsx => features/onboarding/presenters/MiauthServerPresenter.tsx} (83%) create mode 100644 apps/mobile/features/onboarding/presenters/MiauthSignIn.tsx create mode 100644 apps/mobile/features/onboarding/views/PopularMastoServersView.tsx create mode 100644 apps/mobile/features/onboarding/views/ServerInputView.tsx create mode 100644 apps/mobile/services/suvamio.service.ts create mode 100644 apps/proxy/.env rename apps/{mobile/components/screens/profile/stack/MyBlocks.tsx => proxy/.gitignore} (100%) create mode 100644 apps/proxy/README.md create mode 100644 apps/proxy/api/atproto-oauth-callack.ts create mode 100644 apps/proxy/api/atproto-oauth-generate.ts create mode 100644 apps/proxy/api/giphy.ts create mode 100644 apps/proxy/api/tenor.ts create mode 100644 apps/proxy/api/translate.ts create mode 100644 apps/proxy/package.json create mode 100644 apps/proxy/tsconfig.json create mode 100644 apps/proxy/utils/api-response.utils.ts create mode 100644 apps/proxy/utils/atproto.utils.ts create mode 100644 apps/push/README.md delete mode 100644 packages/bridge/src/adapters/_client/_router/utils/casing.utils.ts create mode 100644 packages/bridge/src/utiils/casing.utils.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b8e1e1c1..f10fe49a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,21 +8,13 @@ body: - type: markdown attributes: value: | - Thank you for taking the time to report a bug. - - No need to search for duplicate issues. Just ask away. - - #### Considerations - - * Support is currently only offered for latest version. So, update your app! - * The only supported backends are [Mastodon, Misskey, Firefish, Sharkey, Pleroma, Akkoma] - * ^ You may request support for additional backends via "Feature Request" - + Don't worry about duplicate issues. Just ask what you want ^^ - type: textarea id: description attributes: label: Description - description: A clear and concise description of what the bug is. + description: | + Describe the issue you are facing. validations: required: true @@ -30,8 +22,8 @@ body: id: version attributes: label: App Version - description: The app version this issue occurs on. - placeholder: "v0.9.0 or v0.9.0-lite" + description: App version can be seen in the settings screen + placeholder: "v0.15.3" validations: required: true - type: dropdown @@ -42,10 +34,9 @@ body: multiple: false options: - Play Store - - GitHub Releases + - GitHub (Lite) - IzzyOnDroid (Lite) - Self-Compiled - - Self-Compiled (Lite) validations: required: true diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index dfb5ffd9..81a5a9b8 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -10,11 +10,12 @@ const expo = ({ config }: ConfigContext): ExpoConfig => ({ ...config, name: APP_NAME, slug: 'dhaaga', - version: '0.15.3', + version: '0.15.4', orientation: 'portrait', icon: './assets/icon.png', userInterfaceStyle: 'dark', scheme: 'dhaaga', + jsEngine: 'hermes', platforms: ['android'], developmentClient: { silentLaunch: true, @@ -25,7 +26,7 @@ const expo = ({ config }: ConfigContext): ExpoConfig => ({ }, android: { package: BUNDLE_ID, - versionCode: 28, + versionCode: 29, blockedPermissions: [ 'android.permission.SYSTEM_ALERT_WINDOW', 'android.permission.READ_EXTERNAL_STORAGE', diff --git a/apps/mobile/app/(tabs)/index/index.tsx b/apps/mobile/app/(tabs)/index/index.tsx index 2931df69..21582190 100644 --- a/apps/mobile/app/(tabs)/index/index.tsx +++ b/apps/mobile/app/(tabs)/index/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; import { APP_LANDING_PAGE_TYPE } from '../../../components/shared/topnavbar/AppTabLandingNavbar'; -import AppNoAccount from '../../../components/error-screen/AppNoAccount'; +import AddAccountPresenter from '../../../features/onboarding/presenters/AddAccountPresenter'; import SocialHubPresenter from '../../../features/social-hub/presenters/SocialHubPresenter'; import SoftwareHeader from '../../../screens/accounts/fragments/SoftwareHeader'; import { Account } from '../../../database/_schema'; @@ -167,7 +167,7 @@ function Screen() { loadAccounts(); }, [acct]); - if (!acct) return ; + if (!acct) return ; return ; } diff --git a/apps/mobile/app/(tabs)/profile/account/lists.tsx b/apps/mobile/app/(tabs)/profile/account/lists.tsx index c5979cbe..ed7defb2 100644 --- a/apps/mobile/app/(tabs)/profile/account/lists.tsx +++ b/apps/mobile/app/(tabs)/profile/account/lists.tsx @@ -1,7 +1,7 @@ -import MyLists from '../../../../components/screens/profile/stack/MyLists'; +import MyListsPresenter from '../../../../features/my-account/presenters/MyListsPresenter'; function BookmarkClassic() { - return ; + return ; } export default BookmarkClassic; diff --git a/apps/mobile/app/(tabs)/profile/index.tsx b/apps/mobile/app/(tabs)/profile/index.tsx index 8415ce3a..d184316f 100644 --- a/apps/mobile/app/(tabs)/profile/index.tsx +++ b/apps/mobile/app/(tabs)/profile/index.tsx @@ -1,7 +1,3 @@ import MyAccountPresenter from '../../../features/my-account/presenters/MyAccountPresenter'; -function Page() { - return ; -} - -export default Page; +export default MyAccountPresenter; diff --git a/apps/mobile/app/(tabs)/profile/onboard/signin-bsky.tsx b/apps/mobile/app/(tabs)/profile/onboard/add-bluesky.tsx similarity index 59% rename from apps/mobile/app/(tabs)/profile/onboard/signin-bsky.tsx rename to apps/mobile/app/(tabs)/profile/onboard/add-bluesky.tsx index 1fbcc194..e7e019b6 100644 --- a/apps/mobile/app/(tabs)/profile/onboard/signin-bsky.tsx +++ b/apps/mobile/app/(tabs)/profile/onboard/add-bluesky.tsx @@ -19,17 +19,33 @@ import { } from '../../../../hooks/utility/global-state-extractors'; import { APP_EVENT_ENUM } from '../../../../services/publishers/app.publisher'; import { Loader } from '../../../../components/lib/Loader'; +import { Image } from 'expo-image'; +import { useAssets } from 'expo-asset'; +import Ionicons from '@expo/vector-icons/Ionicons'; +import { LinkingUtils } from '../../../../utils/linking.utils'; +import useAtprotoLogin from '../../../../features/onboarding/interactors/useAtprotoLogin'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../../types/app.types'; -function SigninBsky() { +// WebBrowser.maybeCompleteAuthSession(); + +function AddBluesky() { const [IsLoading, setIsLoading] = useState(false); const { theme } = useAppTheme(); const { db } = useAppDb(); const { loadAccounts } = useHub(); const { translateY } = useScrollMoreOnPageEnd(); const { appSub } = useAppPublishers(); + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); const [Username, setUsername] = useState(null); const [Password, setPassword] = useState(null); + const [assets, error] = useAssets([ + require('../../../../assets/branding/bluesky/logo.png'), + require('../../../../assets/icon.png'), + ]); + + const { isLoading } = useAtprotoLogin(); async function onSubmit() { setIsLoading(true); @@ -59,9 +75,20 @@ function SigninBsky() { } } + if (error || !assets) + return ( + + + + ); + return ( @@ -69,11 +96,67 @@ function SigninBsky() { contentContainerStyle={{ paddingTop: 54, paddingHorizontal: 8 }} > - - Enter Server Details - + {/*@ts-ignore-next-line*/} + + + {/*@ts-ignore-next-line*/} + + + + + {t(`onboarding.needBlueskyAccount`)} + + + {t(`onboarding.createOneHere`)} + + + @@ -82,14 +165,14 @@ function SigninBsky() { @@ -101,13 +184,13 @@ function SigninBsky() { - {IsLoading ? ( + {IsLoading || isLoading ? ( @@ -127,7 +210,7 @@ function SigninBsky() { buttonStyle={{ width: 128, borderRadius: 8 }} > - Log In + {t(`onboarding.loginButton`)} )} @@ -148,4 +231,4 @@ const styles = StyleSheet.create({ inputContainer: { width: 24 + 8 * 2, padding: 8 }, }); -export default SigninBsky; +export default AddBluesky; diff --git a/apps/mobile/app/(tabs)/profile/onboard/add-mastodon.tsx b/apps/mobile/app/(tabs)/profile/onboard/add-mastodon.tsx index fd48c4bd..eb7dbf6b 100644 --- a/apps/mobile/app/(tabs)/profile/onboard/add-mastodon.tsx +++ b/apps/mobile/app/(tabs)/profile/onboard/add-mastodon.tsx @@ -1,8 +1,3 @@ -import { memo } from 'react'; -import MastodonServerSelect from '../../../../components/screens/profile/stack/onboard/stacks/MastodonServerSelection'; +import MastodonServerSelect from '../../../../features/onboarding/presenters/MastoApiServerPresenter'; -const AddMastodonStack = memo(function Foo() { - return ; -}); - -export default AddMastodonStack; +export default MastodonServerSelect; diff --git a/apps/mobile/app/(tabs)/profile/onboard/add-misskey.tsx b/apps/mobile/app/(tabs)/profile/onboard/add-misskey.tsx index bbfcba10..b3b5ddda 100644 --- a/apps/mobile/app/(tabs)/profile/onboard/add-misskey.tsx +++ b/apps/mobile/app/(tabs)/profile/onboard/add-misskey.tsx @@ -1,8 +1,3 @@ -import { memo } from 'react'; -import MisskeyServerSelection from '../../../../components/screens/profile/stack/onboard/stacks/MisskeyServerSelection'; +import MiauthServerPresenter from '../../../../features/onboarding/presenters/MiauthServerPresenter'; -const AddMisskeyStack = memo(function Foo() { - return ; -}); - -export default AddMisskeyStack; +export default MiauthServerPresenter; diff --git a/apps/mobile/app/(tabs)/profile/onboard/signin-atproto.tsx b/apps/mobile/app/(tabs)/profile/onboard/signin-atproto.tsx new file mode 100644 index 00000000..0f493dab --- /dev/null +++ b/apps/mobile/app/(tabs)/profile/onboard/signin-atproto.tsx @@ -0,0 +1,3 @@ +import AtProtoLoginPresenter from '../../../../features/onboarding/features/atproto/presenters/AtProtoLoginPresenter'; + +export default AtProtoLoginPresenter; diff --git a/apps/mobile/app/(tabs)/profile/onboard/signin-md.tsx b/apps/mobile/app/(tabs)/profile/onboard/signin-md.tsx index 783388fc..cbfe8527 100644 --- a/apps/mobile/app/(tabs)/profile/onboard/signin-md.tsx +++ b/apps/mobile/app/(tabs)/profile/onboard/signin-md.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import MastoSignIn from '../../../../components/screens/profile/stack/onboard/stacks/MastodonSignIn'; +import MastoSignIn from '../../../../features/onboarding/presenters/MastoApiSignIn'; const SigninMd = memo(function Foo() { return ; diff --git a/apps/mobile/app/(tabs)/profile/onboard/signin-mk.tsx b/apps/mobile/app/(tabs)/profile/onboard/signin-mk.tsx index 3a9dbda5..fa6385e1 100644 --- a/apps/mobile/app/(tabs)/profile/onboard/signin-mk.tsx +++ b/apps/mobile/app/(tabs)/profile/onboard/signin-mk.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import MisskeySignIn from '../../../../components/screens/profile/stack/onboard/stacks/MisskeySignIn'; +import MisskeySignIn from '../../../../features/onboarding/presenters/MiauthSignIn'; const SigninMk = memo(function Foo() { return ; diff --git a/apps/mobile/app/(tabs)/profile/pick-driver.tsx b/apps/mobile/app/(tabs)/profile/pick-driver.tsx index e12c860b..e0f82a9c 100644 --- a/apps/mobile/app/(tabs)/profile/pick-driver.tsx +++ b/apps/mobile/app/(tabs)/profile/pick-driver.tsx @@ -1,6 +1,6 @@ import useScrollMoreOnPageEnd from '../../../states/useScrollMoreOnPageEnd'; import AppTopNavbar from '../../../components/shared/topnavbar/AppTopNavbar'; -import { AddAccountLandingFragment } from '../../../components/error-screen/AppNoAccount'; +import { AddAccountLandingFragment } from '../../../features/onboarding/presenters/AddAccountPresenter'; function Page() { const { translateY } = useScrollMoreOnPageEnd(); diff --git a/apps/mobile/app/(tabs)/profile/settings/index.tsx b/apps/mobile/app/(tabs)/profile/settings/index.tsx index 5076801c..1bb61921 100644 --- a/apps/mobile/app/(tabs)/profile/settings/index.tsx +++ b/apps/mobile/app/(tabs)/profile/settings/index.tsx @@ -1,7 +1,3 @@ import AppSettingsPage from '../../../../components/screens/profile/stack/AppSettingsPage'; -function Page() { - return ; -} - -export default Page; +export default AppSettingsPage; diff --git a/apps/mobile/components/common/media/NotificationMediaThumbs.tsx b/apps/mobile/components/common/media/NotificationMediaThumbs.tsx deleted file mode 100644 index 2104302f..00000000 --- a/apps/mobile/components/common/media/NotificationMediaThumbs.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { FlatList, View } from 'react-native'; -import { Image } from 'expo-image'; -import { useImageAutoHeight } from '../../../hooks/app/useImageDims'; -import { appDimensions } from '../../../styles/dimensions'; -import { AppMediaObject, AppPostObject } from '../../../types/app-post.types'; -import { KNOWN_SOFTWARE } from '@dhaaga/bridge'; -import ActivityPubService from '../../../services/activitypub.service'; - -type ThumbItemProps = { - item: AppMediaObject; - post: AppPostObject; - server?: KNOWN_SOFTWARE; -}; - -function ThumbItem({ item, post, server }: ThumbItemProps) { - const Data = useImageAutoHeight(item, 100, 200); - - // too resource expensive - // const isMastodonLike = ActivityPubService.mastodonLike(server); - - if (!Data.resolved) return ; - return ( - - {/*@ts-ignore-next-line*/} - - - ); -} - -type Props = { - items: AppMediaObject[]; - post: AppPostObject; - server?: KNOWN_SOFTWARE; -}; - -function NotificationMediaThumbs({ items, post, server }: Props) { - if (items.length === 0) return ; - - return ( - ( - - )} - style={{ - marginBottom: appDimensions.timelines.sectionBottomMargin, - marginTop: 8, - }} - /> - ); -} - -export default NotificationMediaThumbs; diff --git a/apps/mobile/components/common/status/TextContentView/index.tsx b/apps/mobile/components/common/status/TextContentView/index.tsx index 049a616e..9c6cf152 100644 --- a/apps/mobile/components/common/status/TextContentView/index.tsx +++ b/apps/mobile/components/common/status/TextContentView/index.tsx @@ -77,7 +77,7 @@ function TextContentNode({ value={node.text} link={node.url} mentions={mentions} - fontFamily={APP_FONTS.INTER_500_MEDIUM} + fontFamily={APP_FONTS.ROBOTO_500} /> ); } diff --git a/apps/mobile/components/common/status/fragments/PostCreatedBy.tsx b/apps/mobile/components/common/status/fragments/PostCreatedBy.tsx index 65ec518f..2a3525e0 100644 --- a/apps/mobile/components/common/status/fragments/PostCreatedBy.tsx +++ b/apps/mobile/components/common/status/fragments/PostCreatedBy.tsx @@ -210,30 +210,28 @@ type OriginalPosterProps = { * the bottom-most post item */ function PostCreatedBy({ style }: OriginalPosterProps) { - // const { appManager } = useAppManager(); - // const { show, refresh } = useAppModalState(APP_KNOWN_MODAL.USER_PEEK); const { show, setCtx } = useAppBottomSheet(); const { dto } = useAppStatusItem(); const STATUS_DTO = PostMiddleware.getContentTarget(dto); const { toProfile } = useAppNavigator(); - const { server } = useAppApiClient(); + const { driver } = useAppApiClient(); const UserDivRef = useRef(null); function onAvatarClicked() { - if (ActivitypubService.blueskyLike(server)) { + if (ActivitypubService.blueskyLike(driver)) { setCtx({ - did: PostMiddleware.getContentTarget(dto)?.postedBy?.userId, + did: PostMiddleware.getContentTarget(dto)?.postedBy?.id, }); } else { setCtx({ - userId: PostMiddleware.getContentTarget(dto)?.postedBy?.userId, + userId: PostMiddleware.getContentTarget(dto)?.postedBy?.id, }); } show(APP_BOTTOM_SHEET_ENUM.PROFILE_PEEK, true); // UserDivRef.current.measureInWindow((x, y, width, height) => { - // appManager.storage.setUserPeekModalData(STATUS_DTO.postedBy.userId, { + // appManager.storage.setUserPeekModalData(STATUS_DTO.postedBy.id, { // x, // y, // width, @@ -247,7 +245,7 @@ function PostCreatedBy({ style }: OriginalPosterProps) { } function onProfileClicked() { - toProfile(PostMiddleware.getContentTarget(dto)?.postedBy?.userId); + toProfile(PostMiddleware.getContentTarget(dto)?.postedBy?.id); } return ( diff --git a/apps/mobile/components/common/status/fragments/PostCreatedByIconOnly.tsx b/apps/mobile/components/common/status/fragments/PostCreatedByIconOnly.tsx index f282391c..469c560a 100644 --- a/apps/mobile/components/common/status/fragments/PostCreatedByIconOnly.tsx +++ b/apps/mobile/components/common/status/fragments/PostCreatedByIconOnly.tsx @@ -3,6 +3,7 @@ import { Image } from 'expo-image'; import useAppNavigator from '../../../../states/useAppNavigator'; import { AppPostObject } from '../../../../types/app-post.types'; import { appDimensions } from '../../../../styles/dimensions'; +import { PostMiddleware } from '../../../../services/middlewares/post.middleware'; const TIMELINE_PFP_SIZE = appDimensions.timelines.avatarIconSize; @@ -56,14 +57,10 @@ type OriginalPosterProps = { function PostCreatedByIconOnly({ dto, style }: OriginalPosterProps) { const { toProfile } = useAppNavigator(); - const STATUS_DTO = dto.meta.isBoost - ? dto.content.raw - ? dto - : dto.boostedFrom - : dto; + const STATUS_DTO = PostMiddleware.getContentTarget(dto); function onPress() { - toProfile(STATUS_DTO.postedBy.userId); + toProfile(STATUS_DTO.postedBy.id); } return ( diff --git a/apps/mobile/components/common/status/fragments/StatusInteraction.tsx b/apps/mobile/components/common/status/fragments/StatusInteraction.tsx index bd7ecf75..6276a2f9 100644 --- a/apps/mobile/components/common/status/fragments/StatusInteraction.tsx +++ b/apps/mobile/components/common/status/fragments/StatusInteraction.tsx @@ -143,10 +143,37 @@ function ReactButton() { ); } -function StatusInteractionButtons() { +function SettingsButton() { + const { dto: item } = useAppStatusItem(); + const { theme } = useAppTheme(); + const { show, setCtx } = useAppBottomSheet(); const { acct } = useAppAcct(); - const IS_MISSKEY = ActivityPubService.misskeyLike(acct.driver); + const _target = PostMiddleware.getContentTarget(item); + if (_target.postedBy.id !== acct.identifier) return ; + + function onPress() { + setCtx({ uuid: item.uuid }); + show(APP_BOTTOM_SHEET_ENUM.ADD_REACTION, true); + } + + return ( + + ); +} + +function StatusInteractionButtons() { + const { driver } = useAppApiClient(); + const IS_MISSKEY = ActivityPubService.misskeyLike(driver); return ( @@ -154,6 +181,7 @@ function StatusInteractionButtons() { {IS_MISSKEY && } + - + - + {/*@ts-ignore-next-line*/} @@ -134,6 +127,12 @@ const styles = StyleSheet.create({ padding: 10, paddingTop: appDimensions.bottomSheet.clearanceTop, }, + avatarBorderBox: { + borderWidth: 0.75, + borderColor: '#666', + borderRadius: '100%', + overflow: 'hidden', + }, avatarContainer: { height: 36, width: 36, diff --git a/apps/mobile/components/dhaaga-bottom-sheet/modules/post-composer/fragments/PostButton.tsx b/apps/mobile/components/dhaaga-bottom-sheet/modules/post-composer/fragments/PostButton.tsx index 3c44733c..d3116971 100644 --- a/apps/mobile/components/dhaaga-bottom-sheet/modules/post-composer/fragments/PostButton.tsx +++ b/apps/mobile/components/dhaaga-bottom-sheet/modules/post-composer/fragments/PostButton.tsx @@ -7,9 +7,7 @@ import { APP_POST_VISIBILITY } from '../../../../../hooks/app/useVisibility'; import { KNOWN_SOFTWARE } from '@dhaaga/bridge'; import ActivityPubService from '../../../../../services/activitypub.service'; import { PostMiddleware } from '../../../../../services/middlewares/post.middleware'; -import AtprotoComposerService, { - AtprotoReplyEmbed, -} from '../../../../../services/atproto/atproto-compose'; +import AtprotoComposerService from '../../../../../services/atproto/atproto-compose'; import { useAppApiClient, useAppBottomSheet, @@ -37,53 +35,32 @@ function PostButton() { async function onClick() { setLoading(true); let _visibility: any = state.visibility; - if (driver === KNOWN_SOFTWARE.BLUESKY) { - let reply: AtprotoReplyEmbed = null; - // if (ParentRef.current) { - // if (ParentRef.current.rootPost) { - // // both parent and root available - // reply = { - // root: { - // uri: ParentRef.current.rootPost.meta.uri, - // cid: ParentRef.current.rootPost.meta.cid, - // }, - // parent: { - // uri: ParentRef.current.meta.uri, - // cid: ParentRef.current.meta.cid, - // }, - // }; - // } else { - // // parent must be root - // reply = { - // root: { - // uri: ParentRef.current.meta.uri, - // cid: ParentRef.current.meta.cid, - // }, - // parent: { - // uri: ParentRef.current.meta.uri, - // cid: ParentRef.current.meta.cid, - // }, - // }; - // } - // } - - const data = await AtprotoComposerService.postUsingReducerState( + const newPost = await AtprotoComposerService.postUsingReducerState( client as BlueskyRestClient, state, ); - if (data) { - /** - * FIXME: Currently only shows the latest record - * We can use the logic from context builder - * to render the parent and root, as well - */ - const _data = PostMiddleware.deserialize(data, driver, server); - postPub.writeCache(_data.uuid, _data); - setCtx({ uuid: _data.uuid }); - show(APP_BOTTOM_SHEET_ENUM.POST_PREVIEW, true); + + if (!newPost) { + setLoading(false); return; } + + /** + * FIXME: Currently only shows the latest record + * We can use the logic from context builder + * to render the parent and root, as well + */ + const _newPostObject = PostMiddleware.deserialize( + newPost, + driver, + server, + ); + postPub.writeCache(_newPostObject.uuid, _newPostObject); + setCtx({ uuid: _newPostObject.uuid }); + show(APP_BOTTOM_SHEET_ENUM.POST_PREVIEW, true); + setLoading(false); + return; } switch (_visibility) { @@ -127,7 +104,6 @@ function PostButton() { spoilerText: state.cw === '' ? undefined : state.cw, }); if (error) throw new Error(error.message); - console.log('resounding success...'); if (ActivityPubService.mastodonLike(driver)) { const _data = PostMiddleware.deserialize(data, driver, server); diff --git a/apps/mobile/components/error-screen/AppNoAccount.tsx b/apps/mobile/components/error-screen/AppNoAccount.tsx deleted file mode 100644 index ee093301..00000000 --- a/apps/mobile/components/error-screen/AppNoAccount.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { - Pressable, - RefreshControl, - ScrollView, - StyleProp, - StyleSheet, - Text, - View, - ViewStyle, -} from 'react-native'; -import useGlobalState from '../../states/_global'; -import { useShallow } from 'zustand/react/shallow'; -import { APP_FONTS } from '../../styles/AppFonts'; -import SoftwareHeader from '../../screens/accounts/fragments/SoftwareHeader'; -import { KNOWN_SOFTWARE } from '@dhaaga/bridge'; -import { AppIcon } from '../lib/Icon'; -import { router } from 'expo-router'; -import AppTabLandingNavbar, { - APP_LANDING_PAGE_TYPE, -} from '../shared/topnavbar/AppTabLandingNavbar'; -import { useState } from 'react'; -import { AccountService } from '../../database/entities/account'; -import { APP_COLOR_PALETTE_EMPHASIS } from '../../utils/theming.util'; -import { - useAppAcct, - useAppTheme, -} from '../../hooks/utility/global-state-extractors'; -import { AppText } from '../lib/Text'; - -/** - * This UI fragment can be shared with other - * screens (that might have a different header, - * footer or page decorations) - * @constructor - */ -export function DriverSelectionFragment() { - const { theme } = useAppTheme(); - const options: { - label: string; - padding: number; - rightComponent: any; - to: string; - desc?: string; - }[] = [ - { - label: 'Bluesky', - desc: '- Custom PDS (for now)', - padding: 0, - rightComponent: ( - - ), - to: '/profile/onboard/signin-bsky', - }, - { - label: 'Mastodon', - padding: 20, - desc: '+ Pleroma, Akkoma', - rightComponent: ( - - ), - to: '/profile/onboard/add-mastodon', - }, - { - label: 'Misskey', - padding: 12, - desc: '+ Sharkey, CherryPick', - rightComponent: ( - - ), - to: '/profile/onboard/add-misskey', - }, - ]; - - return ( - - {options.map((option, i) => ( - { - router.push(option.to); - }} - > - - - {option.label} - - {option.desc && ( - - {option.desc} - - )} - - - {option.rightComponent} - - - ))} - - ); -} - -type AddAccountLandingFragmentProps = { - containerStyle?: StyleProp; -}; - -/** - * This UI fragment can be shared with other - * screens (that might have a different header, - * but share footer or page decorations) - * @constructor - */ -export function AddAccountLandingFragment({ - containerStyle, -}: AddAccountLandingFragmentProps) { - const { theme } = useAppTheme(); - return ( - - - Add Account - - - - - - - Account creation is not supported. - - - - ); -} - -/** - * A full screen cover when no account is selected - * @constructor - */ - -type AppNoAccountProps = { - tab: APP_LANDING_PAGE_TYPE; -}; - -function AppNoAccount({ tab }: AppNoAccountProps) { - const [IsRefreshing, setIsRefreshing] = useState(false); - const { theme } = useAppTheme(); - const { acct } = useAppAcct(); - const { db, loadApp } = useGlobalState( - useShallow((o) => ({ - db: o.db, - loadApp: o.loadApp, - })), - ); - - function onRefresh() { - setIsRefreshing(true); - try { - // possibly locked because of added/deleted account - if (!acct) { - AccountService.ensureAccountSelection(db); - loadApp(); - setIsRefreshing(false); - } - } catch (e) { - setIsRefreshing(false); - } finally { - setIsRefreshing(false); - } - } - - return ( - - } - > - { - router.navigate('/user-guide'); - }, - }, - ]} - /> - - - ); -} - -export default AppNoAccount; - -const styles = StyleSheet.create({ - noAccountText: { - fontSize: 24, - textAlign: 'center', - marginTop: 48, - fontFamily: APP_FONTS.INTER_700_BOLD, - marginBottom: 32, - }, - selectSnsBox: { - padding: 6, - flexDirection: 'row', - alignItems: 'center', - margin: 10, - borderRadius: 16, - paddingHorizontal: 20, - }, - selectSnsLabel: { - // padding: 6, - fontFamily: APP_FONTS.INTER_600_SEMIBOLD, - fontSize: 22, - }, - tipContainer: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'center', - width: '100%', - marginTop: 32, - }, - tipText: { - fontFamily: APP_FONTS.INTER_500_MEDIUM, - marginLeft: 6, - }, -}); diff --git a/apps/mobile/components/lib/AppDialog.tsx b/apps/mobile/components/lib/AppDialog.tsx index 4a8794fa..0d208577 100644 --- a/apps/mobile/components/lib/AppDialog.tsx +++ b/apps/mobile/components/lib/AppDialog.tsx @@ -1,4 +1,11 @@ -import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { + Pressable, + StyleProp, + StyleSheet, + Text, + View, + ViewStyle, +} from 'react-native'; import { useAppDialog, useAppTheme, @@ -7,17 +14,17 @@ import { APP_FONTS } from '../../styles/AppFonts'; import { Fragment, useEffect, useState } from 'react'; import { Loader } from './Loader'; import { AppTextInput } from './TextInput'; -import { APP_FONT } from '../../styles/AppTheme'; -import { appVerticalIndex } from '../../styles/dimensions'; +import { appDimensions, appVerticalIndex } from '../../styles/dimensions'; import { AppText } from './Text'; type DialogOptionsProps = { label: string; onPress: () => Promise; variant?: 'default' | 'dismiss' | 'destructive'; + style?: StyleProp; }; -function DialogOption({ label, onPress, variant }: DialogOptionsProps) { +function DialogOption({ label, onPress, variant, style }: DialogOptionsProps) { const [IsLoading, setIsLoading] = useState(false); const { theme } = useAppTheme(); @@ -30,20 +37,18 @@ function DialogOption({ label, onPress, variant }: DialogOptionsProps) { setIsLoading(false); }); } catch (e) { - // - // setIsLoading(false); + setIsLoading(false); } } const color = variant && (variant === 'dismiss' || variant === 'destructive') ? '#fd413b' - : theme.textColor.medium; + : theme.secondary.a10; return ( - - - + + {IsLoading ? ( @@ -88,101 +93,103 @@ export function AppDialog() { if (!visible) return ; return ( - + - - - - {state.title} - + + + {state.title} + - {state.description.map((text, i) => ( - - {text} - - ))} - {IS_TXT_MODE && ( - - )} - - - {state.actions.map((action, i) => ( - - ))} - {IS_TXT_MODE && ( - { - textSubmitCallback(Input); - hide(); - }} - variant={'default'} - /> - )} - { - hide(); + {state.description.map((text, i) => ( + + {text} + + ))} + {IS_TXT_MODE && ( + - + )} + + {/* ---- Additional Dialog Options ---- */} + {state.actions.map((action, i) => ( + + ))} + + {/* ---- Save Option (TextInput Dialogs) ---- */} + {IS_TXT_MODE && ( + { + textSubmitCallback(Input); + hide(); + }} + variant={'default'} + /> + )} + + {/* ---- Dismiss Option (Universal) ---- */} + { + hide(); + }} + variant={'dismiss'} + style={{ paddingBottom: MARGIN_BOTTOM * 0.5 }} + /> ); } +const MARGIN_BOTTOM = appDimensions.timelines.sectionBottomMargin; + const styles = StyleSheet.create({ + root: { + maxWidth: '75%', + borderRadius: 8, + zIndex: appVerticalIndex.dialogContent, + position: 'absolute', + left: '50%', + top: '50%', + }, modalTitle: { textAlign: 'center', fontSize: 22, @@ -205,22 +212,12 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, -}); - -const modalStyles = StyleSheet.create({ - modalTitle: { - fontFamily: APP_FONTS.MONTSERRAT_700_BOLD, - textAlign: 'center', - color: APP_FONT.MONTSERRAT_BODY, - fontSize: 18, - marginBottom: 16, - }, - modalDescription: { - textAlign: 'center', - fontSize: 14, - }, - actionButtonContainer: { - marginVertical: 16, - marginTop: 32, + backdrop: { + position: 'absolute', + backgroundColor: 'black', + height: '100%', + width: '100%', + opacity: 0.64, + zIndex: appVerticalIndex.dialogBackdrop, }, }); diff --git a/apps/mobile/components/lib/Icon.tsx b/apps/mobile/components/lib/Icon.tsx index 0e2fdf5c..271e8468 100644 --- a/apps/mobile/components/lib/Icon.tsx +++ b/apps/mobile/components/lib/Icon.tsx @@ -40,6 +40,7 @@ export type APP_ICON_ENUM = | 'bookmark-outline' | 'block' | 'browser' + | 'cog' | 'chatbox-outline' | 'chat-ellipses-outline' | 'checkmark' @@ -51,7 +52,8 @@ export type APP_ICON_ENUM = | 'chevron-down-circle' | 'cloud-upload-outline' | 'close-outline' - | 'cog' + | 'settings' + | 'settings-outline' | 'copy' | 'create' | 'clear' @@ -86,6 +88,7 @@ export type APP_ICON_ENUM = | 'phonebook' | 'pin' | 'pin-octicons' + | 'quote' | 'retweet' | 'save' | 'search' @@ -170,7 +173,7 @@ export function ProfileTabNavbarIcon({ color, size }: NavigationIconType) { function onPress(e: any) { router.navigate('/profile'); - router.dismissAll(); + router.dismiss(2); } // if (visible && isAnimating) return ; @@ -346,6 +349,15 @@ export function AppIcon({ style={iconStyle} /> ); + case 'cog': + return ( + + ); case 'chatbox-outline': return ( ); - case 'cog': - return Platform.OS === 'ios' ? ( - - ) : ( - + ); + case 'settings-outline': + return ( + ); + case 'quote': + return ( + + ); case 'retweet': return ( - { - const { translateY } = useScrollMoreOnPageEnd(); - - return ( - - - - ); -}); - -export default MyLists; diff --git a/apps/mobile/components/screens/profile/stack/SelectAccount.tsx b/apps/mobile/components/screens/profile/stack/SelectAccount.tsx index f4329492..20cde7af 100644 --- a/apps/mobile/components/screens/profile/stack/SelectAccount.tsx +++ b/apps/mobile/components/screens/profile/stack/SelectAccount.tsx @@ -26,6 +26,8 @@ import { import { APP_EVENT_ENUM } from '../../../../services/publishers/app.publisher'; import { APP_ROUTING_ENUM } from '../../../../utils/route-list'; import { appDimensions } from '../../../../styles/dimensions'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../../types/app.types'; function SelectAccountStack() { const { theme } = useAppTheme(); @@ -37,6 +39,7 @@ function SelectAccountStack() { ); const [Data, setData] = useState([]); const [Refreshing, setRefreshing] = useState(false); + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); function refresh() { try { @@ -84,7 +87,7 @@ function SelectAccountStack() { return ( @@ -109,19 +112,10 @@ function SelectAccountStack() { }, ]} > - Add Account + + {t(`onboarding.addAccountButton`)} + - - Bluesky, Mastodon, Misskey, Pleroma, Akkoma, Sharkey, Firefish - (Legacy) - } refreshControl={ @@ -137,12 +131,15 @@ export default SelectAccountStack; const styles = StyleSheet.create({ footerContainer: { marginHorizontal: 16, - marginBottom: 32, + marginBottom: 48, marginTop: 72, + marginLeft: 'auto', + marginRight: 'auto', }, ctaButtonContainer: { borderRadius: appDimensions.buttons.borderRadius, padding: 8, + paddingHorizontal: 16, }, ctaButtonText: { color: 'black', diff --git a/apps/mobile/components/screens/profile/stack/onboard/fragments/EnterYourServer.tsx b/apps/mobile/components/screens/profile/stack/onboard/fragments/EnterYourServer.tsx deleted file mode 100644 index 2d28be0c..00000000 --- a/apps/mobile/components/screens/profile/stack/onboard/fragments/EnterYourServer.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Dispatch, memo, SetStateAction } from 'react'; -import { StyleSheet, Text, TextInput, View } from 'react-native'; -import { APP_FONT } from '../../../../../../styles/AppTheme'; -import { APP_FONTS } from '../../../../../../styles/AppFonts'; -import Feather from '@expo/vector-icons/Feather'; -import { Button } from '@rneui/base'; -import { useAppTheme } from '../../../../../../hooks/utility/global-state-extractors'; -import { Loader } from '../../../../../lib/Loader'; - -type EnterYourServerProps = { - setServerText: Dispatch>; - ServerText: string; - onPressLogin: () => Promise; - buttonColor: string; - isLoading: boolean; -}; - -const EnterYourServer = memo( - ({ - ServerText, - setServerText, - onPressLogin, - buttonColor, - isLoading, - }: EnterYourServerProps) => { - const { theme } = useAppTheme(); - - return ( - - - Enter your server - - - - - - - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); - }, -); - -export default EnterYourServer; - -const styles = StyleSheet.create({ - sectionHeaderText: { - marginTop: 32, - marginBottom: 12, - color: APP_FONT.MONTSERRAT_BODY, - fontSize: 16, - textAlign: 'center', - fontFamily: APP_FONTS.INTER_500_MEDIUM, - }, - inputContainerRoot: { - flexDirection: 'row', - borderWidth: 2, - borderColor: 'rgba(136,136,136,0.4)', - borderRadius: 8, - marginBottom: 12, - }, - inputContainer: { width: 24 + 8 * 2, padding: 8 }, -}); diff --git a/apps/mobile/components/screens/profile/stack/onboard/stacks/MastodonServerSelection.tsx b/apps/mobile/components/screens/profile/stack/onboard/stacks/MastodonServerSelection.tsx deleted file mode 100644 index 8fed47a9..00000000 --- a/apps/mobile/components/screens/profile/stack/onboard/stacks/MastodonServerSelection.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { KeyboardAvoidingView, Platform, ScrollView, View } from 'react-native'; -import { useState } from 'react'; -import ActivityPubService from '../../../../../../services/activitypub.service'; -import { router } from 'expo-router'; -import HideOnKeyboardVisibleContainer from '../../../../../containers/HideOnKeyboardVisibleContainer'; -import AppTopNavbar, { - APP_TOPBAR_TYPE_ENUM, -} from '../../../../../shared/topnavbar/AppTopNavbar'; -import useScrollMoreOnPageEnd from '../../../../../../states/useScrollMoreOnPageEnd'; -import PopularServers from '../fragments/PopularServers'; -import { - POPULAR_AKKOMA_SERVERS, - POPULAR_MASTODON_SERVERS, - POPULAR_PLEROMA_SERVERS, -} from '../data/server-meta'; -import EnterYourServer from '../fragments/EnterYourServer'; -import { useAppManager } from '../../../../../../hooks/utility/global-state-extractors'; - -function AccountsScreen() { - const { appManager } = useAppManager(); - const [Subdomain, setSubdomain] = useState('mastodon.social'); - const [IsLoading, setIsLoading] = useState(false); - - async function onPressNext() { - setIsLoading(true); - try { - const signInStrategy = await ActivityPubService.signInUrl( - Subdomain, - appManager, - ); - if (signInStrategy?.clientId && signInStrategy?.clientSecret) { - appManager.storage.setAtprotoServerClientTokens( - Subdomain, - signInStrategy?.clientId, - signInStrategy?.clientSecret, - ); - } - router.push({ - pathname: 'profile/onboard/signin-md', - params: { - signInUrl: signInStrategy?.loginUrl, - subdomain: Subdomain, - domain: signInStrategy?.software, - clientId: signInStrategy?.clientId, - clientSecret: signInStrategy?.clientSecret, - }, - }); - } finally { - setIsLoading(false); - } - } - - const { translateY } = useScrollMoreOnPageEnd(); - - return ( - - - - - - - - - - - - - - - ); -} - -export default AccountsScreen; diff --git a/apps/mobile/components/screens/profile/stack/onboard/stacks/MisskeySignIn.tsx b/apps/mobile/components/screens/profile/stack/onboard/stacks/MisskeySignIn.tsx deleted file mode 100644 index 0dde420d..00000000 --- a/apps/mobile/components/screens/profile/stack/onboard/stacks/MisskeySignIn.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { Alert, Dimensions, View, Text, StyleSheet } from 'react-native'; -import { useEffect, useState } from 'react'; -import WebView from 'react-native-webview'; -import { Button, Card } from '@rneui/base'; -import { verifyMisskeyToken } from '@dhaaga/bridge'; -import { FontAwesome, Ionicons } from '@expo/vector-icons'; -import { APP_FONT } from '../../../../../../styles/AppTheme'; -import { router, useLocalSearchParams } from 'expo-router'; -import WithAutoHideTopNavBar from '../../../../../containers/WithAutoHideTopNavBar'; -import HideOnKeyboardVisibleContainer from '../../../../../containers/HideOnKeyboardVisibleContainer'; -import useScrollMoreOnPageEnd from '../../../../../../states/useScrollMoreOnPageEnd'; -import { AccountService } from '../../../../../../database/entities/account'; -import { RandomUtil } from '../../../../../../utils/random.utils'; -import { APP_FONTS } from '../../../../../../styles/AppFonts'; -import { APP_ROUTING_ENUM } from '../../../../../../utils/route-list'; -import { ACCOUNT_METADATA_KEY } from '../../../../../../database/entities/account-metadata'; -import { Image } from 'expo-image'; -import { - useAppDb, - useAppPublishers, - useAppTheme, - useHub, -} from '../../../../../../hooks/utility/global-state-extractors'; -import { APP_EVENT_ENUM } from '../../../../../../services/publishers/app.publisher'; - -export type AccountCreationPreviewProps = { - avatar: string; - displayName: string; - username: string; -}; - -function AccountCreationPreview({ - avatar, - displayName, - username, -}: AccountCreationPreviewProps) { - return ( - - - {avatar && ( - - {/*@ts-ignore-next-line*/} - - - )} - - - {displayName} - {username} - - - - - - ); -} - -function MisskeySignInStack() { - const [Session, setSession] = useState(''); - const [PreviewCard, setPreviewCard] = - useState(null); - const [Token, setToken] = useState(null); - const [MisskeyId, setMisskeyId] = useState(null); - const { appSub } = useAppPublishers(); - const { db } = useAppDb(); - const { theme } = useAppTheme(); - const { loadAccounts } = useHub(); - - const params = useLocalSearchParams(); - const _signInUrl: string = params['signInUrl'] as string; - const _subdomain: string = params['subdomain'] as string; - const _domain: string = params['domain'] as string; - - useEffect(() => { - try { - const regex = /^https:\/\/(.*?)\/miauth\/(.*?)\?.*?/; - if (regex.test(_signInUrl)) { - const session = regex.exec(_signInUrl)[2]; - setSession(session); - } - } catch (e) { - setSession(RandomUtil.nanoId()); - } - }, []); - - const [SessionConfirmed, setSessionConfirmed] = useState(false); - - // Misskey has no use for the callback token - function callback(state) { - const regex = /^https:\/\/suvam.io\/\?session=(.*?)/; - if (regex.test(state.url)) { - setSessionConfirmed(true); - autoVerifyFromSession(); - } - } - - async function autoVerifyFromSession() { - const res = await verifyMisskeyToken(`https://${_subdomain}`, Session); - if (res.ok) { - setPreviewCard({ - displayName: res?.user?.name, - username: res?.user?.username, - avatar: res?.user?.avatarUrl, - }); - setToken(res.token); - setMisskeyId(res.user.id); - } - } - - async function onPressConfirm() { - const upsertResult = AccountService.upsert( - db, - { - identifier: MisskeyId, - server: _subdomain, - driver: _domain, - username: PreviewCard.username, - avatarUrl: PreviewCard.avatar, - displayName: PreviewCard.displayName, - }, - [ - { - key: ACCOUNT_METADATA_KEY.DISPLAY_NAME, - value: PreviewCard.displayName, - type: 'string', - }, - { - key: ACCOUNT_METADATA_KEY.AVATAR_URL, - value: PreviewCard.avatar, - type: 'string', - }, - { - key: ACCOUNT_METADATA_KEY.USER_IDENTIFIER, - value: MisskeyId, - type: 'string', - }, - { - key: ACCOUNT_METADATA_KEY.ACCESS_TOKEN, - value: Token, - type: 'string', - }, - ], - ); - if (upsertResult.type === 'success') { - Alert.alert('Account Added. Refresh if any screen is outdated.'); - appSub.publish(APP_EVENT_ENUM.ACCOUNT_LIST_CHANGED); - loadAccounts(); - ``; - router.replace(APP_ROUTING_ENUM.SETTINGS_TAB_ACCOUNTS); - } else { - console.log(upsertResult); - } - } - - const { translateY } = useScrollMoreOnPageEnd({ - itemCount: 0, - updateQueryCache: () => {}, - }); - - return ( - - - {!SessionConfirmed && ( - - )} - - - - - Step 2: Confirm your account - - {PreviewCard && } - {SessionConfirmed ? ( - - - - - - - Your token has been confirmed. - - - - - - - - - Confirm that you want to use this account. - - - - - ) : ( - - )} - - - - - - ); -} - -export default MisskeySignInStack; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, - image: { - flex: 1, - width: 48, - backgroundColor: '#0553', - }, -}); diff --git a/apps/mobile/components/screens/settings/stack/SelectProvider.tsx b/apps/mobile/components/screens/settings/stack/SelectProvider.tsx index 56b41ac1..a9f32d04 100644 --- a/apps/mobile/components/screens/settings/stack/SelectProvider.tsx +++ b/apps/mobile/components/screens/settings/stack/SelectProvider.tsx @@ -1,6 +1,6 @@ import AppTopNavbar from '../../../shared/topnavbar/AppTopNavbar'; import useScrollMoreOnPageEnd from '../../../../states/useScrollMoreOnPageEnd'; -import AppNoAccount from '../../../error-screen/AppNoAccount'; +import AddAccountPresenter from '../../../../features/onboarding/presenters/AddAccountPresenter'; import { APP_LANDING_PAGE_TYPE } from '../../../shared/topnavbar/AppTabLandingNavbar'; function SelectProviderStack() { @@ -8,7 +8,7 @@ function SelectProviderStack() { return ( - + ); } diff --git a/apps/mobile/components/shared/mfm/MentionSegment.tsx b/apps/mobile/components/shared/mfm/MentionSegment.tsx index c3a6b8e3..419680a4 100644 --- a/apps/mobile/components/shared/mfm/MentionSegment.tsx +++ b/apps/mobile/components/shared/mfm/MentionSegment.tsx @@ -52,7 +52,7 @@ function MentionSegment({ value, link, fontFamily, mentions }: Props) { diff --git a/apps/mobile/database/entities/account-saved-user.ts b/apps/mobile/database/entities/account-saved-user.ts index 353bbb30..04a7f474 100644 --- a/apps/mobile/database/entities/account-saved-user.ts +++ b/apps/mobile/database/entities/account-saved-user.ts @@ -22,11 +22,11 @@ class Service { static upsert(db: DataSource, acct: Account, user: AppPostAuthorType) { const conflict = db.accountSavedUser.findOne({ active: true, - identifier: user.userId, + identifier: user.id, }); if (conflict) { db.accountSavedUser.updateById(conflict.id, { - identifier: user.userId, + identifier: user.id, remoteServer: user.instance, avatarUrl: user.avatarUrl, displayName: user.displayName, @@ -38,7 +38,7 @@ class Service { } else { db.accountSavedUser.insert({ uuid: RandomUtil.nanoId(), - identifier: user.userId, + identifier: user.id, remoteServer: user.instance, avatarUrl: user.avatarUrl, displayName: user.displayName, @@ -50,7 +50,7 @@ class Service { } return db.accountSavedUser.findOne({ - identifier: user.userId, + identifier: user.id, }); } } diff --git a/apps/mobile/features/composer/ComposerLandingPage.tsx b/apps/mobile/features/composer/ComposerLandingPage.tsx index 5777c753..a8dc220d 100644 --- a/apps/mobile/features/composer/ComposerLandingPage.tsx +++ b/apps/mobile/features/composer/ComposerLandingPage.tsx @@ -5,7 +5,7 @@ import AppTabLandingNavbar, { import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; import Ionicons from '@expo/vector-icons/Ionicons'; import { router } from 'expo-router'; -import AppNoAccount from '../../components/error-screen/AppNoAccount'; +import AddAccountPresenter from '../onboarding/presenters/AddAccountPresenter'; import { useAppAcct, useAppBottomSheet, @@ -80,7 +80,7 @@ function ComposerLandingPage() { }, ]; - if (!acct) return ; + if (!acct) return ; function onQuickPost() { setCtx({ diff --git a/apps/mobile/features/composer/presenters/BottomMenuPresenter.tsx b/apps/mobile/features/composer/presenters/BottomMenuPresenter.tsx index 998d3303..17ba7c9f 100644 --- a/apps/mobile/features/composer/presenters/BottomMenuPresenter.tsx +++ b/apps/mobile/features/composer/presenters/BottomMenuPresenter.tsx @@ -44,7 +44,7 @@ function BottomMenuPresenter() { function handleThreadGate() { show({ title: 'Set Interaction', - description: ['Customize who can interact with this post.'], + description: ['Set who can interact with this post.'], actions: [ { label: 'Allow Quotes', diff --git a/apps/mobile/features/composer/presenters/ThreadGateDialogPresenter.tsx b/apps/mobile/features/composer/presenters/ThreadGateDialogPresenter.tsx new file mode 100644 index 00000000..c4368f41 --- /dev/null +++ b/apps/mobile/features/composer/presenters/ThreadGateDialogPresenter.tsx @@ -0,0 +1,8 @@ +import { ThreadGateSetting } from '../reducers/composer.reducer'; + +type Props = { + seed: ThreadGateSetting[]; + onSubmit: (threadGates: ThreadGateSetting[]) => void; +}; + +function ThreadGateDialogPresenter({}: Props) {} diff --git a/apps/mobile/features/composer/reducers/composer.reducer.ts b/apps/mobile/features/composer/reducers/composer.reducer.ts index 7534d5db..2d54375e 100644 --- a/apps/mobile/features/composer/reducers/composer.reducer.ts +++ b/apps/mobile/features/composer/reducers/composer.reducer.ts @@ -48,7 +48,9 @@ type State = { keyboardSelection: { start: number; end: number }; visibility: APP_POST_VISIBILITY; + parent: AppPostObject | null; + isQuote: boolean; suggestions: AutoFillResultsType; @@ -116,6 +118,7 @@ const DEFAULT: State = { text: null, visibility: APP_POST_VISIBILITY.PUBLIC, parent: null, + isQuote: false, keyboardSelection: { start: 0, end: 0, diff --git a/apps/mobile/features/inbox/presenters/InboxPresenter.tsx b/apps/mobile/features/inbox/presenters/InboxPresenter.tsx index 85cd4747..c421bc65 100644 --- a/apps/mobile/features/inbox/presenters/InboxPresenter.tsx +++ b/apps/mobile/features/inbox/presenters/InboxPresenter.tsx @@ -2,7 +2,7 @@ import { useAppAcct, useAppTheme, } from '../../../hooks/utility/global-state-extractors'; -import AppNoAccount from '../../../components/error-screen/AppNoAccount'; +import AddAccountPresenter from '../../onboarding/presenters/AddAccountPresenter'; import { APP_LANDING_PAGE_TYPE } from '../../../components/shared/topnavbar/AppTabLandingNavbar'; import { useRef, useState } from 'react'; import PagerView from 'react-native-pager-view'; @@ -55,7 +55,7 @@ function InboxPresenter() { const ref = useRef(null); - if (!acct) return ; + if (!acct) return ; /** * ---- User Logged In ---- diff --git a/apps/mobile/features/inbox/presenters/MediaThumbListPresenter.tsx b/apps/mobile/features/inbox/presenters/MediaThumbListPresenter.tsx new file mode 100644 index 00000000..636687d4 --- /dev/null +++ b/apps/mobile/features/inbox/presenters/MediaThumbListPresenter.tsx @@ -0,0 +1,39 @@ +import { FlatList } from 'react-native'; +import { appDimensions } from '../../../styles/dimensions'; +import { AppMediaObject, AppPostObject } from '../../../types/app-post.types'; +import { KNOWN_SOFTWARE } from '@dhaaga/bridge'; +import MediaThumbView from '../view/MediaThumbView'; +import { useAppDialog } from '../../../hooks/utility/global-state-extractors'; + +type Props = { + items: AppMediaObject[]; + post: AppPostObject; + server?: KNOWN_SOFTWARE; +}; + +function MediaThumbListPresenter({ items, post, server }: Props) { + const { show } = useAppDialog(); + function onPress() {} + + function onLongPress() {} + + return ( + ( + + )} + style={{ + marginBottom: appDimensions.timelines.sectionBottomMargin, + marginTop: 8, + }} + /> + ); +} + +export default MediaThumbListPresenter; diff --git a/apps/mobile/features/inbox/view/MediaThumbView.tsx b/apps/mobile/features/inbox/view/MediaThumbView.tsx new file mode 100644 index 00000000..972552e9 --- /dev/null +++ b/apps/mobile/features/inbox/view/MediaThumbView.tsx @@ -0,0 +1,48 @@ +import { useImageAutoHeight } from '../../../hooks/app/useImageDims'; +import { Pressable, View } from 'react-native'; +import { Image } from 'expo-image'; + +type Props = { + url: string; + onPress: () => void; + onLongPress: () => void; +}; + +function MediaThumbView({ url, onPress, onLongPress }: Props) { + const Data = useImageAutoHeight(url); + + if (!Data.resolved) return ; + + return ( + + {/*@ts-ignore-next-line*/} + {}} + /> + + ); +} + +export default MediaThumbView; diff --git a/apps/mobile/features/my-account/presenters/MyAccountPresenter.tsx b/apps/mobile/features/my-account/presenters/MyAccountPresenter.tsx index 26ed5b4e..50ef87ff 100644 --- a/apps/mobile/features/my-account/presenters/MyAccountPresenter.tsx +++ b/apps/mobile/features/my-account/presenters/MyAccountPresenter.tsx @@ -7,7 +7,7 @@ import { import useScrollMoreOnPageEnd from '../../../states/useScrollMoreOnPageEnd'; import ProfileLandingAccountOverview from '../../../components/screens/profile/stack/landing/fragments/ProfileLandingAccountOverview'; import { APP_FONTS } from '../../../styles/AppFonts'; -import AppNoAccount from '../../../components/error-screen/AppNoAccount'; +import AddAccountPresenter from '../../onboarding/presenters/AddAccountPresenter'; import { APP_LANDING_PAGE_TYPE } from '../../../components/shared/topnavbar/AppTabLandingNavbar'; import { useAppAcct, @@ -47,7 +47,7 @@ function MyAccountPresenter() { }); } - if (!acct) return ; + if (!acct) return ; const serverModules: AppModulesProps[] = DriverService.getAccountModules( t, diff --git a/apps/mobile/features/my-account/presenters/MyListsPresenter.tsx b/apps/mobile/features/my-account/presenters/MyListsPresenter.tsx new file mode 100644 index 00000000..d3de4b6a --- /dev/null +++ b/apps/mobile/features/my-account/presenters/MyListsPresenter.tsx @@ -0,0 +1,15 @@ +import useScrollMoreOnPageEnd from '../../../states/useScrollMoreOnPageEnd'; +import WithAutoHideTopNavBar from '../../../components/containers/WithAutoHideTopNavBar'; +import { View } from 'react-native'; + +function MyListsPresenter() { + const { translateY } = useScrollMoreOnPageEnd(); + + return ( + + + + ); +} + +export default MyListsPresenter; diff --git a/apps/mobile/components/screens/profile/stack/onboard/fragments/PleromaPasteToken.tsx b/apps/mobile/features/onboarding/components/PleromaPasteToken.tsx similarity index 90% rename from apps/mobile/components/screens/profile/stack/onboard/fragments/PleromaPasteToken.tsx rename to apps/mobile/features/onboarding/components/PleromaPasteToken.tsx index f4508c0c..a972d1f1 100644 --- a/apps/mobile/components/screens/profile/stack/onboard/fragments/PleromaPasteToken.tsx +++ b/apps/mobile/features/onboarding/components/PleromaPasteToken.tsx @@ -1,8 +1,8 @@ import { Dispatch, memo, SetStateAction } from 'react'; import { StyleSheet, TextInput, View } from 'react-native'; import { KNOWN_SOFTWARE } from '@dhaaga/bridge'; -import { APP_FONT } from '../../../../../../styles/AppTheme'; -import { APP_FONTS } from '../../../../../../styles/AppFonts'; +import { APP_FONT } from '../../../styles/AppTheme'; +import { APP_FONTS } from '../../../styles/AppFonts'; type PleromaPasteTokenProps = { domain: string; diff --git a/apps/mobile/components/screens/profile/stack/onboard/fragments/PopularServers.tsx b/apps/mobile/features/onboarding/components/PopularServers.tsx similarity index 90% rename from apps/mobile/components/screens/profile/stack/onboard/fragments/PopularServers.tsx rename to apps/mobile/features/onboarding/components/PopularServers.tsx index 4846e9bc..645ddca9 100644 --- a/apps/mobile/components/screens/profile/stack/onboard/fragments/PopularServers.tsx +++ b/apps/mobile/features/onboarding/components/PopularServers.tsx @@ -1,7 +1,7 @@ import { Dispatch, memo, SetStateAction } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import { APP_FONT, APP_THEME } from '../../../../../../styles/AppTheme'; -import { APP_FONTS } from '../../../../../../styles/AppFonts'; +import { APP_FONT, APP_THEME } from '../../../styles/AppTheme'; +import { APP_FONTS } from '../../../styles/AppFonts'; type PopularServersDto = { label: string; diff --git a/apps/mobile/features/onboarding/components/ProtocolCards.tsx b/apps/mobile/features/onboarding/components/ProtocolCards.tsx new file mode 100644 index 00000000..4725853a --- /dev/null +++ b/apps/mobile/features/onboarding/components/ProtocolCards.tsx @@ -0,0 +1,143 @@ +import { useAppTheme } from '../../../hooks/utility/global-state-extractors'; +import SoftwareHeader from '../../../screens/accounts/fragments/SoftwareHeader'; +import { KNOWN_SOFTWARE } from '@dhaaga/bridge'; +import { APP_ROUTING_ENUM } from '../../../utils/route-list'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { router } from 'expo-router'; +import { AppText } from '../../../components/lib/Text'; +import { APP_COLOR_PALETTE_EMPHASIS } from '../../../utils/theming.util'; +import { APP_FONTS } from '../../../styles/AppFonts'; + +/** + * This UI fragment can be shared with other + * screens (that might have a different header, + * footer or page decorations) + * @constructor + */ +function ProtocolCards() { + const { theme } = useAppTheme(); + const options: { + label: string; + padding: number; + rightComponent: any; + to: string; + desc?: string; + }[] = [ + { + label: 'Bluesky', + desc: '- Custom PDS (for now)', + padding: 0, + rightComponent: ( + + ), + to: APP_ROUTING_ENUM.ATPROTO_SIGNIN, + }, + { + label: 'Mastodon', + padding: 20, + desc: '+ Pleroma, Akkoma', + rightComponent: ( + + ), + to: APP_ROUTING_ENUM.MASTODON_SERVER_SELECTION, + }, + { + label: 'Misskey', + padding: 12, + desc: '+ Sharkey, CherryPick', + rightComponent: ( + + ), + to: APP_ROUTING_ENUM.MISSKEY_SERVER_SELECTION, + }, + ]; + + return ( + + {options.map((option, i) => ( + { + router.push(option.to); + }} + > + + + {option.label} + + {option.desc && ( + + {option.desc} + + )} + + + {option.rightComponent} + + + ))} + + ); +} + +export default ProtocolCards; + +const styles = StyleSheet.create({ + noAccountText: { + fontSize: 24, + textAlign: 'center', + marginTop: 48, + fontFamily: APP_FONTS.INTER_700_BOLD, + marginBottom: 32, + }, + selectSnsBox: { + padding: 6, + flexDirection: 'row', + alignItems: 'center', + margin: 10, + borderRadius: 16, + paddingHorizontal: 20, + }, + selectSnsLabel: { + fontSize: 22, + }, + tipContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + width: '100%', + marginTop: 32, + }, + tipText: { + marginLeft: 6, + }, +}); diff --git a/apps/mobile/components/screens/profile/stack/onboard/data/server-meta.ts b/apps/mobile/features/onboarding/data/server-meta.ts similarity index 100% rename from apps/mobile/components/screens/profile/stack/onboard/data/server-meta.ts rename to apps/mobile/features/onboarding/data/server-meta.ts diff --git a/apps/mobile/features/onboarding/features/atproto/presenters/AtProtoLoginPresenter.tsx b/apps/mobile/features/onboarding/features/atproto/presenters/AtProtoLoginPresenter.tsx new file mode 100644 index 00000000..01c49522 --- /dev/null +++ b/apps/mobile/features/onboarding/features/atproto/presenters/AtProtoLoginPresenter.tsx @@ -0,0 +1,55 @@ +import { useLocalSearchParams } from 'expo-router'; +import TitleOnlyNoScrollContainer from '../../../../../components/containers/TitleOnlyNoScrollContainer'; +import { Dimensions, ScrollView, Text, View } from 'react-native'; +import WebView from 'react-native-webview'; +import { useState } from 'react'; +import { AppText } from '../../../../../components/lib/Text'; +import HideOnKeyboardVisibleContainer from '../../../../../components/containers/HideOnKeyboardVisibleContainer'; +import { useAppTheme } from '../../../../../hooks/utility/global-state-extractors'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../../../types/app.types'; + +function AtProtoLoginPresenter() { + const params = useLocalSearchParams(); + const _pds: string = params['pds'] as string; + const _handle: string = params['handle'] as string; + const _signInUrl: string = params['signInUrl'] as string; + const [CallbackUrl, setCallbackUrl] = useState(null); + const { theme } = useAppTheme(); + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); + + // Mastodon/Pleroma need to use the callback code to work + function callback(state) { + setCallbackUrl(state.url); + const regex = /^https:\/\/(.*?)\?code=(.*?)$/; + if (regex.test(state.url)) { + // const code = state.url.match(regex)[2]; + // setCode(code); + } + } + + return ( + + + + + + + + + Waiting for you to Sign-In + + + + + + ); +} + +export default AtProtoLoginPresenter; diff --git a/apps/mobile/features/onboarding/interactors/useAtprotoLogin.ts b/apps/mobile/features/onboarding/interactors/useAtprotoLogin.ts new file mode 100644 index 00000000..dd94a2d2 --- /dev/null +++ b/apps/mobile/features/onboarding/interactors/useAtprotoLogin.ts @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react'; +import { SuvamIoService } from '../../../services/suvamio.service'; +import * as WebBrowser from 'expo-web-browser'; +import { useLocalSearchParams } from 'expo-router'; +import AtprotoSessionService from '../../../services/atproto/atproto-session.service'; + +function useAtprotoLogin() { + const [IsLoading, setIsLoading] = useState(false); + const [Verifier, setVerifier] = useState(null); + + const params = useLocalSearchParams(); + const _state: string = params['state'] as string; + const _code: string = params['code'] as string; + + async function onObtainCode() { + setIsLoading(true); + try { + console.log(_state, _code, Verifier); + const { success, data, error } = + await AtprotoSessionService.exchangeCodeForSession(_code, Verifier); + + console.log(success); + } catch (e) { + console.log(e); + } finally { + setIsLoading(false); + } + } + + useEffect(() => { + if (_state && _code) { + // redirection successful! + onObtainCode(); + } + }, [_state, _code]); + + async function startLoginFlow(handle: string) { + setIsLoading(true); + const { data } = await SuvamIoService.generateAtprotoRedirectUrl(handle); + console.log('obtained data', data); + setVerifier(data.verifier); + + /** + * will perform the oauth, redirecting the + * user here after completion + */ + await WebBrowser.openAuthSessionAsync(data.href, undefined, {}); + setIsLoading(false); + } + + return { startLoginFlow, isLoading: IsLoading }; +} + +export default useAtprotoLogin; diff --git a/apps/mobile/features/onboarding/interactors/useMastoApiLogin.ts b/apps/mobile/features/onboarding/interactors/useMastoApiLogin.ts new file mode 100644 index 00000000..648701bd --- /dev/null +++ b/apps/mobile/features/onboarding/interactors/useMastoApiLogin.ts @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import { useAppManager } from '../../../hooks/utility/global-state-extractors'; +import ActivityPubService from '../../../services/activitypub.service'; +import { router } from 'expo-router'; +import { APP_ROUTING_ENUM } from '../../../utils/route-list'; + +function useMastoApiLogin() { + const [Server, setServer] = useState('mastodon.social'); + const [IsLoading, setIsLoading] = useState(false); + const { appManager } = useAppManager(); + + /** + * On selection of a server, try to + * detect the software used in it + * and redirect the user accordingly + */ + async function resolve() { + setIsLoading(true); + try { + const signInStrategy = await ActivityPubService.signInUrl( + Server, + appManager, + ); + if (signInStrategy?.clientId && signInStrategy?.clientSecret) { + appManager.storage.setAtprotoServerClientTokens( + Server, + signInStrategy?.clientId, + signInStrategy?.clientSecret, + ); + } + router.push({ + pathname: APP_ROUTING_ENUM.MASTODON_SIGNIN, + params: { + signInUrl: signInStrategy?.loginUrl, + subdomain: Server, + domain: signInStrategy?.software, + clientId: signInStrategy?.clientId, + clientSecret: signInStrategy?.clientSecret, + }, + }); + } finally { + setIsLoading(false); + } + } + + return { resolve, server: Server, setServer, isLoading: IsLoading }; +} +export default useMastoApiLogin; diff --git a/apps/mobile/features/onboarding/interactors/useMiauthLogin.ts b/apps/mobile/features/onboarding/interactors/useMiauthLogin.ts new file mode 100644 index 00000000..006bd52e --- /dev/null +++ b/apps/mobile/features/onboarding/interactors/useMiauthLogin.ts @@ -0,0 +1,121 @@ +import { useEffect, useState } from 'react'; +import { verifyMisskeyToken } from '@dhaaga/bridge'; +import { AccountCreationPreviewProps } from '../presenters/MiauthSignIn'; +import { router, useLocalSearchParams } from 'expo-router'; +import { RandomUtil } from '../../../utils/random.utils'; +import { AccountService } from '../../../database/entities/account'; +import { ACCOUNT_METADATA_KEY } from '../../../database/entities/account-metadata'; +import { Alert } from 'react-native'; +import { APP_EVENT_ENUM } from '../../../services/publishers/app.publisher'; +import { APP_ROUTING_ENUM } from '../../../utils/route-list'; +import { + useAppDb, + useAppPublishers, + useHub, +} from '../../../hooks/utility/global-state-extractors'; + +function useMiauthLogin() { + const [Session, setSession] = useState(''); + const [PreviewCard, setPreviewCard] = + useState(null); + const [Token, setToken] = useState(null); + const [MisskeyId, setMisskeyId] = useState(null); + const [SessionConfirmed, setSessionConfirmed] = useState(false); + const { db } = useAppDb(); + const { loadAccounts } = useHub(); + const { appSub } = useAppPublishers(); + + const params = useLocalSearchParams(); + const _signInUrl: string = params['signInUrl'] as string; + const _subdomain: string = params['subdomain'] as string; + const _domain: string = params['domain'] as string; + + async function autoVerifyFromSession() { + const res = await verifyMisskeyToken(`https://${_subdomain}`, Session); + if (res.ok) { + setPreviewCard({ + displayName: res?.user?.name, + username: res?.user?.username, + avatar: res?.user?.avatarUrl, + }); + setToken(res.token); + setMisskeyId(res.user.id); + } + } + + // Misskey has no use for the callback token + function webviewCallback(state: any) { + const regex = /^https:\/\/suvam.io\/\?session=(.*?)/; + if (regex.test(state.url)) { + setSessionConfirmed(true); + autoVerifyFromSession(); + } + } + + useEffect(() => { + try { + const regex = /^https:\/\/(.*?)\/miauth\/(.*?)\?.*?/; + if (regex.test(_signInUrl)) { + const session = regex.exec(_signInUrl)[2]; + setSession(session); + } + } catch (e) { + setSession(RandomUtil.nanoId()); + } + }, []); + + async function confirm() { + const upsertResult = AccountService.upsert( + db, + { + identifier: MisskeyId, + server: _subdomain, + driver: _domain, + username: PreviewCard.username, + avatarUrl: PreviewCard.avatar, + displayName: PreviewCard.displayName, + }, + [ + { + key: ACCOUNT_METADATA_KEY.DISPLAY_NAME, + value: PreviewCard.displayName, + type: 'string', + }, + { + key: ACCOUNT_METADATA_KEY.AVATAR_URL, + value: PreviewCard.avatar, + type: 'string', + }, + { + key: ACCOUNT_METADATA_KEY.USER_IDENTIFIER, + value: MisskeyId, + type: 'string', + }, + { + key: ACCOUNT_METADATA_KEY.ACCESS_TOKEN, + value: Token, + type: 'string', + }, + ], + ); + if (upsertResult.type === 'success') { + Alert.alert('Account Added. Refresh if any screen feels outdated.'); + appSub.publish(APP_EVENT_ENUM.ACCOUNT_LIST_CHANGED); + loadAccounts(); + ``; + router.replace(APP_ROUTING_ENUM.SETTINGS_TAB_ACCOUNTS); + } else { + console.log(upsertResult); + } + } + + return { + callback: webviewCallback, + confirm, + sessionConfirmed: SessionConfirmed, + loginUri: _signInUrl, + previewCard: PreviewCard, + }; +} + +export default useMiauthLogin; diff --git a/apps/mobile/features/onboarding/presenters/AddAccountPresenter.tsx b/apps/mobile/features/onboarding/presenters/AddAccountPresenter.tsx new file mode 100644 index 00000000..3fdfeb82 --- /dev/null +++ b/apps/mobile/features/onboarding/presenters/AddAccountPresenter.tsx @@ -0,0 +1,164 @@ +import { + RefreshControl, + ScrollView, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native'; +import useGlobalState from '../../../states/_global'; +import { useShallow } from 'zustand/react/shallow'; +import { APP_FONTS } from '../../../styles/AppFonts'; +import { AppIcon } from '../../../components/lib/Icon'; +import { router } from 'expo-router'; +import AppTabLandingNavbar, { + APP_LANDING_PAGE_TYPE, +} from '../../../components/shared/topnavbar/AppTabLandingNavbar'; +import { useState } from 'react'; +import { AccountService } from '../../../database/entities/account'; +import { APP_COLOR_PALETTE_EMPHASIS } from '../../../utils/theming.util'; +import { + useAppAcct, + useAppTheme, +} from '../../../hooks/utility/global-state-extractors'; +import { AppText } from '../../../components/lib/Text'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../types/app.types'; +import ProtocolCards from '../components/ProtocolCards'; + +type AddAccountLandingFragmentProps = { + containerStyle?: StyleProp; +}; + +/** + * This UI fragment can be shared with other + * screens (that might have a different header, + * but share footer or page decorations) + * @constructor + */ +export function AddAccountLandingFragment({ + containerStyle, +}: AddAccountLandingFragmentProps) { + const { theme } = useAppTheme(); + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); + return ( + + + {t(`onboarding.addAccountButton`)} + + + + + + {t(`onboarding.accountCreationNotSupported`)} + + + + ); +} + +/** + * A full screen cover when no account is selected + * @constructor + */ + +type AppNoAccountProps = { + tab: APP_LANDING_PAGE_TYPE; +}; + +function AddAccountPresenter({ tab }: AppNoAccountProps) { + const [IsRefreshing, setIsRefreshing] = useState(false); + const { theme } = useAppTheme(); + const { acct } = useAppAcct(); + const { db, loadApp } = useGlobalState( + useShallow((o) => ({ + db: o.db, + loadApp: o.loadApp, + })), + ); + + function onRefresh() { + setIsRefreshing(true); + try { + // possibly locked because of added/deleted account + if (!acct) { + AccountService.ensureAccountSelection(db); + loadApp(); + setIsRefreshing(false); + } + } catch (e) { + setIsRefreshing(false); + } finally { + setIsRefreshing(false); + } + } + + return ( + + } + > + { + router.navigate('/user-guide'); + }, + }, + ]} + /> + + + ); +} + +export default AddAccountPresenter; + +const styles = StyleSheet.create({ + noAccountText: { + fontSize: 24, + textAlign: 'center', + marginTop: 48, + fontFamily: APP_FONTS.INTER_700_BOLD, + marginBottom: 32, + }, + selectSnsBox: { + padding: 6, + flexDirection: 'row', + alignItems: 'center', + margin: 10, + borderRadius: 16, + paddingHorizontal: 20, + }, + selectSnsLabel: { + fontSize: 22, + }, + tipContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + width: '100%', + marginTop: 32, + }, + tipText: { + marginLeft: 6, + }, +}); diff --git a/apps/mobile/features/onboarding/presenters/MastoApiServerPresenter.tsx b/apps/mobile/features/onboarding/presenters/MastoApiServerPresenter.tsx new file mode 100644 index 00000000..8653c187 --- /dev/null +++ b/apps/mobile/features/onboarding/presenters/MastoApiServerPresenter.tsx @@ -0,0 +1,52 @@ +import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native'; +import AppTopNavbar, { + APP_TOPBAR_TYPE_ENUM, +} from '../../../components/shared/topnavbar/AppTopNavbar'; +import useScrollMoreOnPageEnd from '../../../states/useScrollMoreOnPageEnd'; +import ServerInputView from '../views/ServerInputView'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../types/app.types'; +import PopularMastoServersView from '../views/PopularMastoServersView'; +import useMastoApiLogin from '../interactors/useMastoApiLogin'; +import { appDimensions } from '../../../styles/dimensions'; + +function AccountsScreen() { + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); + const { isLoading, server, setServer, resolve } = useMastoApiLogin(); + const { translateY } = useScrollMoreOnPageEnd(); + + return ( + + + + + + + + + ); +} + +export default AccountsScreen; diff --git a/apps/mobile/components/screens/profile/stack/onboard/stacks/MastodonSignIn.tsx b/apps/mobile/features/onboarding/presenters/MastoApiSignIn.tsx similarity index 88% rename from apps/mobile/components/screens/profile/stack/onboard/stacks/MastodonSignIn.tsx rename to apps/mobile/features/onboarding/presenters/MastoApiSignIn.tsx index 692b6c9d..b89dd2a8 100644 --- a/apps/mobile/components/screens/profile/stack/onboard/stacks/MastodonSignIn.tsx +++ b/apps/mobile/features/onboarding/presenters/MastoApiSignIn.tsx @@ -2,22 +2,22 @@ import { Dimensions, View, ScrollView, Alert, Text } from 'react-native'; import { useState } from 'react'; import WebView from 'react-native-webview'; import { Button } from '@rneui/base'; -import TitleOnlyNoScrollContainer from '../../../../../containers/TitleOnlyNoScrollContainer'; -import HideOnKeyboardVisibleContainer from '../../../../../containers/HideOnKeyboardVisibleContainer'; +import TitleOnlyNoScrollContainer from '../../../components/containers/TitleOnlyNoScrollContainer'; +import HideOnKeyboardVisibleContainer from '../../../components/containers/HideOnKeyboardVisibleContainer'; import { router, useLocalSearchParams } from 'expo-router'; import { UnknownRestClient, KNOWN_SOFTWARE } from '@dhaaga/bridge'; -import PleromaPasteToken from '../fragments/PleromaPasteToken'; -import { AccountService } from '../../../../../../database/entities/account'; -import { APP_ROUTING_ENUM } from '../../../../../../utils/route-list'; -import { ACCOUNT_METADATA_KEY } from '../../../../../../database/entities/account-metadata'; -import { APP_EVENT_ENUM } from '../../../../../../services/publishers/app.publisher'; +import PleromaPasteToken from '../components/PleromaPasteToken'; +import { AccountService } from '../../../database/entities/account'; +import { APP_ROUTING_ENUM } from '../../../utils/route-list'; +import { ACCOUNT_METADATA_KEY } from '../../../database/entities/account-metadata'; +import { APP_EVENT_ENUM } from '../../../services/publishers/app.publisher'; import { useAppDb, useAppPublishers, useAppTheme, useHub, -} from '../../../../../../hooks/utility/global-state-extractors'; -import { APP_FONTS } from '../../../../../../styles/AppFonts'; +} from '../../../hooks/utility/global-state-extractors'; +import { APP_FONTS } from '../../../styles/AppFonts'; function MastodonSignInStack() { const { theme } = useAppTheme(); diff --git a/apps/mobile/components/screens/profile/stack/onboard/stacks/MisskeyServerSelection.tsx b/apps/mobile/features/onboarding/presenters/MiauthServerPresenter.tsx similarity index 83% rename from apps/mobile/components/screens/profile/stack/onboard/stacks/MisskeyServerSelection.tsx rename to apps/mobile/features/onboarding/presenters/MiauthServerPresenter.tsx index 78899d62..443b0b7b 100644 --- a/apps/mobile/components/screens/profile/stack/onboard/stacks/MisskeyServerSelection.tsx +++ b/apps/mobile/features/onboarding/presenters/MiauthServerPresenter.tsx @@ -9,14 +9,14 @@ import { ScrollView, } from 'react-native'; import { Button } from '@rneui/base'; -import { APP_FONT } from '../../../../../../styles/AppTheme'; -import { APP_FONTS } from '../../../../../../styles/AppFonts'; -import HideOnKeyboardVisibleContainer from '../../../../../containers/HideOnKeyboardVisibleContainer'; +import { APP_FONT } from '../../../styles/AppTheme'; +import { APP_FONTS } from '../../../styles/AppFonts'; +import HideOnKeyboardVisibleContainer from '../../../components/containers/HideOnKeyboardVisibleContainer'; import { router } from 'expo-router'; -import ActivityPubService from '../../../../../../services/activitypub.service'; -import useScrollMoreOnPageEnd from '../../../../../../states/useScrollMoreOnPageEnd'; +import ActivityPubService from '../../../services/activitypub.service'; +import useScrollMoreOnPageEnd from '../../../states/useScrollMoreOnPageEnd'; import Feather from '@expo/vector-icons/Feather'; -import PopularServers from '../fragments/PopularServers'; +import PopularServers from '../components/PopularServers'; import { POPULAR_FIREFISH_SERVERS, POPULAR_MISSKEY_SERVERS, @@ -24,15 +24,15 @@ import { } from '../data/server-meta'; import AppTopNavbar, { APP_TOPBAR_TYPE_ENUM, -} from '../../../../../shared/topnavbar/AppTopNavbar'; -import { APP_ROUTING_ENUM } from '../../../../../../utils/route-list'; +} from '../../../components/shared/topnavbar/AppTopNavbar'; +import { APP_ROUTING_ENUM } from '../../../utils/route-list'; import { useAppManager, useAppTheme, -} from '../../../../../../hooks/utility/global-state-extractors'; -import { Loader } from '../../../../../lib/Loader'; +} from '../../../hooks/utility/global-state-extractors'; +import { Loader } from '../../../components/lib/Loader'; -function MisskeyServerSelection() { +function MiauthServerPresenter() { const [InputText, setInputText] = useState('misskey.io'); const { appManager } = useAppManager(); const { theme } = useAppTheme(); @@ -177,4 +177,4 @@ const styles = StyleSheet.create({ inputContainer: { width: 24 + 8 * 2, padding: 8 }, }); -export default MisskeyServerSelection; +export default MiauthServerPresenter; diff --git a/apps/mobile/features/onboarding/presenters/MiauthSignIn.tsx b/apps/mobile/features/onboarding/presenters/MiauthSignIn.tsx new file mode 100644 index 00000000..ede06dc6 --- /dev/null +++ b/apps/mobile/features/onboarding/presenters/MiauthSignIn.tsx @@ -0,0 +1,209 @@ +import { Dimensions, View, Text, StyleSheet } from 'react-native'; +import WebView from 'react-native-webview'; +import { Button, Card } from '@rneui/base'; +import { FontAwesome, Ionicons } from '@expo/vector-icons'; +import { APP_FONT } from '../../../styles/AppTheme'; +import WithAutoHideTopNavBar from '../../../components/containers/WithAutoHideTopNavBar'; +import HideOnKeyboardVisibleContainer from '../../../components/containers/HideOnKeyboardVisibleContainer'; +import useScrollMoreOnPageEnd from '../../../states/useScrollMoreOnPageEnd'; +import { APP_FONTS } from '../../../styles/AppFonts'; +import { Image } from 'expo-image'; +import { useAppTheme } from '../../../hooks/utility/global-state-extractors'; +import useMiauthLogin from '../interactors/useMiauthLogin'; +import { appDimensions } from '../../../styles/dimensions'; + +export type AccountCreationPreviewProps = { + avatar: string; + displayName: string; + username: string; +}; + +function AccountCreationPreview({ + avatar, + displayName, + username, +}: AccountCreationPreviewProps) { + return ( + + + {avatar && ( + + {/*@ts-ignore-next-line*/} + + + )} + + + {displayName} + {username} + + + + + + ); +} + +function MisskeySignInStack() { + const { theme } = useAppTheme(); + const { callback, confirm, sessionConfirmed, loginUri, previewCard } = + useMiauthLogin(); + const { translateY } = useScrollMoreOnPageEnd(); + + return ( + + + {!sessionConfirmed && ( + + )} + + + + {previewCard && } + {sessionConfirmed ? ( + + + + + + + Your token has been confirmed. + + + + + + + + + Confirm that you want to use this account. + + + + + ) : ( + + )} + + + + + + ); +} + +export default MisskeySignInStack; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'center', + }, + image: { + flex: 1, + width: 48, + backgroundColor: '#0553', + }, +}); diff --git a/apps/mobile/features/onboarding/views/PopularMastoServersView.tsx b/apps/mobile/features/onboarding/views/PopularMastoServersView.tsx new file mode 100644 index 00000000..76bc66fd --- /dev/null +++ b/apps/mobile/features/onboarding/views/PopularMastoServersView.tsx @@ -0,0 +1,39 @@ +import PopularServers from '../components/PopularServers'; +import { + POPULAR_AKKOMA_SERVERS, + POPULAR_MASTODON_SERVERS, + POPULAR_PLEROMA_SERVERS, +} from '../data/server-meta'; +import HideOnKeyboardVisibleContainer from '../../../components/containers/HideOnKeyboardVisibleContainer'; +import { Dispatch, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../types/app.types'; + +type Props = { + onSelect: Dispatch>; +}; + +function PopularMastoServersView({ onSelect }: Props) { + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); + return ( + + + + + + ); +} + +export default PopularMastoServersView; diff --git a/apps/mobile/features/onboarding/views/ServerInputView.tsx b/apps/mobile/features/onboarding/views/ServerInputView.tsx new file mode 100644 index 00000000..3e3f9de5 --- /dev/null +++ b/apps/mobile/features/onboarding/views/ServerInputView.tsx @@ -0,0 +1,103 @@ +import { Dispatch, SetStateAction } from 'react'; +import { StyleSheet, Text, TextInput, View } from 'react-native'; +import { APP_FONT } from '../../../styles/AppTheme'; +import { APP_FONTS } from '../../../styles/AppFonts'; +import Feather from '@expo/vector-icons/Feather'; +import { Button } from '@rneui/base'; +import { useAppTheme } from '../../../hooks/utility/global-state-extractors'; +import { Loader } from '../../../components/lib/Loader'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../types/app.types'; + +type EnterYourServerProps = { + setServerText: Dispatch>; + ServerText: string; + onPressLogin: () => Promise; + buttonColor: string; + isLoading: boolean; +}; + +function ServerInputView({ + ServerText, + setServerText, + onPressLogin, + buttonColor, + isLoading, +}: EnterYourServerProps) { + const { theme } = useAppTheme(); + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); + + return ( + + + {t(`onboarding.enterYourServer`)} + + + + + + + + + + {isLoading ? ( + + + + ) : ( + + )} + + + ); +} + +export default ServerInputView; + +const styles = StyleSheet.create({ + sectionHeaderText: { + marginTop: 32, + marginBottom: 12, + color: APP_FONT.MONTSERRAT_BODY, + fontSize: 16, + textAlign: 'center', + fontFamily: APP_FONTS.INTER_500_MEDIUM, + }, + inputContainerRoot: { + flexDirection: 'row', + borderWidth: 2, + borderColor: 'rgba(136,136,136,0.4)', + borderRadius: 8, + marginBottom: 12, + }, + inputContainer: { width: 24 + 8 * 2, padding: 8 }, +}); diff --git a/apps/mobile/features/search/components/SearchResultFull.tsx b/apps/mobile/features/search/components/SearchResultFull.tsx index 380d1bfc..02923dbc 100644 --- a/apps/mobile/features/search/components/SearchResultFull.tsx +++ b/apps/mobile/features/search/components/SearchResultFull.tsx @@ -53,8 +53,9 @@ function WidgetExpanded() { }, ]} > - {Tabs.map((o) => ( + {Tabs.map((o, i) => ( { setCategory(o); }} diff --git a/apps/mobile/features/search/interactors/SearchTabInteractor.tsx b/apps/mobile/features/search/interactors/SearchTabInteractor.tsx index 9b62bc98..c656b14d 100644 --- a/apps/mobile/features/search/interactors/SearchTabInteractor.tsx +++ b/apps/mobile/features/search/interactors/SearchTabInteractor.tsx @@ -4,7 +4,7 @@ import { useAppAcct, useAppTheme, } from '../../../hooks/utility/global-state-extractors'; -import AppNoAccount from '../../../components/error-screen/AppNoAccount'; +import AddAccountPresenter from '../../onboarding/presenters/AddAccountPresenter'; import { APP_LANDING_PAGE_TYPE } from '../../../components/shared/topnavbar/AppTabLandingNavbar'; import { View } from 'react-native'; import SearchWidget from '../components/SearchWidget'; @@ -13,7 +13,8 @@ function WithSearchBar({ children }: any) { const { theme } = useAppTheme(); const { acct } = useAppAcct(); - if (!acct) return ; + if (!acct) + return ; return ( diff --git a/apps/mobile/features/search/views/LandingPageView.tsx b/apps/mobile/features/search/views/LandingPageView.tsx index c4f02826..b699819d 100644 --- a/apps/mobile/features/search/views/LandingPageView.tsx +++ b/apps/mobile/features/search/views/LandingPageView.tsx @@ -5,6 +5,7 @@ import Header from '../components/Header'; function LandingPageView() { const { theme } = useAppTheme(); + return (
diff --git a/apps/mobile/features/timelines/view/TimelineErrorView.tsx b/apps/mobile/features/timelines/view/TimelineErrorView.tsx index aad8023b..3ee9a84b 100644 --- a/apps/mobile/features/timelines/view/TimelineErrorView.tsx +++ b/apps/mobile/features/timelines/view/TimelineErrorView.tsx @@ -5,18 +5,17 @@ import useScrollMoreOnPageEnd from '../../../states/useScrollMoreOnPageEnd'; import { AppText } from '../../../components/lib/Text'; import { appDimensions } from '../../../styles/dimensions'; import { APP_COLOR_PALETTE_EMPHASIS } from '../../../utils/theming.util'; +import { useTranslation } from 'react-i18next'; +import { LOCALIZATION_NAMESPACE } from '../../../types/app.types'; type Props = { error: any; }; + function TimelineErrorView({ error }: Props) { const { theme } = useAppTheme(); - /** - * Composite Hook Collection - */ - const { onScroll, translateY } = useScrollMoreOnPageEnd({ - itemCount: 0, - }); + const { translateY } = useScrollMoreOnPageEnd(); + const { t } = useTranslation([LOCALIZATION_NAMESPACE.CORE]); return ( - Error + {t(`errors.errorLabel`)} - Dhaaga failed to load this timeline. Report this error to the - developer. + {t(`errors.timelineErrorLabel`)} (); - const { data: acct, error } = useGetProfile({ userId: id }); + const { data: acct, error } = useGetProfile({ userId: id, did: id }); const fields = acct?.meta?.fields; const avatarUrl = acct?.avatarUrl; @@ -38,10 +38,7 @@ export function ProfileContextWrapped() { const IS_LOCKED = acct?.meta?.isProfileLocked; - const { onScroll } = useScrollMoreOnPageEnd({ - itemCount: 0, - updateQueryCache: () => {}, - }); + const { onScroll } = useScrollMoreOnPageEnd(); const { show } = useAppBottomSheet(); const { appManager } = useAppManager(); diff --git a/apps/mobile/hooks/api/useNotifications.ts b/apps/mobile/hooks/api/useNotifications.ts index c04fe5aa..6c57c809 100644 --- a/apps/mobile/hooks/api/useNotifications.ts +++ b/apps/mobile/hooks/api/useNotifications.ts @@ -3,6 +3,7 @@ import { KNOWN_SOFTWARE, MastodonRestClient, MisskeyRestClient, + PleromaRestClient, } from '@dhaaga/bridge'; import { useEffect, useState } from 'react'; import { AppNotificationObject } from '../../types/app-notification.types'; @@ -21,7 +22,10 @@ import { import ChatService, { AppChatRoom } from '../../services/chat.service'; import { AppResultPageType, pageResultDefault } from '../../types/app.types'; import { MisskeyService } from '../../services/misskey.service'; -import { MastoApiV2Service } from '../../services/masto-api.service'; +import { + MastoApiV1Service, + MastoApiV2Service, +} from '../../services/masto-api.service'; const NOTIFICATION_PAGE_SIZE = 20; @@ -108,8 +112,15 @@ function useApiGetMentionUpdates(maxId?: string | null) { if (ActivityPubService.misskeyLike(driver)) { return MisskeyService.packNotifs(results.data, driver, server); - } else if (driver === KNOWN_SOFTWARE.MASTODON) { + } else if (ActivityPubService.supportsV2(driver)) { return MastoApiV2Service.packNotifs(results.data, driver, server); + } else if (ActivityPubService.pleromaLike(driver)) { + return MastoApiV1Service.packNotifs( + results.data, + driver, + server, + 'mentions', + ); } else if (ActivityPubService.blueskyLike(driver)) { const _data = results.data as AppBskyNotificationListNotifications.OutputSchema; @@ -194,6 +205,13 @@ function useApiGetSocialUpdates(maxId?: string | null) { return MisskeyService.packNotifs(result.data, driver, server); } else if (ActivityPubService.supportsV2(driver)) { return MastoApiV2Service.packNotifs(result.data, driver, server); + } else if (ActivityPubService.pleromaLike(driver)) { + return MastoApiV1Service.packNotifs( + result.data, + driver, + server, + 'social', + ); } else { return pageResultDefault; } @@ -215,6 +233,7 @@ function useApiGetSubscriptionUpdates(maxId?: string | null) { return useQuery({ queryKey: ['notifications/subs', acct, maxId], queryFn: async () => { + console.log(ActivityPubService.pleromaLike(driver)); if (ActivityPubService.misskeyLike(driver)) { const result = await ( client as MisskeyRestClient @@ -232,6 +251,19 @@ function useApiGetSubscriptionUpdates(maxId?: string | null) { maxId, }); return MastoApiV2Service.packNotifs(result.data, driver, server); + } else if (ActivityPubService.pleromaLike(driver)) { + const result = await ( + client as PleromaRestClient + ).notifications.getSubscriptionUpdates({ + limit: NOTIFICATION_PAGE_SIZE, + maxId, + }); + return MastoApiV1Service.packNotifs( + result.data, + driver, + server, + 'updates', + ); } else { return pageResultDefault; } diff --git a/apps/mobile/hooks/app/useImageDims.tsx b/apps/mobile/hooks/app/useImageDims.tsx index e5c9e775..68130393 100644 --- a/apps/mobile/hooks/app/useImageDims.tsx +++ b/apps/mobile/hooks/app/useImageDims.tsx @@ -2,74 +2,33 @@ import { AppMediaObject } from '../../types/app-post.types'; import { useEffect, useRef, useState } from 'react'; import MediaService from '../../services/media.service'; import MediaUtils from '../../utils/media.utils'; +import { useImage } from 'expo-image'; /** * calculates the image width * for a fixed height container + * + * @param imageUrl + * @param maxH + * @param defaultW */ export function useImageAutoHeight( - item: AppMediaObject, - W: number, - maxH: number, + imageUrl: string, + maxH?: number, + defaultW?: number, ) { - const [Data, setData] = useState({ resolved: false, height: maxH, width: W }); - const ValueRef = useRef(null); - useEffect(() => { - if (!item) { - setData({ - resolved: false, - height: maxH, - width: W, - }); - return; - } - if (ValueRef.current === item.previewUrl) return; - setData({ - resolved: false, - height: maxH, - width: W, - }); + const DEFAULT_WIDTH = 240; + const DEFAULT_HEIGHT = 240; - /** - * Use image dimensions provided - * by server to calculate dimensions - */ - if (item.height && item.width) { - const { height } = MediaService.calculateDimensions({ - maxW: W, - maxH, - H: item.height, - W: item.width, - }); - setData({ resolved: true, height, width: W }); - ValueRef.current = item.previewUrl; - return; - } + const _maxH = maxH || DEFAULT_HEIGHT; + const _maxW = defaultW || DEFAULT_WIDTH; + const image = useImage({ uri: imageUrl, height: _maxH }); - /** - * Fetch the original image size - * to calculate dimensions - */ - MediaUtils.fetchImageSize(item.url) - .then(({ width: rnWidth, height: rnHeight }) => { - const { height } = MediaService.calculateDimensions({ - maxW: W, - maxH, - H: rnHeight, - W: rnWidth, - }); - setData({ resolved: false, height, width: W }); - }) - .finally(() => { - setData({ - ...Data, - resolved: true, - }); - ValueRef.current = item.previewUrl; - }); - }, [item]); - - return Data; + return { + resolved: true, + height: _maxH, + width: image ? (_maxH / image.height) * image.width : _maxW, + }; } /** diff --git a/apps/mobile/i18n/locales/de/core.json b/apps/mobile/i18n/locales/de/core.json index edef8f5b..a14cd7bf 100644 --- a/apps/mobile/i18n/locales/de/core.json +++ b/apps/mobile/i18n/locales/de/core.json @@ -5,6 +5,23 @@ "desc": "Dieses Feature ist entweder nicht von Dhaaga implementiert oder auf deiner SNS-Plattform nicht verfügbar." } }, + "errors": { + "errorLabel": "Fehler", + "timelineErrorLabel": "Dhaaga konnte diese Zeitleiste nicht laden. Melden Sie diesen Fehler dem Entwickler." + }, + "onboarding": { + "addAccountButton": "Konto hinzufügen", + "accountCreationNotSupported": "Die Kontenerstellung wird nicht unterstützt", + "needBlueskyAccount": "Du benötigst ein Bluesky-Konto", + "createOneHere": "Erstelle eines hier", + "loginButton": "Einloggen", + "appPassword": "App-Passwort", + "enterYourServer": "Geben Sie Ihren Server ein", + "serverUrl": "Ihre Server-URL", + "popularMastodon": "Beliebte Mastodon-Server", + "popularPleroma": "Beliebte Pleroma-Server", + "popularAkkoma": "Beliebte Akkoma-Server" + }, "topNav": { "primary": { "hub": "Soziales Hub", @@ -24,7 +41,12 @@ "myBookmarks": "Meine Lesezeichen", "myAccount": "Mein Konto", "myProfile": "Mein Profil", - "appSettings": "App-Einstellungen" + "appSettings": "App-Einstellungen", + "manageAccounts": "Konten verwalten", + "blueskySignIn": "Bluesky Anmelden", + "selectServer": "Server auswählen", + "mastodonSignIn": "Mastodon Anmelden", + "misskeySignIn": "Misskey Anmelden" } }, "dialogs": { diff --git a/apps/mobile/i18n/locales/en/core.json b/apps/mobile/i18n/locales/en/core.json index 44354d51..c0c68be8 100644 --- a/apps/mobile/i18n/locales/en/core.json +++ b/apps/mobile/i18n/locales/en/core.json @@ -5,6 +5,23 @@ "desc": "This feature is either not implemented by Dhaaga, or not available for your SNS platform." } }, + "errors": { + "errorLabel": "Error", + "timelineErrorLabel": "Dhaaga failed to load this timeline. Report this error to the developer." + }, + "onboarding": { + "addAccountButton": "Add Account", + "accountCreationNotSupported": "Account creation is not supported", + "needBlueskyAccount": "You need a Bluesky account", + "createOneHere": "Create one here", + "loginButton": "Log-In", + "appPassword": "App Password", + "enterYourServer": "Enter your server", + "serverUrl": "Your server url", + "popularMastodon": "Popular Mastodon Servers", + "popularPleroma": "Popular Pleroma Servers", + "popularAkkoma": "Popular Akkoma Servers" + }, "topNav": { "primary": { "hub": "Social Hub", @@ -24,7 +41,12 @@ "myBookmarks": "My Bookmarks", "myAccount": "My Account", "myProfile": "My Profile", - "appSettings": "App Settings" + "appSettings": "App Settings", + "manageAccounts": "Manage Accounts", + "blueskySignIn": "Bluesky Sign-In", + "selectServer": "Select Server", + "mastodonSignIn": "Mastodon Sign-In", + "misskeySignIn": "Misskey Sign-In" } }, "dialogs": { @@ -161,6 +183,7 @@ "ctaPlaceholder": "What's on your mind?", "postButton": "Post", "replyingTo": "Replying to: ", + "quoting": "Quoting: ", "visibility": { "public": "Public", "unlisted": "Home", diff --git a/apps/mobile/i18n/locales/id/core.json b/apps/mobile/i18n/locales/id/core.json index ea319632..4ace9c2e 100644 --- a/apps/mobile/i18n/locales/id/core.json +++ b/apps/mobile/i18n/locales/id/core.json @@ -5,6 +5,23 @@ "desc": "Fitur ini belum diimplementasikan oleh Dhaaga, atau tidak tersedia untuk platform SNS Anda." } }, + "errors": { + "errorLabel": "Kesalahan", + "timelineErrorLabel": "Dhaaga gagal memuat timeline ini. Laporkan kesalahan ini ke pengembang." + }, + "onboarding": { + "addAccountButton": "Tambah Akun", + "accountCreationNotSupported": "Pembuatan akun tidak didukung", + "needBlueskyAccount": "Anda memerlukan akun Bluesky", + "createOneHere": "Buat akun di sini", + "loginButton": "Masuk", + "appPassword": "Kata Sandi Aplikasi", + "enterYourServer": "Masukkan server Anda", + "serverUrl": "URL server Anda", + "popularMastodon": "Server Mastodon Populer", + "popularPleroma": "Server Pleroma Populer", + "popularAkkoma": "Server Akkoma Populer" + }, "topNav": { "primary": { "hub": "Pusat Sosial", @@ -24,7 +41,12 @@ "myBookmarks": "Tandai Saya", "myAccount": "Akun Saya", "myProfile": "Profil Saya", - "appSettings": "Pengaturan Aplikasi" + "appSettings": "Pengaturan Aplikasi", + "manageAccounts": "Kelola Akun", + "blueskySignIn": "Masuk Bluesky", + "selectServer": "Pilih Server", + "mastodonSignIn": "Masuk Mastodon", + "misskeySignIn": "Masuk Misskey" } }, "dialogs": { @@ -199,4 +221,4 @@ }, "footer": "Dibuat dengan \uD83D\uDC9B oleh Debashish Patra" } -} +} \ No newline at end of file diff --git a/apps/mobile/i18n/locales/jp/core.json b/apps/mobile/i18n/locales/jp/core.json index 51a302a0..3ffb0b02 100644 --- a/apps/mobile/i18n/locales/jp/core.json +++ b/apps/mobile/i18n/locales/jp/core.json @@ -5,6 +5,23 @@ "desc": "この機能はDhaagaによって実装されていないか、またはあなたのSNSプラットフォームでは利用できません。" } }, + "errors": { + "errorLabel": "エラー", + "timelineErrorLabel": "Dhaagaがこのタイムラインを読み込めませんでした。開発者にこのエラーを報告してください。" + }, + "onboarding": { + "addAccountButton": "アカウントを追加", + "accountCreationNotSupported": "アカウントの作成はサポートされていません", + "needBlueskyAccount": "Blueskyアカウントが必要です", + "createOneHere": "こちらで作成してください", + "loginButton": "ログイン", + "appPassword": "アプリパスワード", + "enterYourServer": "サーバーを入力してください", + "serverUrl": "あなたのサーバーURL", + "popularMastodon": "人気のMastodonサーバー", + "popularPleroma": "人気のPleromaサーバー", + "popularAkkoma": "人気のAkkomaサーバー" + }, "topNav": { "primary": { "hub": "ソーシャルハブ", @@ -24,7 +41,12 @@ "myBookmarks": "マイブックマーク", "myAccount": "マイアカウント", "myProfile": "マイプロフィール", - "appSettings": "アプリ設定" + "appSettings": "アプリ設定", + "manageAccounts": "アカウント管理", + "blueskySignIn": "Bluesky サインイン", + "selectServer": "サーバーを選択", + "mastodonSignIn": "Mastodon サインイン", + "misskeySignIn": "Misskey サインイン" } }, "dialogs": { diff --git a/apps/mobile/index.js b/apps/mobile/index.js index b66540ae..98b492e4 100644 --- a/apps/mobile/index.js +++ b/apps/mobile/index.js @@ -1,10 +1,9 @@ -// polyfills -import 'fast-text-encoding'; - import { registerRootComponent } from 'expo'; import { ExpoRoot } from 'expo-router'; -// Must be exported or Fast Refresh won't update the context +// needed by atproto +import 'fast-text-encoding'; + export function App() { const ctx = require.context('./app'); return ; diff --git a/apps/mobile/metro.config.cjs b/apps/mobile/metro.config.cjs index d259d0c1..cdc126e0 100644 --- a/apps/mobile/metro.config.cjs +++ b/apps/mobile/metro.config.cjs @@ -1,41 +1,14 @@ -// Learn more https://docs.expo.dev/guides/monorepos const { getDefaultConfig } = require('expo/metro-config'); const { FileStore } = require('metro-cache'); const path = require('path'); -const projectRoot = __dirname; -const workspaceRoot = path.resolve(projectRoot, '../..'); - -const config = getDefaultConfig(projectRoot, { +const config = getDefaultConfig(__dirname, { isCSSEnabled: false, }); -// #1 - Watch all files in the monorepo -config.watchFolders = [workspaceRoot]; -// #3 - Force resolving nested modules to the folders below config.resolver.disableHierarchicalLookup = true; -// #2 - Try resolving with project modules first, then workspace modules -config.resolver.nodeModulesPaths = [ - path.resolve(projectRoot, 'node_modules'), - path.resolve(workspaceRoot, 'node_modules'), -]; config.resolver.sourceExts.push('sql'); -// config.resolver.sourceExts = ['jsx', 'js', 'ts', 'tsx', 'cjs', 'json'] //add here -// config.transformer.getTransformOptions = async () => ({ -// transform: { -// experimentalImportSupport: false, -// inlineRequires: true, -// }, -// }) - -// Use turborepo to restore the cache when possible -config.cacheStores = [ - new FileStore({ - root: path.join(projectRoot, 'node_modules', '.cache', 'metro'), - }), -]; - /** * Move the Metro cache to the `node_modules/.cache/metro` folder. * This repository configured Turborepo to use this cache location as well. @@ -54,17 +27,13 @@ function withTurborepoManagedCache(config) { /** * Add the monorepo paths to the Metro config. - * This allows Metro to resolve modules from the monorepo. - * - * @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config - * @param {import('expo/metro-config').MetroConfig} config - * @returns {import('expo/metro-config').MetroConfig} */ function withMonorepoPaths(config) { const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, '../..'); // #1 - Watch all files in the monorepo + // config.watchFolders = [workspaceRoot]; config.watchFolders = [workspaceRoot]; // #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules @@ -77,4 +46,3 @@ function withMonorepoPaths(config) { } module.exports = withTurborepoManagedCache(withMonorepoPaths(config)); -module.exports = config; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 5cdb1535..1fd59844 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@dhaaga/mobile", - "version": "0.15.3", + "version": "0.15.4", "private": true, "main": "index.js", "scripts": { @@ -30,13 +30,13 @@ "@rneui/themed": "^4.0.0-rc.8", "@tanstack/react-query": "^5.64.2", "@types/react": "~18.3.12", - "expo": "~52.0.30", + "expo": "~52.0.31", "expo-asset": "~11.0.3", "expo-build-properties": "~0.13.2", "expo-clipboard": "~7.0.1", "expo-constants": "~17.0.5", "expo-dev-client": "~5.0.11", - "expo-file-system": "~18.0.9", + "expo-file-system": "~18.0.10", "expo-font": "~13.0.3", "expo-haptics": "~14.0.1", "expo-image": "~2.0.4", @@ -48,6 +48,7 @@ "expo-splash-screen": "~0.29.21", "expo-sqlite": "~15.1.2", "expo-video": "~2.0.5", + "expo-web-browser": "~14.0.2", "fast-text-encoding": "^1.0.6", "i18next": "^24.2.2", "immer": "^10.1.1", diff --git a/apps/mobile/services/activitypub.service.ts b/apps/mobile/services/activitypub.service.ts index c1d53e91..0c87f354 100644 --- a/apps/mobile/services/activitypub.service.ts +++ b/apps/mobile/services/activitypub.service.ts @@ -59,6 +59,13 @@ class ActivityPubService { return [KNOWN_SOFTWARE.MASTODON].includes(driver as KNOWN_SOFTWARE); } + static supportsQuotesNatively(driver: string) { + return ( + ActivityPubService.blueskyLike(driver) || + ActivityPubService.misskeyLike(driver) + ); + } + static pleromaLike(driver: string) { return [KNOWN_SOFTWARE.PLEROMA, KNOWN_SOFTWARE.AKKOMA].includes( driver as KNOWN_SOFTWARE, diff --git a/apps/mobile/services/atproto/atproto-compose.ts b/apps/mobile/services/atproto/atproto-compose.ts index ce021081..8a2b69af 100644 --- a/apps/mobile/services/atproto/atproto-compose.ts +++ b/apps/mobile/services/atproto/atproto-compose.ts @@ -9,6 +9,8 @@ import { AtpAgent, BlobRef, Facet } from '@atproto/api'; import { PostComposerReducerStateType } from '../../features/composer/reducers/composer.reducer'; import MediaUtils from '../../utils/media.utils'; import { AppBskyFeedPost } from '@atproto/api/src/client'; +import { AppPostObject } from '../../types/app-post.types'; +import { PostMiddleware } from '../middlewares/post.middleware'; type AtProtoPostRecordType = Partial & Omit; @@ -84,6 +86,11 @@ class AtprotoComposerService { return items; } + /** + * Generate post record from reducer state + * @param client + * @param state + */ static async postUsingReducerState( client: BlueskyRestClient, state: PostComposerReducerStateType, @@ -130,9 +137,36 @@ class AtprotoComposerService { }; } - // handle reply - - // handle quotes + if (state.parent) { + const _replyTarget = PostMiddleware.getContentTarget(state.parent); + if (state.isQuote) { + // handle quotes + record.embed = { + $type: 'app.bsky.embed.record', + record: { + uri: _replyTarget.meta.uri, + cid: _replyTarget.meta.cid, + }, + }; + } else { + // handle reply + record.reply = { + root: _replyTarget.rootPost + ? { + uri: _replyTarget.rootPost.meta.uri, + cid: _replyTarget.rootPost.meta.cid, + } + : { + uri: _replyTarget.meta.uri, + cid: _replyTarget.meta.cid, + }, + parent: { + uri: _replyTarget.meta.uri, + cid: _replyTarget.meta.cid, + }, + }; + } + } const result = await this.post(client, record); diff --git a/apps/mobile/services/atproto/atproto-session.service.ts b/apps/mobile/services/atproto/atproto-session.service.ts index 4509f624..72a73668 100644 --- a/apps/mobile/services/atproto/atproto-session.service.ts +++ b/apps/mobile/services/atproto/atproto-session.service.ts @@ -365,6 +365,38 @@ class AtprotoSessionService { return false; } } + + static async exchangeCodeForSession( + code: string, + verifier: string, + pds?: string, + ) { + const body = { + grant_type: 'authorization_code', + redirect_uri: 'https://suvam.io/dhaaga', + code, + code_verifier: verifier, + client_id: 'https://suvam.io/dhaaga/client-metadata.json', + }; + + const response = await fetch( + pds ? `https://${pds}/oauth/token` : 'https://bsky.social/oauth/token', + { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + if (!response.ok) { + console.log(response); + return null; + } + const data = await response.json(); + console.log(data); + return data; + } } export default AtprotoSessionService; diff --git a/apps/mobile/services/driver.service.ts b/apps/mobile/services/driver.service.ts index 6b56e513..ecd9da2a 100644 --- a/apps/mobile/services/driver.service.ts +++ b/apps/mobile/services/driver.service.ts @@ -193,13 +193,8 @@ class DriverService { SEARCH_RESULT_TAB.TAGS, SEARCH_RESULT_TAB.NEWS, ]; - } else if (ActivityPubService.mastodonLike(driver)) { - return [ - SEARCH_RESULT_TAB.POSTS, - SEARCH_RESULT_TAB.PEOPLE, - SEARCH_RESULT_TAB.TAGS, - SEARCH_RESULT_TAB.NEWS, - ]; + } else if (ActivityPubService.pleromaLike(driver)) { + return [SEARCH_RESULT_TAB.POSTS, SEARCH_RESULT_TAB.PEOPLE]; } } } diff --git a/apps/mobile/services/masto-api.service.ts b/apps/mobile/services/masto-api.service.ts index 82e373f3..172ef456 100644 --- a/apps/mobile/services/masto-api.service.ts +++ b/apps/mobile/services/masto-api.service.ts @@ -16,7 +16,40 @@ export type MastoApiGroupedNotificationType = { statusId: string; }; -class ServiceV1 {} +class ServiceV1 { + /** + * Resolve notifications for v1 api + * grouped notification objects + * @param input + * @param driver + * @param server + * @param category + */ + static packNotifs( + input: any, + driver: KNOWN_SOFTWARE, + server: string, + category: 'mentions' | 'chat' | 'social' | 'updates', + ): AppResultPageType { + return { + items: input.data.map((o) => { + return { + id: o.id, + // akkoma uses "mention" type for "status" updates + type: category === 'updates' ? 'status' : o.type, + post: PostMiddleware.deserialize(o.status, driver, server), + user: UserMiddleware.deserialize(o.account, driver, server), + read: o.pleroma?.isSeen, // also have o.pleroma.isMuted + createdAt: new Date(o.createdAt), + extraData: {}, + }; + }), + maxId: input.maxId, + minId: input.minId, + success: true, + }; + } +} class ServiceV2 { /** diff --git a/apps/mobile/services/suvamio.service.ts b/apps/mobile/services/suvamio.service.ts new file mode 100644 index 00000000..3b6b5700 --- /dev/null +++ b/apps/mobile/services/suvamio.service.ts @@ -0,0 +1,103 @@ +const PROXY_SERVICE_BASE_URL = 'https://suvam.io/api'; + +class Service { + /** + * Obtain the redirect url to use for oauth + * + * Based on a "BFF" (Backend for Frontend) implementation + * + * See https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node#from-a-native-application + * + * By right, this can be done fully client-side. But the current + * implementation of oauth-client-node is very restrictive, + * when it comes to react-native + * + * @param handle of the user logging in + * @param pds if not bluesky + * + * @returns the redirect url to use for oauth + */ + static async generateAtprotoRedirectUrl( + handle: string, + pds?: string, + ): Promise<{ + success: boolean; + data?: { + href: string; + verifier: string; + }; + error?: string; + }> { + try { + const response = await fetch( + `${PROXY_SERVICE_BASE_URL}/atproto-oauth-redirect`, + { + method: 'POST', + body: JSON.stringify({ + handle, + pds, + }), + }, + ); + if (!response.ok) { + return { + success: false, + error: 'Some unknown error occurred', + }; + } + return response.json(); + } catch (e) { + return { + success: false, + error: 'Some unknown error occurred', + }; + } + } + + /** + * Exchange the code with session token + * + * Based on a "BFF" (Backend for Frontend) implementation + * + * See https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node#from-a-native-application + * + * By right, this can be done fully client-side. But the current + * implementation of oauth-client-node is very restrictive, + * when it comes to react-native + * + * + * @returns the session object to use in the app + * @param state + * @param code + * @param verifier + */ + static async exchangeRedirectUrlForSessionObject( + state: string, + code: string, + verifier: string, + ): Promise<{ + success: boolean; + data?: { + session: any; // OAuthSession; + state: string | null; + }; + error?: string; + }> { + const response = await fetch( + `${PROXY_SERVICE_BASE_URL}/atproto-oauth-callback`, + { + method: 'POST', + body: JSON.stringify({ + state, + code, + verifier, + }), + }, + ); + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + + return response.json(); + } +} + +export { Service as SuvamIoService }; diff --git a/apps/mobile/types/app-post.types.ts b/apps/mobile/types/app-post.types.ts index 14f04782..4262b746 100644 --- a/apps/mobile/types/app-post.types.ts +++ b/apps/mobile/types/app-post.types.ts @@ -33,8 +33,8 @@ export type AppPostAuthorType = z.infer; export const AppActivityPubMediaDto = z.object({ url: z.string(), previewUrl: z.string().nullable().optional(), - width: z.number().optional(), - height: z.number().optional(), + width: z.number().optional().nullable(), // bsky can be null + height: z.number().optional().nullable(), // bsky can be null alt: z.string().nullable(), type: z.string(), blurhash: z.string().nullable(), diff --git a/apps/mobile/utils/linking.utils.ts b/apps/mobile/utils/linking.utils.ts index 0fa86e24..e7864f52 100644 --- a/apps/mobile/utils/linking.utils.ts +++ b/apps/mobile/utils/linking.utils.ts @@ -14,6 +14,10 @@ export class LinkingUtils { } } + static openBluesky() { + LinkingUtils.openURL('https://bsky.app/'); + } + static openCoffeeLink() { LinkingUtils.openURL('https://buymeacoffee.com/suvam'); } diff --git a/apps/mobile/utils/route-list.ts b/apps/mobile/utils/route-list.ts index 01f1f29a..ff06e248 100644 --- a/apps/mobile/utils/route-list.ts +++ b/apps/mobile/utils/route-list.ts @@ -3,6 +3,7 @@ export enum APP_ROUTING_ENUM { MISSKEY_SERVER_SELECTION = '/profile/onboard/add-misskey', MASTODON_SIGNIN = '/profile/onboard/signin-md', MASTODON_SERVER_SELECTION = '/profile/onboard/add-mastodon', + ATPROTO_SIGNIN = '/profile/onboard/add-bluesky', // guides GUIDE_NEW_TAB_INTERFACE = '/user-guide-profiles', diff --git a/apps/proxy/.env b/apps/proxy/.env new file mode 100644 index 00000000..b742e070 --- /dev/null +++ b/apps/proxy/.env @@ -0,0 +1,5 @@ +SUVAMIO_DEEPL_FREE_API_KEY= +SUVAMIO_DEEPL_PRO_API_KEY= +SUVAMIO_GIPHY_API_KEY= +SUVAMIO_TENOR_API_KEY= +ATPROTO_CLIENT_METADATA_URL=https://suvam.io/dhaaga/client-metadata.json \ No newline at end of file diff --git a/apps/mobile/components/screens/profile/stack/MyBlocks.tsx b/apps/proxy/.gitignore similarity index 100% rename from apps/mobile/components/screens/profile/stack/MyBlocks.tsx rename to apps/proxy/.gitignore diff --git a/apps/proxy/README.md b/apps/proxy/README.md new file mode 100644 index 00000000..25a92717 --- /dev/null +++ b/apps/proxy/README.md @@ -0,0 +1,39 @@ +# Proxy Services + +Dhaaga uses [suvam.io](https://suvam.io) to avail services that +require a backend proxy to use. + +This is the source code for those services. + +The documentation is minimal by design, +so that you can copy and use the code with your cloud hosting of choice. + +### Play Store vs Lite edition + +Please note that the lite edition of the app does not use the Giphy/Tenor/DeepL +integrations. + +The atproto oauth proxy service has to be used for lite edition due to lack of +better alternatives. + +### List of services: + +- Atproto OAuth + - Handles generating authentication urls for a pds + - Handles authentication callbacks to generate session tokens +- Tenor + - Handling user search for giraffes *(this is how you spell gifs, btw :))* +- Giphy + - Handling user search for jeffs *(this spelling is also fine ;)) +- DeepL + - Handles translating lines of text + +### Deploying with Vercel + +You will notice that each file in `api` folder is a Next.js api route. + +1. Create a folder by the same names, and put the files in them +2. Rename the files to `route.ts` +3. All folders need to be in `src/app/api` folder +4. Get all tokens specified in `.env` file +5. Deploy to vercel, and your copy of the proxy services is live! diff --git a/apps/proxy/api/atproto-oauth-callack.ts b/apps/proxy/api/atproto-oauth-callack.ts new file mode 100644 index 00000000..f14f380c --- /dev/null +++ b/apps/proxy/api/atproto-oauth-callack.ts @@ -0,0 +1,42 @@ +// @ts-ignore +import { NextRequest } from 'next/server'; +import { z } from 'zod'; +import { ResponseUtil } from '../utils/api-response.utils.ts'; +import { AtprotoUtils } from '../utils/atproto.utils.ts'; + +const postBodySchema = z.object({ + callbackUrl: z.string(), +}); + +export async function POST(request: NextRequest) { + const rawBody = await request.text(); + if (!rawBody) return ResponseUtil.badRequest_bodyMissing(); + + const { + success, + error, + data: body, + } = postBodySchema.safeParse(JSON.parse(rawBody)); + if (!success) return ResponseUtil.badRequest_invalidInput(error.errors); + + const params = new URLSearchParams(body.callbackUrl.split('?')[1]); + + const client = await AtprotoUtils.buildClient(); + const { session, state } = await client.callback(params); + + return new Response( + JSON.stringify({ + success: true, + data: { + session, + state, + }, + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ); +} diff --git a/apps/proxy/api/atproto-oauth-generate.ts b/apps/proxy/api/atproto-oauth-generate.ts new file mode 100644 index 00000000..73eaa618 --- /dev/null +++ b/apps/proxy/api/atproto-oauth-generate.ts @@ -0,0 +1,44 @@ +// @ts-ignore +import { NextRequest } from 'next/server'; +import { z } from 'zod'; +import crypto from 'crypto'; +import { ResponseUtil } from '../utils/api-response.utils.ts'; +import { AtprotoUtils } from '../utils/atproto.utils.ts'; + +const postBodySchema = z.object({ + pds: z.string().optional(), + handle: z.string(), +}); + +export async function POST(request: NextRequest) { + const rawBody = await request.text(); + if (!rawBody) return ResponseUtil.badRequest_bodyMissing(); + + const { + success, + error, + data: body, + } = postBodySchema.safeParse(JSON.parse(rawBody)); + if (!success) return ResponseUtil.badRequest_invalidInput(error.errors); + + const client = await AtprotoUtils.buildClient(); + + const url: URL = await client.authorize(body.handle, { + state: crypto.randomBytes(16).toString('hex'), + }); + + return new Response( + JSON.stringify({ + success: true, + data: { + href: url.href, + }, + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ); +} diff --git a/apps/proxy/api/giphy.ts b/apps/proxy/api/giphy.ts new file mode 100644 index 00000000..a3ba3189 --- /dev/null +++ b/apps/proxy/api/giphy.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +// @ts-ignore +import { GiphyFetch } from '@giphy/js-fetch-api'; + +const postBodySchema = z.object({ + op: z.enum(['search']), + q: z.string(), + // suvam.io uses a mini puzzle to prevent spam + puzzle: z.object({}).optional(), +}); + +// function to call the trending and category endpoints +async function searchGifs(q: string) { + const gf = new GiphyFetch(process.env.SUVAMIO_GIPHY_API_KEY!); + + // set the apikey and limit + const apikey = process.env.SUVAMIO_TENOR_API_KEY; + const clientkey = 'suvam_io'; + const lmt = 8; + + // using default locale of en_US + const search_url = await fetch( + 'https://tenor.googleapis.com/v2/search?q=' + + q + + '&key=' + + apikey + + '&client_key=' + + clientkey + + '&limit=' + + lmt, + { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }, + ); + return await search_url.json(); +} diff --git a/apps/proxy/api/tenor.ts b/apps/proxy/api/tenor.ts new file mode 100644 index 00000000..4d39174c --- /dev/null +++ b/apps/proxy/api/tenor.ts @@ -0,0 +1,95 @@ +// @ts-ignore +import { NextRequest } from 'next/server'; +import { z } from 'zod'; + +const postBodySchema = z.object({ + op: z.enum(['search']), + q: z.string(), + // suvam.io uses a mini puzzle to prevent spam + puzzle: z.object({}).optional(), +}); + +// function to call the trending and category endpoints +async function searchGifs(q: string) { + // set the apikey and limit + const apikey = process.env.SUVAMIO_TENOR_API_KEY; + const clientkey = 'suvam_io'; + const lmt = 8; + + // using default locale of en_US + const search_url = await fetch( + 'https://tenor.googleapis.com/v2/search?q=' + + q + + '&key=' + + apikey + + '&client_key=' + + clientkey + + '&limit=' + + lmt, + { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }, + ); + return await search_url.json(); +} + +export async function POST(request: NextRequest) { + const rawBody = await request.text(); + + if (!rawBody) + return new Response( + JSON.stringify({ + success: false, + errors: ['E_Body_Missing'], + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + const { + success, + error, + data: body, + } = postBodySchema.safeParse(JSON.parse(rawBody)); + if (!success) + return new Response( + JSON.stringify({ + success: false, + errors: error.errors, + }), + { + status: 400, + headers: { + 'X-RateLimit-Limit': '30', + 'X-RateLimit-Remaining': '30', + 'X-RateLimit-Reset': '20', + 'Content-Type': 'application/json', + }, + }, + ); + + const resp = await searchGifs(body.q); + + return new Response( + JSON.stringify({ + success: true, + results: resp, + }), + { + status: 200, + headers: { + 'X-RateLimit-Limit': '30', + 'X-RateLimit-Remaining': '30', + 'X-RateLimit-Reset': '20', + 'Content-Type': 'application/json', + }, + }, + ); +} diff --git a/apps/proxy/api/translate.ts b/apps/proxy/api/translate.ts new file mode 100644 index 00000000..3b291d4d --- /dev/null +++ b/apps/proxy/api/translate.ts @@ -0,0 +1,106 @@ +// @ts-ignore +import type { NextRequest } from 'next/server'; +import { z } from 'zod'; + +const postBodySchema = z.object({ + items: z.array( + z.object({ + uuid: z.string(), + text: z.string(), + }), + ), + toLocale: z.string(), + type: z.enum(['free', 'dedicated'] as const), +}); + +const authKey = process.env.SUVAMIO_DEEPL_PRO_API_KEY; + +export async function GET(request: NextRequest) { + const rawBody = await request.text(); + + return new Response( + JSON.stringify({ + success: true, + request, + ip: (request as any).ip, + // result + url: request.url, + body: rawBody, + }), + { + status: 200, + headers: { + 'X-RateLimit-Limit': '30', + 'X-RateLimit-Remaining': '30', + 'X-RateLimit-Reset': '20', + }, + }, + ); +} + +export async function POST(request: NextRequest) { + const rawBody = await request.text(); + + if (!rawBody) + return new Response( + JSON.stringify({ + success: false, + errors: ['E_Body_Missing'], + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + const { success, error, data } = postBodySchema.safeParse( + JSON.parse(rawBody), + ); + if (!success) + return new Response( + JSON.stringify({ + success: false, + errors: error.errors, + }), + { + status: 400, + headers: { + 'X-RateLimit-Limit': '30', + 'X-RateLimit-Remaining': '30', + 'X-RateLimit-Reset': '20', + 'Content-Type': 'application/json', + }, + }, + ); + + const result = await fetch('https://api-free.deepl.com/v2/translate', { + method: 'POST', + headers: { + Authorization: `DeepL-Auth-Key ${authKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + text: data.items.map((o) => o.text), + target_lang: data.toLocale, + }), + }); + const _body = await result.text(); + + return new Response( + JSON.stringify({ + success: true, + data: JSON.parse(_body)?.translations, + }), + { + status: 200, + headers: { + 'X-RateLimit-Limit': '30', + 'X-RateLimit-Remaining': '30', + 'X-RateLimit-Reset': '20', + 'Content-Type': 'application/json', + }, + }, + ); +} diff --git a/apps/proxy/package.json b/apps/proxy/package.json new file mode 100644 index 00000000..59ede6fc --- /dev/null +++ b/apps/proxy/package.json @@ -0,0 +1,6 @@ +{ + "name": "@dhaaga/proxy", + "description": "Source code for proxied services offered by suvam.io", + "module": "index.ts", + "type": "module" +} \ No newline at end of file diff --git a/apps/proxy/tsconfig.json b/apps/proxy/tsconfig.json new file mode 100644 index 00000000..238655f2 --- /dev/null +++ b/apps/proxy/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/apps/proxy/utils/api-response.utils.ts b/apps/proxy/utils/api-response.utils.ts new file mode 100644 index 00000000..d69ca0cf --- /dev/null +++ b/apps/proxy/utils/api-response.utils.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; + +export class ResponseUtil { + static badRequest_bodyMissing() { + return new Response(JSON.stringify({ + success: false, + errors: ['E_Body_Missing'] + }), { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + }); + } + + static badRequest_invalidInput(errors: z.ZodIssue[]) { + return new Response(JSON.stringify({ + success: false, + errors: errors + }), { + status: 400, + headers: { + 'X-RateLimit-Limit': '30', + 'X-RateLimit-Remaining': '30', + 'X-RateLimit-Reset': '20', + 'Content-Type': 'application/json' + } + }); + } + + static success(data: any) { + new Response(JSON.stringify(data), { + status: 200, + headers: { + 'X-RateLimit-Limit': '30', + 'X-RateLimit-Remaining': '30', + 'X-RateLimit-Reset': '20', + 'Content-Type': 'application/json' + } + }); + } +} \ No newline at end of file diff --git a/apps/proxy/utils/atproto.utils.ts b/apps/proxy/utils/atproto.utils.ts new file mode 100644 index 00000000..23dcce5b --- /dev/null +++ b/apps/proxy/utils/atproto.utils.ts @@ -0,0 +1,28 @@ +import { + NodeOAuthClient, + NodeSavedSession, + NodeSavedState, + // @ts-ignore=next-line +} from '@atproto/oauth-client-node'; + +export class AtprotoUtils { + static buildClient() { + return NodeOAuthClient.fromClientId({ + clientId: 'https://suvam.io/dhaaga/client-metadata.json', + stateStore: { + async set(key: string, internalState: NodeSavedState): Promise {}, + async get(key: string): Promise { + return undefined; + }, + async del(key: string): Promise {}, + }, + sessionStore: { + async set(sub: string, session: NodeSavedSession): Promise {}, + async get(sub: string): Promise { + return undefined; + }, + async del(sub: string): Promise {}, + }, + }); + } +} diff --git a/apps/push/README.md b/apps/push/README.md new file mode 100644 index 00000000..4d323014 --- /dev/null +++ b/apps/push/README.md @@ -0,0 +1 @@ +Push notifications wen? \ No newline at end of file diff --git a/bun.lock b/bun.lock index 6adc41ec..ed547a49 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ }, "apps/mobile": { "name": "@dhaaga/mobile", - "version": "0.15.1", + "version": "0.15.3", "dependencies": { "@atproto/api": "^0.13.31", "@dhaaga/bridge": "*", @@ -25,13 +25,13 @@ "@rneui/themed": "^4.0.0-rc.8", "@tanstack/react-query": "^5.64.2", "@types/react": "~18.3.12", - "expo": "~52.0.30", + "expo": "~52.0.31", "expo-asset": "~11.0.3", "expo-build-properties": "~0.13.2", "expo-clipboard": "~7.0.1", "expo-constants": "~17.0.5", "expo-dev-client": "~5.0.11", - "expo-file-system": "~18.0.9", + "expo-file-system": "~18.0.10", "expo-font": "~13.0.3", "expo-haptics": "~14.0.1", "expo-image": "~2.0.4", @@ -43,6 +43,7 @@ "expo-splash-screen": "~0.29.21", "expo-sqlite": "~15.1.2", "expo-video": "~2.0.5", + "expo-web-browser": "~14.0.2", "fast-text-encoding": "^1.0.6", "i18next": "^24.2.2", "immer": "^10.1.1", @@ -71,9 +72,6 @@ "dependencies": { "@atproto/api": "^0.13.31", "axios": "^1.7.9", - "camelcase-keys": "^9.1.3", - "change-case": "^5.4.4", - "decamelize-keys": "^2.0.1", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "html-entities": "^2.5.2", @@ -81,8 +79,7 @@ "masto": "^6.10.3", "megalodon": "^10.1.1", "mfm-js": "^0.24.0", - "misskey-js": "^2025.1.0", - "snakecase-keys": "^8.0.1", + "misskey-js": "^2025.2.0", "zod": "^3.24.1", }, "devDependencies": { @@ -92,7 +89,7 @@ }, "packages/orm": { "name": "@dhaaga/orm", - "version": "0.14.0", + "version": "0.15.3", "devDependencies": { "typescript": "^5.7.3", }, @@ -392,13 +389,13 @@ "@expo/bunyan": ["@expo/bunyan@4.0.1", "", { "dependencies": { "uuid": "^8.0.0" } }, "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg=="], - "@expo/cli": ["@expo/cli@0.22.12", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.8", "@expo/config-plugins": "~9.0.14", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.1", "@expo/image-utils": "^0.6.4", "@expo/json-file": "^9.0.1", "@expo/metro-config": "~0.19.9", "@expo/osascript": "^2.1.5", "@expo/package-manager": "^1.7.1", "@expo/plist": "^0.2.1", "@expo/prebuild-config": "^8.0.25", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.6", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-i7mYb2s4RzlcLIsMewYtKol5rOIECOgLymAUfTwgvsAjg1r+11gmovsi3xdM0k1QiHmeJf0Wqz3sl7FJNsSKnw=="], + "@expo/cli": ["@expo/cli@0.22.13", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.8", "@expo/config-plugins": "~9.0.15", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.1", "@expo/image-utils": "^0.6.4", "@expo/json-file": "^9.0.1", "@expo/metro-config": "~0.19.9", "@expo/osascript": "^2.1.5", "@expo/package-manager": "^1.7.1", "@expo/plist": "^0.2.1", "@expo/prebuild-config": "^8.0.26", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.7", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-GpRkCt5Eb0pWXrvpvYswFhUxm0R5197P1acAgj242XfsdNhjVUfjScj+Z6raTRsW0SdIxmingpef2fZaHvUFKQ=="], "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="], "@expo/config": ["@expo/config@10.0.8", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~9.0.14", "@expo/config-types": "^52.0.3", "@expo/json-file": "^9.0.1", "deepmerge": "^4.3.1", "getenv": "^1.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-RaKwi8e6PbkMilRexdsxObLMdQwxhY6mlgel+l/eW+IfIw8HEydSU0ERlzYUjlGJxHLHUXe4rC2vw8FEvaowyQ=="], - "@expo/config-plugins": ["@expo/config-plugins@9.0.14", "", { "dependencies": { "@expo/config-types": "^52.0.3", "@expo/json-file": "~9.0.1", "@expo/plist": "^0.2.1", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^1.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-Lx1ebV95rTFKKQmbu4wMPLz65rKn7mqSpfANdCx+KwRxuLY2JQls8V4h3lQjG6dW8NWf9qV5QaEFAgNB6VMyOQ=="], + "@expo/config-plugins": ["@expo/config-plugins@9.0.15", "", { "dependencies": { "@expo/config-types": "^52.0.4", "@expo/json-file": "~9.0.1", "@expo/plist": "^0.2.1", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^1.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-elKY/zIpAJ40RH26iwfyp+hwgeyPgIXX0SrCSOcjeJLsMsCmMac9ewvb+AN8y4k+N7m5lD/dMZupsaateKTFwA=="], "@expo/config-types": ["@expo/config-types@52.0.3", "", {}, "sha512-muxvuARmbysH5OGaiBRlh1Y6vfdmL56JtpXxB+y2Hfhu0ezG1U4FjZYBIacthckZPvnDCcP3xIu1R+eTo7/QFA=="], @@ -494,17 +491,17 @@ "@react-native/assets-registry": ["@react-native/assets-registry@0.77.0", "", {}, "sha512-Ms4tYYAMScgINAXIhE4riCFJPPL/yltughHS950l0VP5sm5glbimn9n7RFn9Tc8cipX74/ddbk19+ydK2iDMmA=="], - "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.76.6", "", { "dependencies": { "@react-native/codegen": "0.76.6" } }, "sha512-yFC9I/aDBOBz3ZMlqKn2NY/mDUtCksUNZ7AQmBiTAeVTUP0ujEjE0hTOx5Qd+kok7A7hwZEX87HdSgjiJZfr5g=="], + "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.76.7", "", { "dependencies": { "@react-native/codegen": "0.76.7" } }, "sha512-+8H4DXJREM4l/pwLF/wSVMRzVhzhGDix5jLezNrMD9J1U1AMfV2aSkWA1XuqR7pjPs/Vqf6TaPL7vJMZ4LU05Q=="], - "@react-native/babel-preset": ["@react-native/babel-preset@0.76.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.76.6", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-ojlVWY6S/VE/nb9hIRetPMTsW9ZmGb2R3dnToEXAtQQDz41eHMHXbkw/k2h0THp6qhas25ruNvn3N5n2o+lBzg=="], + "@react-native/babel-preset": ["@react-native/babel-preset@0.76.7", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.76.7", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-/c5DYZ6y8tyg+g8tgXKndDT7mWnGmkZ9F+T3qNDfoE3Qh7ucrNeC2XWvU9h5pk8eRtj9l4SzF4aO1phzwoibyg=="], "@react-native/codegen": ["@react-native/codegen@0.77.0", "", { "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "jscodeshift": "^17.0.0", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-rE9lXx41ZjvE8cG7e62y/yGqzUpxnSvJ6me6axiX+aDewmI4ZrddvRGYyxCnawxy5dIBHSnrpZse3P87/4Lm7w=="], "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.77.0", "", { "dependencies": { "@react-native/dev-middleware": "0.77.0", "@react-native/metro-babel-transformer": "0.77.0", "chalk": "^4.0.0", "debug": "^2.2.0", "invariant": "^2.2.4", "metro": "^0.81.0", "metro-config": "^0.81.0", "metro-core": "^0.81.0", "readline": "^1.3.0", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli-server-api": "*" }, "optionalPeers": ["@react-native-community/cli-server-api"] }, "sha512-GRshwhCHhtupa3yyCbel14SlQligV8ffNYN5L1f8HCo2SeGPsBDNjhj2U+JTrMPnoqpwowPGvkCwyqwqYff4MQ=="], - "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.76.6", "", {}, "sha512-kP97xMQjiANi5/lmf8MakS7d8FTJl+BqYHQMqyvNiY+eeWyKnhqW2GL2v3eEUBAuyPBgJGivuuO4RvjZujduJg=="], + "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.76.7", "", {}, "sha512-89ZtZXt7ZxE94i7T94qzZMhp4Gfcpr/QVpGqEaejAxZD+gvDCH21cYSF+/Rz2ttBazm0rk5MZ0mFqb0Iqp1jmw=="], - "@react-native/dev-middleware": ["@react-native/dev-middleware@0.76.6", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.76.6", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-1bAyd2/X48Nzb45s5l2omM75vy764odx/UnDs4sJfFCuK+cupU4nRPgl0XWIqgdM/2+fbQ3E4QsVS/WIKTFxvQ=="], + "@react-native/dev-middleware": ["@react-native/dev-middleware@0.76.7", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.76.7", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-Jsw8g9DyLPnR9yHEGuT09yHZ7M88/GL9CtU9WmyChlBwdXSeE3AmRqLegsV3XcgULQ1fqdemokaOZ/MwLYkjdA=="], "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.77.0", "", {}, "sha512-rmfh93jzbndSq7kihYHUQ/EGHTP8CCd3GDCmg5SbxSOHAaAYx2HZ28ZG7AVcGUsWeXp+e/90zGIyfOzDRx0Zaw=="], @@ -692,7 +689,7 @@ "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="], - "babel-preset-expo": ["babel-preset-expo@12.0.6", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.6", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-az3H7gDVo0wxNBAFES8h5vLLWE8NPGkD9g5P962hDEOqZUdyPacb9MOzicypeLmcq9zQWr6E3iVtEHoNagCTTQ=="], + "babel-preset-expo": ["babel-preset-expo@12.0.7", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.7", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-XT2ZOnonTU343eRnd/UBuqYLxmaB47g+RtLMANMsj/j9XL2kkTk3a6yepLbV4BrACaTf2ddiBZDi+BQ0lgjVaw=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], @@ -750,9 +747,7 @@ "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], - "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], - - "camelcase-keys": ["camelcase-keys@9.1.3", "", { "dependencies": { "camelcase": "^8.0.0", "map-obj": "5.0.0", "quick-lru": "^6.1.1", "type-fest": "^4.3.2" } }, "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg=="], + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], "caniuse-lite": ["caniuse-lite@1.0.30001695", "", {}, "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw=="], @@ -760,7 +755,7 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + "change-case": ["change-case@4.1.2", "", { "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", "constant-case": "^3.0.4", "dot-case": "^3.0.4", "header-case": "^2.0.4", "no-case": "^3.0.4", "param-case": "^3.0.4", "pascal-case": "^3.1.2", "path-case": "^3.0.4", "sentence-case": "^3.0.4", "snake-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A=="], "charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="], @@ -852,10 +847,6 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "decamelize": ["decamelize@6.0.0", "", {}, "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA=="], - - "decamelize-keys": ["decamelize-keys@2.0.1", "", { "dependencies": { "decamelize": "^6.0.0", "map-obj": "^4.3.0", "quick-lru": "^6.1.1", "type-fest": "^3.1.0" } }, "sha512-nrNeSCtU2gV3Apcmn/EZ+aR20zKDuNDStV67jPiupokD3sOAFeMzslLMCFdKv1sPqzwoe5ZUhsSW9IAVgKSL/Q=="], - "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], @@ -954,7 +945,7 @@ "execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], - "expo": ["expo@52.0.30", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.12", "@expo/config": "~10.0.8", "@expo/config-plugins": "~9.0.14", "@expo/fingerprint": "0.11.8", "@expo/metro-config": "0.19.9", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.6", "expo-asset": "~11.0.3", "expo-constants": "~17.0.5", "expo-file-system": "~18.0.9", "expo-font": "~13.0.3", "expo-keep-awake": "~14.0.2", "expo-modules-autolinking": "2.0.7", "expo-modules-core": "2.2.0", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-CQP75djQOMWpNqniV7IBhsUiY/hpVnGWCPfEuYQIsZBEGa/ZQOuOQEVF7MaFeWEOYR1/qaDyv7ON3ON9QPJydg=="], + "expo": ["expo@52.0.31", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.13", "@expo/config": "~10.0.8", "@expo/config-plugins": "~9.0.15", "@expo/fingerprint": "0.11.8", "@expo/metro-config": "0.19.9", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.7", "expo-asset": "~11.0.3", "expo-constants": "~17.0.5", "expo-file-system": "~18.0.10", "expo-font": "~13.0.3", "expo-keep-awake": "~14.0.2", "expo-modules-autolinking": "2.0.7", "expo-modules-core": "2.2.1", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-OCpr2iWGaC0azRsPzF622ur1hTCZvyJL27GYHVeKIkNY65CV3m0Hw2IUrfmdGJWCnKI/1Kotg3IvUMSk+vnaJA=="], "expo-asset": ["expo-asset@11.0.3", "", { "dependencies": { "@expo/image-utils": "^0.6.4", "expo-constants": "~17.0.5", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-vgJnC82IooAVMy5PxbdFIMNJhW4hKAUyxc5VIiAPPf10vFYw6CqHm+hrehu4ST1I4bvg5PV4uKdPxliebcbgLg=="], @@ -972,7 +963,7 @@ "expo-dev-menu-interface": ["expo-dev-menu-interface@1.9.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-KY/dWTBE1l47i9V366JN5rC6YIdOc9hz8yAmZzkl5DrPia5l3M2WIjtnpHC9zUkNjiSiG2urYoOAq4H/uLdmyg=="], - "expo-file-system": ["expo-file-system@18.0.9", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-DPuAyLP1012SFC92LKNJpLJw3QBbyxfYXNj9nTOdq299MYTf8kyOjLuVNMQqg1jX+yGiRDp0lHFoCgln0xsZYA=="], + "expo-file-system": ["expo-file-system@18.0.10", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-+GnxkI+J9tOzUQMx+uIOLBEBsO2meyoYHxd87m9oT9M//BpepYqI1AvYBH8YM4dgr9HaeaeLr7z5XFVqfL8tWg=="], "expo-font": ["expo-font@13.0.3", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-9IdYz+A+b3KvuCYP7DUUXF4VMZjPU+IsvAnLSVJ2TfP6zUD2JjZFx3jeo/cxWRkYk/aLj5+53Te7elTAScNl4Q=="], @@ -998,7 +989,7 @@ "expo-modules-autolinking": ["expo-modules-autolinking@2.0.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-rkGc6a/90AC3q8wSy4V+iIpq6Fd0KXmQICKrvfmSWwrMgJmLfwP4QTrvLYPYOOMjFwNJcTaohcH8vzW/wYKrMg=="], - "expo-modules-core": ["expo-modules-core@2.2.0", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-mOFEHIe6jZ7G5pYUVSQ2Ghs3CUr9Uz6DOh4JI+4PsTf0gmEvMmMEOrxirS89jRWQjXPJ7QaGBK0CJrZlj/Sdeg=="], + "expo-modules-core": ["expo-modules-core@2.2.1", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-pxQpfgevHiy5EVRDE0w3mrVu0UTNHELr4GDXEQWAE1g4JVS5ZGNq/Gu2VGgFbBP18KGPNB+gEy8UFI48ADbiuw=="], "expo-router": ["expo-router@4.0.17", "", { "dependencies": { "@expo/metro-runtime": "4.0.1", "@expo/server": "^0.5.1", "@radix-ui/react-slot": "1.0.1", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", "client-only": "^0.0.1", "react-helmet-async": "^1.3.0", "react-native-helmet-async": "2.0.4", "react-native-is-edge-to-edge": "^1.1.6", "schema-utils": "^4.0.1", "semver": "~7.6.3", "server-only": "^0.0.1" }, "peerDependencies": { "@react-navigation/drawer": "^7.1.1", "expo": "*", "expo-constants": "*", "expo-linking": "*", "react-native-reanimated": "*", "react-native-safe-area-context": "*", "react-native-screens": "*" }, "optionalPeers": ["@react-navigation/drawer", "react-native-reanimated"] }, "sha512-8ybo6bVwdG1S9hafh9BTOjX1hpCgomdUvs6hKHMM01koo8mQ7zocH/+zxQeaMVDxGhboz2dO5GiDchWJ0OheRA=="], @@ -1010,6 +1001,8 @@ "expo-video": ["expo-video@2.0.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-K5Q4bFKtYq0wEC38mckWUYeaTXsmhl6duidhSdbA63VBy6cwxDOk8uPsFPTQD3FXKJg6wFB0z8ZUASSPuUaY5A=="], + "expo-web-browser": ["expo-web-browser@14.0.2", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-Hncv2yojhTpHbP6SGWARBFdl7P6wBHc1O8IKaNsH0a/IEakq887o1eRhLxZ5IwztPQyRDhpqHdgJ+BjWolOnwA=="], + "exponential-backoff": ["exponential-backoff@3.1.1", "", {}, "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1318,8 +1311,6 @@ "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], - "map-obj": ["map-obj@5.0.0", "", {}, "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA=="], - "marky": ["marky@1.2.5", "", {}, "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q=="], "masto": ["masto@6.10.3", "", { "dependencies": { "change-case": "^4.1.2", "events-to-async": "^2.0.1", "isomorphic-ws": "^5.0.0", "ts-custom-error": "^3.3.1", "ws": "^8.18.0" } }, "sha512-/ajJpdH0jXEqEBgSDTzYc4RS5CcDR0AGtweQmWNAaaFagdBHiNtJZHr7xFvdhR88C83qxispIGYNkLaL2GWumg=="], @@ -1394,7 +1385,7 @@ "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], - "misskey-js": ["misskey-js@2025.1.0", "", { "dependencies": { "@simplewebauthn/types": "11.0.0", "eventemitter3": "5.0.1", "reconnecting-websocket": "4.4.0" } }, "sha512-n/aZpo1cJ9y304IcS/WKCfIhQOBt0izQhUyf62Ml2VPiEn+oi2K465zq/E4to9+SIpb5v31JJEPpKCqaiwCm9g=="], + "misskey-js": ["misskey-js@2025.2.0", "", { "dependencies": { "@simplewebauthn/types": "11.0.0", "eventemitter3": "5.0.1", "reconnecting-websocket": "4.4.0" } }, "sha512-+nq0SAf8TYUsD5H4As+ohlZnirbYqYSH7hYgT8vtbNXE2rUQY5dbhkOyOFUMIqjAFBAYC+LQGQkXfLai/7Oe0A=="], "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], @@ -1556,8 +1547,6 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "quick-lru": ["quick-lru@6.1.2", "", {}, "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], @@ -1710,8 +1699,6 @@ "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], - "snakecase-keys": ["snakecase-keys@8.0.1", "", { "dependencies": { "map-obj": "^4.1.0", "snake-case": "^3.0.4", "type-fest": "^4.15.0" } }, "sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw=="], - "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1816,7 +1803,7 @@ "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - "type-fest": ["type-fest@4.33.0", "", {}, "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g=="], + "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], @@ -1950,12 +1937,18 @@ "@expo/bunyan/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@expo/cli/@expo/prebuild-config": ["@expo/prebuild-config@8.0.26", "", { "dependencies": { "@expo/config": "~10.0.8", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.4", "@expo/json-file": "^9.0.1", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-SryIKBXPkykUzoCUVP3nt7GclChE74TM8xAstxZ7paXxgcSMN68mNVyUr/zOivLTYQijXltD1I9rNj20Vm5aFw=="], + "@expo/cli/form-data": ["form-data@3.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ=="], "@expo/cli/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], + "@expo/config/@expo/config-plugins": ["@expo/config-plugins@9.0.14", "", { "dependencies": { "@expo/config-types": "^52.0.3", "@expo/json-file": "~9.0.1", "@expo/plist": "^0.2.1", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^1.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-Lx1ebV95rTFKKQmbu4wMPLz65rKn7mqSpfANdCx+KwRxuLY2JQls8V4h3lQjG6dW8NWf9qV5QaEFAgNB6VMyOQ=="], + + "@expo/config-plugins/@expo/config-types": ["@expo/config-types@52.0.4", "", {}, "sha512-oMGrb2o3niVCIfjnIHFrOoiDA9jGb0lc3G4RI1UiO//KjULBaQr3QTBoKDzZQwMqDV1AgYgSr9mgEcnX3LqhIg=="], + "@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "@expo/image-utils/fs-extra": ["fs-extra@9.0.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^1.0.0" } }, "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g=="], @@ -1964,6 +1957,8 @@ "@expo/package-manager/sudo-prompt": ["sudo-prompt@9.1.1", "", {}, "sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA=="], + "@expo/prebuild-config/@expo/config-plugins": ["@expo/config-plugins@9.0.14", "", { "dependencies": { "@expo/config-types": "^52.0.3", "@expo/json-file": "~9.0.1", "@expo/plist": "^0.2.1", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^1.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-Lx1ebV95rTFKKQmbu4wMPLz65rKn7mqSpfANdCx+KwRxuLY2JQls8V4h3lQjG6dW8NWf9qV5QaEFAgNB6VMyOQ=="], + "@expo/prebuild-config/@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.6", "", {}, "sha512-1n4udXH2Cla31iA/8eLRdhFHpYUYK1NKWCn4m1Sr9L4SarWKAYuRFliK1fcLvPPALCFoFlWvn8I0ekdUOHMzDQ=="], "@expo/rudder-sdk-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -1998,7 +1993,7 @@ "@react-native-community/cli-tools/sudo-prompt": ["sudo-prompt@9.1.1", "", {}, "sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA=="], - "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.76.6", "", { "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.23.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-BABb3e5G/+hyQYEYi0AODWh2km2d8ERoASZr6Hv90pVXdUHRYR+yxCatX7vSd9rnDUYndqRTzD0hZWAucPNAKg=="], + "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.76.7", "", { "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.23.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-FAn585Ll65YvkSrKDyAcsdjHhhAGiMlSTUpHh0x7J5ntudUns+voYms0xMP+pEPt0XuLdjhD7zLIIlAWP407+g=="], "@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -2050,10 +2045,6 @@ "css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "decamelize-keys/map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], - - "decamelize-keys/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], - "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], @@ -2086,8 +2077,6 @@ "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "jest-validate/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jscodeshift/tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="], @@ -2102,8 +2091,6 @@ "make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "masto/change-case": ["change-case@4.1.2", "", { "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", "constant-case": "^3.0.4", "dot-case": "^3.0.4", "header-case": "^2.0.4", "no-case": "^3.0.4", "param-case": "^3.0.4", "pascal-case": "^3.1.2", "path-case": "^3.0.4", "sentence-case": "^3.0.4", "snake-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A=="], - "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], "metro/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2170,14 +2157,10 @@ "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], - "snakecase-keys/map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -2220,6 +2203,12 @@ "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "@expo/cli/@expo/prebuild-config/@expo/config-types": ["@expo/config-types@52.0.4", "", {}, "sha512-oMGrb2o3niVCIfjnIHFrOoiDA9jGb0lc3G4RI1UiO//KjULBaQr3QTBoKDzZQwMqDV1AgYgSr9mgEcnX3LqhIg=="], + + "@expo/cli/@expo/prebuild-config/@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.7", "", {}, "sha512-ST1xxBuYVIXPdD81dR6+tzIgso7m3pa9+6rOBXTh5Xm7KEEFik7tnQX+GydXYMp3wr1gagJjragdXkPnxK6WNg=="], + + "@expo/cli/@expo/prebuild-config/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "@expo/cli/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "@expo/cli/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], diff --git a/package.json b/package.json index 5cbf3c54..960b0e34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dhaaga", - "version": "0.15.3", + "version": "0.15.4", "description": "Dhaaga - An indie SNS app that blends sleek design 💅, useful features ✨ and fun ways to discover and connect 🎉", "author": "Debashish Patra ", "license": "AGPLv3", diff --git a/packages/bridge/package.json b/packages/bridge/package.json index 75c206e3..3e3b5e67 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -1,6 +1,6 @@ { "name": "@dhaaga/bridge", - "description": "using design patterns to work around the differences between various Mastodon forks", + "description": "Bridging the fediverse using various design patterns!", "private": true, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -18,9 +18,6 @@ "dependencies": { "@atproto/api": "^0.13.31", "axios": "^1.7.9", - "camelcase-keys": "^9.1.3", - "change-case": "^5.4.4", - "decamelize-keys": "^2.0.1", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "html-entities": "^2.5.2", @@ -28,8 +25,7 @@ "masto": "^6.10.3", "megalodon": "^10.1.1", "mfm-js": "^0.24.0", - "misskey-js": "^2025.1.0", - "snakecase-keys": "^8.0.1", + "misskey-js": "^2025.2.0", "zod": "^3.24.1" }, "devDependencies": { diff --git a/packages/bridge/src/adapters/_client/_router/_runner.ts b/packages/bridge/src/adapters/_client/_router/_runner.ts index 18a18d86..f6216d18 100644 --- a/packages/bridge/src/adapters/_client/_router/_runner.ts +++ b/packages/bridge/src/adapters/_client/_router/_runner.ts @@ -15,7 +15,6 @@ async function CommonErrorHandler(e: any) { }, }; } else if (e?.code) { - // axios return { error: { code: e.code, diff --git a/packages/bridge/src/adapters/_client/_router/routes/statuses.ts b/packages/bridge/src/adapters/_client/_router/routes/statuses.ts index f721305e..e4ab4df4 100644 --- a/packages/bridge/src/adapters/_client/_router/routes/statuses.ts +++ b/packages/bridge/src/adapters/_client/_router/routes/statuses.ts @@ -7,6 +7,10 @@ import { MastoStatus, } from '../../../../types/mastojs.types.js'; import { MissContext, MissNote } from '../../../../types/misskey-js.types.js'; +import { + MegaScheduledStatus, + MegaStatus, +} from '../../../../types/megalodon.types.js'; export type DhaagaJsPostCreateDto = { inReplyToId: null | string; @@ -45,7 +49,9 @@ export type DhaagaJsPostCreateDto = { export interface StatusesRoute { get( id: string, - ): LibraryPromise; + ): LibraryPromise< + MastoStatus | MegaStatus | MissNote | AppBskyFeedGetPostThread.Response + >; bookmark( id: string, @@ -72,6 +78,8 @@ export interface StatusesRoute { create(dto: DhaagaJsPostCreateDto): LibraryPromise< | MastoScheduledStatus + | MegaStatus + | MegaScheduledStatus | { uri: string; cid: string; diff --git a/packages/bridge/src/adapters/_client/_router/utils/casing.utils.ts b/packages/bridge/src/adapters/_client/_router/utils/casing.utils.ts deleted file mode 100644 index 9af4438a..00000000 --- a/packages/bridge/src/adapters/_client/_router/utils/casing.utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as snakecaseKeys from 'snakecase-keys'; - -/** - * ESM, smh - */ -export function toSnakeCase(obj: any) { - if (!obj) return obj; - - // kill undefined - Object.keys(obj).forEach((key) => { - if (obj[key] === undefined) { - delete obj[key]; - } - }); - return snakecaseKeys.default(obj, { deep: true }) as Record; -} diff --git a/packages/bridge/src/adapters/_client/default/accounts.ts b/packages/bridge/src/adapters/_client/default/accounts.ts index be22b7ec..09ed8507 100644 --- a/packages/bridge/src/adapters/_client/default/accounts.ts +++ b/packages/bridge/src/adapters/_client/default/accounts.ts @@ -128,7 +128,7 @@ export abstract class BaseAccountsRouter implements AccountRoute { async relationships( ids: string[], - ): Promise> { + ): Promise> { return notImplementedErrorBuilder(); } diff --git a/packages/bridge/src/adapters/_client/mastodon/notifications.ts b/packages/bridge/src/adapters/_client/mastodon/notifications.ts index 8154f5ff..8940c4d5 100644 --- a/packages/bridge/src/adapters/_client/mastodon/notifications.ts +++ b/packages/bridge/src/adapters/_client/mastodon/notifications.ts @@ -19,19 +19,26 @@ import { LibraryResponse, } from '../../../types/result.types.js'; import { MastoJsWrapper } from '../../../custom-clients/custom-clients.js'; +import { + MegaConversation, + MegaNotification, +} from '../../../types/megalodon.types.js'; export class MastodonNotificationsRouter implements NotificationsRoute { direct: FetchWrapper; - client: MastoJsWrapper; + mastoClient: MastoJsWrapper; constructor(forwarded: FetchWrapper) { this.direct = forwarded; - this.client = MastoJsWrapper.create(forwarded.baseUrl, forwarded.token); + this.mastoClient = MastoJsWrapper.create( + forwarded.baseUrl, + forwarded.token, + ); } async get(query: NotificationGetQueryDto): Promise< LibraryResponse<{ - data: MastoNotification[]; + data: MastoNotification[] | MegaNotification[]; minId?: string | null; maxId?: string | null; }> @@ -127,11 +134,10 @@ export class MastodonNotificationsRouter implements NotificationsRoute { /** * a.k.a. - conversations - * @param driver */ - async getChats(driver: KNOWN_SOFTWARE): LibraryPromise { + async getChats(): LibraryPromise { try { - const data = await this.client.lib.v1.conversations.list(); + const data = await this.mastoClient.lib.v1.conversations.list(); return { data }; } catch (e) { return errorBuilder(DhaagaErrorCode.UNKNOWN_ERROR); @@ -148,7 +154,9 @@ export class MastodonNotificationsRouter implements NotificationsRoute { async markChatRead(id: string): LibraryPromise { try { - const data = await this.client.lib.v1.conversations.$select(id).read(); + const data = await this.mastoClient.lib.v1.conversations + .$select(id) + .read(); return { data }; } catch (e) { return errorBuilder(DhaagaErrorCode.UNKNOWN_ERROR); @@ -157,7 +165,9 @@ export class MastodonNotificationsRouter implements NotificationsRoute { async markChatUnread(id: string): LibraryPromise { try { - const data = await this.client.lib.v1.conversations.$select(id).unread(); + const data = await this.mastoClient.lib.v1.conversations + .$select(id) + .unread(); return { data }; } catch (e) { return errorBuilder(DhaagaErrorCode.UNKNOWN_ERROR); @@ -166,7 +176,9 @@ export class MastodonNotificationsRouter implements NotificationsRoute { async markChatRemove(id: string): LibraryPromise { try { - const data = await this.client.lib.v1.conversations.$select(id).remove(); + const data = await this.mastoClient.lib.v1.conversations + .$select(id) + .remove(); return { data }; } catch (e) { return errorBuilder(DhaagaErrorCode.UNKNOWN_ERROR); diff --git a/packages/bridge/src/adapters/_client/pleroma/accounts.ts b/packages/bridge/src/adapters/_client/pleroma/accounts.ts index dd08e5d9..39d44be6 100644 --- a/packages/bridge/src/adapters/_client/pleroma/accounts.ts +++ b/packages/bridge/src/adapters/_client/pleroma/accounts.ts @@ -15,11 +15,8 @@ import { PaginatedLibraryPromise, } from '../_router/routes/_types.js'; import FetchWrapper from '../../../custom-clients/custom-fetch.js'; -import camelcaseKeys from 'camelcase-keys'; -import snakecaseKeys from 'snakecase-keys'; import type { MastoAccount, - MastoRelationship, MastoStatus, } from '../../../types/mastojs.types.js'; import type { @@ -29,6 +26,7 @@ import type { } from '../../../types/megalodon.types.js'; import { DhaagaErrorCode } from '../../../types/result.types.js'; import { MegalodonPleromaWrapper } from '../../../custom-clients/custom-clients.js'; +import { CasingUtils } from '../../../utiils/casing.utils.js'; export class PleromaAccountsRouter extends BaseAccountsRouter @@ -49,7 +47,7 @@ export class PleromaAccountsRouter async get(id: string): LibraryPromise { const data = await this.client.client.getAccount(id); if (data.status !== 200) return errorBuilder(data.statusText); - return { data: camelcaseKeys(data.data) as any }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async lookup(webfingerUrl: string): LibraryPromise { @@ -67,21 +65,21 @@ export class PleromaAccountsRouter try { const data = await this.client.client.getAccountStatuses( id, - snakecaseKeys(query) as any, + CasingUtils.snakeCaseKeys(query) as any, ); - return { data: camelcaseKeys(data.data, { deep: true }) }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } catch (e) { console.log('[ERROR]: getting pleroma user timeline', e); return { data: [] }; } } - async relationships(ids: string[]): LibraryPromise { + async relationships(ids: string[]): LibraryPromise { const data = await this.client.client.getRelationships(ids); if (data.status !== 200) { - return errorBuilder(data.statusText); + return errorBuilder(data.statusText); } - return { data: camelcaseKeys(data.data) as any }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async likes(query: GetPostsQueryDTO): PaginatedLibraryPromise { @@ -150,7 +148,7 @@ export class PleromaAccountsRouter return errorBuilder(data.statusText); } // console.log(data, error); - return { data: camelcaseKeys(data.data as any) as any }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async unfollow(id: string): LibraryPromise { @@ -158,7 +156,7 @@ export class PleromaAccountsRouter if (data.status !== 200) { return errorBuilder(data.statusText); } - return { data: camelcaseKeys(data.data) as any }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async followers(query: FollowerGetQueryDTO): LibraryPromise<{ diff --git a/packages/bridge/src/adapters/_client/pleroma/media.ts b/packages/bridge/src/adapters/_client/pleroma/media.ts index 6c0eaed3..1ce1cfdd 100644 --- a/packages/bridge/src/adapters/_client/pleroma/media.ts +++ b/packages/bridge/src/adapters/_client/pleroma/media.ts @@ -1,10 +1,10 @@ import { DhaagaJsMediaCreateDTO, MediaRoute } from '../_router/routes/media.js'; import { LibraryPromise } from '../_router/routes/_types.js'; import { errorBuilder } from '../_router/dto/api-responses.dto.js'; -import camelcaseKeys from 'camelcase-keys'; import { MastoMediaAttachment } from '../../../types/mastojs.types.js'; import FetchWrapper from '../../../custom-clients/custom-fetch.js'; import { MegalodonPleromaWrapper } from '../../../custom-clients/custom-clients.js'; +import { CasingUtils } from '../../../utiils/casing.utils.js'; export class PleromaMediaRoute implements MediaRoute { direct: FetchWrapper; @@ -33,6 +33,6 @@ export class PleromaMediaRoute implements MediaRoute { console.log(data.statusText); return errorBuilder(data.statusText); } - return { data: camelcaseKeys(data.data) }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } } diff --git a/packages/bridge/src/adapters/_client/pleroma/notifications.ts b/packages/bridge/src/adapters/_client/pleroma/notifications.ts index 2119d217..2af77a3f 100644 --- a/packages/bridge/src/adapters/_client/pleroma/notifications.ts +++ b/packages/bridge/src/adapters/_client/pleroma/notifications.ts @@ -2,8 +2,7 @@ import { NotificationGetQueryDto, NotificationsRoute, } from '../_router/routes/notifications.js'; -import { KNOWN_SOFTWARE } from '../_router/routes/instance.js'; -import { toSnakeCase } from '../_router/utils/casing.utils.js'; +import { CasingUtils } from '../../../utiils/casing.utils.js'; import { errorBuilder, notImplementedErrorBuilder, @@ -19,14 +18,20 @@ import { } from '../../../types/result.types.js'; import FetchWrapper from '../../../custom-clients/custom-fetch.js'; import { MegalodonPleromaWrapper } from '../../../custom-clients/custom-clients.js'; +import { MastoGroupedNotificationsResults } from '../../../types/mastojs.types.js'; +import { MastodonNotificationsRouter } from '../mastodon/notifications.js'; -export class PleromaNotificationsRouter implements NotificationsRoute { +export class PleromaNotificationsRouter + extends MastodonNotificationsRouter + implements NotificationsRoute +{ direct: FetchWrapper; - client: MegalodonPleromaWrapper; + pleromaClient: MegalodonPleromaWrapper; constructor(forwarded: FetchWrapper) { + super(forwarded); this.direct = forwarded; - this.client = MegalodonPleromaWrapper.create( + this.pleromaClient = MegalodonPleromaWrapper.create( forwarded.baseUrl, forwarded.token, ); @@ -39,7 +44,9 @@ export class PleromaNotificationsRouter implements NotificationsRoute { maxId?: string | null; }> > { - const data = await this.client.client.getNotifications(toSnakeCase(query)); + const data = await this.pleromaClient.client.getNotifications( + CasingUtils.snakeCaseKeys(query), + ); return { data: { data: data.data, @@ -52,19 +59,11 @@ export class PleromaNotificationsRouter implements NotificationsRoute { /** * Pleroma/Akkoma have not implemented grouped notifications */ - async getChats(driver: KNOWN_SOFTWARE): LibraryPromise<{ - data: MegaConversation[]; - minId?: string | null; - maxId?: string | null; - }> { + async getChats(): LibraryPromise { try { - const data = await this.client.client.getConversationTimeline(); + const data = await this.pleromaClient.client.getConversationTimeline(); return { - data: { - data: data.data, - maxId: undefined, - minId: undefined, - }, + data: data.data, }; } catch (e) { return errorBuilder(DhaagaErrorCode.UNKNOWN_ERROR); @@ -79,12 +78,68 @@ export class PleromaNotificationsRouter implements NotificationsRoute { return notImplementedErrorBuilder(); } - async getMentions() { - return notImplementedErrorBuilder(); + async getMentions(query: NotificationGetQueryDto): Promise< + LibraryResponse<{ + data: MastoGroupedNotificationsResults; + minId?: string | null; + maxId?: string | null; + }> + > { + let url = + '/api/v1/notifications' + + '?exclude_types[]=follow' + + '&exclude_types[]=follow_request&exclude_types[]=favourite' + + '&exclude_types[]=reblog&exclude_types[]=poll' + + '&exclude_types[]=status&exclude_types[]=update' + + '&exclude_types[]=admin.sign_up&exclude_types[]=admin.report' + + '&exclude_types[]=moderation_warning' + + '&exclude_types[]=severed_relationships' + + '&exclude_types[]=annual_report'; + + if (query.limit) url += '&limit=' + query.limit; + if (query.maxId) url += '&max_id=' + query.maxId; + + const result = + await this.direct.getCamelCaseWithLinkPagination( + url, + ); + if (result.error) return errorBuilder(); + return { data: result.data }; } - async getSocialUpdates() { - return notImplementedErrorBuilder(); + async getSocialUpdates(query: NotificationGetQueryDto) { + let url = + '/api/v1/notifications' + + '?exclude_types[]=follow_request' + + '&exclude_types[]=poll' + + '&exclude_types[]=status&exclude_types[]=update' + + '&exclude_types[]=admin.sign_up&exclude_types[]=admin.report' + + '&exclude_types[]=moderation_warning' + + '&exclude_types[]=severed_relationships' + + '&exclude_types[]=annual_report&exclude_types[]=mention'; + if (query.limit) url += '&limit=' + query.limit; + if (query.maxId) url += '&max_id=' + query.maxId; + + const result = + await this.direct.getCamelCaseWithLinkPagination( + url, + ); + if (result.error) return errorBuilder(); + return { data: result.data }; + } + + async getSubscriptionUpdates(query: NotificationGetQueryDto) { + let url = '/api/v1/notifications' + '?types[]=status'; + + if (query.limit) url += '&limit=' + query.limit; + if (query.maxId) url += '&max_id=' + query.maxId; + + const result = + await this.direct.getCamelCaseWithLinkPagination( + url, + ); + if (result.error) return errorBuilder(); + return { data: result.data }; } async sendMessage() { diff --git a/packages/bridge/src/adapters/_client/pleroma/search.ts b/packages/bridge/src/adapters/_client/pleroma/search.ts index 5fa557a8..6a7d2f94 100644 --- a/packages/bridge/src/adapters/_client/pleroma/search.ts +++ b/packages/bridge/src/adapters/_client/pleroma/search.ts @@ -4,7 +4,6 @@ import { } from '../_router/routes/search.js'; import { errorBuilder } from '../_router/dto/api-responses.dto.js'; import { LibraryPromise } from '../_router/routes/_types.js'; -import snakecaseKeys from 'snakecase-keys'; import type { MegaAccount, MegaStatus, @@ -12,6 +11,7 @@ import type { import { DhaagaErrorCode } from '../../../types/result.types.js'; import FetchWrapper from '../../../custom-clients/custom-fetch.js'; import { MegalodonPleromaWrapper } from '../../../custom-clients/custom-clients.js'; +import { CasingUtils } from '../../../utiils/casing.utils.js'; export class PleromaSearchRouter implements SearchRoute { direct: FetchWrapper; @@ -48,10 +48,9 @@ export class PleromaSearchRouter implements SearchRoute { async findPosts(query: DhaagaJsUserSearchDTO): LibraryPromise { try { - console.log(snakecaseKeys(query)); const data = await this.client.client.search( query.q || query.query, - snakecaseKeys(query), + CasingUtils.snakeCaseKeys(query), ); if (data.status !== 200) { return errorBuilder(DhaagaErrorCode.UNAUTHORIZED); diff --git a/packages/bridge/src/adapters/_client/pleroma/statuses.ts b/packages/bridge/src/adapters/_client/pleroma/statuses.ts index 82d09763..56c27c4e 100644 --- a/packages/bridge/src/adapters/_client/pleroma/statuses.ts +++ b/packages/bridge/src/adapters/_client/pleroma/statuses.ts @@ -4,15 +4,15 @@ import { } from '../_router/routes/statuses.js'; import { errorBuilder } from '../_router/dto/api-responses.dto.js'; import { LibraryPromise } from '../_router/routes/_types.js'; -import camelcaseKeys from 'camelcase-keys'; import { - MastoScheduledStatus, - MastoStatus, -} from '../../../types/mastojs.types.js'; -import { MegaReaction, MegaStatus } from '../../../types/megalodon.types.js'; + MegaReaction, + MegaScheduledStatus, + MegaStatus, +} from '../../../types/megalodon.types.js'; import { LibraryResponse } from '../../../types/result.types.js'; import FetchWrapper from '../../../custom-clients/custom-fetch.js'; import { MegalodonPleromaWrapper } from '../../../custom-clients/custom-clients.js'; +import { CasingUtils } from '../../../utiils/casing.utils.js'; export class PleromaStatusesRouter implements StatusesRoute { direct: FetchWrapper; @@ -26,19 +26,19 @@ export class PleromaStatusesRouter implements StatusesRoute { ); } - async get(id: string): Promise> { + async get(id: string): LibraryPromise { const response = await this.client.client.getStatus(id); if (response.status !== 200) { console.log('[ERROR]: failed to get status', response.statusText); } return { - data: camelcaseKeys(response.data) as any, + data: CasingUtils.camelCaseKeys(response.data), }; } async create( dto: DhaagaJsPostCreateDto, - ): LibraryPromise { + ): LibraryPromise { const response = await this.client.client.postStatus(dto.status, { language: dto.language, visibility: dto.mastoVisibility, @@ -51,7 +51,7 @@ export class PleromaStatusesRouter implements StatusesRoute { console.log('[ERROR]: failed to create status', response.statusText); } - return { data: camelcaseKeys(response.data, { deep: true }) as any }; + return { data: CasingUtils.camelCaseKeys(response.data) }; } async delete(id: string): Promise<{ success: boolean; deleted: boolean }> { @@ -68,19 +68,19 @@ export class PleromaStatusesRouter implements StatusesRoute { async getReactions(id: string): Promise> { const data = await this.client.client.getEmojiReactions(id); - return { data: camelcaseKeys(data.data, { deep: true }) as any }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async getReactionDetails( postId: string, reactionId: string, ): LibraryPromise { - const data = await this.client.client.getEmojiReaction(postId, reactionId); + const data = await this.client.client.getEmojiReactions(postId); if (data.status !== 200) { console.log('[ERROR]: failed to get reaction details', data.statusText); return errorBuilder(data.statusText); } - return { data: camelcaseKeys(data.data) as any }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async addReaction(id: string, shortCode: string): LibraryPromise { @@ -89,7 +89,7 @@ export class PleromaStatusesRouter implements StatusesRoute { console.log('[ERROR]: failed to add reaction', data.statusText); return errorBuilder(data.statusText); } - return { data: camelcaseKeys(data.data, { deep: true }) as any }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async removeReaction(id: string, shortCode: string): LibraryPromise { @@ -98,7 +98,7 @@ export class PleromaStatusesRouter implements StatusesRoute { console.log('[ERROR]: failed to remove reaction', data.statusText); return errorBuilder(data.statusText); } - return { data: camelcaseKeys(data.data, { deep: true }) }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async bookmark(id: string) { @@ -127,7 +127,7 @@ export class PleromaStatusesRouter implements StatusesRoute { async getContext(id: string) { const data = await this.client.client.getStatusContext(id); - return { data: camelcaseKeys(data.data) }; + return { data: CasingUtils.camelCaseKeys(data.data) }; } async boost(id: string): LibraryPromise { diff --git a/packages/bridge/src/adapters/_client/pleroma/timelines.ts b/packages/bridge/src/adapters/_client/pleroma/timelines.ts index 8fed5e7d..0ad6b17e 100644 --- a/packages/bridge/src/adapters/_client/pleroma/timelines.ts +++ b/packages/bridge/src/adapters/_client/pleroma/timelines.ts @@ -4,12 +4,11 @@ import { TimelinesRoute, } from '../_router/routes/timelines.js'; import { DefaultTimelinesRouter } from '../default/timelines.js'; -import { toSnakeCase } from '../_router/utils/casing.utils.js'; +import { CasingUtils } from '../../../utiils/casing.utils.js'; import FetchWrapper from '../../../custom-clients/custom-fetch.js'; -import snakecaseKeys from 'snakecase-keys'; -import camelcaseKeys from 'camelcase-keys'; import { errorBuilder } from '../_router/dto/api-responses.dto.js'; import { MegalodonPleromaWrapper } from '../../../custom-clients/custom-clients.js'; +import { DhaagaErrorCode } from '../../../types/result.types.js'; export class PleromaTimelinesRouter extends DefaultTimelinesRouter @@ -30,8 +29,15 @@ export class PleromaTimelinesRouter async home( query: DhaagaJsTimelineQueryOptions, ): DhaagaJsTimelineArrayPromise { - const data = await this.client.client.getHomeTimeline(toSnakeCase(query)); - return { data: data.data }; + try { + const data = await this.client.client.getHomeTimeline( + CasingUtils.snakeCaseKeys(query), + ); + return { data: data.data }; + } catch (e) { + console.log('error is here', e); + return errorBuilder(DhaagaErrorCode.UNKNOWN_ERROR); + } } async public( @@ -39,12 +45,12 @@ export class PleromaTimelinesRouter ): DhaagaJsTimelineArrayPromise { if (query.local === true) { const data = await this.client.client.getLocalTimeline( - toSnakeCase(query), + CasingUtils.snakeCaseKeys(query), ); return { data: data.data }; } else { const data = await this.client.client.getPublicTimeline( - toSnakeCase(query), + CasingUtils.snakeCaseKeys(query), ); return { data: data.data }; } @@ -55,10 +61,10 @@ export class PleromaTimelinesRouter ): DhaagaJsTimelineArrayPromise { const { data: _data, error } = await this.direct.get( '/api/v1/timelines/bubble', - snakecaseKeys(query), + CasingUtils.snakeCaseKeys(query), ); if (error) return errorBuilder(error.code); - return { data: camelcaseKeys(_data as any, { deep: true }) }; + return { data: CasingUtils.camelCaseKeys(_data) }; } async list( @@ -67,7 +73,7 @@ export class PleromaTimelinesRouter ): DhaagaJsTimelineArrayPromise { const data = await this.client.client.getListTimeline( q, - toSnakeCase(query), + CasingUtils.snakeCaseKeys(query), ); return { data: data.data }; } @@ -76,7 +82,10 @@ export class PleromaTimelinesRouter q: string, query: DhaagaJsTimelineQueryOptions, ): DhaagaJsTimelineArrayPromise { - const data = await this.client.client.getTagTimeline(q, toSnakeCase(query)); + const data = await this.client.client.getTagTimeline( + q, + CasingUtils.snakeCaseKeys(query), + ); return { data: data.data, }; diff --git a/packages/bridge/src/custom-clients/custom-fetch.ts b/packages/bridge/src/custom-clients/custom-fetch.ts index efa34769..965d012d 100644 --- a/packages/bridge/src/custom-clients/custom-fetch.ts +++ b/packages/bridge/src/custom-clients/custom-fetch.ts @@ -1,6 +1,5 @@ -import camelcaseKeys from 'camelcase-keys'; -import * as snakecaseKeys from 'snakecase-keys'; import { DhaagaErrorCode, LibraryResponse } from '../types/result.types.js'; +import { CasingUtils } from '../utiils/casing.utils.js'; /** * Use Fetch API to @@ -42,7 +41,7 @@ class FetchWrapper { // You do me dirty, ruby gang :) let typesOverride = obj['types[]']; - const retval = snakecaseKeys.default(obj) as Record; + const retval = CasingUtils.snakeCaseKeys(obj) as Record; if (typesOverride) { delete retval['types']; retval['types[]'] = typesOverride; @@ -101,7 +100,7 @@ class FetchWrapper { const { minId, maxId } = DhaagaApiUtils.extractPaginationFromLinkHeader( response.headers, ); - const _data = camelcaseKeys(await response.json(), { deep: true }); + const _data = CasingUtils.camelCaseKeys(await response.json()); return { data: { data: _data, @@ -140,7 +139,7 @@ class FetchWrapper { ); } DhaagaApiUtils.extractPaginationFromLinkHeader(response.headers); - const data = camelcaseKeys(await response.json(), { deep: true }); + const data = CasingUtils.camelCaseKeys(await response.json()); return { data }; }) .catch((e) => { @@ -201,7 +200,6 @@ class FetchWrapper { new URLSearchParams(this.cleanObject(query)) : `${this.baseUrl}${endpoint}?`; - console.log(endpoint, query); return await fetch(endpoint, { method: 'GET', headers: this.token diff --git a/packages/bridge/src/implementors/profile/_interface.ts b/packages/bridge/src/implementors/profile/_interface.ts index 9dbce393..7b7b0d2e 100644 --- a/packages/bridge/src/implementors/profile/_interface.ts +++ b/packages/bridge/src/implementors/profile/_interface.ts @@ -2,7 +2,6 @@ import MisskeyUser from './misskey.js'; import MastodonUser from './mastodon.js'; import DefaultUser from './default.js'; import { KNOWN_SOFTWARE } from '../../adapters/_client/_router/routes/instance.js'; -import camelcaseKeys from 'camelcase-keys'; import BlueskyUserInterface from './bluesky.js'; import type { MissNote, @@ -10,6 +9,7 @@ import type { MissUserDetailed, } from '../../types/misskey-js.types.js'; import type { MastoAccount } from '../../types/mastojs.types.js'; +import { CasingUtils } from '../../utiils/casing.utils.js'; export type UserType = | MastoAccount @@ -113,7 +113,7 @@ export function ActivityPubUserAdapter( } case KNOWN_SOFTWARE.PLEROMA: case KNOWN_SOFTWARE.AKKOMA: { - const _camel = camelcaseKeys(profile, { deep: true }); + const _camel = CasingUtils.camelCaseKeys(profile); return new MastodonUser(new AccountInstance(_camel as MastoAccount)); } case KNOWN_SOFTWARE.BLUESKY: diff --git a/packages/bridge/src/implementors/status/_adapters.ts b/packages/bridge/src/implementors/status/_adapters.ts index 9cbd318a..006a60b5 100644 --- a/packages/bridge/src/implementors/status/_adapters.ts +++ b/packages/bridge/src/implementors/status/_adapters.ts @@ -3,8 +3,8 @@ import MastodonToStatusAdapter from './mastodon.js'; import UnknownToStatusAdapter from './default.js'; import { StatusInterface } from './_interface.js'; import { KNOWN_SOFTWARE } from '../../adapters/_client/_router/routes/instance.js'; -import camelcaseKeys from 'camelcase-keys'; import BlueskyStatusAdapter from './bluesky.js'; +import { CasingUtils } from '../../utiils/casing.utils.js'; /** * @param status any status object @@ -31,7 +31,7 @@ export function ActivitypubStatusAdapter( case KNOWN_SOFTWARE.PLEROMA: case KNOWN_SOFTWARE.AKKOMA: { - const _camel = camelcaseKeys(status, { deep: true }); + const _camel = CasingUtils.camelCaseKeys(status); return new MastodonToStatusAdapter(_camel as any); } case KNOWN_SOFTWARE.BLUESKY: { diff --git a/packages/bridge/src/implementors/status/mastodon.ts b/packages/bridge/src/implementors/status/mastodon.ts index ec587824..a09a8738 100644 --- a/packages/bridge/src/implementors/status/mastodon.ts +++ b/packages/bridge/src/implementors/status/mastodon.ts @@ -1,9 +1,9 @@ import { DhaagaJsMentionObject, StatusInterface } from './_interface.js'; import { MediaAttachmentToMediaAttachmentAdapter } from '../media-attachment/adapter.js'; import { MediaAttachmentInstance } from '../media-attachment/unique.js'; -import camelcaseKeys from 'camelcase-keys'; import UnknownToStatusAdapter from './default.js'; import { MastoAccount, MastoStatus } from '../../types/mastojs.types.js'; +import { CasingUtils } from '../../utiils/casing.utils.js'; class MastodonToStatusAdapter extends UnknownToStatusAdapter @@ -121,7 +121,7 @@ class MastodonToStatusAdapter getMediaAttachments() { return this.ref.mediaAttachments?.map((o) => { return new MediaAttachmentToMediaAttachmentAdapter( - new MediaAttachmentInstance(camelcaseKeys(o as any, { deep: true })), + new MediaAttachmentInstance(CasingUtils.camelCaseKeys(o)), ); }); } diff --git a/packages/bridge/src/types/megalodon.types.ts b/packages/bridge/src/types/megalodon.types.ts index 49480ae5..dc799948 100644 --- a/packages/bridge/src/types/megalodon.types.ts +++ b/packages/bridge/src/types/megalodon.types.ts @@ -7,6 +7,7 @@ import type { Reaction as MLReaction } from 'megalodon/lib/esm/src/entities/reac import type { Status as MLStatus } from 'megalodon/lib/esm/src/entities/status.js'; import type { Conversation as MLConversation } from 'megalodon/lib/esm/src/entities/conversation.js'; import type { Relationship as MLRelationship } from 'megalodon/lib/esm/src/entities/relationship.js'; +import type { ScheduledStatus as MLScheduledStatus } from 'megalodon/lib/esm/src/entities/scheduled_status.js'; export type MegaStatus = MLStatus; export type MegaReaction = MLReaction; @@ -16,4 +17,5 @@ export type MegaTag = MLTag; export type MegaFeaturedTag = MLFeaturedTag; export type MegaAccount = MLAccount; export type MegaList = MLList; -export type MegaRelationship = MLRelationship; \ No newline at end of file +export type MegaRelationship = MLRelationship; +export type MegaScheduledStatus = MLScheduledStatus; diff --git a/packages/bridge/src/utiils/casing.utils.ts b/packages/bridge/src/utiils/casing.utils.ts new file mode 100644 index 00000000..e0713b82 --- /dev/null +++ b/packages/bridge/src/utiils/casing.utils.ts @@ -0,0 +1,47 @@ +export class CasingUtils { + private static toSnakeCase = (str: string) => + str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + + private static toCamelCase = (str: string) => + str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase()); + + /** + * Recursively convert object keys to snake_case + * @param obj input object + */ + static snakeCaseKeys(obj: any): Object { + if (!obj || typeof obj !== 'object') return obj; + + if (Array.isArray(obj)) + return obj.map((item) => CasingUtils.snakeCaseKeys(item)); // Recursively handle each element in the array + + return Object.entries(obj) + .filter(([k, v]) => v !== undefined) + .reduce((acc: any, [key, value]) => { + const newKey = CasingUtils.toSnakeCase(key); + acc[newKey] = + value && typeof value === 'object' + ? CasingUtils.snakeCaseKeys(value) + : value; + return acc; + }, {}); + } + + static camelCaseKeys(obj: any): any { + if (!obj || typeof obj !== 'object') return obj; + + if (Array.isArray(obj)) + return obj.map((item) => CasingUtils.camelCaseKeys(item)); // Recursively handle each element in the array + + return Object.entries(obj) + .filter(([k, v]) => v !== undefined) + .reduce((acc: any, [key, value]) => { + const newKey = CasingUtils.toCamelCase(key); + acc[newKey] = + value && typeof value === 'object' + ? CasingUtils.camelCaseKeys(value) + : value; + return acc; + }, {}); + } +}