Mirror — see github.com/blacksky-algorithms/blacksky.community
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix merge issues + rename

+5168 -717
+1
.envrc
··· 1 + use flake
+1 -1
.github/ISSUE_TEMPLATE/feature_request.yml
··· 1 1 name: "Feature Request" 2 - description: "Suggest an idea for the Bluesky app." 2 + description: "Suggest an idea for the blacksky.community app." 3 3 labels: ["feature-request"] 4 4 body: 5 5 - type: markdown
+4
.gitignore
··· 119 119 120 120 # ogcard assets 121 121 bskyogcard/src/assets/fonts/noto-* 122 + 123 + # blacksky 124 + .direnv 125 + .wrangler
+1 -1
LICENSE
··· 1 - Copyright 2023–2025 Bluesky Social PBC 1 + Copyright 2025 Blacksky Algorithms Inc. 2 2 3 3 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 4
+1 -1
Makefile
··· 1 1 2 - SHELL = /bin/bash 2 + SHELL = /usr/bin/env bash 3 3 .SHELLFLAGS = -o pipefail -c 4 4 5 5 .PHONY: help
+70 -37
README.md
··· 1 - # Bluesky Social App 1 + # blacksky.community 2 2 3 - Welcome friends! This is the codebase for the Bluesky Social app. 3 + This is a soft fork of [social app](https://github.com/bluesky-social/social-app). 4 4 5 5 Get the app itself: 6 6 7 - - **Web: [bsky.app](https://bsky.app)** 8 - - **iOS: [App Store](https://apps.apple.com/us/app/bluesky-social/id6444370199)** 9 - - **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app)** 7 + - **Web: [blacksky.community](https://blacksky.community)** 8 + - ~**iOS: [App Store]()**~ WIP 9 + - ~**Android: [Play Store]()**~ WIP 10 + 11 + ## Features Today 12 + 13 + - toggle to disable go.bsky.app link proxying for analytics 14 + - toggle to disable default app labeler 15 + - toggle to disable falling back to discover feed in the following feed 16 + - see through quote blocks and detatchments (nuclear block wrt quotes) 17 + - <img src="https://github.com/user-attachments/assets/e5084afd-b17e-43a7-9622-f6d7f19f53ca" width="300px" /> 18 + - enable features gates 19 + - configure the location used to determine regional labelers 20 + - entirely ignore `!no-unauthenticated` labels, even for logged out users 21 + 22 + ### WIP/Planned 23 + 24 + - rewrite shared URLs to reference blacksky.community 25 + - opengraph support for sharing posts and profiles 26 + - selecting custom appviews 27 + - seeing past blocks in post threads (nuclear block for reply chains) 28 + 29 + ## Philosophy 30 + 31 + - by default, blacksky.community should very similar to the official client 32 + - color and branding are different to distinguish from social-app 33 + - `!no-unauthenticated` behavior is different 34 + - analytics are not present 35 + - opinionated features behind toggles 36 + - focus on high impact, low diff size patches 37 + - specifically patches that won't require large conflicts to be resolved 38 + - focus on power users (but all users are welome!) 39 + - enable things that are possible but annoying today **without** egging on antisocial behavior 10 40 11 41 ## Development Resources 12 42 13 - This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), which are also open source, but in [a different git repository](https://github.com/bluesky-social/atproto). 43 + This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto). It is regularly rebased 44 + on top of new releases of [social-app](https://github.com/bluesky-social/social-app). 14 45 15 - There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application. 46 + There is vestigial Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application in the social app deployment. However, it is not used in current 47 + blacksky.community deployments. 48 + For blacksky, the intended deployment is with a websever than can serve static files, and reroute to `index.html` as needed. Today [blacksky.community](https://blacksky.community) is hosted on [cloudflare pages](https://pages.cloudflare.com/). 16 49 17 - The [Build Instructions](./docs/build.md) are a good place to get started with the app itself. 50 + The [Build Instructions](./docs/build.md) are a good place to get started with the app itself. If you use nix (and especially direnv) then `flake.nix` will get you a working environment for 51 + the web version of the app. 18 52 19 - The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at: 53 + The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. 54 + You may wish to reference [resources linked in social-app](https://github.com/bluesky-social/social-app#development-resources). However, please don't harass the Bluesky team with issues or questions 55 + pertaining to blacksky.community. 20 56 21 - - [Overview and Guides](https://atproto.com/guides/overview) 22 - - [GitHub Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions 23 - - [Protocol Specifications](https://atproto.com/specs/atp) 24 - - [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol) 25 - 26 - The Bluesky Social application encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these "Lexicons" is `app.bsky.*`. 57 + Blacksky is a fork of the official client, social-app. It encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these "Lexicons" is `app.bsky.*`. 27 58 28 59 ## Contributions 29 60 30 - > [!NOTE] 31 - > While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review. 61 + > blacksky.community is a community fork, and we'd love to merge your PR! 62 + 63 + As a rule of thumb, the best features for blacksky.community are ones that have a disproportionately positive impact on the user experience compared to the matinance overhead. 64 + Unlike some open source projects, since blacksky.community is a soft fork, any features (patches) we add on top of upstream social-app need to be maintained. For example, 65 + a change to the way posts are composed may be very invasive, touching lots of code across the codebase. If upstream refactors this component, we will need to rewrite this 66 + feature to be compatible or drop it from the client. 32 67 33 - **Rules:** 68 + For this reason, bias towards features that change a relatively small amount of code that is present upstream. 34 69 35 - - We may not respond to your issue or PR. 36 - - We may close an issue or PR without much feedback. 37 - - We may lock discussions or contributions if our attention is getting DDOSed. 38 - - We're not going to provide support for build issues. 70 + Without an overriding motivation, opinionated features should exist behind a toggle that is not enabled by default. This allows blacksky.community to cater to as many users as possible. 39 71 40 72 **Guidelines:** 41 73 42 74 - Check for existing issues before filing a new one please. 43 75 - Open an issue and give some time for discussion before submitting a PR. 76 + - This isn't strictly necessary, but I'd love to give my thoughts and scope out your willingness to maintain the feature before you write it. 44 77 - Stay away from PRs like... 45 78 - Changing "Post" to "Skeet." 46 - - Refactoring the codebase, e.g., to replace React Query with Redux Toolkit or something. 47 - - Adding entirely new features without prior discussion. 79 + - Refactoring the codebase, e.g., to replace MobX with Redux or something. 80 + - Include a new toggle and preference for your feature. 48 81 49 - Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for! 82 + If we don't merge your PR for whatever reason, you are welcome to fork and/or self host: 50 83 51 84 ## Forking guidelines 52 85 53 - You have our blessing 🪄✨ to fork this application! However, it's very important to be clear to users when you're giving them a fork. 86 + Just like social-app, you have our blessing 🪄✨ to fork this application! However, it's very important to be clear to users when you're giving them a fork. 54 87 55 88 Please be sure to: 56 89 57 - - Change all branding in the repository and UI to clearly differentiate from Bluesky. 58 - - Change any support links (feedback, email, terms of service, etc) to your own systems. 59 - - Replace any analytics or error-collection systems with your own so we don't get super confused. 90 + - Change all branding in the repository and UI to clearly differentiate from blacksky.community. 91 + - Change any support links (feedback, email, terms of service, issue tracker, etc) to your own systems. 60 92 61 - ## Security disclosures 93 + ## Self hosting & personal builds 62 94 63 - If you discover any security issues, please send an email to security@bsky.app. The email is automatically CC'd to the entire team and we'll respond promptly. 95 + Self hosting is great! It is our intention that blacksky.community is easy to self host and build on your own. If you host your own instance of blacksky.community, or make your own builds, please 96 + make some level of effort to clarify that it is not an "official" build or instance. This can be in the form of a different domain or branding, but can also be as simple as not 97 + advertising your hosted instance or builds as "official" releases. 64 98 65 - ## Are you a developer interested in building on atproto? 99 + ## Security disclosures 66 100 67 - Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party integration can be as seamless as first-party through custom feeds, federated services, clients, and more. 101 + If you discover any security issues, please send an email to aviva@rubenfamily.com. 102 + If the issue pertains to infastructure, code, or systems outside the scope of blacksky.community, please refer to the 103 + [disclosure guidelines on social-app](https://github.com/bluesky-social/social-app#security-disclosures) if it is hosted by Bluesky PBC. Otherwise, reference the 104 + security policy of that system as applicable <3 68 105 69 106 ## License (MIT) 70 107 71 108 See [./LICENSE](./LICENSE) for the full license. 72 - 73 - ## P.S. 74 - 75 - We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place!
+110 -102
app.config.js
··· 18 18 const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION 19 19 20 20 const ASSOCIATED_DOMAINS = [ 21 - 'applinks:bsky.app', 22 - 'applinks:staging.bsky.app', 23 - 'appclips:bsky.app', 24 - 'appclips:go.bsky.app', // Allows App Clip to work when scanning QR codes 21 + 'applinks:blacksky.community', 25 22 // When testing local services, enter an ngrok (et al) domain here. It must use a standard HTTP/HTTPS port. 26 23 ...(IS_DEV || IS_TESTFLIGHT ? [] : []), 27 24 ] 28 25 29 - const UPDATES_ENABLED = IS_TESTFLIGHT || IS_PRODUCTION 26 + // const UPDATES_CHANNEL = IS_TESTFLIGHT 27 + // ? 'testflight' 28 + // : IS_PRODUCTION 29 + // ? 'production' 30 + // : undefined 31 + // const UPDATES_ENABLED = !!UPDATES_CHANNEL 30 32 31 33 const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN) 32 34 33 35 return { 34 36 expo: { 35 37 version: VERSION, 36 - name: 'Bluesky', 37 - slug: 'bluesky', 38 - scheme: 'bluesky', 39 - owner: 'blueskysocial', 38 + name: 'blacksky.community', 39 + slug: 'blacksky', 40 + scheme: ['bluesky', 'blacksky'], 41 + owner: 'blackskyalgorithms', 40 42 runtimeVersion: { 41 43 policy: 'appVersion', 42 44 }, 43 45 icon: './assets/app-icons/ios_icon_default_light.png', 44 46 userInterfaceStyle: 'automatic', 45 - primaryColor: '#1083fe', 47 + primaryColor: '#6060E9', 46 48 ios: { 47 49 supportsTablet: false, 48 - bundleIdentifier: 'xyz.blueskyweb.app', 50 + bundleIdentifier: 'community.blacksky', 49 51 config: { 50 52 usesNonExemptEncryption: false, 51 53 }, ··· 59 61 'Used to save images to your library.', 60 62 NSPhotoLibraryUsageDescription: 61 63 'Used for profile pictures, posts, and other kinds of content', 62 - CFBundleSpokenName: 'Blue Sky', 64 + CFBundleSpokenName: 'Black Sky', 63 65 CFBundleLocalizations: [ 64 66 'en', 65 67 'an', ··· 107 109 entitlements: { 108 110 'com.apple.developer.kernel.increased-memory-limit': true, 109 111 'com.apple.developer.kernel.extended-virtual-addressing': true, 110 - 'com.apple.security.application-groups': 'group.app.bsky', 112 + 'com.apple.security.application-groups': 'group.community.blacksky', 111 113 }, 112 114 privacyManifests: { 113 115 NSPrivacyAccessedAPITypes: [ ··· 146 148 foregroundImage: './assets/icon-android-foreground.png', 147 149 monochromeImage: './assets/icon-android-foreground.png', 148 150 backgroundImage: './assets/icon-android-background.png', 149 - backgroundColor: '#1185FE', 151 + backgroundColor: '#6060E9', 150 152 }, 151 153 googleServicesFile: './google-services.json', 152 - package: 'xyz.blueskyweb.app', 154 + package: 'community.blacksky', 153 155 intentFilters: [ 154 156 { 155 157 action: 'VIEW', 156 158 autoVerify: true, 157 159 data: [ 160 + { 161 + scheme: 'https', 162 + host: 'community.blacksky', 163 + }, 158 164 { 159 165 scheme: 'https', 160 166 host: 'bsky.app', ··· 171 177 web: { 172 178 favicon: './assets/favicon.png', 173 179 }, 174 - updates: { 175 - url: 'https://updates.bsky.app/manifest', 176 - enabled: UPDATES_ENABLED, 177 - fallbackToCacheTimeout: 30000, 178 - codeSigningCertificate: UPDATES_ENABLED 179 - ? './code-signing/certificate.pem' 180 - : undefined, 181 - codeSigningMetadata: UPDATES_ENABLED 182 - ? { 183 - keyid: 'main', 184 - alg: 'rsa-v1_5-sha256', 185 - } 186 - : undefined, 187 - checkAutomatically: 'NEVER', 188 - }, 180 + // updates: { 181 + // url: 'https://updates.bsky.app/manifest', 182 + // enabled: UPDATES_ENABLED, 183 + // fallbackToCacheTimeout: 30000, 184 + // codeSigningCertificate: UPDATES_ENABLED 185 + // ? './code-signing/certificate.pem' 186 + // : undefined, 187 + // codeSigningMetadata: UPDATES_ENABLED 188 + // ? { 189 + // keyid: 'main', 190 + // alg: 'rsa-v1_5-sha256', 191 + // } 192 + // : undefined, 193 + // checkAutomatically: 'NEVER', 194 + // channel: UPDATES_CHANNEL, 195 + // }, 189 196 plugins: [ 190 197 'expo-video', 191 198 'expo-localization', ··· 221 228 'expo-notifications', 222 229 { 223 230 icon: './assets/icon-android-notification.png', 224 - color: '#1185fe', 231 + color: '#6060E9', 225 232 sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'], 226 233 }, 227 234 ], ··· 276 283 }, 277 284 }, 278 285 android: { 279 - backgroundColor: '#0c7cff', 286 + backgroundColor: '#6060E9', 280 287 image: './assets/splash-android-icon.png', 281 288 imageWidth: 150, 282 289 dark: { ··· 312 319 /** 313 320 * Bluesky+ core set 314 321 */ 315 - core_aurora: { 316 - ios: './assets/app-icons/ios_icon_core_aurora.png', 317 - android: './assets/app-icons/android_icon_core_aurora.png', 318 - prerendered: true, 319 - }, 320 - core_bonfire: { 321 - ios: './assets/app-icons/ios_icon_core_bonfire.png', 322 - android: './assets/app-icons/android_icon_core_bonfire.png', 323 - prerendered: true, 324 - }, 325 - core_sunrise: { 326 - ios: './assets/app-icons/ios_icon_core_sunrise.png', 327 - android: './assets/app-icons/android_icon_core_sunrise.png', 328 - prerendered: true, 329 - }, 330 - core_sunset: { 331 - ios: './assets/app-icons/ios_icon_core_sunset.png', 332 - android: './assets/app-icons/android_icon_core_sunset.png', 333 - prerendered: true, 334 - }, 335 - core_midnight: { 336 - ios: './assets/app-icons/ios_icon_core_midnight.png', 337 - android: './assets/app-icons/android_icon_core_midnight.png', 338 - prerendered: true, 339 - }, 340 - core_flat_blue: { 341 - ios: './assets/app-icons/ios_icon_core_flat_blue.png', 342 - android: './assets/app-icons/android_icon_core_flat_blue.png', 343 - prerendered: true, 344 - }, 345 - core_flat_white: { 346 - ios: './assets/app-icons/ios_icon_core_flat_white.png', 347 - android: './assets/app-icons/android_icon_core_flat_white.png', 348 - prerendered: true, 349 - }, 350 - core_flat_black: { 351 - ios: './assets/app-icons/ios_icon_core_flat_black.png', 352 - android: './assets/app-icons/android_icon_core_flat_black.png', 353 - prerendered: true, 354 - }, 355 - core_classic: { 356 - ios: './assets/app-icons/ios_icon_core_classic.png', 357 - android: './assets/app-icons/android_icon_core_classic.png', 358 - prerendered: true, 359 - }, 322 + // core_aurora: { 323 + // ios: './assets/app-icons/ios_icon_core_aurora.png', 324 + // android: './assets/app-icons/android_icon_core_aurora.png', 325 + // prerendered: true, 326 + // }, 327 + // core_bonfire: { 328 + // ios: './assets/app-icons/ios_icon_core_bonfire.png', 329 + // android: './assets/app-icons/android_icon_core_bonfire.png', 330 + // prerendered: true, 331 + // }, 332 + // core_sunrise: { 333 + // ios: './assets/app-icons/ios_icon_core_sunrise.png', 334 + // android: './assets/app-icons/android_icon_core_sunrise.png', 335 + // prerendered: true, 336 + // }, 337 + // core_sunset: { 338 + // ios: './assets/app-icons/ios_icon_core_sunset.png', 339 + // android: './assets/app-icons/android_icon_core_sunset.png', 340 + // prerendered: true, 341 + // }, 342 + // core_midnight: { 343 + // ios: './assets/app-icons/ios_icon_core_midnight.png', 344 + // android: './assets/app-icons/android_icon_core_midnight.png', 345 + // prerendered: true, 346 + // }, 347 + // core_flat_blue: { 348 + // ios: './assets/app-icons/ios_icon_core_flat_blue.png', 349 + // android: './assets/app-icons/android_icon_core_flat_blue.png', 350 + // prerendered: true, 351 + // }, 352 + // core_flat_white: { 353 + // ios: './assets/app-icons/ios_icon_core_flat_white.png', 354 + // android: './assets/app-icons/android_icon_core_flat_white.png', 355 + // prerendered: true, 356 + // }, 357 + // core_flat_black: { 358 + // ios: './assets/app-icons/ios_icon_core_flat_black.png', 359 + // android: './assets/app-icons/android_icon_core_flat_black.png', 360 + // prerendered: true, 361 + // }, 362 + // core_classic: { 363 + // ios: './assets/app-icons/ios_icon_core_classic.png', 364 + // android: './assets/app-icons/android_icon_core_classic.png', 365 + // prerendered: true, 366 + // }, 360 367 }, 361 368 ], 362 369 ['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}], ··· 367 374 build: { 368 375 experimental: { 369 376 ios: { 370 - appExtensions: [ 371 - { 372 - targetName: 'Share-with-Bluesky', 373 - bundleIdentifier: 'xyz.blueskyweb.app.Share-with-Bluesky', 374 - entitlements: { 375 - 'com.apple.security.application-groups': [ 376 - 'group.app.bsky', 377 - ], 378 - }, 379 - }, 380 - { 381 - targetName: 'BlueskyNSE', 382 - bundleIdentifier: 'xyz.blueskyweb.app.BlueskyNSE', 383 - entitlements: { 384 - 'com.apple.security.application-groups': [ 385 - 'group.app.bsky', 386 - ], 387 - }, 388 - }, 389 - { 390 - targetName: 'BlueskyClip', 391 - bundleIdentifier: 'xyz.blueskyweb.app.AppClip', 392 - }, 393 - ], 377 + // appExtensions: [ 378 + // { 379 + // targetName: 'Share-with-Bluesky', 380 + // bundleIdentifier: 'xyz.blueskyweb.app.Share-with-Bluesky', 381 + // entitlements: { 382 + // 'com.apple.security.application-groups': [ 383 + // 'group.app.bsky', 384 + // ], 385 + // }, 386 + // }, 387 + // { 388 + // targetName: 'BlueskyNSE', 389 + // bundleIdentifier: 'xyz.blueskyweb.app.BlueskyNSE', 390 + // entitlements: { 391 + // 'com.apple.security.application-groups': [ 392 + // 'group.app.bsky', 393 + // ], 394 + // }, 395 + // }, 396 + // { 397 + // targetName: 'BlueskyClip', 398 + // bundleIdentifier: 'xyz.blueskyweb.app.AppClip', 399 + // }, 400 + // ], 394 401 }, 395 402 }, 396 403 }, 397 - projectId: '55bd077a-d905-4184-9c7f-94789ba0f302', 404 + //projectId: '55bd077a-d905-4184-9c7f-94789ba0f302', 405 + projectId: '86ff94e3-dce0-4f7c-99f4-1651a2f1bc2a', 398 406 }, 399 407 }, 400 408 },
+284
assets/app-icons/android_base.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + width="512" 6 + height="512" 7 + viewBox="0 0 512 512" 8 + version="1.1" 9 + id="svg1" 10 + sodipodi:docname="android_base.svg" 11 + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" 12 + inkscape:export-filename="android_icon_default_dark.png" 13 + inkscape:export-xdpi="192" 14 + inkscape:export-ydpi="192" 15 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 17 + xmlns:xlink="http://www.w3.org/1999/xlink" 18 + xmlns="http://www.w3.org/2000/svg" 19 + xmlns:svg="http://www.w3.org/2000/svg"> 20 + <sodipodi:namedview 21 + id="namedview1" 22 + pagecolor="#ffffff" 23 + bordercolor="#000000" 24 + borderopacity="0.25" 25 + inkscape:showpageshadow="2" 26 + inkscape:pageopacity="0.0" 27 + inkscape:pagecheckerboard="0" 28 + inkscape:deskcolor="#d1d1d1" 29 + inkscape:document-units="px" 30 + showgrid="false" 31 + showguides="false" 32 + inkscape:zoom="0.85483824" 33 + inkscape:cx="289.52846" 34 + inkscape:cy="208.81144" 35 + inkscape:window-width="1706" 36 + inkscape:window-height="905" 37 + inkscape:window-x="20" 38 + inkscape:window-y="20" 39 + inkscape:window-maximized="0" 40 + inkscape:current-layer="svg1"> 41 + <inkscape:grid 42 + id="grid1" 43 + units="px" 44 + originx="0" 45 + originy="0" 46 + spacingx="1" 47 + spacingy="1" 48 + empcolor="#0099e5" 49 + empopacity="0.30196078" 50 + color="#0099e5" 51 + opacity="0.14901961" 52 + empspacing="5" 53 + enabled="true" 54 + visible="false" /> 55 + <sodipodi:guide 56 + position="7.2092363,499.98461" 57 + orientation="0.73456475,0.6785386" 58 + id="guide15" 59 + inkscape:locked="false" /> 60 + <sodipodi:guide 61 + position="250,12" 62 + orientation="0,-1" 63 + id="guide16" 64 + inkscape:locked="false" /> 65 + <sodipodi:guide 66 + position="7.2114897,499.98617" 67 + orientation="0.90391787,0.42770606" 68 + id="guide17" 69 + inkscape:locked="false" /> 70 + <sodipodi:guide 71 + position="114.38555,198.20474" 72 + orientation="0.90391787,0.42770608" 73 + id="guide18" 74 + inkscape:label="" 75 + inkscape:locked="false" 76 + inkscape:color="rgb(0,134,229)" /> 77 + <sodipodi:guide 78 + position="372.00185,356.22842" 79 + orientation="0.73454936,-0.67855525" 80 + id="guide19" 81 + inkscape:locked="false" /> 82 + </sodipodi:namedview> 83 + <defs 84 + id="defs1"> 85 + <linearGradient 86 + id="linearGradient27" 87 + inkscape:collect="always"> 88 + <stop 89 + style="stop-color:#1a251e;stop-opacity:1;" 90 + offset="0" 91 + id="stop26" /> 92 + <stop 93 + style="stop-color:#3c452c;stop-opacity:1;" 94 + offset="1" 95 + id="stop27" /> 96 + </linearGradient> 97 + <linearGradient 98 + id="linearGradient23" 99 + inkscape:collect="always"> 100 + <stop 101 + style="stop-color:#344e41;stop-opacity:1;" 102 + offset="0" 103 + id="stop23" /> 104 + <stop 105 + style="stop-color:#a3b18a;stop-opacity:1;" 106 + offset="1" 107 + id="stop24" /> 108 + </linearGradient> 109 + <filter 110 + id="selectable_hidder_filter" 111 + width="1" 112 + height="1" 113 + x="0" 114 + y="0" 115 + style="color-interpolation-filters:sRGB;" 116 + inkscape:label="LPE boolean visibility"> 117 + <feComposite 118 + id="boolops_hidder_primitive" 119 + result="composite1" 120 + operator="arithmetic" 121 + in2="SourceGraphic" 122 + in="BackgroundImage" /> 123 + </filter> 124 + <inkscape:path-effect 125 + effect="bool_op" 126 + operand-path="" 127 + id="path-effect16" 128 + is_visible="true" 129 + lpeversion="1" 130 + operation="cut" 131 + swap-operands="false" 132 + filltype-this="from-curve" 133 + filter="" 134 + filltype-operand="from-curve" /> 135 + <inkscape:path-effect 136 + effect="mirror_symmetry" 137 + start_point="153.53846,-190.49753" 138 + end_point="529.50144,157.05943" 139 + center_point="341.51995,-16.71905" 140 + id="path-effect15" 141 + is_visible="true" 142 + lpeversion="1.2" 143 + lpesatellites="" 144 + mode="vertical" 145 + discard_orig_path="false" 146 + fuse_paths="false" 147 + oposite_fuse="false" 148 + split_items="false" 149 + split_open="false" 150 + link_styles="false" /> 151 + <inkscape:path-effect 152 + effect="mirror_symmetry" 153 + start_point="242.40625,64.77241" 154 + end_point="242.40625,593.03423" 155 + center_point="242.40625,328.90332" 156 + id="path-effect14" 157 + is_visible="true" 158 + lpeversion="1.2" 159 + lpesatellites="" 160 + mode="vertical" 161 + discard_orig_path="false" 162 + fuse_paths="true" 163 + oposite_fuse="false" 164 + split_items="false" 165 + split_open="false" 166 + link_styles="false" /> 167 + <inkscape:path-effect 168 + effect="mirror_symmetry" 169 + start_point="238.18975,-35.620493" 170 + end_point="238.18975,476.37951" 171 + center_point="238.18975,220.37951" 172 + id="path-effect13" 173 + is_visible="true" 174 + lpeversion="1.2" 175 + lpesatellites="" 176 + mode="vertical" 177 + discard_orig_path="false" 178 + fuse_paths="false" 179 + oposite_fuse="false" 180 + split_items="false" 181 + split_open="false" 182 + link_styles="false" /> 183 + <inkscape:path-effect 184 + effect="mirror_symmetry" 185 + start_point="259.66682,-1.5714927" 186 + end_point="259.66682,510.42851" 187 + center_point="259.66682,254.42851" 188 + id="path-effect12" 189 + is_visible="true" 190 + lpeversion="1.2" 191 + lpesatellites="" 192 + mode="vertical" 193 + discard_orig_path="false" 194 + fuse_paths="true" 195 + oposite_fuse="false" 196 + split_items="false" 197 + split_open="false" 198 + link_styles="false" /> 199 + <inkscape:path-effect 200 + effect="clone_original" 201 + css_properties="" 202 + attributes="style,clip-path,mask" 203 + linkeditem="" 204 + is_visible="true" 205 + method="d" 206 + allow_transforms="true" 207 + id="path-effect11" 208 + lpeversion="1" /> 209 + <inkscape:path-effect 210 + effect="mirror_symmetry" 211 + start_point="274.33408,-38.239647" 212 + end_point="274.33408,473.76035" 213 + center_point="274.33408,217.76035" 214 + id="path-effect10" 215 + is_visible="true" 216 + lpeversion="1.2" 217 + lpesatellites="" 218 + mode="vertical" 219 + discard_orig_path="false" 220 + fuse_paths="false" 221 + oposite_fuse="false" 222 + split_items="false" 223 + split_open="false" 224 + link_styles="false" /> 225 + <inkscape:path-effect 226 + effect="mirror_symmetry" 227 + start_point="241.42806,-153.19556" 228 + end_point="241.42806,358.80444" 229 + center_point="241.42806,102.80444" 230 + id="path-effect9" 231 + is_visible="true" 232 + lpeversion="1.2" 233 + lpesatellites="" 234 + mode="vertical" 235 + discard_orig_path="false" 236 + fuse_paths="false" 237 + oposite_fuse="false" 238 + split_items="false" 239 + split_open="false" 240 + link_styles="false" /> 241 + <linearGradient 242 + inkscape:collect="always" 243 + xlink:href="#linearGradient23" 244 + id="linearGradient24" 245 + x1="256" 246 + y1="0" 247 + x2="256" 248 + y2="512" 249 + gradientUnits="userSpaceOnUse" /> 250 + <linearGradient 251 + inkscape:collect="always" 252 + xlink:href="#linearGradient27" 253 + id="linearGradient26" 254 + gradientUnits="userSpaceOnUse" 255 + x1="256" 256 + y1="0" 257 + x2="256" 258 + y2="512" /> 259 + </defs> 260 + <rect 261 + style="display:inline;opacity:1;fill:url(#linearGradient24);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:5;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" 262 + id="rect23" 263 + width="512" 264 + height="512" 265 + x="0" 266 + y="0" 267 + inkscape:label="light background" /> 268 + <rect 269 + style="display:inline;fill:url(#linearGradient26);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:5;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" 270 + id="rect23-8" 271 + width="512" 272 + height="512" 273 + x="0" 274 + y="0" 275 + inkscape:label="dark background" /> 276 + <path 277 + style="fill:#ffffff" 278 + d="m 149.96484,186.56641 46.09766,152.95898 c 0,0 -6.30222,-9.61174 -15.60547,-17.47656 -8.87322,-7.50128 -28.4082,-4.04492 -28.4082,-4.04492 0,0 6.14721,39.88867 15.53125,44.39843 10.71251,5.1482 22.19726,0.16993 22.19726,0.16993 0,0 11.76131,-4.87282 22.82032,31.82421 5.26535,17.47195 15.33258,50.877 20.9707,69.58594 2.16611,7.18779 8.83139,7.25775 8.83789,7.25781 0.006,-6e-5 6.67178,-0.07 8.83789,-7.25781 5.63812,-18.70894 15.70535,-52.11399 20.9707,-69.58594 11.05901,-36.69703 22.82032,-31.82421 22.82032,-31.82421 0,0 11.48475,4.97827 22.19726,-0.16993 9.38404,-4.50976 15.53125,-44.39843 15.53125,-44.39843 0,0 -19.53498,-3.45636 -28.4082,4.04492 -9.30325,7.86482 -15.60547,17.47656 -15.60547,17.47656 l 46.09766,-152.95898 -49.32618,83.84179 -20.34375,-31.1914 6.35547,54.96875 -23.1582,39.36132 c 0,0 -2.97595,5.06226 -5.94336,4.68946 -0.009,-0.001 -0.017,0.003 -0.0254,0.01 -0.008,-0.007 -0.0168,-0.0108 -0.0254,-0.01 -2.96741,0.3728 -5.94336,-4.68946 -5.94336,-4.68946 l -23.1582,-39.36132 6.35547,-54.96875 -20.34375,31.1914 z" 279 + id="path13" 280 + transform="matrix(1.5217795,0,0,0.96921636,-112.88886,-62.778479)" 281 + sodipodi:nodetypes="scsccccccscscss" 282 + inkscape:original-d="m 233.56872,463.9817 c 2.16717,7.1913 8.83768,7.25821 8.83768,7.25821 0,0 1.53461,-133.19881 -0.0257,-133.00279 -2.96741,0.3728 -5.94331,-4.69028 -5.94331,-4.69028 l -23.15733,-39.3618 6.35509,-54.96755 -20.34345,31.19075 -49.32605,-83.84223 46.09711,152.95859 c 0,0 -6.30275,-9.61063 -15.606,-17.47545 -8.87322,-7.50128 -28.40888,-4.04487 -28.40888,-4.04487 0,0 6.14833,39.88845 15.53237,44.39821 10.71251,5.1482 22.19654,0.17066 22.19654,0.17066 0,0 11.76274,-4.87365 22.82175,31.82338 5.26535,17.47195 15.33206,50.87623 20.97018,69.58517 z" 283 + inkscape:path-effect="#path-effect14" /> 284 + </svg>
assets/app-icons/android_icon_core_aurora.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_bonfire.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_classic.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_flat_black.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_flat_blue.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_flat_white.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_midnight.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_sunrise.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_core_sunset.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_default_dark.png

This is a binary file and will not be displayed.

assets/app-icons/android_icon_default_light.png

This is a binary file and will not be displayed.

+284
assets/app-icons/ios_base.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + width="512" 6 + height="512" 7 + viewBox="0 0 512 512" 8 + version="1.1" 9 + id="svg1" 10 + sodipodi:docname="ios_base.svg" 11 + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" 12 + inkscape:export-filename="ios_icon_default_dark.png" 13 + inkscape:export-xdpi="192" 14 + inkscape:export-ydpi="192" 15 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 17 + xmlns:xlink="http://www.w3.org/1999/xlink" 18 + xmlns="http://www.w3.org/2000/svg" 19 + xmlns:svg="http://www.w3.org/2000/svg"> 20 + <sodipodi:namedview 21 + id="namedview1" 22 + pagecolor="#ffffff" 23 + bordercolor="#000000" 24 + borderopacity="0.25" 25 + inkscape:showpageshadow="2" 26 + inkscape:pageopacity="0.0" 27 + inkscape:pagecheckerboard="0" 28 + inkscape:deskcolor="#d1d1d1" 29 + inkscape:document-units="px" 30 + showgrid="false" 31 + showguides="false" 32 + inkscape:zoom="0.85483824" 33 + inkscape:cx="288.35865" 34 + inkscape:cy="208.81144" 35 + inkscape:window-width="1706" 36 + inkscape:window-height="905" 37 + inkscape:window-x="20" 38 + inkscape:window-y="20" 39 + inkscape:window-maximized="0" 40 + inkscape:current-layer="svg1"> 41 + <inkscape:grid 42 + id="grid1" 43 + units="px" 44 + originx="0" 45 + originy="0" 46 + spacingx="1" 47 + spacingy="1" 48 + empcolor="#0099e5" 49 + empopacity="0.30196078" 50 + color="#0099e5" 51 + opacity="0.14901961" 52 + empspacing="5" 53 + enabled="true" 54 + visible="false" /> 55 + <sodipodi:guide 56 + position="7.2092363,499.98461" 57 + orientation="0.73456475,0.6785386" 58 + id="guide15" 59 + inkscape:locked="false" /> 60 + <sodipodi:guide 61 + position="250,12" 62 + orientation="0,-1" 63 + id="guide16" 64 + inkscape:locked="false" /> 65 + <sodipodi:guide 66 + position="7.2114897,499.98617" 67 + orientation="0.90391787,0.42770606" 68 + id="guide17" 69 + inkscape:locked="false" /> 70 + <sodipodi:guide 71 + position="114.38555,198.20474" 72 + orientation="0.90391787,0.42770608" 73 + id="guide18" 74 + inkscape:label="" 75 + inkscape:locked="false" 76 + inkscape:color="rgb(0,134,229)" /> 77 + <sodipodi:guide 78 + position="372.00185,356.22842" 79 + orientation="0.73454936,-0.67855525" 80 + id="guide19" 81 + inkscape:locked="false" /> 82 + </sodipodi:namedview> 83 + <defs 84 + id="defs1"> 85 + <linearGradient 86 + id="linearGradient27" 87 + inkscape:collect="always"> 88 + <stop 89 + style="stop-color:#1a251e;stop-opacity:1;" 90 + offset="0" 91 + id="stop26" /> 92 + <stop 93 + style="stop-color:#3c452c;stop-opacity:1;" 94 + offset="1" 95 + id="stop27" /> 96 + </linearGradient> 97 + <linearGradient 98 + id="linearGradient23" 99 + inkscape:collect="always"> 100 + <stop 101 + style="stop-color:#344e41;stop-opacity:1;" 102 + offset="0" 103 + id="stop23" /> 104 + <stop 105 + style="stop-color:#a3b18a;stop-opacity:1;" 106 + offset="1" 107 + id="stop24" /> 108 + </linearGradient> 109 + <filter 110 + id="selectable_hidder_filter" 111 + width="1" 112 + height="1" 113 + x="0" 114 + y="0" 115 + style="color-interpolation-filters:sRGB;" 116 + inkscape:label="LPE boolean visibility"> 117 + <feComposite 118 + id="boolops_hidder_primitive" 119 + result="composite1" 120 + operator="arithmetic" 121 + in2="SourceGraphic" 122 + in="BackgroundImage" /> 123 + </filter> 124 + <inkscape:path-effect 125 + effect="bool_op" 126 + operand-path="" 127 + id="path-effect16" 128 + is_visible="true" 129 + lpeversion="1" 130 + operation="cut" 131 + swap-operands="false" 132 + filltype-this="from-curve" 133 + filter="" 134 + filltype-operand="from-curve" /> 135 + <inkscape:path-effect 136 + effect="mirror_symmetry" 137 + start_point="153.53846,-190.49753" 138 + end_point="529.50144,157.05943" 139 + center_point="341.51995,-16.71905" 140 + id="path-effect15" 141 + is_visible="true" 142 + lpeversion="1.2" 143 + lpesatellites="" 144 + mode="vertical" 145 + discard_orig_path="false" 146 + fuse_paths="false" 147 + oposite_fuse="false" 148 + split_items="false" 149 + split_open="false" 150 + link_styles="false" /> 151 + <inkscape:path-effect 152 + effect="mirror_symmetry" 153 + start_point="242.40624,113.23531" 154 + end_point="242.40624,544.57133" 155 + center_point="242.40624,328.90332" 156 + id="path-effect14" 157 + is_visible="true" 158 + lpeversion="1.2" 159 + lpesatellites="" 160 + mode="vertical" 161 + discard_orig_path="false" 162 + fuse_paths="true" 163 + oposite_fuse="false" 164 + split_items="false" 165 + split_open="false" 166 + link_styles="false" /> 167 + <inkscape:path-effect 168 + effect="mirror_symmetry" 169 + start_point="238.18975,-35.620493" 170 + end_point="238.18975,476.37951" 171 + center_point="238.18975,220.37951" 172 + id="path-effect13" 173 + is_visible="true" 174 + lpeversion="1.2" 175 + lpesatellites="" 176 + mode="vertical" 177 + discard_orig_path="false" 178 + fuse_paths="false" 179 + oposite_fuse="false" 180 + split_items="false" 181 + split_open="false" 182 + link_styles="false" /> 183 + <inkscape:path-effect 184 + effect="mirror_symmetry" 185 + start_point="259.66682,-1.5714927" 186 + end_point="259.66682,510.42851" 187 + center_point="259.66682,254.42851" 188 + id="path-effect12" 189 + is_visible="true" 190 + lpeversion="1.2" 191 + lpesatellites="" 192 + mode="vertical" 193 + discard_orig_path="false" 194 + fuse_paths="true" 195 + oposite_fuse="false" 196 + split_items="false" 197 + split_open="false" 198 + link_styles="false" /> 199 + <inkscape:path-effect 200 + effect="clone_original" 201 + css_properties="" 202 + attributes="style,clip-path,mask" 203 + linkeditem="" 204 + is_visible="true" 205 + method="d" 206 + allow_transforms="true" 207 + id="path-effect11" 208 + lpeversion="1" /> 209 + <inkscape:path-effect 210 + effect="mirror_symmetry" 211 + start_point="274.33408,-38.239647" 212 + end_point="274.33408,473.76035" 213 + center_point="274.33408,217.76035" 214 + id="path-effect10" 215 + is_visible="true" 216 + lpeversion="1.2" 217 + lpesatellites="" 218 + mode="vertical" 219 + discard_orig_path="false" 220 + fuse_paths="false" 221 + oposite_fuse="false" 222 + split_items="false" 223 + split_open="false" 224 + link_styles="false" /> 225 + <inkscape:path-effect 226 + effect="mirror_symmetry" 227 + start_point="241.42806,-153.19556" 228 + end_point="241.42806,358.80444" 229 + center_point="241.42806,102.80444" 230 + id="path-effect9" 231 + is_visible="true" 232 + lpeversion="1.2" 233 + lpesatellites="" 234 + mode="vertical" 235 + discard_orig_path="false" 236 + fuse_paths="false" 237 + oposite_fuse="false" 238 + split_items="false" 239 + split_open="false" 240 + link_styles="false" /> 241 + <linearGradient 242 + inkscape:collect="always" 243 + xlink:href="#linearGradient23" 244 + id="linearGradient24" 245 + x1="256" 246 + y1="0" 247 + x2="256" 248 + y2="512" 249 + gradientUnits="userSpaceOnUse" /> 250 + <linearGradient 251 + inkscape:collect="always" 252 + xlink:href="#linearGradient27" 253 + id="linearGradient26" 254 + gradientUnits="userSpaceOnUse" 255 + x1="256" 256 + y1="0" 257 + x2="256" 258 + y2="512" /> 259 + </defs> 260 + <rect 261 + style="display:inline;opacity:1;fill:url(#linearGradient24);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:5;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" 262 + id="rect23" 263 + width="512" 264 + height="512" 265 + x="0" 266 + y="0" 267 + inkscape:label="light background" /> 268 + <rect 269 + style="display:none;fill:url(#linearGradient26);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:5;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" 270 + id="rect23-8" 271 + width="512" 272 + height="512" 273 + x="0" 274 + y="0" 275 + inkscape:label="dark background" /> 276 + <path 277 + style="fill:#ffffff" 278 + d="m 149.96484,186.56641 46.09766,152.95898 c 0,0 -6.30222,-9.61174 -15.60547,-17.47656 -8.87322,-7.50128 -28.4082,-4.04492 -28.4082,-4.04492 0,0 6.14721,39.88867 15.53125,44.39843 10.71251,5.1482 22.19726,0.16993 22.19726,0.16993 0,0 11.76131,-4.87282 22.82032,31.82421 5.26535,17.47195 15.33258,50.877 20.9707,69.58594 2.16607,7.18766 8.83114,7.25774 8.83789,7.25781 0.006,-7e-5 6.67182,-0.0702 8.83789,-7.25781 5.63812,-18.70894 15.70535,-52.11399 20.9707,-69.58594 11.05901,-36.69703 22.82032,-31.82421 22.82032,-31.82421 0,0 11.48475,4.97827 22.19726,-0.16993 9.38404,-4.50976 15.53125,-44.39843 15.53125,-44.39843 0,0 -19.53498,-3.45636 -28.4082,4.04492 -9.30325,7.86482 -15.60547,17.47656 -15.60547,17.47656 l 46.09766,-152.95898 -49.32618,83.84179 -20.34375,-31.1914 6.35547,54.96875 -23.1582,39.36132 c 0,0 -2.97595,5.06226 -5.94336,4.68946 -0.009,-0.001 -0.017,0.003 -0.0254,0.01 -0.008,-0.007 -0.0168,-0.0108 -0.0254,-0.01 -2.96741,0.3728 -5.94336,-4.68946 -5.94336,-4.68946 l -23.1582,-39.36132 6.35547,-54.96875 -20.34375,31.1914 z" 279 + id="path13" 280 + transform="matrix(1.8637395,0,0,1.1870096,-195.78209,-134.4114)" 281 + sodipodi:nodetypes="scsccccccscscss" 282 + inkscape:original-d="m 233.56872,463.9817 c 2.16717,7.1913 8.83768,7.25821 8.83768,7.25821 0,0 1.53461,-133.19881 -0.0257,-133.00279 -2.96741,0.3728 -5.94331,-4.69028 -5.94331,-4.69028 l -23.15733,-39.3618 6.35509,-54.96755 -20.34345,31.19075 -49.32605,-83.84223 46.09711,152.95859 c 0,0 -6.30275,-9.61063 -15.606,-17.47545 -8.87322,-7.50128 -28.40888,-4.04487 -28.40888,-4.04487 0,0 6.14833,39.88845 15.53237,44.39821 10.71251,5.1482 22.19654,0.17066 22.19654,0.17066 0,0 11.76274,-4.87365 22.82175,31.82338 5.26535,17.47195 15.33206,50.87623 20.97018,69.58517 z" 283 + inkscape:path-effect="#path-effect14" /> 284 + </svg>
assets/app-icons/ios_icon_core_aurora.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_bonfire.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_classic.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_flat_black.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_flat_blue.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_flat_white.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_midnight.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_sunrise.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_core_sunset.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_default_dark.png

This is a binary file and will not be displayed.

assets/app-icons/ios_icon_default_light.png

This is a binary file and will not be displayed.

assets/default-avatar.png

This is a binary file and will not be displayed.

assets/favicon.png

This is a binary file and will not be displayed.

assets/icon-android-background.png

This is a binary file and will not be displayed.

assets/icon-android-foreground.png

This is a binary file and will not be displayed.

assets/icon-android-notification.png

This is a binary file and will not be displayed.

assets/kawaii.png

This is a binary file and will not be displayed.

assets/kawaii_smol.png

This is a binary file and will not be displayed.

assets/logo.png

This is a binary file and will not be displayed.

assets/splash-android-icon-dark.png

This is a binary file and will not be displayed.

assets/splash-android-icon.png

This is a binary file and will not be displayed.

assets/splash-dark.png

This is a binary file and will not be displayed.

assets/splash.png

This is a binary file and will not be displayed.

+24
blacksky-static-about/index.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="theme-color"> 6 + <meta 7 + name="viewport" 8 + content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" 9 + /> 10 + <title>blacksky.community | about</title> 11 + <meta http-equiv="refresh" content="0;url=https://github.com/blacksky-algorithms/blacksky.community"> 12 + 13 + <link rel="preload" as="font" type="font/woff2" href="/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin> 14 + 15 + <link rel="stylesheet" href="normalize.css" type="text/css"> 16 + <link rel="stylesheet" href="sakura.css" type="text/css"> 17 + </head> 18 + <script type="text/javascript"> 19 + window.location.href = "https://github.com/blacksky-algorithms/blacksky.community"; 20 + </script> 21 + <body> 22 + <h1>redirecting to github readme...</h1> 23 + </body> 24 + </html>
+351
blacksky-static-about/normalize.css
··· 1 + /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 + 3 + /* Document 4 + ========================================================================== */ 5 + 6 + /** 7 + * 1. Correct the line height in all browsers. 8 + * 2. Prevent adjustments of font size after orientation changes in iOS. 9 + */ 10 + 11 + html { 12 + line-height: 1.15; /* 1 */ 13 + -webkit-text-size-adjust: 100%; /* 2 */ 14 + } 15 + 16 + /* Sections 17 + ========================================================================== */ 18 + 19 + /** 20 + * Remove the margin in all browsers. 21 + */ 22 + 23 + body { 24 + margin: 0; 25 + } 26 + 27 + /** 28 + * Render the `main` element consistently in IE. 29 + */ 30 + 31 + main { 32 + display: block; 33 + } 34 + 35 + /** 36 + * Correct the font size and margin on `h1` elements within `section` and 37 + * `article` contexts in Chrome, Firefox, and Safari. 38 + */ 39 + 40 + h1 { 41 + font-size: 2em; 42 + margin: 0.67em 0; 43 + } 44 + 45 + /* Grouping content 46 + ========================================================================== */ 47 + 48 + /** 49 + * 1. Add the correct box sizing in Firefox. 50 + * 2. Show the overflow in Edge and IE. 51 + */ 52 + 53 + hr { 54 + box-sizing: content-box; /* 1 */ 55 + height: 0; /* 1 */ 56 + overflow: visible; /* 2 */ 57 + } 58 + 59 + /** 60 + * 1. Correct the inheritance and scaling of font size in all browsers. 61 + * 2. Correct the odd `em` font sizing in all browsers. 62 + */ 63 + 64 + pre { 65 + font-family: monospace, monospace; /* 1 */ 66 + font-size: 1em; /* 2 */ 67 + } 68 + 69 + /* Text-level semantics 70 + ========================================================================== */ 71 + 72 + /** 73 + * Remove the gray background on active links in IE 10. 74 + */ 75 + 76 + a { 77 + background-color: transparent; 78 + } 79 + 80 + /** 81 + * 1. Remove the bottom border in Chrome 57- 82 + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 + */ 84 + 85 + abbr[title] { 86 + border-bottom: none; /* 1 */ 87 + text-decoration: underline; /* 2 */ 88 + text-decoration: underline dotted; /* 2 */ 89 + } 90 + 91 + /** 92 + * Add the correct font weight in Chrome, Edge, and Safari. 93 + */ 94 + 95 + b, 96 + strong { 97 + font-weight: bolder; 98 + } 99 + 100 + /** 101 + * 1. Correct the inheritance and scaling of font size in all browsers. 102 + * 2. Correct the odd `em` font sizing in all browsers. 103 + */ 104 + 105 + code, 106 + kbd, 107 + samp { 108 + font-family: monospace, monospace; /* 1 */ 109 + font-size: 1em; /* 2 */ 110 + } 111 + 112 + /** 113 + * Add the correct font size in all browsers. 114 + */ 115 + 116 + small { 117 + font-size: 80%; 118 + } 119 + 120 + /** 121 + * Prevent `sub` and `sup` elements from affecting the line height in 122 + * all browsers. 123 + */ 124 + 125 + sub, 126 + sup { 127 + font-size: 75%; 128 + line-height: 0; 129 + position: relative; 130 + vertical-align: baseline; 131 + } 132 + 133 + sub { 134 + bottom: -0.25em; 135 + } 136 + 137 + sup { 138 + top: -0.5em; 139 + } 140 + 141 + /* Embedded content 142 + ========================================================================== */ 143 + 144 + /** 145 + * Remove the border on images inside links in IE 10. 146 + */ 147 + 148 + img { 149 + border-style: none; 150 + } 151 + 152 + /* Forms 153 + ========================================================================== */ 154 + 155 + /** 156 + * 1. Change the font styles in all browsers. 157 + * 2. Remove the margin in Firefox and Safari. 158 + */ 159 + 160 + button, 161 + input, 162 + optgroup, 163 + select, 164 + textarea { 165 + font-family: inherit; /* 1 */ 166 + font-size: 100%; /* 1 */ 167 + line-height: 1.15; /* 1 */ 168 + margin: 0; /* 2 */ 169 + } 170 + 171 + /** 172 + * Show the overflow in IE. 173 + * 1. Show the overflow in Edge. 174 + */ 175 + 176 + button, 177 + input { 178 + /* 1 */ 179 + overflow: visible; 180 + } 181 + 182 + /** 183 + * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 + * 1. Remove the inheritance of text transform in Firefox. 185 + */ 186 + 187 + button, 188 + select { 189 + /* 1 */ 190 + text-transform: none; 191 + } 192 + 193 + /** 194 + * Correct the inability to style clickable types in iOS and Safari. 195 + */ 196 + 197 + button, 198 + [type='button'], 199 + [type='reset'], 200 + [type='submit'] { 201 + -webkit-appearance: button; 202 + } 203 + 204 + /** 205 + * Remove the inner border and padding in Firefox. 206 + */ 207 + 208 + button::-moz-focus-inner, 209 + [type='button']::-moz-focus-inner, 210 + [type='reset']::-moz-focus-inner, 211 + [type='submit']::-moz-focus-inner { 212 + border-style: none; 213 + padding: 0; 214 + } 215 + 216 + /** 217 + * Restore the focus styles unset by the previous rule. 218 + */ 219 + 220 + button:-moz-focusring, 221 + [type='button']:-moz-focusring, 222 + [type='reset']:-moz-focusring, 223 + [type='submit']:-moz-focusring { 224 + outline: 1px dotted ButtonText; 225 + } 226 + 227 + /** 228 + * Correct the padding in Firefox. 229 + */ 230 + 231 + fieldset { 232 + padding: 0.35em 0.75em 0.625em; 233 + } 234 + 235 + /** 236 + * 1. Correct the text wrapping in Edge and IE. 237 + * 2. Correct the color inheritance from `fieldset` elements in IE. 238 + * 3. Remove the padding so developers are not caught out when they zero out 239 + * `fieldset` elements in all browsers. 240 + */ 241 + 242 + legend { 243 + box-sizing: border-box; /* 1 */ 244 + color: inherit; /* 2 */ 245 + display: table; /* 1 */ 246 + max-width: 100%; /* 1 */ 247 + padding: 0; /* 3 */ 248 + white-space: normal; /* 1 */ 249 + } 250 + 251 + /** 252 + * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 + */ 254 + 255 + progress { 256 + vertical-align: baseline; 257 + } 258 + 259 + /** 260 + * Remove the default vertical scrollbar in IE 10+. 261 + */ 262 + 263 + textarea { 264 + overflow: auto; 265 + } 266 + 267 + /** 268 + * 1. Add the correct box sizing in IE 10. 269 + * 2. Remove the padding in IE 10. 270 + */ 271 + 272 + [type='checkbox'], 273 + [type='radio'] { 274 + box-sizing: border-box; /* 1 */ 275 + padding: 0; /* 2 */ 276 + } 277 + 278 + /** 279 + * Correct the cursor style of increment and decrement buttons in Chrome. 280 + */ 281 + 282 + [type='number']::-webkit-inner-spin-button, 283 + [type='number']::-webkit-outer-spin-button { 284 + height: auto; 285 + } 286 + 287 + /** 288 + * 1. Correct the odd appearance in Chrome and Safari. 289 + * 2. Correct the outline style in Safari. 290 + */ 291 + 292 + [type='search'] { 293 + -webkit-appearance: textfield; /* 1 */ 294 + outline-offset: -2px; /* 2 */ 295 + } 296 + 297 + /** 298 + * Remove the inner padding in Chrome and Safari on macOS. 299 + */ 300 + 301 + [type='search']::-webkit-search-decoration { 302 + -webkit-appearance: none; 303 + } 304 + 305 + /** 306 + * 1. Correct the inability to style clickable types in iOS and Safari. 307 + * 2. Change font properties to `inherit` in Safari. 308 + */ 309 + 310 + ::-webkit-file-upload-button { 311 + -webkit-appearance: button; /* 1 */ 312 + font: inherit; /* 2 */ 313 + } 314 + 315 + /* Interactive 316 + ========================================================================== */ 317 + 318 + /* 319 + * Add the correct display in Edge, IE 10+, and Firefox. 320 + */ 321 + 322 + details { 323 + display: block; 324 + } 325 + 326 + /* 327 + * Add the correct display in all browsers. 328 + */ 329 + 330 + summary { 331 + display: list-item; 332 + } 333 + 334 + /* Misc 335 + ========================================================================== */ 336 + 337 + /** 338 + * Add the correct display in IE 10+. 339 + */ 340 + 341 + template { 342 + display: none; 343 + } 344 + 345 + /** 346 + * Add the correct display in IE 10. 347 + */ 348 + 349 + [hidden] { 350 + display: none; 351 + }
+54
blacksky-static-about/privacy.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="theme-color"> 6 + <!-- 7 + This viewport works for phones with notches. 8 + It's optimized for gestures by disabling global zoom. 9 + --> 10 + <meta 11 + name="viewport" 12 + content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" 13 + /> 14 + <title>blacksky.community | privacy policy</title> 15 + 16 + <link rel="preload" as="font" type="font/woff2" href="/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin> 17 + 18 + <link rel="stylesheet" href="normalize.css" type="text/css"> 19 + <link rel="stylesheet" href="sakura.css" type="text/css"> 20 + </head> 21 + 22 + <body> 23 + 24 + 25 + <h1>blacksky.community Privacy Policy</h1> 26 + 27 + <p><em><strong>Last Updated:</strong> May 07, 2025</em></p> 28 + 29 + <p>Welcome to blacksky.community. This privacy policy explains our approach to your data when you use our application.</p> 30 + 31 + <h2>No Data Collection by blacksky.community</h2> 32 + <p><strong>The blacksky.community application itself does not collect, store, track, or share any of your personal information or user data.</strong> We do not operate servers that store your account details, posts, or activity logs.</p> 33 + <p>Our application functions solely as a client interface to interact with the underlying Bluesky social network (AT Protocol).</p> 34 + 35 + <h2>Reliance on Third-Party Services (AppView and PDS)</h2> 36 + <p>When you use blacksky.community, you connect to the Bluesky network through a specific AppView and your chosen Personal Data Server (PDS). These services are responsible for storing and managing your account information, posts, social graph, and other associated data.</p> 37 + <p><strong>Your data handling and privacy are governed by the Terms of Service and Privacy Policies of the specific AppView and PDS you use.</strong> blacksky.community simply acts on your behalf to communicate with these services based on the credentials you provide locally on your device.</p> 38 + <p>We strongly recommend you review the policies of your chosen PDS provider and the AppView you are using. For reference, the Privacy Policy for the default Bluesky service (AppView provided by <a href="https://bsky.social" target="_blank" rel="noopener noreferrer">bsky.social</a>) can be found here:</p> 39 + <p><a href="https://bsky.social/about/support/privacy-policy" target="_blank" rel="noopener noreferrer">https://bsky.social/about/support/privacy-policy</a></p> 40 + 41 + <h2>Data Stored Locally on Your Device</h2> 42 + <p>To function, blacksky.community may store your login credentials or temporary session information on your own device. This data is not accessed by or transmitted to us.</p> 43 + 44 + <h2>Cookies and Analytics</h2> 45 + <p>blacksky.community does not use cookies or any tracking analytics.</p> 46 + 47 + <h2>Changes to This Policy</h2> 48 + <p>We may update this policy. If we do, we will update the "Effective Date" at the top of this page.</p> 49 + 50 + <h2>Contact Us</h2> 51 + <p>If you have questions specifically about the functionality of the blacksky.community application itself (and not about data managed by your PDS or AppView), please email <a href="mailto:contact@forsythpeak.com" target="_blank" rel="noopener noreferrer">contact@forsythpeak.com</a>.</p> 52 + 53 + </body> 54 + </html>
+283
blacksky-static-about/sakura.css
··· 1 + /* Sakura.css v1.5.0 2 + * ================ 3 + * Minimal css theme. 4 + * Project: https://github.com/oxalorg/sakura/ 5 + */ 6 + /* Body */ 7 + html { 8 + font-size: 62.5%; 9 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 10 + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; 11 + } 12 + 13 + body { 14 + font-size: 1.8rem; 15 + line-height: 1.618; 16 + max-width: 38em; 17 + margin: auto; 18 + color: #4a4a4a; 19 + background-color: #f9f9f9; 20 + padding: 13px; 21 + } 22 + 23 + @media (max-width: 684px) { 24 + body { 25 + font-size: 1.53rem; 26 + } 27 + } 28 + @media (max-width: 382px) { 29 + body { 30 + font-size: 1.35rem; 31 + } 32 + } 33 + @font-face { 34 + font-family: 'InterVariable'; 35 + src: url(/static/media/InterVariable.c504db5c06caaf7cdfba.woff2) 36 + format('woff2'); 37 + font-weight: 300 1000; 38 + font-style: normal; 39 + font-display: swap; 40 + } 41 + @font-face { 42 + font-family: 'InterVariableItalic'; 43 + src: url(/static/media/InterVariable-Italic.01dcbad1bac635f9c9cd.woff2) 44 + format('woff2'); 45 + font-weight: 300 1000; 46 + font-style: italic; 47 + font-display: swap; 48 + } 49 + h1, 50 + h2, 51 + h3, 52 + h4, 53 + h5, 54 + h6 { 55 + line-height: 1.1; 56 + font-family: InterVariable, -apple-system, BlinkMacSystemFont, 'Segoe UI', 57 + Roboto, 'Liberation Sans', Helvetica, Arial, sans-serif; 58 + font-weight: 700; 59 + margin-top: 3rem; 60 + margin-bottom: 1.5rem; 61 + overflow-wrap: break-word; 62 + word-wrap: break-word; 63 + -ms-word-break: break-all; 64 + word-break: break-word; 65 + } 66 + 67 + h1 { 68 + font-size: 2.35em; 69 + } 70 + 71 + h2 { 72 + font-size: 2em; 73 + } 74 + 75 + h3 { 76 + font-size: 1.75em; 77 + } 78 + 79 + h4 { 80 + font-size: 1.5em; 81 + } 82 + 83 + h5 { 84 + font-size: 1.25em; 85 + } 86 + 87 + h6 { 88 + font-size: 1em; 89 + } 90 + 91 + p { 92 + margin-top: 0px; 93 + margin-bottom: 2.5rem; 94 + } 95 + 96 + small, 97 + sub, 98 + sup { 99 + font-size: 75%; 100 + } 101 + 102 + hr { 103 + border-color: #1d7484; 104 + } 105 + 106 + a { 107 + text-decoration: none; 108 + color: #1d7484; 109 + } 110 + a:visited { 111 + color: #144f5a; 112 + } 113 + a:hover { 114 + color: #982c61; 115 + border-bottom: 2px solid #4a4a4a; 116 + } 117 + 118 + ul { 119 + padding-left: 1.4em; 120 + margin-top: 0px; 121 + margin-bottom: 2.5rem; 122 + } 123 + 124 + li { 125 + margin-bottom: 0.4em; 126 + } 127 + 128 + blockquote { 129 + margin-left: 0px; 130 + margin-right: 0px; 131 + padding-left: 1em; 132 + padding-top: 0.8em; 133 + padding-bottom: 0.8em; 134 + padding-right: 0.8em; 135 + border-left: 5px solid #1d7484; 136 + margin-bottom: 2.5rem; 137 + background-color: #f1f1f1; 138 + } 139 + 140 + blockquote p { 141 + margin-bottom: 0; 142 + } 143 + 144 + img, 145 + video { 146 + height: auto; 147 + max-width: 100%; 148 + margin-top: 0px; 149 + margin-bottom: 2.5rem; 150 + } 151 + 152 + /* Pre and Code */ 153 + pre { 154 + background-color: #f1f1f1; 155 + display: block; 156 + padding: 1em; 157 + overflow-x: auto; 158 + margin-top: 0px; 159 + margin-bottom: 2.5rem; 160 + font-size: 0.9em; 161 + } 162 + 163 + code, 164 + kbd, 165 + samp { 166 + font-size: 0.9em; 167 + padding: 0 0.5em; 168 + background-color: #f1f1f1; 169 + white-space: pre-wrap; 170 + } 171 + 172 + pre > code { 173 + padding: 0; 174 + background-color: transparent; 175 + white-space: pre; 176 + font-size: 1em; 177 + } 178 + 179 + /* Tables */ 180 + table { 181 + text-align: justify; 182 + width: 100%; 183 + border-collapse: collapse; 184 + margin-bottom: 2rem; 185 + } 186 + 187 + td, 188 + th { 189 + padding: 0.5em; 190 + border-bottom: 1px solid #f1f1f1; 191 + } 192 + 193 + /* Buttons, forms and input */ 194 + input, 195 + textarea { 196 + border: 1px solid #4a4a4a; 197 + } 198 + input:focus, 199 + textarea:focus { 200 + border: 1px solid #1d7484; 201 + } 202 + 203 + textarea { 204 + width: 100%; 205 + } 206 + 207 + .button, 208 + button, 209 + input[type='submit'], 210 + input[type='reset'], 211 + input[type='button'], 212 + input[type='file']::file-selector-button { 213 + display: inline-block; 214 + padding: 5px 10px; 215 + text-align: center; 216 + text-decoration: none; 217 + white-space: nowrap; 218 + background-color: #1d7484; 219 + color: #f9f9f9; 220 + border-radius: 1px; 221 + border: 1px solid #1d7484; 222 + cursor: pointer; 223 + box-sizing: border-box; 224 + } 225 + .button[disabled], 226 + button[disabled], 227 + input[type='submit'][disabled], 228 + input[type='reset'][disabled], 229 + input[type='button'][disabled], 230 + input[type='file']::file-selector-button[disabled] { 231 + cursor: default; 232 + opacity: 0.5; 233 + } 234 + .button:hover, 235 + button:hover, 236 + input[type='submit']:hover, 237 + input[type='reset']:hover, 238 + input[type='button']:hover, 239 + input[type='file']::file-selector-button:hover { 240 + background-color: #982c61; 241 + color: #f9f9f9; 242 + outline: 0; 243 + } 244 + .button:focus-visible, 245 + button:focus-visible, 246 + input[type='submit']:focus-visible, 247 + input[type='reset']:focus-visible, 248 + input[type='button']:focus-visible, 249 + input[type='file']::file-selector-button:focus-visible { 250 + outline-style: solid; 251 + outline-width: 2px; 252 + } 253 + 254 + textarea, 255 + select, 256 + input { 257 + color: #4a4a4a; 258 + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 259 + margin-bottom: 10px; 260 + background-color: #f1f1f1; 261 + border: 1px solid #f1f1f1; 262 + border-radius: 4px; 263 + box-shadow: none; 264 + box-sizing: border-box; 265 + } 266 + textarea:focus, 267 + select:focus, 268 + input:focus { 269 + border: 1px solid #1d7484; 270 + outline: 0; 271 + } 272 + 273 + input[type='checkbox']:focus { 274 + outline: 1px dotted #1d7484; 275 + } 276 + 277 + label, 278 + legend, 279 + fieldset { 280 + display: block; 281 + margin-bottom: 0.5rem; 282 + font-weight: 600; 283 + }
+65
blacksky-static-about/tos.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="theme-color"> 6 + <!-- 7 + This viewport works for phones with notches. 8 + It's optimized for gestures by disabling global zoom. 9 + --> 10 + <meta 11 + name="viewport" 12 + content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" 13 + /> 14 + <title>blacksky.community | terms of service</title> 15 + 16 + <link rel="preload" as="font" type="font/woff2" href="/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin> 17 + 18 + <link rel="stylesheet" href="normalize.css" type="text/css"> 19 + <link rel="stylesheet" href="sakura.css" type="text/css"> 20 + </head> 21 + 22 + <body> 23 + 24 + 25 + <h1>Terms of Service for blacksky.community</h1> 26 + 27 + <p><em><strong>Last Updated:</strong> May 07, 2025</em></p> 28 + 29 + <h2>Introduction</h2> 30 + <p>Welcome to blacksky.community! These Terms of Service ("Terms") govern your use of the blacksky.community application ("App"). By accessing or using the App, you agree to be bound by these Terms.</p> 31 + 32 + <h2>Your Agreement</h2> 33 + <p>Using blacksky.community signifies your acceptance of these Terms. Please also review our <a href="/privacy">Privacy Policy</a>, which explains how we handle data related to the App itself.</p> 34 + 35 + <h2>Relationship to Other Services and Platforms</h2> 36 + <p>blacksky.community functions as a client or interface. It allows you to interact with decentralized social media protocols, instances, or servers (your "AppView" or "Personal Data Server" / "PDS").</p> 37 + <p><strong>Important:</strong> blacksky.community does not host your data or control the underlying networks or servers you connect to. Your interaction with these third-party services, including the content you post and view, is governed by the Terms of Service, acceptable use policies, and privacy policies of the specific AppView and/or PDS you choose to use.</p> 38 + <p>You are responsible for understanding and complying with the terms of those external services.</p> 39 + 40 + <p>We strongly recommend you review the policies of your chosen PDS provider and the AppView you are using. For reference, the Terms of Service for the default Bluesky service (AppView provided by <a href="https://bsky.social" target="_blank" rel="noopener noreferrer">bsky.social</a>) can be found here: </p> 41 + <p><a href="https://bsky.social/about/support/tos" target="_blank" rel="noopener noreferrer">https://bsky.social/about/support/tos</a></p> 42 + 43 + <h2>User Conduct</h2> 44 + <p>Your conduct while using blacksky.community must comply with the terms and rules set forth by the AppView and PDS you are connected to. blacksky.community is merely a tool to access these services, and Forsyth Peak LLC is not responsible for enforcing the rules of those platforms or for the content accessed through them.</p> 45 + 46 + <h2>Disclaimer of Warranty</h2> 47 + <p>The App is provided "AS IS" and "AS AVAILABLE," without warranty of any kind, express or implied. Forsyth Peak LLC does not warrant that the App will meet your requirements, be available uninterrupted, secure, or error-free.</p> 48 + 49 + <h2>Limitation of Liability</h2> 50 + <p>To the fullest extent permitted by applicable law, Forsyth Peak LLC shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses, resulting from:</p> 51 + <ul> 52 + <li>Your access to or use of or inability to access or use the App;</li> 53 + <li>Any conduct or content of any third party on or through the App or the services it connects to (including your AppView or PDS);</li> 54 + <li>Unauthorized access, use, or alteration of your transmissions or content via the underlying services (AppView/PDS).</li> 55 + </ul> 56 + <p>In no event shall the aggregate liability of Forsyth Peak LLC exceed the greater of zero U.S. dollars ($0.00).</p> 57 + 58 + <h2>Changes to These Terms</h2> 59 + <p>We may revise these Terms from time to time. The most current version will always be available within the App or on its associated website. By continuing to access or use the App after revisions become effective, you agree to be bound by the revised Terms.</p> 60 + 61 + <h2>Contact Information</h2> 62 + <p>If you have any questions about these Terms of Service, please email <a href="mailto:contact@forsythpeak.com" target="_blank" rel="noopener noreferrer">contact@forsythpeak.com</a>.</p> 63 + 64 + </body> 65 + </html>
+35
default.nix
··· 1 + # this doesn't work lol. sad! 2 + { 3 + lib, 4 + stdenv, 5 + fetchYarnDeps, 6 + yarnConfigHook, 7 + yarnBuildHook, 8 + yarnInstallHook, 9 + nodejs, 10 + }: 11 + let 12 + package_json = lib.importJSON ./package.json; 13 + in 14 + stdenv.mkDerivation (finalAttrs: { 15 + pname = package_json.name; 16 + version = package_json.version; 17 + 18 + src = ./.; 19 + 20 + yarnOfflineCache = fetchYarnDeps { 21 + yarnLock = finalAttrs.src + "/yarn.lock"; 22 + hash = "sha256-nuUPWMN6FKFoHOpI/nbM9Uw3Ng6BKcjXaQ38LBAzN1A="; 23 + }; 24 + 25 + nativeBuildInputs = [ 26 + yarnConfigHook 27 + yarnBuildHook 28 + yarnInstallHook 29 + nodejs 30 + ]; 31 + 32 + meta = { 33 + # ... 34 + }; 35 + })
+6
eas.json
··· 86 86 "env": { 87 87 "EXPO_PUBLIC_ENV": "testflight" 88 88 } 89 + }, 90 + "sideload-android": { 91 + "extends": "production", 92 + "android": { 93 + "buildType": "apk" 94 + } 89 95 } 90 96 }, 91 97 "submit": {
+209
flake.lock
··· 1 + { 2 + "nodes": { 3 + "android-nixpkgs": { 4 + "inputs": { 5 + "devshell": "devshell", 6 + "flake-utils": "flake-utils", 7 + "nixpkgs": "nixpkgs" 8 + }, 9 + "locked": { 10 + "lastModified": 1745266877, 11 + "narHash": "sha256-MOEwZIhu3EXSBrtHML04kbb0gII6irVf9YJEMy8MGsU=", 12 + "owner": "tadfisher", 13 + "repo": "android-nixpkgs", 14 + "rev": "3e3987cb51c9ffbd38e62dba0f793940b55a9c07", 15 + "type": "github" 16 + }, 17 + "original": { 18 + "owner": "tadfisher", 19 + "repo": "android-nixpkgs", 20 + "type": "github" 21 + } 22 + }, 23 + "devshell": { 24 + "inputs": { 25 + "nixpkgs": [ 26 + "android-nixpkgs", 27 + "nixpkgs" 28 + ] 29 + }, 30 + "locked": { 31 + "lastModified": 1741473158, 32 + "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", 33 + "owner": "numtide", 34 + "repo": "devshell", 35 + "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", 36 + "type": "github" 37 + }, 38 + "original": { 39 + "owner": "numtide", 40 + "repo": "devshell", 41 + "type": "github" 42 + } 43 + }, 44 + "flake-parts": { 45 + "inputs": { 46 + "nixpkgs-lib": [ 47 + "wrangler-flake", 48 + "nixpkgs" 49 + ] 50 + }, 51 + "locked": { 52 + "lastModified": 1743550720, 53 + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", 54 + "owner": "hercules-ci", 55 + "repo": "flake-parts", 56 + "rev": "c621e8422220273271f52058f618c94e405bb0f5", 57 + "type": "github" 58 + }, 59 + "original": { 60 + "owner": "hercules-ci", 61 + "repo": "flake-parts", 62 + "type": "github" 63 + } 64 + }, 65 + "flake-utils": { 66 + "inputs": { 67 + "systems": "systems" 68 + }, 69 + "locked": { 70 + "lastModified": 1731533236, 71 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 72 + "owner": "numtide", 73 + "repo": "flake-utils", 74 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 75 + "type": "github" 76 + }, 77 + "original": { 78 + "owner": "numtide", 79 + "repo": "flake-utils", 80 + "type": "github" 81 + } 82 + }, 83 + "flake-utils_2": { 84 + "inputs": { 85 + "systems": "systems_2" 86 + }, 87 + "locked": { 88 + "lastModified": 1731533236, 89 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 90 + "owner": "numtide", 91 + "repo": "flake-utils", 92 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 93 + "type": "github" 94 + }, 95 + "original": { 96 + "owner": "numtide", 97 + "repo": "flake-utils", 98 + "type": "github" 99 + } 100 + }, 101 + "nixpkgs": { 102 + "locked": { 103 + "lastModified": 1744932701, 104 + "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=", 105 + "owner": "NixOS", 106 + "repo": "nixpkgs", 107 + "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef", 108 + "type": "github" 109 + }, 110 + "original": { 111 + "owner": "NixOS", 112 + "ref": "nixos-unstable", 113 + "repo": "nixpkgs", 114 + "type": "github" 115 + } 116 + }, 117 + "nixpkgs_2": { 118 + "locked": { 119 + "lastModified": 1743583204, 120 + "narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=", 121 + "owner": "NixOS", 122 + "repo": "nixpkgs", 123 + "rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434", 124 + "type": "github" 125 + }, 126 + "original": { 127 + "owner": "NixOS", 128 + "ref": "nixos-unstable", 129 + "repo": "nixpkgs", 130 + "type": "github" 131 + } 132 + }, 133 + "nixpkgs_3": { 134 + "locked": { 135 + "lastModified": 1744904290, 136 + "narHash": "sha256-ewg0m4mGwl3iO4aN73yZoT8lCgEHtapP3d/trfUE6To=", 137 + "owner": "nixos", 138 + "repo": "nixpkgs", 139 + "rev": "e03df76c3a8ac2119f45ff18c9b994513dbb7a4c", 140 + "type": "github" 141 + }, 142 + "original": { 143 + "owner": "nixos", 144 + "repo": "nixpkgs", 145 + "rev": "e03df76c3a8ac2119f45ff18c9b994513dbb7a4c", 146 + "type": "github" 147 + } 148 + }, 149 + "root": { 150 + "inputs": { 151 + "android-nixpkgs": "android-nixpkgs", 152 + "flake-utils": "flake-utils_2", 153 + "nixpkgs": "nixpkgs_2", 154 + "wrangler-flake": "wrangler-flake" 155 + } 156 + }, 157 + "systems": { 158 + "locked": { 159 + "lastModified": 1681028828, 160 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 161 + "owner": "nix-systems", 162 + "repo": "default", 163 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 164 + "type": "github" 165 + }, 166 + "original": { 167 + "owner": "nix-systems", 168 + "repo": "default", 169 + "type": "github" 170 + } 171 + }, 172 + "systems_2": { 173 + "locked": { 174 + "lastModified": 1681028828, 175 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 176 + "owner": "nix-systems", 177 + "repo": "default", 178 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 179 + "type": "github" 180 + }, 181 + "original": { 182 + "owner": "nix-systems", 183 + "repo": "default", 184 + "type": "github" 185 + } 186 + }, 187 + "wrangler-flake": { 188 + "inputs": { 189 + "flake-parts": "flake-parts", 190 + "nixpkgs": "nixpkgs_3" 191 + }, 192 + "locked": { 193 + "lastModified": 1745836852, 194 + "narHash": "sha256-4rlqhVU89ypXQTWpJchMdocHNSZBVTUehiNWnAy0zJ0=", 195 + "owner": "ryand56", 196 + "repo": "wrangler", 197 + "rev": "070db974683ef1f8e95dadef549f223381ee8544", 198 + "type": "github" 199 + }, 200 + "original": { 201 + "owner": "ryand56", 202 + "repo": "wrangler", 203 + "type": "github" 204 + } 205 + } 206 + }, 207 + "root": "root", 208 + "version": 7 209 + }
+96
flake.nix
··· 1 + { 2 + inputs = { 3 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 + flake-utils.url = "github:numtide/flake-utils"; 5 + android-nixpkgs.url = "github:tadfisher/android-nixpkgs"; 6 + wrangler-flake.url = "github:ryand56/wrangler"; 7 + }; 8 + 9 + outputs = 10 + { 11 + nixpkgs, 12 + flake-utils, 13 + wrangler-flake, 14 + android-nixpkgs, 15 + ... 16 + }: 17 + flake-utils.lib.eachDefaultSystem ( 18 + system: 19 + let 20 + pkgs = import nixpkgs { 21 + inherit system; 22 + config = { 23 + android_sdk.accept_license = true; 24 + allowUnfree = true; 25 + }; 26 + }; 27 + pinnedJDK = pkgs.jdk17; 28 + androidSdk = android-nixpkgs.sdk.${system} ( 29 + sdkPkgs: 30 + with sdkPkgs; 31 + [ 32 + cmdline-tools-latest 33 + build-tools-35-0-0 34 + build-tools-34-0-0 35 + platform-tools 36 + platforms-android-35 37 + emulator 38 + cmake-3-22-1 39 + ndk-26-1-10909125 40 + ndk-28-0-13004108 41 + ] 42 + ++ nixpkgs.lib.optionals (system == "aarch64-darwin") [ 43 + system-images-android-35-google-apis-arm64-v8a 44 + system-images-android-35-google-apis-playstore-arm64-v8a 45 + ] 46 + ++ nixpkgs.lib.optionals (system == "x86_64-darwin" || system == "x86_64-linux") [ 47 + system-images-android-35-google-apis-x86-64 48 + system-images-android-35-google-apis-playstore-x86-64 49 + ] 50 + ); 51 + in 52 + with pkgs; 53 + { 54 + packages = { 55 + default = callPackage ./default.nix { }; 56 + }; 57 + devShells = { 58 + default = mkShell rec { 59 + buildInputs = [ 60 + androidSdk 61 + pinnedJDK 62 + ]; 63 + 64 + JAVA_HOME = pinnedJDK; 65 + ANDROID_HOME = "${androidSdk}/share/android-sdk"; 66 + ANDROID_SDK_ROOT = "${androidSdk}/share/android-sdk"; 67 + 68 + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/35.0.0/aapt2"; 69 + 70 + packages = [ 71 + just 72 + fastmod 73 + nodejs 74 + yarn 75 + crowdin-cli 76 + eas-cli 77 + 78 + bundletool 79 + 80 + typescript 81 + typescript-language-server 82 + 83 + go 84 + gopls 85 + 86 + wrangler-flake.packages.${system}.wrangler 87 + ]; 88 + 89 + shellHook = '' 90 + export GRADLE_USER_HOME=~/.cache/gradle 91 + ''; 92 + }; 93 + }; 94 + } 95 + ); 96 + }
+157
functions/profile/[handleOrDID].ts
··· 1 + import {AtpAgent} from '@atproto/api' 2 + 3 + import {type AnyProfileView} from '#/types/bsky/profile' 4 + 5 + type PResp = Awaited<ReturnType<AtpAgent['getProfile']>> 6 + 7 + // based on https://github.com/Janpot/escape-html-template-tag/blob/master/src/index.ts 8 + 9 + const ENTITIES: { 10 + [key: string]: string 11 + } = { 12 + '&': '&amp;', 13 + '<': '&lt;', 14 + '>': '&gt;', 15 + '"': '&quot;', 16 + "'": '&#39;', 17 + '/': '&#x2F;', 18 + '`': '&#x60;', 19 + '=': '&#x3D;', 20 + } 21 + 22 + const ENT_REGEX = new RegExp(Object.keys(ENTITIES).join('|'), 'g') 23 + 24 + function escapehtml(unsafe: Sub): string { 25 + if (Array.isArray(unsafe)) { 26 + return unsafe.map(escapehtml).join('') 27 + } 28 + if (unsafe instanceof HtmlSafeString) { 29 + return unsafe.toString() 30 + } 31 + return String(unsafe).replace(ENT_REGEX, char => ENTITIES[char]) 32 + } 33 + 34 + type Sub = HtmlSafeString | string | (HtmlSafeString | string)[] 35 + 36 + export class HtmlSafeString { 37 + private _parts: readonly string[] 38 + private _subs: readonly Sub[] 39 + constructor(parts: readonly string[], subs: readonly Sub[]) { 40 + this._parts = parts 41 + this._subs = subs 42 + } 43 + 44 + toString(): string { 45 + let result = this._parts[0] 46 + for (let i = 1; i < this._parts.length; i++) { 47 + result += escapehtml(this._subs[i - 1]) + this._parts[i] 48 + } 49 + return result 50 + } 51 + } 52 + 53 + export function html(parts: TemplateStringsArray, ...subs: Sub[]) { 54 + return new HtmlSafeString(parts, subs) 55 + } 56 + 57 + export const renderHandleString = (profile: AnyProfileView) => 58 + profile.displayName 59 + ? `${profile.displayName} (@${profile.handle})` 60 + : `@${profile.handle}` 61 + 62 + class HeadHandler { 63 + profile: PResp 64 + url: string 65 + constructor(profile: PResp, url: string) { 66 + this.profile = profile 67 + this.url = url 68 + } 69 + async element(element) { 70 + const view = this.profile.data 71 + 72 + const description = view.description 73 + ? html` 74 + <meta name="description" content="${view.description}" /> 75 + <meta property="og:description" content="${view.description}" /> 76 + ` 77 + : '' 78 + const img = view.banner 79 + ? html` 80 + <meta property="og:image" content="${view.banner}" /> 81 + <meta name="twitter:card" content="summary_large_image" /> 82 + ` 83 + : view.avatar 84 + ? html`<meta name="twitter:card" content="summary" />` 85 + : '' 86 + element.append( 87 + html` 88 + <meta property="og:site_name" content="blacksky.community" /> 89 + <meta property="og:type" content="profile" /> 90 + <meta property="profile:username" content="${view.handle}" /> 91 + <meta property="og:url" content="${this.url}" /> 92 + <meta property="og:title" content="${renderHandleString(view)}" /> 93 + ${description} ${img} 94 + <meta name="twitter:label1" content="Account DID" /> 95 + <meta name="twitter:value1" content="${view.did}" /> 96 + <link 97 + rel="alternate" 98 + href="at://${view.did}/app.bsky.actor.profile/self" /> 99 + `, 100 + {html: true}, 101 + ) 102 + } 103 + } 104 + 105 + class TitleHandler { 106 + profile: PResp 107 + constructor(profile: PResp) { 108 + this.profile = profile 109 + } 110 + async element(element) { 111 + element.setInnerContent(renderHandleString(this.profile.data)) 112 + } 113 + } 114 + 115 + class NoscriptHandler { 116 + profile: PResp 117 + constructor(profile: PResp) { 118 + this.profile = profile 119 + } 120 + async element(element) { 121 + const view = this.profile.data 122 + 123 + element.append( 124 + html` 125 + <div id="bsky_profile_summary"> 126 + <h3>Profile</h3> 127 + <p id="bsky_display_name">${view.displayName ?? ''}</p> 128 + <p id="bsky_handle">${view.handle}</p> 129 + <p id="bsky_did">${view.did}</p> 130 + <p id="bsky_profile_description">${view.description ?? ''}</p> 131 + </div> 132 + `, 133 + {html: true}, 134 + ) 135 + } 136 + } 137 + 138 + export async function onRequest(context) { 139 + const agent = new AtpAgent({service: 'https://public.api.bsky.app/'}) 140 + const {request, env} = context 141 + const origin = new URL(request.url).origin 142 + 143 + const base = env.ASSETS.fetch(new URL('/', origin)) 144 + try { 145 + const profile = await agent.getProfile({ 146 + actor: context.params.handleOrDID, 147 + }) 148 + return new HTMLRewriter() 149 + .on(`head`, new HeadHandler(profile, request.url)) 150 + .on(`title`, new TitleHandler(profile)) 151 + .on(`noscript`, new NoscriptHandler(profile)) 152 + .transform(await base) 153 + } catch (e) { 154 + console.error(e) 155 + return await base 156 + } 157 + }
+227
functions/profile/[handleOrDID]/post/[rkey].ts
··· 1 + import { 2 + AppBskyEmbedExternal, 3 + AppBskyEmbedImages, 4 + AppBskyEmbedRecord, 5 + AppBskyEmbedRecordWithMedia, 6 + AppBskyFeedDefs, 7 + AtpAgent, 8 + type Facet, 9 + RichText, 10 + } from '@atproto/api' 11 + import {isViewRecord} from '@atproto/api/dist/client/types/app/bsky/embed/record' 12 + import {isThreadViewPost} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 13 + 14 + import {html, renderHandleString} from '../../[handleOrDID].ts' 15 + 16 + type Thread = AppBskyFeedDefs.ThreadViewPost 17 + 18 + export function expandPostTextRich( 19 + postView: AppBskyFeedDefs.ThreadViewPost, 20 + ): string { 21 + if ( 22 + !postView.post || 23 + AppBskyFeedDefs.isNotFoundPost(postView) || 24 + AppBskyFeedDefs.isBlockedPost(postView) 25 + ) { 26 + return '' 27 + } 28 + 29 + const post = postView.post 30 + const record = post.record 31 + const embed = post.embed 32 + const originalText = typeof record?.text === 'string' ? record.text : '' 33 + const facets = record?.facets as [Facet] | undefined 34 + 35 + let expandedText = originalText 36 + 37 + // Use RichText to process facets if they exist 38 + if (originalText && facets && facets.length > 0) { 39 + try { 40 + const rt = new RichText({text: originalText, facets}) 41 + const modifiedSegmentsText: string[] = [] 42 + 43 + for (const segment of rt.segments()) { 44 + const link = segment.link 45 + if ( 46 + link && 47 + segment.text.endsWith('...') && 48 + link.uri.includes(segment.text.slice(0, -3)) 49 + ) { 50 + // Replace shortened text with full URI 51 + modifiedSegmentsText.push(link.uri) 52 + } else { 53 + // Keep original segment text 54 + modifiedSegmentsText.push(segment.text) 55 + } 56 + } 57 + expandedText = modifiedSegmentsText.join('') 58 + } catch (error) { 59 + console.error('Error processing RichText segments:', error) 60 + // Fallback to original text on error 61 + expandedText = originalText 62 + } 63 + } 64 + 65 + // Append external link URL if present and not already in text 66 + if (AppBskyEmbedExternal.isView(embed) && embed.external?.uri) { 67 + const externalUri = embed.external.uri 68 + if (!expandedText.includes(externalUri)) { 69 + expandedText = expandedText 70 + ? `${expandedText}\n${externalUri}` 71 + : externalUri 72 + } 73 + } 74 + 75 + // Append placeholder for quote posts or other record embeds 76 + if ( 77 + AppBskyEmbedRecord.isView(embed) || 78 + AppBskyEmbedRecordWithMedia.isView(embed) 79 + ) { 80 + // no idea why this is needed lol 81 + const record = embed.record.record ?? embed.record 82 + if (isViewRecord(record)) { 83 + const quote = `↘️ quoting ${renderHandleString(record.author)}:\n\n${ 84 + record.value.text 85 + }` 86 + expandedText = expandedText ? `${expandedText}\n\n${quote}` : quote 87 + } else { 88 + const placeholder = '[quote/embed]' 89 + if (!expandedText.includes(placeholder)) { 90 + expandedText = expandedText 91 + ? `${expandedText}\n\n${placeholder}` 92 + : placeholder 93 + } 94 + } 95 + } 96 + 97 + // prepend reply header 98 + if (isThreadViewPost(postView.parent)) { 99 + const header = `↩️ reply to ${renderHandleString( 100 + postView.parent.post.author, 101 + )}:` 102 + expandedText = expandedText ? `${header}\n\n${expandedText}` : header 103 + } 104 + 105 + return expandedText 106 + } 107 + 108 + class HeadHandler { 109 + thread: Thread 110 + url: string 111 + postTextString: string 112 + constructor(thread: Thread, url: string, postTextString: string) { 113 + this.thread = thread 114 + this.url = url 115 + this.postTextString = postTextString 116 + } 117 + async element(element) { 118 + const author = this.thread.post.author 119 + 120 + const postText = 121 + this.postTextString.length > 0 122 + ? html` 123 + <meta name="description" content="${this.postTextString}" /> 124 + <meta property="og:description" content="${this.postTextString}" /> 125 + ` 126 + : '' 127 + 128 + const embed = this.thread.post.embed 129 + 130 + const embedElems = !embed 131 + ? '' 132 + : AppBskyEmbedImages.isView(embed) 133 + ? html`${embed.images.map( 134 + i => html`<meta property="og:image" content="${i.thumb}" />`, 135 + )} 136 + <meta name="twitter:card" content="summary_large_image" /> ` 137 + : // TODO: in the future, embed videos 138 + 'thumbnail' in embed && embed.thumbnail 139 + ? html` 140 + <meta property="og:image" content="${embed.thumbnail}" /> 141 + <meta name="twitter:card" content="summary_large_image" /> 142 + ` 143 + : html`<meta name="twitter:card" content="summary" />` 144 + 145 + element.append( 146 + html` 147 + <meta property="og:site_name" content="blacksky.community" /> 148 + <meta property="og:type" content="article" /> 149 + <meta property="profile:username" content="${author.handle}" /> 150 + <meta property="og:url" content="${this.url}" /> 151 + <meta property="og:title" content="${renderHandleString(author)}" /> 152 + ${postText} ${embedElems} 153 + <meta name="twitter:label1" content="Account DID" /> 154 + <meta name="twitter:value1" content="${author.did}" /> 155 + <meta 156 + name="article:published_time" 157 + content="${this.thread.post.indexedAt}" /> 158 + `, 159 + {html: true}, 160 + ) 161 + } 162 + } 163 + 164 + class TitleHandler { 165 + thread: Thread 166 + constructor(thread: Thread) { 167 + this.thread = thread 168 + } 169 + async element(element) { 170 + element.setInnerContent(renderHandleString(this.thread.post.author)) 171 + } 172 + } 173 + 174 + class NoscriptHandler { 175 + thread: Thread 176 + postTextString: string 177 + constructor(thread: Thread, postTextString: string) { 178 + this.thread = thread 179 + this.postTextString = postTextString 180 + } 181 + async element(element) { 182 + element.append( 183 + html` 184 + <div id="bsky_post_summary"> 185 + <h3>Post</h3> 186 + <p id="bsky_display_name"> 187 + ${this.thread.post.author.displayName ?? ''} 188 + </p> 189 + <p id="bsky_handle">${this.thread.post.author.handle}</p> 190 + <p id="bsky_did">${this.thread.post.author.did}</p> 191 + <p id="bsky_post_text">${this.postTextString}</p> 192 + <p id="bsky_post_indexedat">${this.thread.post.indexedAt}</p> 193 + </div> 194 + `, 195 + {html: true}, 196 + ) 197 + } 198 + } 199 + 200 + export async function onRequest(context) { 201 + const agent = new AtpAgent({service: 'https://public.api.bsky.app/'}) 202 + const {request, env} = context 203 + const origin = new URL(request.url).origin 204 + const {handleOrDID, rkey}: {handleOrDID: string; rkey: string} = 205 + context.params 206 + 207 + const base = env.ASSETS.fetch(new URL('/', origin)) 208 + try { 209 + const {data} = await agent.getPostThread({ 210 + uri: `at://${handleOrDID}/app.bsky.feed.post/${rkey}`, 211 + depth: 1, 212 + parentHeight: 1, 213 + }) 214 + if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) { 215 + throw new Error('Expected a ThreadViewPost') 216 + } 217 + const postTextString = expandPostTextRich(data.thread) 218 + return new HTMLRewriter() 219 + .on(`head`, new HeadHandler(data.thread, request.url, postTextString)) 220 + .on(`title`, new TitleHandler(data.thread)) 221 + .on(`noscript`, new NoscriptHandler(data.thread, postTextString)) 222 + .transform(await base) 223 + } catch (e) { 224 + console.error(e) 225 + return await base 226 + } 227 + }
+1 -1
google-services.json.example
··· 9 9 "client_info": { 10 10 "mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063", 11 11 "android_client_info": { 12 - "package_name": "xyz.blueskyweb.app" 12 + "package_name": "community.blacksky" 13 13 } 14 14 }, 15 15 "oauth_client": [
+54
justfile
··· 1 + export PATH := "./node_modules/.bin:" + env_var('PATH') 2 + 3 + # lots of just -> yarn, but this lets us chain yarn command deps 4 + 5 + [group('dist')] 6 + dist-build-web: intl build-web 7 + 8 + [group('dist')] 9 + dist-build-android-sideload: intl build-android-sideload 10 + 11 + [group('build')] 12 + intl: 13 + yarn intl:build 14 + 15 + [group('build')] 16 + prebuild-android: 17 + expo prebuild -p android 18 + 19 + [group('build')] 20 + build-web: && postbuild-web 21 + yarn build-web 22 + 23 + [group('build')] 24 + build-android-sideload: prebuild-android 25 + eas build --local --platform android --profile sideload-android 26 + 27 + [group('build')] 28 + postbuild-web: 29 + # build system outputs some srcs and hrefs like src="static/" 30 + # need to rewrite to be src="/static/" to handle non root pages 31 + sed -i 's/\(src\|href\)="static/\1="\/static/g' web-build/index.html 32 + 33 + # we need to copy the static iframe html to support youtube embeds 34 + cp -r bskyweb/static/iframe/ web-build/iframe 35 + 36 + # copy our static pages over! 37 + cp -r blacksky-static-about web-build/about 38 + 39 + [group('dev')] 40 + dev-android-setup: prebuild-android 41 + yarn android 42 + 43 + [group('dev')] 44 + dev-web: 45 + yarn web 46 + 47 + [group('dev')] 48 + dev-web-functions: build-web 49 + wrangler pages dev ./web-build 50 + 51 + [group('lint')] 52 + typecheck: 53 + yarn typecheck 54 +
+222
logo_v0.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + width="512" 6 + height="512" 7 + viewBox="0 0 512 512" 8 + version="1.1" 9 + id="svg1" 10 + sodipodi:docname="drawing.svg" 11 + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" 12 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 13 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 14 + xmlns="http://www.w3.org/2000/svg" 15 + xmlns:svg="http://www.w3.org/2000/svg"> 16 + <sodipodi:namedview 17 + id="namedview1" 18 + pagecolor="#ffffff" 19 + bordercolor="#000000" 20 + borderopacity="0.25" 21 + inkscape:showpageshadow="2" 22 + inkscape:pageopacity="0.0" 23 + inkscape:pagecheckerboard="0" 24 + inkscape:deskcolor="#d1d1d1" 25 + inkscape:document-units="px" 26 + showgrid="false" 27 + showguides="false" 28 + inkscape:zoom="0.67914582" 29 + inkscape:cx="265.03881" 30 + inkscape:cy="314.36548" 31 + inkscape:window-width="1706" 32 + inkscape:window-height="905" 33 + inkscape:window-x="20" 34 + inkscape:window-y="20" 35 + inkscape:window-maximized="0" 36 + inkscape:current-layer="svg1"> 37 + <inkscape:grid 38 + id="grid1" 39 + units="px" 40 + originx="0" 41 + originy="0" 42 + spacingx="1" 43 + spacingy="1" 44 + empcolor="#0099e5" 45 + empopacity="0.30196078" 46 + color="#0099e5" 47 + opacity="0.14901961" 48 + empspacing="5" 49 + enabled="true" 50 + visible="false" /> 51 + <sodipodi:guide 52 + position="7.2092363,499.98461" 53 + orientation="0.73456475,0.6785386" 54 + id="guide15" 55 + inkscape:locked="false" /> 56 + <sodipodi:guide 57 + position="250,12" 58 + orientation="0,-1" 59 + id="guide16" 60 + inkscape:locked="false" /> 61 + <sodipodi:guide 62 + position="7.2114897,499.98617" 63 + orientation="0.90391787,0.42770606" 64 + id="guide17" 65 + inkscape:locked="false" /> 66 + <sodipodi:guide 67 + position="114.38555,198.20474" 68 + orientation="0.90391787,0.42770608" 69 + id="guide18" 70 + inkscape:label="" 71 + inkscape:locked="false" 72 + inkscape:color="rgb(0,134,229)" /> 73 + <sodipodi:guide 74 + position="372.00185,356.22842" 75 + orientation="0.73454936,-0.67855525" 76 + id="guide19" 77 + inkscape:locked="false" /> 78 + </sodipodi:namedview> 79 + <defs 80 + id="defs1"> 81 + <filter 82 + id="selectable_hidder_filter" 83 + width="1" 84 + height="1" 85 + x="0" 86 + y="0" 87 + style="color-interpolation-filters:sRGB;" 88 + inkscape:label="LPE boolean visibility"> 89 + <feComposite 90 + id="boolops_hidder_primitive" 91 + result="composite1" 92 + operator="arithmetic" 93 + in2="SourceGraphic" 94 + in="BackgroundImage" /> 95 + </filter> 96 + <inkscape:path-effect 97 + effect="bool_op" 98 + operand-path="" 99 + id="path-effect16" 100 + is_visible="true" 101 + lpeversion="1" 102 + operation="cut" 103 + swap-operands="false" 104 + filltype-this="from-curve" 105 + filter="" 106 + filltype-operand="from-curve" /> 107 + <inkscape:path-effect 108 + effect="mirror_symmetry" 109 + start_point="153.53846,-190.49753" 110 + end_point="529.50144,157.05943" 111 + center_point="341.51995,-16.71905" 112 + id="path-effect15" 113 + is_visible="true" 114 + lpeversion="1.2" 115 + lpesatellites="" 116 + mode="vertical" 117 + discard_orig_path="false" 118 + fuse_paths="false" 119 + oposite_fuse="false" 120 + split_items="false" 121 + split_open="false" 122 + link_styles="false" /> 123 + <inkscape:path-effect 124 + effect="mirror_symmetry" 125 + start_point="242.4064,179.64357" 126 + end_point="242.4064,478.25704" 127 + center_point="242.4064,328.95031" 128 + id="path-effect14" 129 + is_visible="true" 130 + lpeversion="1.2" 131 + lpesatellites="" 132 + mode="vertical" 133 + discard_orig_path="false" 134 + fuse_paths="true" 135 + oposite_fuse="false" 136 + split_items="false" 137 + split_open="false" 138 + link_styles="false" /> 139 + <inkscape:path-effect 140 + effect="mirror_symmetry" 141 + start_point="238.18975,-35.620493" 142 + end_point="238.18975,476.37951" 143 + center_point="238.18975,220.37951" 144 + id="path-effect13" 145 + is_visible="true" 146 + lpeversion="1.2" 147 + lpesatellites="" 148 + mode="vertical" 149 + discard_orig_path="false" 150 + fuse_paths="false" 151 + oposite_fuse="false" 152 + split_items="false" 153 + split_open="false" 154 + link_styles="false" /> 155 + <inkscape:path-effect 156 + effect="mirror_symmetry" 157 + start_point="259.66682,-1.5714927" 158 + end_point="259.66682,510.42851" 159 + center_point="259.66682,254.42851" 160 + id="path-effect12" 161 + is_visible="true" 162 + lpeversion="1.2" 163 + lpesatellites="" 164 + mode="vertical" 165 + discard_orig_path="false" 166 + fuse_paths="true" 167 + oposite_fuse="false" 168 + split_items="false" 169 + split_open="false" 170 + link_styles="false" /> 171 + <inkscape:path-effect 172 + effect="clone_original" 173 + css_properties="" 174 + attributes="style,clip-path,mask" 175 + linkeditem="" 176 + is_visible="true" 177 + method="d" 178 + allow_transforms="true" 179 + id="path-effect11" 180 + lpeversion="1" /> 181 + <inkscape:path-effect 182 + effect="mirror_symmetry" 183 + start_point="274.33408,-38.239647" 184 + end_point="274.33408,473.76035" 185 + center_point="274.33408,217.76035" 186 + id="path-effect10" 187 + is_visible="true" 188 + lpeversion="1.2" 189 + lpesatellites="" 190 + mode="vertical" 191 + discard_orig_path="false" 192 + fuse_paths="false" 193 + oposite_fuse="false" 194 + split_items="false" 195 + split_open="false" 196 + link_styles="false" /> 197 + <inkscape:path-effect 198 + effect="mirror_symmetry" 199 + start_point="241.42806,-153.19556" 200 + end_point="241.42806,358.80444" 201 + center_point="241.42806,102.80444" 202 + id="path-effect9" 203 + is_visible="true" 204 + lpeversion="1.2" 205 + lpesatellites="" 206 + mode="vertical" 207 + discard_orig_path="false" 208 + fuse_paths="false" 209 + oposite_fuse="false" 210 + split_items="false" 211 + split_open="false" 212 + link_styles="false" /> 213 + </defs> 214 + <path 215 + style="fill:#000000" 216 + d="m 149.96484,186.56641 46.09766,152.95898 c 0,0 -6.30222,-9.61174 -15.60547,-17.47656 -8.87322,-7.50128 -28.4082,-4.04492 -28.4082,-4.04492 0,0 6.14721,39.88867 15.53125,44.39843 10.71251,5.1482 22.19726,0.16993 22.19726,0.16993 0,0 11.7613,-4.87282 22.82032,31.82421 5.26534,17.47196 15.33258,50.877 20.9707,69.58594 2.16717,7.1913 8.83789,7.25781 8.83789,7.25781 0,0 6.67072,-0.0665 8.83789,-7.25781 5.63812,-18.70894 15.70536,-52.11398 20.9707,-69.58594 11.05902,-36.69703 22.82032,-31.82421 22.82032,-31.82421 0,0 11.48475,4.97827 22.19726,-0.16993 9.38404,-4.50976 15.5332,-44.39843 15.5332,-44.39843 0,0 -19.53693,-3.45636 -28.41015,4.04492 -9.30325,7.86482 -15.60547,17.47656 -15.60547,17.47656 l 46.09766,-152.95898 -49.32618,83.84179 -20.34375,-31.1914 6.35547,54.96875 -23.1582,39.36132 c 0,0 -2.97595,5.06226 -5.94336,4.68946 -0.009,-0.001 -0.0169,0.003 -0.0254,0.01 -0.008,-0.007 -0.0167,-0.0109 -0.0254,-0.01 -2.96741,0.3728 -5.94336,-4.68946 -5.94336,-4.68946 l -23.1582,-39.36132 6.35547,-54.96875 -20.34375,31.1914 z" 217 + id="path13" 218 + transform="matrix(2.6921023,0,0,1.7145911,-396.58283,-308.01527)" 219 + sodipodi:nodetypes="scsccccccscscss" 220 + inkscape:original-d="m 233.56872,463.9817 c 2.16717,7.1913 8.83768,7.25821 8.83768,7.25821 0,0 1.53461,-133.19881 -0.0257,-133.00279 -2.96741,0.3728 -5.94331,-4.69028 -5.94331,-4.69028 l -23.15733,-39.3618 6.35509,-54.96755 -20.34345,31.19075 -49.32605,-83.84223 46.09711,152.95859 c 0,0 -6.30275,-9.61063 -15.606,-17.47545 -8.87322,-7.50128 -28.40888,-4.04487 -28.40888,-4.04487 0,0 6.14833,39.88845 15.53237,44.39821 10.71251,5.1482 22.19654,0.17066 22.19654,0.17066 0,0 11.76274,-4.87365 22.82175,31.82338 5.26535,17.47195 15.33206,50.87623 20.97018,69.58517 z" 221 + inkscape:path-effect="#path-effect14" /> 222 + </svg>
+1 -2
package.json
··· 1 1 { 2 - "name": "bsky.app", 2 + "name": "blacksky.community", 3 3 "version": "1.109.0", 4 4 "private": true, 5 5 "engines": { ··· 151 151 "expo-linear-gradient": "~14.1.5", 152 152 "expo-linking": "~7.1.5", 153 153 "expo-localization": "~16.1.5", 154 - "expo-location": "~18.1.6", 155 154 "expo-media-library": "~17.1.7", 156 155 "expo-notifications": "~0.31.3", 157 156 "expo-screen-orientation": "~8.1.7",
+7
pages_build.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + mkdir ./bin 4 + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ./bin --tag 1.40.0 5 + export PATH="$PATH:$(pwd)/.bin" 6 + 7 + ./bin/just dist-build-web
+13
patches/@atproto+api+0.14.21.patch
··· 1 + diff --git a/node_modules/@atproto/api/dist/moderation/decision.js b/node_modules/@atproto/api/dist/moderation/decision.js 2 + index aaac177..d27c0be 100644 3 + --- a/node_modules/@atproto/api/dist/moderation/decision.js 4 + +++ b/node_modules/@atproto/api/dist/moderation/decision.js 5 + @@ -67,6 +67,8 @@ class ModerationDecision { 6 + ui(context) { 7 + const ui = new ui_1.ModerationUI(); 8 + for (const cause of this.causes) { 9 + + if (cause?.label?.val === '!no-unauthenticated') continue; 10 + + 11 + if (cause.type === 'blocking' || 12 + cause.type === 'blocked-by' || 13 + cause.type === 'block-other') {
+1 -1
scripts/bundleUpdate.sh
··· 1 - #!/bin/bash 1 + #!/usr/bin/env bash 2 2 set -o errexit 3 3 set -o pipefail 4 4 set -o nounset
+1 -1
scripts/setGitHubOutput.sh
··· 1 - #!/bin/bash 1 + #!/usr/bin/env bash 2 2 outputIos=$(eas build:version:get -p ios) 3 3 outputAndroid=$(eas build:version:get -p android) 4 4 BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
+1 -1
scripts/updateExtensions.sh
··· 1 - #!/bin/bash 1 + #!/usr/bin/env bash 2 2 IOS_SHARE_EXTENSION_DIRECTORY="./ios/Share-with-Bluesky" 3 3 IOS_NOTIFICATION_EXTENSION_DIRECTORY="./ios/BlueskyNSE" 4 4 IOS_APP_CLIP_DIRECTORY="./ios/BlueskyClip"
+1 -1
scripts/useBuildNumberEnv.sh
··· 1 - #!/bin/bash 1 + #!/usr/bin/env bash 2 2 outputIos=$(eas build:version:get -p ios) 3 3 outputAndroid=$(eas build:version:get -p android) 4 4 BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
+1 -2
scripts/useBuildNumberEnvWithBump.sh
··· 1 - #!/bin/bash 1 + #!/usr/bin/env bash 2 2 outputIos=$(eas build:version:get -p ios) 3 3 outputAndroid=$(eas build:version:get -p android) 4 4 currentIosVersion=${outputIos#*buildNumber - } ··· 8 8 BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1)) 9 9 10 10 bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*" 11 -
+27 -27
src/alf/themes.ts
··· 1 1 import {atoms} from '#/alf/atoms' 2 - import {Palette, Theme} from '#/alf/types' 2 + import {type Palette, type Theme} from '#/alf/types' 3 3 import { 4 4 BLUE_HUE, 5 5 defaultScale, ··· 79 79 gray_975: `hsl(${hues.primary}, 28%, ${defaultScale[1]}%)`, 80 80 gray_1000: `hsl(${hues.primary}, 28%, ${defaultScale[0]}%)`, 81 81 82 - primary_25: `hsl(${hues.primary}, 99%, 97%)`, 83 - primary_50: `hsl(${hues.primary}, 99%, 95%)`, 84 - primary_100: `hsl(${hues.primary}, 99%, 90%)`, 85 - primary_200: `hsl(${hues.primary}, 99%, 80%)`, 86 - primary_300: `hsl(${hues.primary}, 99%, 70%)`, 87 - primary_400: `hsl(${hues.primary}, 99%, 60%)`, 88 - primary_500: `hsl(${hues.primary}, 99%, 53%)`, 89 - primary_600: `hsl(${hues.primary}, 99%, 42%)`, 90 - primary_700: `hsl(${hues.primary}, 99%, 34%)`, 91 - primary_800: `hsl(${hues.primary}, 99%, 26%)`, 92 - primary_900: `hsl(${hues.primary}, 99%, 18%)`, 93 - primary_950: `hsl(${hues.primary}, 99%, 10%)`, 94 - primary_975: `hsl(${hues.primary}, 99%, 7%)`, 82 + primary_25: `hsl(145, 30%, 97%)`, 83 + primary_50: `hsl(145, 30%, 95%)`, 84 + primary_100: `hsl(145, 30%, 90%)`, 85 + primary_200: `hsl(145, 32%, 80%)`, 86 + primary_300: `hsl(145, 34%, 70%)`, 87 + primary_400: `hsl(145, 35%, 58%)`, 88 + primary_500: `hsl(145, 35%, 45%)`, 89 + primary_600: `hsl(145, 38%, 38%)`, 90 + primary_700: `hsl(145, 40%, 32%)`, 91 + primary_800: `hsl(145, 42%, 25%)`, 92 + primary_900: `hsl(145, 45%, 18%)`, 93 + primary_950: `hsl(145, 48%, 10%)`, 94 + primary_975: `hsl(145, 50%, 7%)`, 95 95 96 96 green_25: `hsl(${hues.positive}, 82%, 97%)`, 97 97 green_50: `hsl(${hues.positive}, 82%, 95%)`, ··· 265 265 contrast_950: `hsl(${hues.primary}, 20%, ${dimScale[12]}%)`, 266 266 contrast_975: `hsl(${hues.primary}, 20%, ${dimScale[13]}%)`, 267 267 268 - primary_25: `hsl(${hues.primary}, 50%, ${dimScale[1]}%)`, 269 - primary_50: `hsl(${hues.primary}, 60%, ${dimScale[2]}%)`, 270 - primary_100: `hsl(${hues.primary}, 70%, ${dimScale[3]}%)`, 271 - primary_200: `hsl(${hues.primary}, 82%, ${dimScale[4]}%)`, 272 - primary_300: `hsl(${hues.primary}, 90%, ${dimScale[5]}%)`, 273 - primary_400: `hsl(${hues.primary}, 95%, ${dimScale[6]}%)`, 274 - primary_500: `hsl(${hues.primary}, 99%, ${dimScale[7]}%)`, 275 - primary_600: `hsl(${hues.primary}, 99%, ${dimScale[8]}%)`, 276 - primary_700: `hsl(${hues.primary}, 99%, ${dimScale[9]}%)`, 277 - primary_800: `hsl(${hues.primary}, 99%, ${dimScale[10]}%)`, 278 - primary_900: `hsl(${hues.primary}, 99%, ${dimScale[11]}%)`, 279 - primary_950: `hsl(${hues.primary}, 99%, ${dimScale[12]}%)`, 280 - primary_975: `hsl(${hues.primary}, 99%, ${dimScale[13]}%)`, 268 + primary_25: `hsl(140, 15%, ${dimScale[1]}%)`, 269 + primary_50: `hsl(140, 18%, ${dimScale[2]}%)`, 270 + primary_100: `hsl(140, 22%, ${dimScale[3]}%)`, 271 + primary_200: `hsl(140, 25%, ${dimScale[4]}%)`, 272 + primary_300: `hsl(140, 28%, ${dimScale[5]}%)`, 273 + primary_400: `hsl(140, 32%, ${dimScale[6]}%)`, 274 + primary_500: `hsl(140, 35%, ${dimScale[7]}%)`, 275 + primary_600: `hsl(140, 38%, ${dimScale[8]}%)`, 276 + primary_700: `hsl(140, 42%, ${dimScale[9]}%)`, 277 + primary_800: `hsl(140, 45%, ${dimScale[10]}%)`, 278 + primary_900: `hsl(140, 48%, ${dimScale[11]}%)`, 279 + primary_950: `hsl(140, 50%, ${dimScale[12]}%)`, 280 + primary_975: `hsl(140, 55%, ${dimScale[13]}%)`, 281 281 282 282 positive_25: `hsl(${hues.positive}, 50%, ${dimScale[1]}%)`, 283 283 positive_50: `hsl(${hues.positive}, 60%, ${dimScale[2]}%)`,
+3 -3
src/alf/tokens.ts
··· 70 70 }, 71 71 sky: { 72 72 values: [ 73 - [0, '#0A7AFF'], 74 - [1, '#59B9FF'], 73 + [0, '#344e41'], 74 + [1, '#a3b18a'], 75 75 ], 76 - hover_value: '#0A7AFF', 76 + hover_value: '#344e41', 77 77 }, 78 78 midnight: { 79 79 values: [
+27 -22
src/components/verification/VerifierDialog.tsx
··· 6 6 import {urls} from '#/lib/constants' 7 7 import {getUserDisplayName} from '#/lib/getUserDisplayName' 8 8 import {logger} from '#/logger' 9 + import {useBlackskyVerificationEnabled} from '#/state/preferences/blacksky-verification' 9 10 import {useSession} from '#/state/session' 10 11 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 11 12 import {Button, ButtonText} from '#/components/Button' ··· 59 60 ? _(msg`You are a trusted verifier`) 60 61 : _(msg`${userName} is a trusted verifier`) 61 62 63 + const blackskyVerificationEnabled = useBlackskyVerificationEnabled() 64 + 62 65 return ( 63 66 <Dialog.ScrollableInner 64 67 label={label} ··· 66 69 gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, 67 70 ]}> 68 71 <View style={[a.gap_lg]}> 69 - <View 70 - style={[ 71 - a.w_full, 72 - a.rounded_md, 73 - a.overflow_hidden, 74 - t.atoms.bg_contrast_25, 75 - {minHeight: 100}, 76 - ]}> 77 - <Image 78 - accessibilityIgnoresInvertColors 79 - source={require('../../../assets/images/initial_verification_announcement_1.png')} 72 + {!blackskyVerificationEnabled && ( 73 + <View 80 74 style={[ 81 - { 82 - aspectRatio: 353 / 160, 83 - }, 84 - ]} 85 - alt={_( 86 - msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, 87 - )} 88 - /> 89 - </View> 75 + a.w_full, 76 + a.rounded_md, 77 + a.overflow_hidden, 78 + t.atoms.bg_contrast_25, 79 + {minHeight: 100}, 80 + ]}> 81 + <Image 82 + accessibilityIgnoresInvertColors 83 + source={require('../../../assets/images/initial_verification_announcement_1.png')} 84 + style={[ 85 + { 86 + aspectRatio: 353 / 160, 87 + }, 88 + ]} 89 + alt={_( 90 + msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, 91 + )} 92 + /> 93 + </View> 94 + )} 90 95 91 96 <View style={[a.gap_sm]}> 92 97 <Text style={[a.text_2xl, a.font_bold, a.pr_4xl, a.leading_tight]}> ··· 98 103 <RNText> 99 104 <VerifierCheck width={14} /> 100 105 </RNText>{' '} 101 - can verify others. These trusted verifiers are selected by 102 - Bluesky. 106 + can verify others. These trusted verifiers are selected by{' '} 107 + {blackskyVerificationEnabled ? 'you' : 'Bluesky'}. 103 108 </Trans> 104 109 </Text> 105 110 </View>
+5 -2
src/components/verification/index.ts
··· 1 1 import {useMemo} from 'react' 2 2 3 + import {useMaybeBlackskyVerificationProfileOverlay} from '#/state/queries/blacksky-verification' 3 4 import {usePreferencesQuery} from '#/state/queries/preferences' 4 5 import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' 5 6 import {useSession} from '#/state/session' ··· 46 47 const hasIssuedVerification = Boolean( 47 48 viewerState && 48 49 viewerState.role === 'verifier' && 49 - profileState.role === 'default' && 50 + // profileState.role === 'default' && 50 51 verifications.find(v => v.issuer === currentAccount?.did), 51 52 ) 52 53 ··· 79 80 } 80 81 81 82 export function useSimpleVerificationState({ 82 - profile, 83 + profile: baseProfile, 83 84 }: { 84 85 profile?: bsky.profile.AnyProfileView 85 86 }): SimpleVerificationState { ··· 88 89 () => preferences.data?.verificationPrefs || {hideBadges: false}, 89 90 [preferences.data?.verificationPrefs], 90 91 ) 92 + const profile = useMaybeBlackskyVerificationProfileOverlay(baseProfile) 93 + 91 94 return useMemo(() => { 92 95 if (!profile || !profile.verification) { 93 96 return {
+4 -15
src/lib/constants.ts
··· 11 11 export const BSKY_SERVICE_DID = 'did:web:bsky.social' 12 12 export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' 13 13 export const DEFAULT_SERVICE = BSKY_SERVICE 14 - const HELP_DESK_LANG = 'en-us' 15 - export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` 14 + export const HELP_DESK_URL = `https://github.com/blacksky-algorithms/blacksky.community/issues/new/choose` 16 15 export const EMBED_SERVICE = 'https://embed.bsky.app' 17 16 export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` 18 17 export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download' ··· 38 37 'did:plc:2dzyut5lxna5ljiaasgeuffz': true, // darrin.bsky.team 39 38 } 40 39 41 - const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new` 42 - export function FEEDBACK_FORM_URL({ 43 - email, 44 - handle, 45 - }: { 40 + const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}` 41 + export function FEEDBACK_FORM_URL(_params: { 46 42 email?: string 47 43 handle?: string 48 44 }): string { 49 - let str = BASE_FEEDBACK_FORM_URL 50 - if (email) { 51 - str += `?tf_anonymous_requester_email=${encodeURIComponent(email)}` 52 - if (handle) { 53 - str += `&tf_17205412673421=${encodeURIComponent(handle)}` 54 - } 55 - } 56 - return str 45 + return BASE_FEEDBACK_FORM_URL 57 46 } 58 47 59 48 export const MAX_DISPLAY_NAME = 64
+22
src/lib/media/manip.ts
··· 21 21 import {isAndroid, isIOS} from '#/platform/detection' 22 22 import {type PickerImage} from './picker.shared' 23 23 import {type Dimensions} from './types' 24 + import {mimeToExt} from './video/util' 24 25 25 26 export async function compressIfNeeded( 26 27 img: PickerImage, ··· 135 136 } finally { 136 137 safeDeleteAsync(imagePath) 137 138 } 139 + } 140 + 141 + export async function saveVideoToMediaLibrary({uri}: {uri: string}) { 142 + // download the file to cache 143 + const downloadResponse = await RNFetchBlob.config({ 144 + fileCache: true, 145 + }) 146 + .fetch('GET', uri) 147 + .catch(() => null) 148 + if (downloadResponse == null) return false 149 + let videoPath = downloadResponse.path() 150 + let extension = mimeToExt(downloadResponse.respInfo.headers['content-type']) 151 + videoPath = normalizePath( 152 + await moveToPermanentPath(videoPath, '.' + extension), 153 + true, 154 + ) 155 + 156 + // save 157 + await MediaLibrary.createAssetAsync(videoPath) 158 + safeDeleteAsync(videoPath) 159 + return true 138 160 } 139 161 140 162 export function getImageDim(path: string): Promise<Dimensions> {
+17
src/lib/media/manip.web.ts
··· 3 3 import {type PickerImage} from './picker.shared' 4 4 import {type Dimensions} from './types' 5 5 import {blobToDataUri, getDataUriSize} from './util' 6 + import {mimeToExt} from './video/util' 6 7 7 8 export async function compressIfNeeded( 8 9 img: PickerImage, ··· 47 48 export async function saveImageToMediaLibrary(_opts: {uri: string}) { 48 49 // TODO 49 50 throw new Error('TODO') 51 + } 52 + 53 + export async function downloadVideoWeb({uri}: {uri: string}) { 54 + // download the file to cache 55 + const downloadResponse = await fetch(uri) 56 + .then(res => res.blob()) 57 + .catch(() => null) 58 + if (downloadResponse == null) return false 59 + const extension = mimeToExt(downloadResponse.type) 60 + 61 + const blobUrl = URL.createObjectURL(downloadResponse) 62 + const link = document.createElement('a') 63 + link.setAttribute('download', uri.slice(-10) + '.' + extension) 64 + link.setAttribute('href', blobUrl) 65 + link.click() 66 + return true 50 67 } 51 68 52 69 export async function getImageDim(path: string): Promise<Dimensions> {
+1 -1
src/lib/notifications/notifications.ts
··· 37 37 : PUBLIC_APPVIEW_DID, 38 38 platform: Platform.OS, 39 39 token: token.data, 40 - appId: 'xyz.blueskyweb.app', 40 + appId: 'community.blacksky', 41 41 ageRestricted: extra.ageRestricted ?? false, 42 42 } 43 43
+1 -7
src/lib/routes/links.ts
··· 1 1 import {type AppBskyGraphDefs, AtUri} from '@atproto/api' 2 2 3 - import {isInvalidHandle} from '#/lib/strings/handles' 4 - 5 3 export function makeProfileLink( 6 4 info: { 7 5 did: string ··· 9 7 }, 10 8 ...segments: string[] 11 9 ) { 12 - let handleSegment = info.did 13 - if (info.handle && !isInvalidHandle(info.handle)) { 14 - handleSegment = info.handle 15 - } 16 - return [`/profile`, handleSegment, ...segments].join('/') 10 + return [`/profile`, info.did, ...segments].join('/') 17 11 } 18 12 19 13 export function makeCustomFeedLink(
+1
src/lib/routes/types.ts
··· 66 66 MiscellaneousNotificationSettings: undefined 67 67 InterestsSettings: undefined 68 68 AboutSettings: undefined 69 + BlackskySettings: undefined 69 70 AppIconSettings: undefined 70 71 Search: {q?: string} 71 72 Hashtag: {tag: string; author?: string}
+49 -82
src/lib/statsig/statsig.tsx
··· 1 1 import React from 'react' 2 2 import {Platform} from 'react-native' 3 3 import {AppState, type AppStateStatus} from 'react-native' 4 - import {Statsig, StatsigProvider} from 'statsig-react-native-expo' 4 + import {Statsig} from 'statsig-react-native-expo' 5 5 6 + import {BUNDLE_DATE, BUNDLE_IDENTIFIER} from '#/lib/app-info' 6 7 import {logger} from '#/logger' 7 8 import {type MetricEvents} from '#/logger/metrics' 8 9 import {isWeb} from '#/platform/detection' 9 10 import * as persisted from '#/state/persisted' 10 - import * as env from '#/env' 11 - import {useSession} from '../../state/session' 11 + import {device} from '#/storage' 12 12 import {timeout} from '../async/timeout' 13 - import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' 13 + // import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' 14 14 import {type Gate} from './gates' 15 15 16 - const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV' 16 + // const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV' 17 17 18 18 export const initPromise = initialize() 19 19 ··· 25 25 // This is the place where we can add our own stuff. 26 26 // Fields here have to be non-optional to be visible in the UI. 27 27 platform: 'ios' | 'android' | 'web' 28 - appVersion: string 29 28 bundleIdentifier: string 30 29 bundleDate: number 31 30 refSrc: string ··· 45 44 46 45 export type {MetricEvents as LogEvents} 47 46 48 - function createStatsigOptions(prefetchUsers: StatsigUser[]) { 49 - return { 50 - environment: { 51 - tier: env.IS_DEV 52 - ? 'development' 53 - : env.IS_TESTFLIGHT 54 - ? 'staging' 55 - : 'production', 56 - }, 57 - // Don't block on waiting for network. The fetched config will kick in on next load. 58 - // This ensures the UI is always consistent and doesn't update mid-session. 59 - // Note this makes cold load (no local storage) and private mode return `false` for all gates. 60 - initTimeoutMs: 1, 61 - // Get fresh flags for other accounts as well, if any. 62 - prefetchUsers, 63 - api: 'https://events.bsky.app/v2', 64 - } 65 - } 47 + // function createStatsigOptions(prefetchUsers: StatsigUser[]) { 48 + // return { 49 + // environment: { 50 + // tier: 51 + // process.env.NODE_ENV === 'development' 52 + // ? 'development' 53 + // : IS_TESTFLIGHT 54 + // ? 'staging' 55 + // : 'production', 56 + // }, 57 + // // Don't block on waiting for network. The fetched config will kick in on next load. 58 + // // This ensures the UI is always consistent and doesn't update mid-session. 59 + // // Note this makes cold load (no local storage) and private mode return `false` for all gates. 60 + // initTimeoutMs: 1, 61 + // // Get fresh flags for other accounts as well, if any. 62 + // prefetchUsers, 63 + // api: 'https://events.bsky.app/v2', 64 + // } 65 + // } 66 66 67 67 type FlatJSONRecord = Record< 68 68 string, ··· 147 147 // and it's been difficult to get it to behave in a predictable way. 148 148 // Our own cache ensures consistent evaluation within a single session. 149 149 const GateCache = React.createContext<Map<string, boolean> | null>(null) 150 - GateCache.displayName = 'StatsigGateCacheContext' 151 150 152 151 type GateOptions = { 153 152 dangerouslyDisableExposureLogging?: boolean 153 + } 154 + 155 + export function useGatesCache(): Map<string, boolean> { 156 + const cache = React.useContext(GateCache) 157 + if (!cache) { 158 + throw Error('useGate() cannot be called outside StatsigProvider.') 159 + } 160 + return cache 161 + } 162 + 163 + function writeBlackskyGateCache(cache: Map<string, boolean>) { 164 + device.set(['blackskyGateCache'], JSON.stringify(Object.fromEntries(cache))) 165 + } 166 + 167 + export function resetBlackskyGateCache() { 168 + writeBlackskyGateCache(new Map()) 154 169 } 155 170 156 171 export function useGate(): (gateName: Gate, options?: GateOptions) => boolean { ··· 173 188 } 174 189 } 175 190 cache.set(gateName, value) 191 + writeBlackskyGateCache(cache) 176 192 return value 177 193 }, 178 194 [cache], ··· 196 212 const dangerousSetGate = React.useCallback( 197 213 (gateName: Gate, value: boolean) => { 198 214 cache.set(gateName, value) 215 + writeBlackskyGateCache(cache) 199 216 }, 200 217 [cache], 201 218 ) ··· 211 228 refSrc, 212 229 refUrl, 213 230 platform: Platform.OS as 'ios' | 'android' | 'web', 214 - appVersion: env.RELEASE_VERSION, 215 - bundleIdentifier: env.BUNDLE_IDENTIFIER, 216 - bundleDate: env.BUNDLE_DATE, 231 + bundleIdentifier: BUNDLE_IDENTIFIER, 232 + bundleDate: BUNDLE_DATE, 217 233 appLanguage: languagePrefs.appLanguage, 218 234 contentLanguages: languagePrefs.contentLanguages, 219 235 }, ··· 265 281 } 266 282 267 283 export function initialize() { 268 - return Statsig.initialize(SDK_KEY, null, createStatsigOptions([])) 284 + // return Statsig.initialize(SDK_KEY, null, createStatsigOptions([])) 285 + return new Promise(() => {}) 269 286 } 270 287 271 288 export function Provider({children}: {children: React.ReactNode}) { 272 - const {currentAccount, accounts} = useSession() 273 - const did = currentAccount?.did 274 - const currentStatsigUser = React.useMemo(() => toStatsigUser(did), [did]) 275 - 276 - const otherDidsConcatenated = accounts 277 - .map(account => account.did) 278 - .filter(accountDid => accountDid !== did) 279 - .join(' ') // We're only interested in DID changes. 280 - const otherStatsigUsers = React.useMemo( 281 - () => otherDidsConcatenated.split(' ').map(toStatsigUser), 282 - [otherDidsConcatenated], 283 - ) 284 - const statsigOptions = React.useMemo( 285 - () => createStatsigOptions(otherStatsigUsers), 286 - [otherStatsigUsers], 289 + const gateCache = new Map<string, boolean>( 290 + Object.entries(JSON.parse(device.get(['blackskyGateCache']) ?? '{}')), 287 291 ) 288 292 289 - // Have our own cache in front of Statsig. 290 - // This ensures the results remain stable until the active DID changes. 291 - const [gateCache, setGateCache] = React.useState(() => new Map()) 292 - const [prevDid, setPrevDid] = React.useState(did) 293 - if (did !== prevDid) { 294 - setPrevDid(did) 295 - setGateCache(new Map()) 296 - } 297 - 298 - // Periodically poll Statsig to get the current rule evaluations for all stored accounts. 299 - // These changes are prefetched and stored, but don't get applied until the active DID changes. 300 - // This ensures that when you switch an account, it already has fresh results by then. 301 - const handleIntervalTick = useNonReactiveCallback(() => { 302 - if (Statsig.initializeCalled()) { 303 - // Note: Only first five will be taken into account by Statsig. 304 - Statsig.prefetchUsers([currentStatsigUser, ...otherStatsigUsers]) 305 - } 306 - }) 307 - React.useEffect(() => { 308 - const id = setInterval(handleIntervalTick, 60e3 /* 1 min */) 309 - return () => clearInterval(id) 310 - }, [handleIntervalTick]) 311 - 312 - return ( 313 - <GateCache.Provider value={gateCache}> 314 - <StatsigProvider 315 - key={did} 316 - sdkKey={SDK_KEY} 317 - mountKey={currentStatsigUser.userID} 318 - user={currentStatsigUser} 319 - // This isn't really blocking due to short initTimeoutMs above. 320 - // However, it ensures `isLoading` is always `false`. 321 - waitForInitialization={true} 322 - options={statsigOptions}> 323 - {children} 324 - </StatsigProvider> 325 - </GateCache.Provider> 326 - ) 293 + return <GateCache.Provider value={gateCache}>{children}</GateCache.Provider> 327 294 }
+3 -3
src/lib/strings/embed-player.ts
··· 9 9 ? // @ts-ignore only for web 10 10 window.location.host === 'localhost:8100' 11 11 ? 'http://localhost:8100' 12 - : 'https://bsky.app' 12 + : 'https://blacksky.community' 13 13 : __DEV__ && !process.env.JEST_WORKER_ID 14 - ? 'http://localhost:8100' 15 - : 'https://bsky.app' 14 + ? 'http://localhost:8100' 15 + : 'https://blacksky.community' 16 16 17 17 export const embedPlayerSources = [ 18 18 'youtube',
+1 -1
src/lib/strings/headings.ts
··· 1 1 export function bskyTitle(page: string, unreadCountLabel?: string) { 2 2 const unreadPrefix = unreadCountLabel ? `(${unreadCountLabel}) ` : '' 3 - return `${unreadPrefix}${page} — Bluesky` 3 + return `${unreadPrefix}${page} — blacksky.community` 4 4 }
+10 -3
src/lib/strings/url-helpers.ts
··· 7 7 import {startUriToStarterPackUri} from '#/lib/strings/starter-pack' 8 8 import {logger} from '#/logger' 9 9 10 - export const BSKY_APP_HOST = 'https://bsky.app' 10 + export const BSKY_APP_HOST = 'https://blacksky.community' 11 11 const BSKY_TRUSTED_HOSTS = [ 12 + 'blacksky\\.community', 13 + 'blacksky\\.app', 14 + 'blackskyweb\\.xyz', 12 15 'bsky\\.app', 13 16 'bsky\\.social', 14 17 'blueskyweb\\.xyz', ··· 79 82 80 83 export function toShareUrl(url: string): string { 81 84 if (!url.startsWith('https')) { 82 - const urlp = new URL('https://bsky.app') 85 + const urlp = new URL('https://blacksky.community') 83 86 urlp.pathname = url 84 87 url = urlp.toString() 85 88 } ··· 91 94 } 92 95 93 96 export function isBskyAppUrl(url: string): boolean { 94 - return url.startsWith('https://bsky.app/') 97 + return ( 98 + url.startsWith('https://bsky.app/') || 99 + (url.startsWith('https://blacksky.community/') && 100 + !url.startsWith('https://blacksky.community/about')) 101 + ) 95 102 } 96 103 97 104 export function isRelativeUrl(url: string): boolean {
+11 -11
src/lib/styles.ts
··· 25 25 gray7: '#26272D', 26 26 gray8: '#141417', 27 27 28 - blue0: '#bfe1ff', 29 - blue1: '#8bc7fd', 30 - blue2: '#52acfe', 31 - blue3: '#0085ff', 32 - blue4: '#0062bd', 33 - blue5: '#034581', 34 - blue6: '#012561', 35 - blue7: '#001040', 28 + blue0: '#d5e8d9', 29 + blue1: '#b8d6be', 30 + blue2: '#95bd9f', 31 + blue3: '#729f7c', 32 + blue4: '#528157', 33 + blue5: '#3a6141', 34 + blue6: '#25422c', 35 + blue7: '#14291a', 36 36 37 37 red1: '#ffe6eb', 38 38 red2: '#fba2b2', ··· 66 66 } 67 67 68 68 export const gradients = { 69 - blueLight: {start: '#5A71FA', end: colors.blue3}, // buttons 70 - blue: {start: '#5E55FB', end: colors.blue3}, // fab 71 - blueDark: {start: '#5F45E0', end: colors.blue3}, // avis, banner 69 + blueLight: {start: '#a3b18a', end: colors.blue3}, // buttons 70 + blue: {start: '#8a9e7b', end: colors.blue3}, // fab 71 + blueDark: {start: '#658764', end: colors.blue3}, // avis, banner 72 72 } 73 73 74 74 /**
+1 -1
src/locale/locales/pt-PT/messages.po
··· 10912 10912 10913 10913 #: src/components/verification/VerificationsDialog.tsx:65 10914 10914 msgid "Your verifications" 10915 - msgstr "As suas verificações" 10915 + msgstr "As suas verificações"
+8 -11
src/logger/index.ts
··· 3 3 import {logEvent} from '#/lib/statsig/statsig' 4 4 import {add} from '#/logger/logDump' 5 5 import {type MetricEvents} from '#/logger/metrics' 6 - import {bitdriftTransport} from '#/logger/transports/bitdrift' 7 6 import {consoleTransport} from '#/logger/transports/console' 8 - import {sentryTransport} from '#/logger/transports/sentry' 9 7 import { 10 8 LogContext, 11 9 LogLevel, ··· 13 11 type Transport, 14 12 } from '#/logger/types' 15 13 import {enabledLogLevels} from '#/logger/util' 16 - import {isNative} from '#/platform/detection' 17 - import {ENV} from '#/env' 18 14 19 15 const TRANSPORTS: Transport[] = (function configureTransports() { 20 - switch (ENV) { 21 - case 'production': { 22 - return [sentryTransport, isNative && bitdriftTransport].filter( 23 - Boolean, 24 - ) as Transport[] 25 - } 16 + switch (process.env.NODE_ENV) { 17 + // case 'production': { 18 + // return [sentryTransport, isNative && bitdriftTransport].filter( 19 + // Boolean, 20 + // ) as Transport[] 21 + // } 22 + case 'production': 26 23 case 'test': { 27 24 return [] 28 25 } ··· 105 102 * Optionally also send to StatSig 106 103 */ 107 104 statsig?: boolean 108 - } = {statsig: true}, 105 + } = {statsig: false}, 109 106 ) { 110 107 logEvent(event, metadata, { 111 108 lake: !options.statsig,
+1
src/routes.ts
··· 47 47 PreferencesThreads: '/settings/threads', 48 48 PreferencesExternalEmbeds: '/settings/external-embeds', 49 49 AccessibilitySettings: '/settings/accessibility', 50 + BlackskySettings: '/settings/blacksky', 50 51 AppearanceSettings: '/settings/appearance', 51 52 SavedFeeds: '/settings/saved-feeds', 52 53 AccountSettings: '/settings/account',
+14 -4
src/screens/Profile/Header/DisplayName.tsx
··· 1 1 import {View} from 'react-native' 2 - import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' 2 + import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' 3 3 4 4 import {sanitizeDisplayName} from '#/lib/strings/display-names' 5 5 import {sanitizeHandle} from '#/lib/strings/handles' 6 - import {Shadow} from '#/state/cache/types' 7 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 6 + import {type Shadow} from '#/state/cache/types' 7 + import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 8 8 import {Text} from '#/components/Typography' 9 + import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 9 10 10 11 export function ProfileHeaderDisplayName({ 11 12 profile, ··· 18 19 const {gtMobile} = useBreakpoints() 19 20 20 21 return ( 21 - <View pointerEvents="none"> 22 + <View> 22 23 <Text 23 24 emoji 24 25 testID="profileHeaderDisplayName" ··· 32 33 profile.displayName || sanitizeHandle(profile.handle), 33 34 moderation.ui('displayName'), 34 35 )} 36 + <View 37 + style={[ 38 + a.pl_xs, 39 + { 40 + marginTop: platform({ios: 2}), 41 + }, 42 + ]}> 43 + <VerificationCheckButton profile={profile} size="lg" /> 44 + </View> 35 45 </Text> 36 46 </View> 37 47 )
+3 -5
src/screens/Settings/AboutSettings.tsx
··· 1 - import {useMemo} from 'react' 2 1 import {Platform} from 'react-native' 3 2 import {setStringAsync} from 'expo-clipboard' 4 3 import * as FileSystem from 'expo-file-system' ··· 7 6 import {useLingui} from '@lingui/react' 8 7 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 9 8 import {useMutation} from '@tanstack/react-query' 10 - import {Statsig} from 'statsig-react-native-expo' 11 9 12 10 import {STATUS_PAGE_URL} from '#/lib/constants' 13 11 import {type CommonNavigatorParams} from '#/lib/routes/types' ··· 32 30 const {_, i18n} = useLingui() 33 31 const [devModeEnabled, setDevModeEnabled] = useDevMode() 34 32 const [demoModeEnabled, setDemoModeEnabled] = useDemoMode() 35 - const stableID = useMemo(() => Statsig.getStableID(), []) 33 + const stableID = `BLACKSKY_COMMUNITY_OOPS` 36 34 37 35 const {mutate: onClearImageCache, isPending: isClearingImageCache} = 38 36 useMutation({ ··· 80 78 <Layout.Content> 81 79 <SettingsList.Container> 82 80 <SettingsList.LinkItem 83 - to="https://bsky.social/about/support/tos" 81 + to="https://blacksky.community/about/tos" 84 82 label={_(msg`Terms of Service`)}> 85 83 <SettingsList.ItemIcon icon={NewspaperIcon} /> 86 84 <SettingsList.ItemText> ··· 88 86 </SettingsList.ItemText> 89 87 </SettingsList.LinkItem> 90 88 <SettingsList.LinkItem 91 - to="https://bsky.social/about/support/privacy-policy" 89 + to="https://blacksky.community/about/privacy" 92 90 label={_(msg`Privacy Policy`)}> 93 91 <SettingsList.ItemIcon icon={NewspaperIcon} /> 94 92 <SettingsList.ItemText>
+106 -138
src/screens/Settings/AppIconSettings/useAppIconSets.ts
··· 52 52 /** 53 53 * Bluesky+ 54 54 */ 55 - const core = [ 56 - { 57 - id: 'core_aurora', 58 - name: _(msg({context: 'Name of app icon variant', message: 'Aurora'})), 59 - iosImage: () => { 60 - return require( 61 - `../../../../assets/app-icons/ios_icon_core_aurora.png`, 62 - ) 63 - }, 64 - androidImage: () => { 65 - return require( 66 - `../../../../assets/app-icons/android_icon_core_aurora.png`, 67 - ) 68 - }, 69 - }, 70 - // { 71 - // id: 'core_bonfire', 72 - // name: _(msg({ context: 'Name of app icon variant', message: 'Bonfire' })), 73 - // iosImage: () => { 74 - // return require(`../../../../assets/app-icons/ios_icon_core_bonfire.png`) 75 - // }, 76 - // androidImage: () => { 77 - // return require(`../../../../assets/app-icons/android_icon_core_bonfire.png`) 78 - // }, 79 - // }, 80 - { 81 - id: 'core_sunrise', 82 - name: _(msg({context: 'Name of app icon variant', message: 'Sunrise'})), 83 - iosImage: () => { 84 - return require( 85 - `../../../../assets/app-icons/ios_icon_core_sunrise.png`, 86 - ) 87 - }, 88 - androidImage: () => { 89 - return require( 90 - `../../../../assets/app-icons/android_icon_core_sunrise.png`, 91 - ) 92 - }, 93 - }, 94 - { 95 - id: 'core_sunset', 96 - name: _(msg({context: 'Name of app icon variant', message: 'Sunset'})), 97 - iosImage: () => { 98 - return require( 99 - `../../../../assets/app-icons/ios_icon_core_sunset.png`, 100 - ) 101 - }, 102 - androidImage: () => { 103 - return require( 104 - `../../../../assets/app-icons/android_icon_core_sunset.png`, 105 - ) 106 - }, 107 - }, 108 - { 109 - id: 'core_midnight', 110 - name: _( 111 - msg({context: 'Name of app icon variant', message: 'Midnight'}), 112 - ), 113 - iosImage: () => { 114 - return require( 115 - `../../../../assets/app-icons/ios_icon_core_midnight.png`, 116 - ) 117 - }, 118 - androidImage: () => { 119 - return require( 120 - `../../../../assets/app-icons/android_icon_core_midnight.png`, 121 - ) 122 - }, 123 - }, 124 - { 125 - id: 'core_flat_blue', 126 - name: _( 127 - msg({context: 'Name of app icon variant', message: 'Flat Blue'}), 128 - ), 129 - iosImage: () => { 130 - return require( 131 - `../../../../assets/app-icons/ios_icon_core_flat_blue.png`, 132 - ) 133 - }, 134 - androidImage: () => { 135 - return require( 136 - `../../../../assets/app-icons/android_icon_core_flat_blue.png`, 137 - ) 138 - }, 139 - }, 140 - { 141 - id: 'core_flat_white', 142 - name: _( 143 - msg({context: 'Name of app icon variant', message: 'Flat White'}), 144 - ), 145 - iosImage: () => { 146 - return require( 147 - `../../../../assets/app-icons/ios_icon_core_flat_white.png`, 148 - ) 149 - }, 150 - androidImage: () => { 151 - return require( 152 - `../../../../assets/app-icons/android_icon_core_flat_white.png`, 153 - ) 154 - }, 155 - }, 156 - { 157 - id: 'core_flat_black', 158 - name: _( 159 - msg({context: 'Name of app icon variant', message: 'Flat Black'}), 160 - ), 161 - iosImage: () => { 162 - return require( 163 - `../../../../assets/app-icons/ios_icon_core_flat_black.png`, 164 - ) 165 - }, 166 - androidImage: () => { 167 - return require( 168 - `../../../../assets/app-icons/android_icon_core_flat_black.png`, 169 - ) 170 - }, 171 - }, 172 - { 173 - id: 'core_classic', 174 - name: _( 175 - msg({ 176 - context: 'Name of app icon variant', 177 - message: 'Bluesky Classic™', 178 - }), 179 - ), 180 - iosImage: () => { 181 - return require( 182 - `../../../../assets/app-icons/ios_icon_core_classic.png`, 183 - ) 184 - }, 185 - androidImage: () => { 186 - return require( 187 - `../../../../assets/app-icons/android_icon_core_classic.png`, 188 - ) 189 - }, 190 - }, 191 - ] satisfies AppIconSet[] 55 + // const core = [ 56 + // { 57 + // id: 'core_aurora', 58 + // name: _(msg({context: 'Name of app icon variant', message: 'Aurora'})), 59 + // iosImage: () => { 60 + // return require(`../../../../assets/app-icons/ios_icon_core_aurora.png`) 61 + // }, 62 + // androidImage: () => { 63 + // return require(`../../../../assets/app-icons/android_icon_core_aurora.png`) 64 + // }, 65 + // }, 66 + // // { 67 + // // id: 'core_bonfire', 68 + // // name: _(msg({ context: 'Name of app icon variant', message: 'Bonfire' })), 69 + // // iosImage: () => { 70 + // // return require(`../../../../assets/app-icons/ios_icon_core_bonfire.png`) 71 + // // }, 72 + // // androidImage: () => { 73 + // // return require(`../../../../assets/app-icons/android_icon_core_bonfire.png`) 74 + // // }, 75 + // // }, 76 + // { 77 + // id: 'core_sunrise', 78 + // name: _(msg({context: 'Name of app icon variant', message: 'Sunrise'})), 79 + // iosImage: () => { 80 + // return require(`../../../../assets/app-icons/ios_icon_core_sunrise.png`) 81 + // }, 82 + // androidImage: () => { 83 + // return require(`../../../../assets/app-icons/android_icon_core_sunrise.png`) 84 + // }, 85 + // }, 86 + // { 87 + // id: 'core_sunset', 88 + // name: _(msg({context: 'Name of app icon variant', message: 'Sunset'})), 89 + // iosImage: () => { 90 + // return require(`../../../../assets/app-icons/ios_icon_core_sunset.png`) 91 + // }, 92 + // androidImage: () => { 93 + // return require(`../../../../assets/app-icons/android_icon_core_sunset.png`) 94 + // }, 95 + // }, 96 + // { 97 + // id: 'core_midnight', 98 + // name: _( 99 + // msg({context: 'Name of app icon variant', message: 'Midnight'}), 100 + // ), 101 + // iosImage: () => { 102 + // return require(`../../../../assets/app-icons/ios_icon_core_midnight.png`) 103 + // }, 104 + // androidImage: () => { 105 + // return require(`../../../../assets/app-icons/android_icon_core_midnight.png`) 106 + // }, 107 + // }, 108 + // { 109 + // id: 'core_flat_blue', 110 + // name: _( 111 + // msg({context: 'Name of app icon variant', message: 'Flat Blue'}), 112 + // ), 113 + // iosImage: () => { 114 + // return require(`../../../../assets/app-icons/ios_icon_core_flat_blue.png`) 115 + // }, 116 + // androidImage: () => { 117 + // return require(`../../../../assets/app-icons/android_icon_core_flat_blue.png`) 118 + // }, 119 + // }, 120 + // { 121 + // id: 'core_flat_white', 122 + // name: _( 123 + // msg({context: 'Name of app icon variant', message: 'Flat White'}), 124 + // ), 125 + // iosImage: () => { 126 + // return require(`../../../../assets/app-icons/ios_icon_core_flat_white.png`) 127 + // }, 128 + // androidImage: () => { 129 + // return require(`../../../../assets/app-icons/android_icon_core_flat_white.png`) 130 + // }, 131 + // }, 132 + // { 133 + // id: 'core_flat_black', 134 + // name: _( 135 + // msg({context: 'Name of app icon variant', message: 'Flat Black'}), 136 + // ), 137 + // iosImage: () => { 138 + // return require(`../../../../assets/app-icons/ios_icon_core_flat_black.png`) 139 + // }, 140 + // androidImage: () => { 141 + // return require(`../../../../assets/app-icons/android_icon_core_flat_black.png`) 142 + // }, 143 + // }, 144 + // { 145 + // id: 'core_classic', 146 + // name: _( 147 + // msg({ 148 + // context: 'Name of app icon variant', 149 + // message: 'Bluesky Classic™', 150 + // }), 151 + // ), 152 + // iosImage: () => { 153 + // return require(`../../../../assets/app-icons/ios_icon_core_classic.png`) 154 + // }, 155 + // androidImage: () => { 156 + // return require(`../../../../assets/app-icons/android_icon_core_classic.png`) 157 + // }, 158 + // }, 159 + // ] satisfies AppIconSet[] 192 160 193 161 return { 194 162 defaults, 195 - core, 163 + // core, 196 164 } 197 165 }, [_]) 198 166 }
+594
src/screens/Settings/BlackskySettings.tsx
··· 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 7 + 8 + import {usePalette} from '#/lib/hooks/usePalette' 9 + import {type CommonNavigatorParams} from '#/lib/routes/types' 10 + import {type Gate} from '#/lib/statsig/gates' 11 + import { 12 + resetBlackskyGateCache, 13 + useDangerousSetGate, 14 + useGatesCache, 15 + } from '#/lib/statsig/statsig' 16 + import {isWeb} from '#/platform/detection' 17 + import {setGeolocation, useGeolocation} from '#/state/geolocation' 18 + import * as persisted from '#/state/persisted' 19 + import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' 20 + import { 21 + useBlackskyVerificationEnabled, 22 + useBlackskyVerificationTrusted, 23 + useSetBlackskyVerificationEnabled, 24 + } from '#/state/preferences/blacksky-verification' 25 + import { 26 + useConstellationEnabled, 27 + useSetConstellationEnabled, 28 + } from '#/state/preferences/constellation-enabled' 29 + import { 30 + useConstellationInstance, 31 + useSetConstellationInstance, 32 + } from '#/state/preferences/constellation-instance' 33 + import { 34 + useDirectFetchRecords, 35 + useSetDirectFetchRecords, 36 + } from '#/state/preferences/direct-fetch-records' 37 + import { 38 + useHideFollowNotifications, 39 + useSetHideFollowNotifications, 40 + } from '#/state/preferences/hide-follow-notifications' 41 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 42 + import { 43 + useNoAppLabelers, 44 + useSetNoAppLabelers, 45 + } from '#/state/preferences/no-app-labelers' 46 + import { 47 + useNoDiscoverFallback, 48 + useSetNoDiscoverFallback, 49 + } from '#/state/preferences/no-discover-fallback' 50 + import { 51 + useRepostCarouselEnabled, 52 + useSetRepostCarouselEnabled, 53 + } from '#/state/preferences/repost-carousel-enabled' 54 + import { 55 + useSetShowLinkInHandle, 56 + useShowLinkInHandle, 57 + } from '#/state/preferences/show-link-in-handle.tsx' 58 + import {useProfilesQuery} from '#/state/queries/profile' 59 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 60 + import {atoms as a, useBreakpoints} from '#/alf' 61 + import {Admonition} from '#/components/Admonition' 62 + import {Button, ButtonText} from '#/components/Button' 63 + import * as Dialog from '#/components/Dialog' 64 + import * as Toggle from '#/components/forms/Toggle' 65 + import {Atom_Stroke2_Corner0_Rounded as BlackskyIcon} from '#/components/icons/Atom' 66 + import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell' 67 + import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 68 + import {Earth_Stroke2_Corner2_Rounded as GlobeIcon} from '#/components/icons/Globe' 69 + import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' 70 + import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 71 + import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand' 72 + import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' 73 + import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' 74 + import * as Layout from '#/components/Layout' 75 + import {Text} from '#/components/Typography' 76 + import {SearchProfileCard} from '../Search/components/SearchProfileCard' 77 + 78 + type Props = NativeStackScreenProps<CommonNavigatorParams> 79 + 80 + function GeolocationSettingsDialog({ 81 + control, 82 + }: { 83 + control: Dialog.DialogControlProps 84 + }) { 85 + const pal = usePalette('default') 86 + const {_} = useLingui() 87 + 88 + const [hasChanged, setHasChanged] = useState(false) 89 + const [countryCode, setCountryCode] = useState('') 90 + 91 + const submit = () => { 92 + setGeolocation({countryCode}) 93 + control.close() 94 + } 95 + 96 + return ( 97 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 98 + <Dialog.Handle /> 99 + <Dialog.ScrollableInner label={_(msg`Geolocation ISO 3166-1 Code`)}> 100 + <View style={[a.gap_sm, a.pb_lg]}> 101 + <Text style={[a.text_2xl, a.font_bold]}> 102 + <Trans>Geolocation ISO 3166-1 Code</Trans> 103 + </Text> 104 + </View> 105 + 106 + <View style={a.gap_lg}> 107 + <Dialog.Input 108 + label="Text input field" 109 + autoFocus 110 + style={[styles.textInput, pal.border, pal.text]} 111 + value={countryCode} 112 + onChangeText={value => { 113 + setCountryCode(value.toUpperCase()) 114 + setHasChanged(true) 115 + }} 116 + maxLength={2} 117 + placeholder="BR" 118 + placeholderTextColor={pal.colors.textLight} 119 + onSubmitEditing={submit} 120 + accessibilityHint={_( 121 + msg`Input 2 letter ISO 3166-1 country code to use as location`, 122 + )} 123 + /> 124 + 125 + <View style={isWeb && [a.flex_row, a.justify_end]}> 126 + <Button 127 + label={hasChanged ? _(msg`Save location`) : _(msg`Done`)} 128 + size="large" 129 + onPress={submit} 130 + variant="solid" 131 + color="primary"> 132 + <ButtonText> 133 + {hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>} 134 + </ButtonText> 135 + </Button> 136 + </View> 137 + </View> 138 + 139 + <Dialog.Close /> 140 + </Dialog.ScrollableInner> 141 + </Dialog.Outer> 142 + ) 143 + } 144 + 145 + function ConstellationInstanceDialog({ 146 + control, 147 + }: { 148 + control: Dialog.DialogControlProps 149 + }) { 150 + const pal = usePalette('default') 151 + const {_} = useLingui() 152 + 153 + const [url, setUrl] = useState('') 154 + const setConstellationInstance = useSetConstellationInstance() 155 + 156 + const submit = () => { 157 + setConstellationInstance(url) 158 + control.close() 159 + } 160 + 161 + const shouldDisable = () => { 162 + try { 163 + return !new URL(url).hostname.includes('.') 164 + } catch (e) { 165 + return true 166 + } 167 + } 168 + 169 + return ( 170 + <Dialog.Outer 171 + control={control} 172 + nativeOptions={{preventExpansion: true}} 173 + onClose={() => setUrl('')}> 174 + <Dialog.Handle /> 175 + <Dialog.ScrollableInner label={_(msg`Constellations instance URL`)}> 176 + <View style={[a.gap_sm, a.pb_lg]}> 177 + <Text style={[a.text_2xl, a.font_bold]}> 178 + <Trans>Constellations instance URL</Trans> 179 + </Text> 180 + </View> 181 + 182 + <View style={a.gap_lg}> 183 + <Dialog.Input 184 + label="Text input field" 185 + autoFocus 186 + style={[styles.textInput, pal.border, pal.text]} 187 + onChangeText={value => { 188 + setUrl(value) 189 + }} 190 + placeholder={persisted.defaults.constellationInstance} 191 + placeholderTextColor={pal.colors.textLight} 192 + onSubmitEditing={submit} 193 + accessibilityHint={_( 194 + msg`Input the url of the constellations instance to use`, 195 + )} 196 + /> 197 + 198 + <View style={isWeb && [a.flex_row, a.justify_end]}> 199 + <Button 200 + label={_(msg`Save`)} 201 + size="large" 202 + onPress={submit} 203 + variant="solid" 204 + color="primary" 205 + disabled={shouldDisable()}> 206 + <ButtonText> 207 + <Trans>Save</Trans> 208 + </ButtonText> 209 + </Button> 210 + </View> 211 + </View> 212 + 213 + <Dialog.Close /> 214 + </Dialog.ScrollableInner> 215 + </Dialog.Outer> 216 + ) 217 + } 218 + 219 + const TrustedVerifiers = (): React.ReactNode => { 220 + const trusted = useBlackskyVerificationTrusted() 221 + const moderationOpts = useModerationOpts() 222 + 223 + const results = useProfilesQuery({ 224 + handles: Array.from(trusted), 225 + }) 226 + 227 + const {gtMobile} = useBreakpoints() 228 + 229 + return ( 230 + results.data && 231 + moderationOpts !== undefined && ( 232 + <View style={[gtMobile ? a.pl_md : a.pl_sm, a.pb_sm]}> 233 + {results.data.profiles.map(profile => ( 234 + <SearchProfileCard 235 + key={profile.did} 236 + profile={profile as ProfileViewBasic} 237 + moderationOpts={moderationOpts} 238 + /> 239 + ))} 240 + </View> 241 + ) 242 + ) 243 + } 244 + 245 + export function BlackskySettingsScreen({}: Props) { 246 + const {_} = useLingui() 247 + 248 + const goLinksEnabled = useGoLinksEnabled() 249 + const setGoLinksEnabled = useSetGoLinksEnabled() 250 + 251 + const constellationEnabled = useConstellationEnabled() 252 + const setConstellationEnabled = useSetConstellationEnabled() 253 + 254 + const directFetchRecords = useDirectFetchRecords() 255 + const setDirectFetchRecords = useSetDirectFetchRecords() 256 + 257 + const noAppLabelers = useNoAppLabelers() 258 + const setNoAppLabelers = useSetNoAppLabelers() 259 + 260 + const noDiscoverFallback = useNoDiscoverFallback() 261 + const setNoDiscoverFallback = useSetNoDiscoverFallback() 262 + 263 + const hideFollowNotifications = useHideFollowNotifications() 264 + const setHideFollowNotifications = useSetHideFollowNotifications() 265 + 266 + const location = useGeolocation() 267 + const setLocationControl = Dialog.useDialogControl() 268 + 269 + const constellationInstance = useConstellationInstance() 270 + const setConstellationInstanceControl = Dialog.useDialogControl() 271 + 272 + const blackskyVerificationEnabled = useBlackskyVerificationEnabled() 273 + const setBlackskyVerificationEnabled = useSetBlackskyVerificationEnabled() 274 + 275 + const repostCarouselEnabled = useRepostCarouselEnabled() 276 + const setRepostCarouselEnabled = useSetRepostCarouselEnabled() 277 + 278 + const showLinkInHandle = useShowLinkInHandle() 279 + const setShowLinkInHandle = useSetShowLinkInHandle() 280 + 281 + const [gates, setGatesView] = useState(Object.fromEntries(useGatesCache())) 282 + const dangerousSetGate = useDangerousSetGate() 283 + const setGate = (gate: Gate, value: boolean) => { 284 + dangerousSetGate(gate, value) 285 + setGatesView({ 286 + ...gates, 287 + [gate]: value, 288 + }) 289 + } 290 + 291 + return ( 292 + <Layout.Screen> 293 + <Layout.Header.Outer> 294 + <Layout.Header.BackButton /> 295 + <Layout.Header.Content> 296 + <Layout.Header.TitleText> 297 + <Trans>Blacksky</Trans> 298 + </Layout.Header.TitleText> 299 + </Layout.Header.Content> 300 + <Layout.Header.Slot /> 301 + </Layout.Header.Outer> 302 + <Layout.Content> 303 + <SettingsList.Container> 304 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 305 + <SettingsList.ItemIcon icon={BlackskyIcon} /> 306 + <SettingsList.ItemText> 307 + <Trans>Redirects</Trans> 308 + </SettingsList.ItemText> 309 + <Toggle.Item 310 + name="use_go_links" 311 + label={_(msg`Redirect through go.bsky.app`)} 312 + value={goLinksEnabled ?? false} 313 + onChange={value => setGoLinksEnabled(value)} 314 + style={[a.w_full]}> 315 + <Toggle.LabelText style={[a.flex_1]}> 316 + <Trans>Redirect through go.bsky.app</Trans> 317 + </Toggle.LabelText> 318 + <Toggle.Platform /> 319 + </Toggle.Item> 320 + </SettingsList.Group> 321 + 322 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 323 + <SettingsList.ItemIcon icon={VisibilityIcon} /> 324 + <SettingsList.ItemText> 325 + <Trans>Visibility</Trans> 326 + </SettingsList.ItemText> 327 + <Toggle.Item 328 + name="direct_fetch_records" 329 + label={_( 330 + msg`Fetch records directly from PDS to see through quote blocks`, 331 + )} 332 + value={directFetchRecords} 333 + onChange={value => setDirectFetchRecords(value)} 334 + style={[a.w_full]}> 335 + <Toggle.LabelText style={[a.flex_1]}> 336 + <Trans> 337 + Fetch records directly from PDS to see contents of blocked and 338 + detatched quotes 339 + </Trans> 340 + </Toggle.LabelText> 341 + <Toggle.Platform /> 342 + </Toggle.Item> 343 + <Toggle.Item 344 + name="constellation_fallback" 345 + label={_( 346 + msg`Fall back to constellation api to find blocked replies`, 347 + )} 348 + disabled={true} 349 + value={constellationEnabled} 350 + onChange={value => setConstellationEnabled(value)} 351 + style={[a.w_full]}> 352 + <Toggle.LabelText style={[a.flex_1]}> 353 + <Trans> 354 + TODO: Fall back to constellation api to find blocked replies 355 + </Trans> 356 + </Toggle.LabelText> 357 + <Toggle.Platform /> 358 + </Toggle.Item> 359 + </SettingsList.Group> 360 + 361 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 362 + <SettingsList.ItemIcon icon={VerifiedIcon} /> 363 + <SettingsList.ItemText> 364 + <Trans>Verification</Trans> 365 + </SettingsList.ItemText> 366 + <Toggle.Item 367 + name="custom_verifications" 368 + label={_( 369 + msg`Select your own set of trusted verifiers, and operate as a verifier`, 370 + )} 371 + value={blackskyVerificationEnabled} 372 + onChange={value => setBlackskyVerificationEnabled(value)} 373 + style={[a.w_full]}> 374 + <Toggle.LabelText style={[a.flex_1]}> 375 + <Trans> 376 + Select your own set of trusted verifiers, and operate as a 377 + verifier 378 + </Trans> 379 + </Toggle.LabelText> 380 + <Toggle.Platform /> 381 + </Toggle.Item> 382 + </SettingsList.Group> 383 + 384 + <SettingsList.Item> 385 + <Admonition type="warning" style={[a.flex_1]}> 386 + <Trans> 387 + WIP. May slow down the client or fail to find all labels. Revoke 388 + and grant trust in the meatball menu on a profile.{' '} 389 + {blackskyVerificationEnabled 390 + ? 'You currently' 391 + : 'If enabled, you would'}{' '} 392 + trust the following verifiers: 393 + </Trans> 394 + </Admonition> 395 + </SettingsList.Item> 396 + 397 + <TrustedVerifiers /> 398 + 399 + <SettingsList.Item> 400 + <SettingsList.ItemIcon icon={StarIcon} /> 401 + <SettingsList.ItemText> 402 + <Trans>{`Constellation Instance`}</Trans> 403 + </SettingsList.ItemText> 404 + <SettingsList.BadgeButton 405 + label={_(msg`Change`)} 406 + onPress={() => setConstellationInstanceControl.open()} 407 + /> 408 + </SettingsList.Item> 409 + <SettingsList.Item> 410 + <Admonition type="info" style={[a.flex_1]}> 411 + <Trans> 412 + Constellation is used to supplement AppView responses for custom 413 + verifications and nuclear block bypass, via backlinks. Current 414 + instance: {constellationInstance} 415 + </Trans> 416 + </Admonition> 417 + </SettingsList.Item> 418 + 419 + <SettingsList.Item> 420 + <SettingsList.ItemIcon icon={GlobeIcon} /> 421 + <SettingsList.ItemText> 422 + <Trans>{`ISO 3166-1 Location (currently ${ 423 + location.geolocation?.countryCode ?? '?' 424 + })`}</Trans> 425 + </SettingsList.ItemText> 426 + <SettingsList.BadgeButton 427 + label={_(msg`Change`)} 428 + onPress={() => setLocationControl.open()} 429 + /> 430 + </SettingsList.Item> 431 + <SettingsList.Item> 432 + <Admonition type="info" style={[a.flex_1]}> 433 + <Trans> 434 + Geolocation country code informs required regional app labelers 435 + and currency behavior. 436 + </Trans> 437 + </Admonition> 438 + </SettingsList.Item> 439 + 440 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 441 + <SettingsList.ItemIcon icon={RaisingHandIcon} /> 442 + <SettingsList.ItemText> 443 + <Trans>Labelers</Trans> 444 + </SettingsList.ItemText> 445 + <Toggle.Item 446 + name="no_app_labelers" 447 + label={_(msg`Do not declare any app labelers`)} 448 + value={noAppLabelers} 449 + onChange={value => setNoAppLabelers(value)} 450 + style={[a.w_full]}> 451 + <Toggle.LabelText style={[a.flex_1]}> 452 + <Trans>Do not declare any default app labelers</Trans> 453 + </Toggle.LabelText> 454 + <Toggle.Platform /> 455 + </Toggle.Item> 456 + </SettingsList.Group> 457 + 458 + <SettingsList.Item> 459 + <Admonition type="warning" style={[a.flex_1]}> 460 + <Trans>Restart app after changing this setting.</Trans> 461 + </Admonition> 462 + </SettingsList.Item> 463 + <SettingsList.Item> 464 + <Admonition type="tip" style={[a.flex_1]}> 465 + <Trans> 466 + Some appviews will default to using an app labeler if you have 467 + no labelers, so consider subscribing to at least one labeler if 468 + you have issues. 469 + </Trans> 470 + </Admonition> 471 + </SettingsList.Item> 472 + <SettingsList.Item> 473 + <Admonition type="info" style={[a.flex_1]}> 474 + <Trans> 475 + App labelers are mandatory top-level labelers that can perform 476 + "takedowns". This setting does not influence geolocation based 477 + labelers. 478 + </Trans> 479 + </Admonition> 480 + </SettingsList.Item> 481 + 482 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 483 + <SettingsList.ItemIcon icon={PaintRollerIcon} /> 484 + <SettingsList.ItemText> 485 + <Trans>Tweaks</Trans> 486 + </SettingsList.ItemText> 487 + <Toggle.Item 488 + name="repost_carousel" 489 + label={_(msg`Combine reposts into a horizontal carousel`)} 490 + value={repostCarouselEnabled} 491 + onChange={value => setRepostCarouselEnabled(value)} 492 + style={[a.w_full]}> 493 + <Toggle.LabelText style={[a.flex_1]}> 494 + <Trans>Combine reposts into a horizontal carousel</Trans> 495 + </Toggle.LabelText> 496 + <Toggle.Platform /> 497 + </Toggle.Item> 498 + <Toggle.Item 499 + name="no_discover_fallback" 500 + label={_(msg`Do not fall back to discover feed`)} 501 + value={noDiscoverFallback} 502 + onChange={value => setNoDiscoverFallback(value)} 503 + style={[a.w_full]}> 504 + <Toggle.LabelText style={[a.flex_1]}> 505 + <Trans>Do not fall back to discover feed</Trans> 506 + </Toggle.LabelText> 507 + <Toggle.Platform /> 508 + </Toggle.Item> 509 + <Toggle.Item 510 + name="show_link_in_handle" 511 + label={_( 512 + msg`On non-bsky.social handles, show a link to that URL`, 513 + )} 514 + value={showLinkInHandle} 515 + onChange={value => setShowLinkInHandle(value)} 516 + style={[a.w_full]}> 517 + <Toggle.LabelText style={[a.flex_1]}> 518 + <Trans> 519 + On non-bsky.social handles, show a link to that URL 520 + </Trans> 521 + </Toggle.LabelText> 522 + <Toggle.Platform /> 523 + </Toggle.Item> 524 + </SettingsList.Group> 525 + 526 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 527 + <SettingsList.ItemIcon icon={BellIcon} /> 528 + <SettingsList.ItemText> 529 + <Trans>Notification Filters</Trans> 530 + </SettingsList.ItemText> 531 + <Toggle.Item 532 + name="hide_follow_notifications" 533 + label={_(msg`Hide follow notifications`)} 534 + value={hideFollowNotifications ?? false} 535 + onChange={value => setHideFollowNotifications(value)} 536 + style={[a.w_full]}> 537 + <Toggle.LabelText style={[a.flex_1]}> 538 + <Trans>Hide follow notifications</Trans> 539 + </Toggle.LabelText> 540 + <Toggle.Platform /> 541 + </Toggle.Item> 542 + </SettingsList.Group> 543 + 544 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 545 + <SettingsList.ItemIcon icon={BeakerIcon} /> 546 + <SettingsList.ItemText> 547 + <Trans>Gates</Trans> 548 + </SettingsList.ItemText> 549 + {Object.entries(gates).map(([gate, status]) => ( 550 + <Toggle.Item 551 + key={gate} 552 + name={gate} 553 + label={gate} 554 + value={status} 555 + onChange={value => setGate(gate as Gate, value)} 556 + style={[a.w_full]}> 557 + <Toggle.LabelText style={[a.flex_1]}>{gate}</Toggle.LabelText> 558 + <Toggle.Platform /> 559 + </Toggle.Item> 560 + ))} 561 + <SettingsList.BadgeButton 562 + label={_(msg`Reset gates`)} 563 + onPress={() => { 564 + resetBlackskyGateCache() 565 + setGatesView({}) 566 + }} 567 + /> 568 + </SettingsList.Group> 569 + 570 + <SettingsList.Item> 571 + <Admonition type="warning" style={[a.flex_1]}> 572 + <Trans> 573 + These settings might summon nasel demons! Restart the app after 574 + changing if anything breaks. 575 + </Trans> 576 + </Admonition> 577 + </SettingsList.Item> 578 + </SettingsList.Container> 579 + </Layout.Content> 580 + <GeolocationSettingsDialog control={setLocationControl} /> 581 + <ConstellationInstanceDialog control={setConstellationInstanceControl} /> 582 + </Layout.Screen> 583 + ) 584 + } 585 + 586 + const styles = { 587 + textInput: { 588 + borderWidth: 1, 589 + borderRadius: 6, 590 + paddingHorizontal: 14, 591 + paddingVertical: 10, 592 + fontSize: 16, 593 + }, 594 + }
-43
src/screens/Settings/Settings.tsx
··· 25 25 import {useModerationOpts} from '#/state/preferences/moderation-opts' 26 26 import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' 27 27 import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' 28 - import {useAgent} from '#/state/session' 29 28 import {type SessionAccount, useSession, useSessionApi} from '#/state/session' 30 29 import {useOnboardingDispatch} from '#/state/shell' 31 30 import {useLoggedOutViewControls} from '#/state/shell/logged-out' ··· 34 33 import {UserAvatar} from '#/view/com/util/UserAvatar' 35 34 import * as SettingsList from '#/screens/Settings/components/SettingsList' 36 35 import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf' 37 - import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice' 38 36 import {AvatarStackWithFetch} from '#/components/AvatarStack' 39 - import {Button, ButtonText} from '#/components/Button' 40 37 import {useDialogControl} from '#/components/Dialog' 41 38 import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 42 39 import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' ··· 60 57 import * as Layout from '#/components/Layout' 61 58 import {Loader} from '#/components/Loader' 62 59 import * as Menu from '#/components/Menu' 63 - import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' 64 60 import * as Prompt from '#/components/Prompt' 65 61 import {Text} from '#/components/Typography' 66 62 import {useFullVerificationState} from '#/components/verification' ··· 69 65 VerificationCheckButton, 70 66 } from '#/components/verification/VerificationCheckButton' 71 67 import {IS_INTERNAL} from '#/env' 72 - import {device, useStorage} from '#/storage' 73 68 import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' 74 69 75 70 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> ··· 103 98 </Layout.Header.Outer> 104 99 <Layout.Content> 105 100 <SettingsList.Container> 106 - <AgeAssuranceDismissibleNotice style={[a.px_lg, a.pt_xs, a.pb_xl]} /> 107 - 108 101 <View 109 102 style={[ 110 103 a.px_xl, ··· 367 360 368 361 function DevOptions() { 369 362 const {_} = useLingui() 370 - const agent = useAgent() 371 - const [override, setOverride] = useStorage(device, [ 372 - 'policyUpdateDebugOverride', 373 - ]) 374 363 const onboardingDispatch = useOnboardingDispatch() 375 364 const navigation = useNavigation<NavigationProp>() 376 365 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() ··· 511 500 </SettingsList.PressableItem> 512 501 ) : null} 513 502 514 - <SettingsList.Divider /> 515 - <View style={[a.p_xl, a.gap_md]}> 516 - <Text style={[a.text_lg, a.font_bold]}>PolicyUpdate202508 Debug</Text> 517 - 518 - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_md]}> 519 - <Button 520 - onPress={() => { 521 - setOverride(!override) 522 - }} 523 - label="Toggle" 524 - color={override ? 'primary' : 'secondary'} 525 - size="small" 526 - style={[a.flex_1]}> 527 - <ButtonText> 528 - {override ? 'Disable debug mode' : 'Enable debug mode'} 529 - </ButtonText> 530 - </Button> 531 - 532 - <Button 533 - onPress={() => { 534 - device.set([PolicyUpdate202508], false) 535 - agent.bskyAppRemoveNuxs([PolicyUpdate202508]) 536 - Toast.show(`Done`, 'info') 537 - }} 538 - label="Reset policy update nux" 539 - color="secondary" 540 - size="small" 541 - disabled={!override}> 542 - <ButtonText>Reset state</ButtonText> 543 - </Button> 544 - </View> 545 - </View> 546 503 <SettingsList.Divider /> 547 504 </> 548 505 )
+77 -8
src/screens/Signup/StepInfo/index.tsx
··· 5 5 import * as EmailValidator from 'email-validator' 6 6 import type tldts from 'tldts' 7 7 8 + import {DEFAULT_SERVICE} from '#/lib/constants' 8 9 import {isEmailMaybeInvalid} from '#/lib/strings/email' 9 10 import {logger} from '#/logger' 11 + import {isWeb} from '#/platform/detection' 10 12 import {ScreenTransition} from '#/screens/Login/ScreenTransition' 11 13 import {is13, is18, useSignupContext} from '#/screens/Signup/state' 12 14 import {Policies} from '#/screens/Signup/StepInfo/Policies' 13 15 import {atoms as a, native} from '#/alf' 16 + import {Button, ButtonText} from '#/components/Button' 17 + import {Divider} from '#/components/Divider' 14 18 import * as DateField from '#/components/forms/DateField' 15 19 import {type DateFieldRef} from '#/components/forms/DateField/types' 16 20 import {FormError} from '#/components/forms/FormError' ··· 19 23 import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' 20 24 import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 21 25 import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' 26 + import {InlineLinkText} from '#/components/Link' 22 27 import {Loader} from '#/components/Loader' 23 - import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate' 28 + import {Text} from '#/components/Typography' 24 29 import {BackNextButtons} from '../BackNextButtons' 25 30 26 31 function sanitizeDate(date: Date): Date { ··· 35 40 36 41 export function StepInfo({ 37 42 onPressBack, 43 + onPressSignIn, 38 44 isServerError, 39 45 refetchServer, 40 46 isLoadingStarterPack, 41 47 }: { 42 48 onPressBack: () => void 49 + onPressSignIn: () => void 43 50 isServerError: boolean 44 51 refetchServer: () => void 45 52 isLoadingStarterPack: boolean 46 53 }) { 47 54 const {_} = useLingui() 48 55 const {state, dispatch} = useSignupContext() 49 - const preemptivelyCompleteActivePolicyUpdate = 50 - usePreemptivelyCompleteActivePolicyUpdate() 51 56 52 57 const inviteCodeValueRef = useRef<string>(state.inviteCode) 53 58 const emailValueRef = useRef<string>(state.email) ··· 81 86 return 82 87 } 83 88 89 + if (state.serviceUrl === DEFAULT_SERVICE) { 90 + return dispatch({ 91 + type: 'setError', 92 + value: _( 93 + msg`Please choose a 3rd party service host, or sign up on bsky.app.`, 94 + ), 95 + }) 96 + } 97 + 84 98 if (state.serviceDescription?.inviteCodeRequired && !inviteCode) { 85 99 return dispatch({ 86 100 type: 'setError', ··· 132 146 }) 133 147 } 134 148 135 - preemptivelyCompleteActivePolicyUpdate() 136 149 dispatch({type: 'setInviteCode', value: inviteCode}) 137 150 dispatch({type: 'setEmail', value: email}) 138 151 dispatch({type: 'setPassword', value: password}) ··· 148 161 149 162 return ( 150 163 <ScreenTransition> 151 - <View style={[a.gap_md, a.pt_lg]}> 164 + <View style={[a.gap_md]}> 165 + {state.serviceUrl === DEFAULT_SERVICE && ( 166 + <View style={[a.gap_xl]}> 167 + <Text style={[a.gap_md, a.leading_normal]}> 168 + <Trans> 169 + blacksky.community is part of the{' '} 170 + { 171 + <InlineLinkText 172 + label={_(msg`ATmosphere`)} 173 + to="https://atproto.com/"> 174 + <Trans>ATmosphere</Trans> 175 + </InlineLinkText> 176 + } 177 + —the network of apps, services, and accounts built on the AT 178 + Protocol. 179 + </Trans> 180 + </Text> 181 + <Text style={[a.gap_md, a.leading_normal]}> 182 + <Trans> 183 + If you have one, sign in with an existing Bluesky account. 184 + </Trans> 185 + </Text> 186 + <View style={isWeb && [a.flex_row, a.justify_center]}> 187 + <Button 188 + testID="signInButton" 189 + onPress={onPressSignIn} 190 + label={_(msg`Sign in with ATmosphere`)} 191 + accessibilityHint={_( 192 + msg`Opens flow to sign in to your existing ATmosphere account`, 193 + )} 194 + size="large" 195 + variant="solid" 196 + color="primary"> 197 + <ButtonText> 198 + <Trans>Sign in with ATmosphere</Trans> 199 + </ButtonText> 200 + </Button> 201 + </View> 202 + <Divider style={[a.mb_xl]} /> 203 + </View> 204 + )} 152 205 <FormError error={state.error} /> 153 206 <HostingProvider 154 - minimal 155 207 serviceUrl={state.serviceUrl} 156 208 onSelectServiceUrl={v => dispatch({type: 'setServiceUrl', value: v})} 157 209 /> 210 + {state.serviceUrl === DEFAULT_SERVICE && ( 211 + <Text style={[a.gap_md, a.leading_normal, a.mt_md]}> 212 + <Trans> 213 + Don't have an account provider or an existing Bluesky account? To 214 + create a new account on a Bluesky-hosted PDS, sign up through{' '} 215 + { 216 + <InlineLinkText label={_(msg`bsky.app`)} to="https://bsky.app"> 217 + <Trans>bsky.app</Trans> 218 + </InlineLinkText> 219 + }{' '} 220 + first, then return to blacksky.community and log in with the 221 + account you created. 222 + </Trans> 223 + </Text> 224 + )} 158 225 {state.isLoading || isLoadingStarterPack ? ( 159 226 <View style={[a.align_center]}> 160 227 <Loader size="xl" /> 161 228 </View> 162 - ) : state.serviceDescription ? ( 229 + ) : state.serviceDescription && state.serviceUrl !== DEFAULT_SERVICE ? ( 163 230 <> 164 231 {state.serviceDescription.inviteCodeRequired && ( 165 232 <View> ··· 284 351 ) : undefined} 285 352 </View> 286 353 <BackNextButtons 287 - hideNext={!is13(state.dateOfBirth)} 354 + hideNext={ 355 + !is13(state.dateOfBirth) || state.serviceUrl === DEFAULT_SERVICE 356 + } 288 357 showRetry={isServerError} 289 358 isLoading={state.isLoading} 290 359 onBackPress={onPressBack}
+11 -4
src/screens/Signup/index.tsx
··· 32 32 import {GCP_PROJECT_ID} from '#/env' 33 33 import * as bsky from '#/types/bsky' 34 34 35 - export function Signup({onPressBack}: {onPressBack: () => void}) { 35 + export function Signup({ 36 + onPressBack, 37 + onPressSignIn, 38 + }: { 39 + onPressBack: () => void 40 + onPressSignIn: () => void 41 + }) { 36 42 const {_} = useLingui() 37 43 const t = useTheme() 38 44 const [state, dispatch] = useReducer(reducer, initialState) ··· 120 126 <LoggedOutLayout 121 127 leadin="" 122 128 title={_(msg`Create Account`)} 123 - description={_(msg`We're so excited to have you join us!`)} 129 + description={_(msg`Welcome to the ATmosphere!`)} 124 130 scrollable> 125 131 <View testID="createAccount" style={a.flex_1}> 126 132 {showStarterPackCard && ··· 170 176 </Text> 171 177 <Text style={[a.text_3xl, a.font_heavy]}> 172 178 {state.activeStep === SignupStep.INFO ? ( 173 - <Trans>Your account</Trans> 179 + <Trans>The ATmosphere ✨</Trans> 174 180 ) : state.activeStep === SignupStep.HANDLE ? ( 175 181 <Trans>Choose your username</Trans> 176 182 ) : ( ··· 183 189 {state.activeStep === SignupStep.INFO ? ( 184 190 <StepInfo 185 191 onPressBack={onPressBack} 192 + onPressSignIn={onPressSignIn} 186 193 isLoadingStarterPack={ 187 194 isFetchingStarterPack && !isErrorStarterPack 188 195 } ··· 212 219 label={_(msg`Contact support`)} 213 220 to={FEEDBACK_FORM_URL({email: state.email})} 214 221 style={[!gtMobile && a.text_md]}> 215 - <Trans>Contact support</Trans> 222 + <Trans>Open a Github Issue</Trans> 216 223 </InlineLinkText> 217 224 </Text> 218 225 </View>
+3 -1
src/state/cache/profile-shadow.ts
··· 24 24 import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery' 25 25 import {findAllProfilesInQueryData as findAllProfilesInPostThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' 26 26 import type * as bsky from '#/types/bsky' 27 + import {useBlackskyVerificationProfileOverlay} from '../queries/blacksky-verification' 27 28 import {castAsShadow, type Shadow} from './types' 28 29 29 30 export type {Shadow} from './types' ··· 63 64 } 64 65 }, [profile]) 65 66 66 - return useMemo(() => { 67 + const shadowed = useMemo(() => { 67 68 if (shadow) { 68 69 return mergeShadow(profile, shadow) 69 70 } else { 70 71 return castAsShadow(profile) 71 72 } 72 73 }, [profile, shadow]) 74 + return useBlackskyVerificationProfileOverlay(shadowed) 73 75 } 74 76 75 77 /**
+39
src/state/persisted/schema.ts
··· 123 123 kawaii: z.boolean().optional(), 124 124 hasCheckedForStarterPack: z.boolean().optional(), 125 125 subtitlesEnabled: z.boolean().optional(), 126 + 127 + // blacksky 128 + goLinksEnabled: z.boolean().optional(), 129 + constellationEnabled: z.boolean().optional(), 130 + directFetchRecords: z.boolean().optional(), 131 + noAppLabelers: z.boolean().optional(), 132 + noDiscoverFallback: z.boolean().optional(), 133 + repostCarouselEnabled: z.boolean().optional(), 134 + hideFollowNotifications: z.boolean().optional(), 135 + constellationInstance: z.string().optional(), 136 + showLinkInHandle: z.boolean().optional(), 137 + blackskyVerification: z 138 + .object({ 139 + enabled: z.boolean(), 140 + trusted: z.array(z.string()), 141 + }) 142 + .optional(), 143 + 126 144 /** @deprecated */ 127 145 mutedThreads: z.array(z.string()), 128 146 trendingDisabled: z.boolean().optional(), ··· 174 192 subtitlesEnabled: true, 175 193 trendingDisabled: false, 176 194 trendingVideoDisabled: false, 195 + 196 + // blacksky 197 + goLinksEnabled: true, 198 + constellationEnabled: false, 199 + directFetchRecords: false, 200 + noAppLabelers: false, 201 + noDiscoverFallback: false, 202 + repostCarouselEnabled: false, 203 + hideFollowNotifications: false, 204 + constellationInstance: 'https://constellation.microcosm.blue/', 205 + showLinkInHandle: false, 206 + blackskyVerification: { 207 + enabled: false, 208 + // https://blacksky.community/profile/did:plc:p2cp5gopk7mgjegy6wadk3ep/post/3lndyqyyr4k2k 209 + trusted: [ 210 + 'did:plc:z72i7hdynmk6r22z27h6tvur', 211 + 'did:plc:eclio37ymobqex2ncko63h4r', 212 + 'did:plc:inz4fkbbp7ms3ixufw6xuvdi', 213 + 'did:plc:b2kutgxqlltwc6lhs724cfwr', 214 + ], 215 + }, 177 216 } 178 217 179 218 export function tryParse(rawData: string): Schema | undefined {
+106
src/state/preferences/blacksky-verification.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['blackskyVerification'] 6 + type SetContext = (v: persisted.Schema['blackskyVerification']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.blackskyVerification, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['blackskyVerification']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState( 17 + persisted.get('blackskyVerification'), 18 + ) 19 + 20 + const setStateWrapped = React.useCallback( 21 + (blackskyVerification: persisted.Schema['blackskyVerification']) => { 22 + setState(blackskyVerification) 23 + persisted.write('blackskyVerification', blackskyVerification) 24 + }, 25 + [setState], 26 + ) 27 + 28 + React.useEffect(() => { 29 + return persisted.onUpdate( 30 + 'blackskyVerification', 31 + nextBlackskyVerification => { 32 + setState(nextBlackskyVerification) 33 + }, 34 + ) 35 + }, [setStateWrapped]) 36 + 37 + return ( 38 + <stateContext.Provider value={state}> 39 + <setContext.Provider value={setStateWrapped}> 40 + {children} 41 + </setContext.Provider> 42 + </stateContext.Provider> 43 + ) 44 + } 45 + 46 + export function useBlackskyVerification() { 47 + return ( 48 + React.useContext(stateContext) ?? persisted.defaults.blackskyVerification! 49 + ) 50 + } 51 + 52 + export function useBlackskyVerificationEnabled() { 53 + return useBlackskyVerification().enabled 54 + } 55 + 56 + export function useBlackskyVerificationTrusted( 57 + mandatory: string | undefined = undefined, 58 + ) { 59 + const trusted = new Set(useBlackskyVerification().trusted) 60 + if (mandatory) { 61 + trusted.add(mandatory) 62 + } 63 + return trusted 64 + } 65 + 66 + export function useSetBlackskyVerification() { 67 + return React.useContext(setContext) 68 + } 69 + 70 + export function useSetBlackskyVerificationEnabled() { 71 + const blackskyVerification = useBlackskyVerification() 72 + const setBlackskyVerification = useSetBlackskyVerification() 73 + 74 + return React.useMemo( 75 + () => (enabled: boolean) => 76 + setBlackskyVerification({...blackskyVerification, enabled}), 77 + [blackskyVerification, setBlackskyVerification], 78 + ) 79 + } 80 + 81 + export function useSetBlackskyVerificationTrust() { 82 + const blackskyVerification = useBlackskyVerification() 83 + const setBlackskyVerification = useSetBlackskyVerification() 84 + 85 + return React.useMemo( 86 + () => ({ 87 + add: (add: string) => { 88 + const trusted = new Set(blackskyVerification.trusted) 89 + trusted.add(add) 90 + setBlackskyVerification({ 91 + ...blackskyVerification, 92 + trusted: Array.from(trusted), 93 + }) 94 + }, 95 + remove: (remove: string) => { 96 + const trusted = new Set(blackskyVerification.trusted) 97 + trusted.delete(remove) 98 + setBlackskyVerification({ 99 + ...blackskyVerification, 100 + trusted: Array.from(trusted), 101 + }) 102 + }, 103 + }), 104 + [blackskyVerification, setBlackskyVerification], 105 + ) 106 + }
+52
src/state/preferences/constellation-enabled.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['constellationEnabled'] 6 + type SetContext = (v: persisted.Schema['constellationEnabled']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.constellationEnabled, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['constellationEnabled']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState( 17 + persisted.get('constellationEnabled'), 18 + ) 19 + 20 + const setStateWrapped = React.useCallback( 21 + (constellationEnabled: persisted.Schema['constellationEnabled']) => { 22 + setState(constellationEnabled) 23 + persisted.write('constellationEnabled', constellationEnabled) 24 + }, 25 + [setState], 26 + ) 27 + 28 + React.useEffect(() => { 29 + return persisted.onUpdate( 30 + 'constellationEnabled', 31 + nextConstellationEnabled => { 32 + setState(nextConstellationEnabled) 33 + }, 34 + ) 35 + }, [setStateWrapped]) 36 + 37 + return ( 38 + <stateContext.Provider value={state}> 39 + <setContext.Provider value={setStateWrapped}> 40 + {children} 41 + </setContext.Provider> 42 + </stateContext.Provider> 43 + ) 44 + } 45 + 46 + export function useConstellationEnabled() { 47 + return React.useContext(stateContext) 48 + } 49 + 50 + export function useSetConstellationEnabled() { 51 + return React.useContext(setContext) 52 + }
+54
src/state/preferences/constellation-instance.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['constellationInstance'] 6 + type SetContext = (v: persisted.Schema['constellationInstance']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.constellationInstance, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['constellationInstance']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState( 17 + persisted.get('constellationInstance'), 18 + ) 19 + 20 + const setStateWrapped = React.useCallback( 21 + (constellationInstance: persisted.Schema['constellationInstance']) => { 22 + setState(constellationInstance) 23 + persisted.write('constellationInstance', constellationInstance) 24 + }, 25 + [setState], 26 + ) 27 + 28 + React.useEffect(() => { 29 + return persisted.onUpdate( 30 + 'constellationInstance', 31 + nextConstellationInstance => { 32 + setState(nextConstellationInstance) 33 + }, 34 + ) 35 + }, [setStateWrapped]) 36 + 37 + return ( 38 + <stateContext.Provider value={state}> 39 + <setContext.Provider value={setStateWrapped}> 40 + {children} 41 + </setContext.Provider> 42 + </stateContext.Provider> 43 + ) 44 + } 45 + 46 + export function useConstellationInstance() { 47 + return ( 48 + React.useContext(stateContext) ?? persisted.defaults.constellationInstance! 49 + ) 50 + } 51 + 52 + export function useSetConstellationInstance() { 53 + return React.useContext(setContext) 54 + }
+47
src/state/preferences/country-code.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['countryCode'] 6 + type SetContext = (v: persisted.Schema['countryCode']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.countryCode, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['countryCode']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('countryCode')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (countryCode: persisted.Schema['countryCode']) => { 20 + setState(countryCode) 21 + persisted.write('countryCode', countryCode) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('countryCode', nextCountryCode => { 28 + setState(nextCountryCode) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useCountryCode() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetCountryCode() { 46 + return React.useContext(setContext) 47 + }
+47
src/state/preferences/direct-fetch-records.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['directFetchRecords'] 6 + type SetContext = (v: persisted.Schema['directFetchRecords']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.directFetchRecords, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['directFetchRecords']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('directFetchRecords')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (directFetchRecords: persisted.Schema['directFetchRecords']) => { 20 + setState(directFetchRecords) 21 + persisted.write('directFetchRecords', directFetchRecords) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('directFetchRecords', nextDirectFetchRecords => { 28 + setState(nextDirectFetchRecords) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useDirectFetchRecords() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetDirectFetchRecords() { 46 + return React.useContext(setContext) 47 + }
+52
src/state/preferences/hide-follow-notifications.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['hideFollowNotifications'] 6 + type SetContext = (v: persisted.Schema['hideFollowNotifications']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.hideFollowNotifications, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['hideFollowNotifications']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState( 17 + persisted.get('hideFollowNotifications'), 18 + ) 19 + 20 + const setStateWrapped = React.useCallback( 21 + (hideFollowNotifications: persisted.Schema['hideFollowNotifications']) => { 22 + setState(hideFollowNotifications) 23 + persisted.write('hideFollowNotifications', hideFollowNotifications) 24 + }, 25 + [setState], 26 + ) 27 + 28 + React.useEffect(() => { 29 + return persisted.onUpdate( 30 + 'hideFollowNotifications', 31 + nextHideFollowNotifications => { 32 + setState(nextHideFollowNotifications) 33 + }, 34 + ) 35 + }, [setStateWrapped]) 36 + 37 + return ( 38 + <stateContext.Provider value={state}> 39 + <setContext.Provider value={setStateWrapped}> 40 + {children} 41 + </setContext.Provider> 42 + </stateContext.Provider> 43 + ) 44 + } 45 + 46 + export function useHideFollowNotifications() { 47 + return React.useContext(stateContext) 48 + } 49 + 50 + export function useSetHideFollowNotifications() { 51 + return React.useContext(setContext) 52 + }
+53 -20
src/state/preferences/index.tsx
··· 1 - import React from 'react' 1 + import type React from 'react' 2 2 3 3 import {Provider as AltTextRequiredProvider} from './alt-text-required' 4 4 import {Provider as AutoplayProvider} from './autoplay' 5 + import {Provider as BlackskyVerificationProvider} from './blacksky-verification' 6 + import {Provider as ConstellationProvider} from './constellation-enabled' 7 + import {Provider as ConstellationInstanceProvider} from './constellation-instance' 8 + import {Provider as DirectFetchRecordsProvider} from './direct-fetch-records' 5 9 import {Provider as DisableHapticsProvider} from './disable-haptics' 6 10 import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' 11 + import {Provider as GoLinksProvider} from './go-links-enabled' 7 12 import {Provider as HiddenPostsProvider} from './hidden-posts' 13 + import {Provider as FollowNotificationsProvider} from './hide-follow-notifications' 8 14 import {Provider as InAppBrowserProvider} from './in-app-browser' 9 15 import {Provider as KawaiiProvider} from './kawaii' 10 16 import {Provider as LanguagesProvider} from './languages' 11 17 import {Provider as LargeAltBadgeProvider} from './large-alt-badge' 18 + import {Provider as NoAppLabelersProvider} from './no-app-labelers' 19 + import {Provider as NoDiscoverProvider} from './no-discover-fallback' 20 + import {Provider as RepostCarouselProvider} from './repost-carousel-enabled' 21 + import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle' 12 22 import {Provider as SubtitlesProvider} from './subtitles' 13 23 import {Provider as TrendingSettingsProvider} from './trending' 14 24 import {Provider as UsedStarterPacksProvider} from './used-starter-packs' ··· 23 33 useExternalEmbedsPrefs, 24 34 useSetExternalEmbedPref, 25 35 } from './external-embeds-prefs' 36 + export {useGoLinksEnabled, useSetGoLinksEnabled} from './go-links-enabled' 26 37 export * from './hidden-posts' 27 38 export {useLabelDefinitions} from './label-defs' 28 39 export {useLanguagePrefs, useLanguagePrefsApi} from './languages' ··· 32 43 return ( 33 44 <LanguagesProvider> 34 45 <AltTextRequiredProvider> 35 - <LargeAltBadgeProvider> 36 - <ExternalEmbedsProvider> 37 - <HiddenPostsProvider> 38 - <InAppBrowserProvider> 39 - <DisableHapticsProvider> 40 - <AutoplayProvider> 41 - <UsedStarterPacksProvider> 42 - <SubtitlesProvider> 43 - <TrendingSettingsProvider> 44 - <KawaiiProvider>{children}</KawaiiProvider> 45 - </TrendingSettingsProvider> 46 - </SubtitlesProvider> 47 - </UsedStarterPacksProvider> 48 - </AutoplayProvider> 49 - </DisableHapticsProvider> 50 - </InAppBrowserProvider> 51 - </HiddenPostsProvider> 52 - </ExternalEmbedsProvider> 53 - </LargeAltBadgeProvider> 46 + <GoLinksProvider> 47 + <NoAppLabelersProvider> 48 + <FollowNotificationsProvider> 49 + <DirectFetchRecordsProvider> 50 + <ConstellationProvider> 51 + <ConstellationInstanceProvider> 52 + <BlackskyVerificationProvider> 53 + <NoDiscoverProvider> 54 + <ShowLinkInHandleProvider> 55 + <LargeAltBadgeProvider> 56 + <ExternalEmbedsProvider> 57 + <HiddenPostsProvider> 58 + <InAppBrowserProvider> 59 + <DisableHapticsProvider> 60 + <AutoplayProvider> 61 + <UsedStarterPacksProvider> 62 + <SubtitlesProvider> 63 + <TrendingSettingsProvider> 64 + <RepostCarouselProvider> 65 + <KawaiiProvider> 66 + {children} 67 + </KawaiiProvider> 68 + </RepostCarouselProvider> 69 + </TrendingSettingsProvider> 70 + </SubtitlesProvider> 71 + </UsedStarterPacksProvider> 72 + </AutoplayProvider> 73 + </DisableHapticsProvider> 74 + </InAppBrowserProvider> 75 + </HiddenPostsProvider> 76 + </ExternalEmbedsProvider> 77 + </LargeAltBadgeProvider> 78 + </ShowLinkInHandleProvider> 79 + </NoDiscoverProvider> 80 + </BlackskyVerificationProvider> 81 + </ConstellationInstanceProvider> 82 + </ConstellationProvider> 83 + </DirectFetchRecordsProvider> 84 + </FollowNotificationsProvider> 85 + </NoAppLabelersProvider> 86 + </GoLinksProvider> 54 87 </AltTextRequiredProvider> 55 88 </LanguagesProvider> 56 89 )
+51
src/state/preferences/no-app-labelers.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['noAppLabelers'] 6 + type SetContext = (v: persisted.Schema['noAppLabelers']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.noAppLabelers, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['noAppLabelers']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('noAppLabelers')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (noAppLabelers: persisted.Schema['noAppLabelers']) => { 20 + setState(noAppLabelers) 21 + persisted.write('noAppLabelers', noAppLabelers) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('noAppLabelers', nextNoAppLabelers => { 28 + setState(nextNoAppLabelers) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useNoAppLabelers() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetNoAppLabelers() { 46 + return React.useContext(setContext) 47 + } 48 + 49 + export function getNoAppLabelers() { 50 + return persisted.get('noAppLabelers') || persisted.defaults.noAppLabelers! 51 + }
+47
src/state/preferences/no-discover-fallback.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['noDiscoverFallback'] 6 + type SetContext = (v: persisted.Schema['noDiscoverFallback']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.noDiscoverFallback, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['noDiscoverFallback']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('noDiscoverFallback')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (noDiscoverFallback: persisted.Schema['noDiscoverFallback']) => { 20 + setState(noDiscoverFallback) 21 + persisted.write('noDiscoverFallback', noDiscoverFallback) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('noDiscoverFallback', nextNoDiscoverFallback => { 28 + setState(nextNoDiscoverFallback) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useNoDiscoverFallback() { 42 + return React.useContext(stateContext) 43 + } 44 + 45 + export function useSetNoDiscoverFallback() { 46 + return React.useContext(setContext) 47 + }
+217
src/state/queries/blacksky-verification.ts
··· 1 + import {AppBskyGraphVerification, AtUri} from '@atproto/api' 2 + import { 3 + type VerificationState, 4 + type VerificationView, 5 + } from '@atproto/api/dist/client/types/app/bsky/actor/defs' 6 + import {useQuery} from '@tanstack/react-query' 7 + 8 + import {STALE} from '#/state/queries' 9 + import * as bsky from '#/types/bsky' 10 + import {type AnyProfileView} from '#/types/bsky/profile' 11 + import { 12 + useBlackskyVerificationEnabled, 13 + useBlackskyVerificationTrusted, 14 + } from '../preferences/blacksky-verification' 15 + import {useConstellationInstance} from '../preferences/constellation-instance' 16 + import { 17 + asUri, 18 + asyncGenCollect, 19 + asyncGenDedupe, 20 + asyncGenFilter, 21 + asyncGenTryMap, 22 + type ConstellationLink, 23 + constellationLinks, 24 + } from './constellation' 25 + import {LRU} from './direct-fetch-record' 26 + import {resolvePdsServiceUrl} from './resolve-identity' 27 + import {useCurrentAccountProfile} from './useCurrentAccountProfile' 28 + 29 + const RQKEY_ROOT = 'blacksky-verification' 30 + export const RQKEY = (did: string, trusted: Set<string>) => [ 31 + RQKEY_ROOT, 32 + did, 33 + Array.from(trusted).sort(), 34 + ] 35 + 36 + type LinkedRecord = { 37 + link: ConstellationLink 38 + record: AppBskyGraphVerification.Record 39 + } 40 + 41 + const verificationCache = new LRU<string, any>() 42 + 43 + export function getTrustedConstellationVerifications( 44 + instance: string, 45 + did: string, 46 + trusted: Set<string>, 47 + ) { 48 + const urip = new AtUri(did) 49 + const verificationLinks = constellationLinks(instance, { 50 + target: urip.host, 51 + collection: 'app.bsky.graph.verification', 52 + path: '.subject', 53 + from_dids: Array.from(trusted), 54 + }) 55 + return asyncGenDedupe( 56 + asyncGenFilter(verificationLinks, ({did}) => trusted.has(did)), 57 + ({did}) => did, 58 + ) 59 + } 60 + 61 + async function getBlackskyVerificationLinkedRecords( 62 + instance: string, 63 + did: string, 64 + trusted: Set<string>, 65 + ): Promise<LinkedRecord[] | undefined> { 66 + try { 67 + const trustedVerificationLinks = getTrustedConstellationVerifications( 68 + instance, 69 + did, 70 + trusted, 71 + ) 72 + 73 + const verificationRecords = asyncGenFilter( 74 + asyncGenTryMap<ConstellationLink, {link: ConstellationLink; record: any}>( 75 + trustedVerificationLinks, 76 + // using try map lets us: 77 + // - cache the service url and verificatin record in independent lrus 78 + // - clear the promise from the lru on failure 79 + // - skip links that cause errors 80 + async link => { 81 + const {did, rkey} = link 82 + 83 + let service = await resolvePdsServiceUrl(did) 84 + 85 + const request = `${service}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.graph.verification&rkey=${rkey}` 86 + const record = await verificationCache.getOrTryInsertWith( 87 + request, 88 + async () => { 89 + const resp = await (await fetch(request)).json() 90 + return resp.value 91 + }, 92 + ) 93 + return {link, record} 94 + }, 95 + (_, e) => { 96 + console.error(e) 97 + }, 98 + ), 99 + // the explicit return type shouldn't be needed... 100 + (d: {link: ConstellationLink; record: unknown}): d is LinkedRecord => 101 + bsky.validate<AppBskyGraphVerification.Record>( 102 + d.record, 103 + AppBskyGraphVerification.validateRecord, 104 + ), 105 + ) 106 + 107 + // Array.fromAsync will do this but not available everywhere yet 108 + return asyncGenCollect(verificationRecords) 109 + } catch (e) { 110 + console.error(e) 111 + return undefined 112 + } 113 + } 114 + 115 + function createVerificationViews( 116 + linkedRecords: LinkedRecord[], 117 + profile: AnyProfileView, 118 + ): VerificationView[] { 119 + return linkedRecords.map(({link, record}) => ({ 120 + issuer: link.did, 121 + isValid: 122 + (profile.displayName ?? '') === record.displayName && 123 + profile.handle === record.handle, 124 + createdAt: record.createdAt, 125 + uri: asUri(link), 126 + })) 127 + } 128 + 129 + function createVerificationState( 130 + verifications: VerificationView[], 131 + profile: AnyProfileView, 132 + trusted: Set<string>, 133 + ): VerificationState { 134 + return { 135 + verifications, 136 + verifiedStatus: 137 + verifications.length > 0 138 + ? verifications.findIndex(v => v.isValid) !== -1 139 + ? 'valid' 140 + : 'invalid' 141 + : 'none', 142 + trustedVerifierStatus: trusted.has(profile.did) ? 'valid' : 'none', 143 + } 144 + } 145 + 146 + export function useBlackskyVerificationState({ 147 + profile, 148 + enabled, 149 + }: { 150 + profile: AnyProfileView | undefined 151 + enabled?: boolean 152 + }) { 153 + const instance = useConstellationInstance() 154 + const currentAccountProfile = useCurrentAccountProfile() 155 + const trusted = useBlackskyVerificationTrusted(currentAccountProfile?.did) 156 + 157 + const linkedRecords = useQuery<LinkedRecord[] | undefined>({ 158 + staleTime: STALE.HOURS.ONE, 159 + queryKey: RQKEY(profile?.did || '', trusted), 160 + async queryFn() { 161 + if (!profile) return undefined 162 + 163 + return await getBlackskyVerificationLinkedRecords( 164 + instance, 165 + profile.did, 166 + trusted, 167 + ) 168 + }, 169 + enabled: enabled && profile !== undefined, 170 + }) 171 + 172 + if (linkedRecords.data === undefined || profile === undefined) return 173 + const verifications = createVerificationViews(linkedRecords.data, profile) 174 + const verificationState = createVerificationState( 175 + verifications, 176 + profile, 177 + trusted, 178 + ) 179 + 180 + return verificationState 181 + } 182 + 183 + export function useBlackskyVerificationProfileOverlay<V extends AnyProfileView>( 184 + profile: V, 185 + ): V { 186 + const enabled = useBlackskyVerificationEnabled() 187 + const verificationState = useBlackskyVerificationState({ 188 + profile, 189 + enabled, 190 + }) 191 + 192 + return enabled 193 + ? { 194 + ...profile, 195 + verification: verificationState, 196 + } 197 + : profile 198 + } 199 + 200 + export function useMaybeBlackskyVerificationProfileOverlay< 201 + V extends AnyProfileView, 202 + >(profile: V | undefined): V | undefined { 203 + const enabled = useBlackskyVerificationEnabled() 204 + const verificationState = useBlackskyVerificationState({ 205 + profile, 206 + enabled, 207 + }) 208 + 209 + if (!profile) return undefined 210 + 211 + return enabled 212 + ? { 213 + ...profile, 214 + verification: verificationState, 215 + } 216 + : profile 217 + }
+200
src/state/queries/constellation.ts
··· 1 + export type ConstellationLink = { 2 + did: `did:${string}` 3 + collection: string 4 + rkey: string 5 + } 6 + 7 + type Collection = 8 + | 'app.bsky.actor.profile' 9 + | 'app.bsky.feed.generator' 10 + | 'app.bsky.feed.like' 11 + | 'app.bsky.feed.post' 12 + | 'app.bsky.feed.repost' 13 + | 'app.bsky.feed.threadgate' 14 + | 'app.bsky.graph.block' 15 + | 'app.bsky.graph.follow' 16 + | 'app.bsky.graph.list' 17 + | 'app.bsky.graph.listblock' 18 + | 'app.bsky.graph.listitem' 19 + | 'app.bsky.graph.starterpack' 20 + | 'app.bsky.graph.verification' 21 + | 'chat.bsky.actor.declaration' 22 + 23 + const headers = new Headers({ 24 + Accept: 'application/json', 25 + 'User-Agent': 'blacksky.community (contact @aviva.gay)', 26 + }) 27 + 28 + const makeReqUrl = ( 29 + instance: string, 30 + route: string, 31 + params: Record<string, string | string[]>, 32 + ) => { 33 + const url = new URL(instance) 34 + url.pathname = route 35 + for (const [k, v] of Object.entries(params)) { 36 + // NOTE: in the future this should probably be a repeated param... 37 + if (Array.isArray(v)) { 38 + url.searchParams.set(k, v.join(',')) 39 + } else { 40 + url.searchParams.set(k, v) 41 + } 42 + } 43 + return url 44 + } 45 + 46 + // using an async generator lets us kick off dependent requests before finishing pagination 47 + // this doesn't solve the gross N+1 queries thing going on here to get records, but it should make it faster :3 48 + export async function* constellationLinks( 49 + instance: string, 50 + params: { 51 + target: string 52 + collection: Collection 53 + path: string 54 + from_dids?: string[] 55 + }, 56 + ) { 57 + const url = makeReqUrl(instance, 'links', params) 58 + 59 + const req = async () => 60 + (await (await fetch(url, {method: 'GET', headers})).json()) as { 61 + total: number 62 + linking_records: ConstellationLink[] 63 + cursor: string | null 64 + } 65 + 66 + let cursor: string | null = null 67 + while (true) { 68 + const resp = await req() 69 + 70 + for (const link of resp.linking_records) { 71 + yield link 72 + } 73 + 74 + cursor = resp.cursor 75 + if (cursor === null) break 76 + url.searchParams.set('cursor', cursor) 77 + } 78 + } 79 + 80 + export async function constellationCounts( 81 + instance: string, 82 + params: {target: string}, 83 + ) { 84 + const url = makeReqUrl(instance, 'links/all', params) 85 + const json = (await (await fetch(url, {method: 'GET', headers})).json()) as { 86 + links: { 87 + [P in Collection]?: { 88 + [k: string]: {distinct_dids: number; records: number} | undefined 89 + } 90 + } 91 + } 92 + const links = json.links 93 + return { 94 + likeCount: 95 + links?.['app.bsky.feed.like']?.['.subject.uri']?.distinct_dids ?? 0, 96 + repostCount: 97 + links?.['app.bsky.feed.repost']?.['.subject.uri']?.distinct_dids ?? 0, 98 + replyCount: 99 + links?.['app.bsky.feed.post']?.['.reply.parent.uri']?.records ?? 0, 100 + } 101 + } 102 + 103 + export function asUri(link: ConstellationLink): string { 104 + return `at://${link.did}/${link.collection}/${link.rkey}` 105 + } 106 + 107 + export async function* asyncGenMap<K, V>( 108 + gen: AsyncGenerator<K, void, unknown>, 109 + fn: (val: K) => V, 110 + ) { 111 + for await (const v of gen) { 112 + yield fn(v) 113 + } 114 + } 115 + 116 + export async function* asyncGenTryMap<K, V>( 117 + gen: AsyncGenerator<K, void, unknown>, 118 + fn: (val: K) => Promise<V>, 119 + err: (val: K, e: unknown) => void, 120 + ) { 121 + for await (const v of gen) { 122 + try { 123 + // make sure we resolve inside the try catch 124 + yield await fn(v) 125 + } catch (e) { 126 + err(v, e) 127 + } 128 + } 129 + } 130 + 131 + export function asyncGenFilter<K, V extends K>( 132 + gen: AsyncGenerator<K, void, unknown>, 133 + predicate: (item: K) => item is V, 134 + ): AsyncGenerator<Awaited<V>, void, unknown> 135 + 136 + export function asyncGenFilter<K>( 137 + gen: AsyncGenerator<K, void, unknown>, 138 + predicate: (item: K) => boolean, 139 + ): AsyncGenerator<Awaited<K>, void, unknown> 140 + 141 + export async function* asyncGenFilter<K>( 142 + gen: AsyncGenerator<K, void, unknown>, 143 + predicate: (item: K) => boolean, 144 + ) { 145 + for await (const v of gen) { 146 + if (predicate(v)) yield v 147 + } 148 + } 149 + 150 + export async function* asyncGenTake<V>( 151 + gen: AsyncGenerator<V, void, unknown>, 152 + n: number, 153 + ) { 154 + if (n <= 0) return 155 + 156 + let taken = 0 157 + for await (const v of gen) { 158 + yield v 159 + if (++taken >= n) break 160 + } 161 + } 162 + 163 + export async function* asyncGenDedupe<V, K>( 164 + gen: AsyncGenerator<V, void, unknown>, 165 + keyFn: (_: V) => K, 166 + ) { 167 + const seen = new Set<K>() 168 + for await (const v of gen) { 169 + const key = keyFn(v) 170 + if (!seen.has(key)) { 171 + seen.add(key) 172 + yield v 173 + } 174 + } 175 + } 176 + 177 + export async function asyncGenCollect<V>( 178 + gen: AsyncGenerator<V, void, unknown>, 179 + ) { 180 + const out = [] 181 + for await (const v of gen) { 182 + out.push(v) 183 + } 184 + return out 185 + } 186 + 187 + export async function asyncGenFind<V>( 188 + gen: AsyncGenerator<V, void, unknown>, 189 + predicate: (item: V) => boolean, 190 + ) { 191 + for await (const v of gen) { 192 + if (predicate(v)) return v 193 + } 194 + return undefined 195 + } 196 + 197 + export function dbg<V>(v: V): V { 198 + console.log(v) 199 + return v 200 + }
+183
src/state/queries/direct-fetch-record.ts
··· 1 + import { 2 + type AppBskyEmbedRecord, 3 + type AppBskyFeedDefs, 4 + AppBskyFeedPost, 5 + AtUri, 6 + type BskyAgent, 7 + } from '@atproto/api' 8 + import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' 9 + import {useQuery} from '@tanstack/react-query' 10 + 11 + import {retry} from '#/lib/async/retry' 12 + import {STALE} from '#/state/queries' 13 + import {useAgent} from '#/state/session' 14 + import * as bsky from '#/types/bsky' 15 + 16 + const RQKEY_ROOT = 'direct-fetch-record' 17 + export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] 18 + 19 + export async function directFetchRecordAndProfile( 20 + agent: BskyAgent, 21 + uri: string, 22 + ) { 23 + const urip = new AtUri(uri) 24 + 25 + if (!urip.host.startsWith('did:')) { 26 + const res = await agent.resolveHandle({ 27 + handle: urip.host, 28 + }) 29 + urip.host = res.data.did 30 + } 31 + 32 + try { 33 + const [profile, record] = await Promise.all([ 34 + (async () => (await agent.getProfile({actor: urip.host})).data)(), 35 + (async () => 36 + ( 37 + await retry( 38 + 2, 39 + e => { 40 + if (e.message.includes(`Could not locate record:`)) { 41 + return false 42 + } 43 + return true 44 + }, 45 + () => 46 + agent.api.com.atproto.repo.getRecord({ 47 + repo: urip.host, 48 + collection: 'app.bsky.feed.post', 49 + rkey: urip.rkey, 50 + }), 51 + ) 52 + ).data.value)(), 53 + ]) 54 + 55 + return {profile, record} 56 + } catch (e) { 57 + console.error(e) 58 + return undefined 59 + } 60 + } 61 + 62 + export async function directFetchEmbedRecord( 63 + agent: BskyAgent, 64 + uri: string, 65 + ): Promise<AppBskyEmbedRecord.ViewRecord | undefined> { 66 + const res = await directFetchRecordAndProfile(agent, uri) 67 + if (res === undefined) return undefined 68 + const {profile, record} = res 69 + 70 + if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) { 71 + return { 72 + $type: 'app.bsky.embed.record#viewRecord', 73 + uri, 74 + author: profile as ProfileViewBasic, 75 + cid: 'directfetch', 76 + value: record, 77 + indexedAt: new Date().toISOString(), 78 + } satisfies AppBskyEmbedRecord.ViewRecord 79 + } else { 80 + return undefined 81 + } 82 + } 83 + 84 + export function useDirectFetchEmbedRecord({ 85 + uri, 86 + enabled, 87 + }: { 88 + uri: string 89 + enabled?: boolean 90 + }) { 91 + const agent = useAgent() 92 + return useQuery<AppBskyEmbedRecord.ViewRecord | undefined>({ 93 + staleTime: STALE.HOURS.ONE, 94 + queryKey: RQKEY(uri || ''), 95 + async queryFn() { 96 + return directFetchEmbedRecord(agent, uri) 97 + }, 98 + enabled: enabled && !!uri, 99 + }) 100 + } 101 + 102 + export async function directFetchPostRecord( 103 + agent: BskyAgent, 104 + uri: string, 105 + ): Promise<AppBskyFeedDefs.PostView | undefined> { 106 + const res = await directFetchRecordAndProfile(agent, uri) 107 + if (res === undefined) return undefined 108 + const {profile, record} = res 109 + 110 + if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) { 111 + return { 112 + $type: 'app.bsky.feed.defs#postView', 113 + uri, 114 + author: profile as ProfileViewBasic, 115 + cid: 'directfetch', 116 + record, 117 + indexedAt: new Date().toISOString(), 118 + } satisfies AppBskyFeedDefs.PostView 119 + } else { 120 + return undefined 121 + } 122 + } 123 + 124 + // based on https://stackoverflow.com/a/46432113 125 + export class LRU<K, V> { 126 + max: number 127 + private cache: Map<K, Promise<V>> 128 + constructor(max = 1_024) { 129 + this.max = max 130 + this.cache = new Map() 131 + } 132 + 133 + get(key: K) { 134 + let item = this.cache.get(key) 135 + if (item !== undefined) { 136 + // refresh key 137 + this.cache.delete(key) 138 + this.cache.set(key, item) 139 + } 140 + return item 141 + } 142 + 143 + set(key: K, val: Promise<V>) { 144 + // refresh key 145 + if (this.cache.has(key)) this.cache.delete(key) 146 + // evict oldest 147 + else if (this.cache.size >= this.max) 148 + this.cache.delete(this.nonemptyFirst()) 149 + this.cache.set(key, val) 150 + } 151 + 152 + delete(key: K) { 153 + return this.cache.delete(key) 154 + } 155 + 156 + private nonemptyFirst() { 157 + return this.cache.keys().next().value! 158 + } 159 + 160 + async getOrInsertWith(key: K, fn: () => Promise<V>): Promise<V> { 161 + const val = this.get(key) 162 + if (val !== undefined) return val 163 + 164 + const promise = fn() 165 + this.set(key, promise) 166 + return promise 167 + } 168 + 169 + // try to insert, but remove from cache on error and bubble 170 + async getOrTryInsertWith(key: K, fn: () => Promise<V>): Promise<V> { 171 + const val = this.get(key) 172 + if (val !== undefined) return val 173 + 174 + const promise = fn() 175 + this.set(key, promise) 176 + try { 177 + return await promise 178 + } catch (e) { 179 + this.delete(key) 180 + throw e 181 + } 182 + } 183 + }
+3
src/state/queries/notifications/feed.ts
··· 32 32 useQueryClient, 33 33 } from '@tanstack/react-query' 34 34 35 + import {useHideFollowNotifications} from '#/state/preferences/hide-follow-notifications' 35 36 import {useModerationOpts} from '#/state/preferences/moderation-opts' 36 37 import {STALE} from '#/state/queries' 37 38 import {useAgent} from '#/state/session' ··· 63 64 const agent = useAgent() 64 65 const queryClient = useQueryClient() 65 66 const moderationOpts = useModerationOpts() 67 + const hideFollowNotifications = useHideFollowNotifications() 66 68 const unreads = useUnreadNotificationsApi() 67 69 const enabled = opts.enabled !== false 68 70 const filter = opts.filter ··· 111 113 cursor: pageParam, 112 114 queryClient, 113 115 moderationOpts, 116 + hideFollowNotifications, 114 117 fetchAdditionalData: true, 115 118 reasons, 116 119 })
+10 -1
src/state/queries/notifications/unread.tsx
··· 10 10 import BroadcastChannel from '#/lib/broadcast' 11 11 import {resetBadgeCount} from '#/lib/notifications/notifications' 12 12 import {logger} from '#/logger' 13 + import {useHideFollowNotifications} from '#/state/preferences/hide-follow-notifications' 13 14 import {useAgent, useSession} from '#/state/session' 14 15 import {useModerationOpts} from '../../preferences/moderation-opts' 15 16 import {truncateAndInvalidate} from '../util' ··· 49 50 const agent = useAgent() 50 51 const queryClient = useQueryClient() 51 52 const moderationOpts = useModerationOpts() 53 + const hideFollowNotifications = useHideFollowNotifications() 52 54 53 55 const [numUnread, setNumUnread] = React.useState('') 54 56 ··· 155 157 limit: 40, 156 158 queryClient, 157 159 moderationOpts, 160 + hideFollowNotifications, 158 161 reasons: [], 159 162 160 163 // only fetch subjects when the page is going to be used ··· 203 206 } 204 207 }, 205 208 } 206 - }, [setNumUnread, queryClient, moderationOpts, agent]) 209 + }, [ 210 + setNumUnread, 211 + queryClient, 212 + moderationOpts, 213 + hideFollowNotifications, 214 + agent, 215 + ]) 207 216 checkUnreadRef.current = api.checkUnread 208 217 209 218 return (
+7 -1
src/state/queries/notifications/util.ts
··· 43 43 limit, 44 44 queryClient, 45 45 moderationOpts, 46 + hideFollowNotifications, 46 47 fetchAdditionalData, 47 48 reasons, 48 49 }: { ··· 51 52 limit: number 52 53 queryClient: QueryClient 53 54 moderationOpts: ModerationOpts | undefined 55 + hideFollowNotifications: boolean | undefined 54 56 fetchAdditionalData: boolean 55 57 reasons: string[] 56 58 }): Promise<{ ··· 67 69 68 70 // filter out notifs by mod rules 69 71 const notifs = res.data.notifications.filter( 70 - notif => !shouldFilterNotif(notif, moderationOpts), 72 + notif => !shouldFilterNotif(notif, moderationOpts, hideFollowNotifications), 71 73 ) 72 74 73 75 // group notifications which are essentially similar (follows, likes on a post) ··· 118 120 export function shouldFilterNotif( 119 121 notif: AppBskyNotificationListNotifications.Notification, 120 122 moderationOpts: ModerationOpts | undefined, 123 + hideFollowNotifications: boolean | undefined, 121 124 ): boolean { 122 125 const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense) 123 126 if (containsImperative) { 127 + return true 128 + } 129 + if (hideFollowNotifications && notif.reason == 'follow') { 124 130 return true 125 131 } 126 132 if (!moderationOpts) {
+4 -1
src/state/queries/post-feed.ts
··· 40 40 import {KnownError} from '#/view/com/posts/PostFeedErrorMessage' 41 41 import {useFeedTuners} from '../preferences/feed-tuners' 42 42 import {useModerationOpts} from '../preferences/moderation-opts' 43 + import {useNoDiscoverFallback} from '../preferences/no-discover-fallback' 43 44 import {usePreferencesQuery} from './preferences' 44 45 import { 45 46 didOrHandleUriMatches, ··· 153 154 preferences?.savedFeeds?.findIndex( 154 155 f => f.pinned && f.value === 'following', 155 156 ) ?? -1 156 - const enableFollowingToDiscoverFallback = followingPinnedIndex === 0 157 + const noDiscoverFallback = useNoDiscoverFallback() 158 + const enableFollowingToDiscoverFallback = 159 + followingPinnedIndex === 0 && !noDiscoverFallback 157 160 const agent = useAgent() 158 161 const lastRun = useRef<{ 159 162 data: InfiniteData<FeedPageUnselected>
+26
src/state/queries/resolve-identity.ts
··· 1 + import {LRU} from './direct-fetch-record' 2 + 3 + const serviceCache = new LRU<`did:${string}`, string>() 4 + 5 + export async function resolvePdsServiceUrl(did: `did:${string}`) { 6 + return await serviceCache.getOrTryInsertWith(did, async () => { 7 + const docUrl = did.startsWith('did:plc:') 8 + ? `https://plc.directory/${did}` 9 + : `https://${did.substring(8)}/.well-known/did.json` 10 + 11 + // TODO: validate! 12 + const doc: { 13 + service: { 14 + serviceEndpoint: string 15 + type: string 16 + }[] 17 + } = await (await fetch(docUrl)).json() 18 + const service = doc.service.find( 19 + s => s.type === 'AtprotoPersonalDataServer', 20 + )?.serviceEndpoint 21 + 22 + if (service === undefined) 23 + throw new Error(`could not find a service for ${did}`) 24 + return service 25 + }) 26 + }
+60 -17
src/state/queries/verification/useVerificationCreateMutation.tsx
··· 1 1 import {type AppBskyActorGetProfile} from '@atproto/api' 2 - import {useMutation} from '@tanstack/react-query' 2 + import {useMutation, useQueryClient} from '@tanstack/react-query' 3 3 4 4 import {until} from '#/lib/async/until' 5 5 import {logger} from '#/logger' 6 + import { 7 + useBlackskyVerificationEnabled, 8 + useBlackskyVerificationTrusted, 9 + } from '#/state/preferences/blacksky-verification' 10 + import {useConstellationInstance} from '#/state/preferences/constellation-instance' 6 11 import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' 7 12 import {useAgent, useSession} from '#/state/session' 8 13 import type * as bsky from '#/types/bsky' 14 + import { 15 + getTrustedConstellationVerifications, 16 + RQKEY as BLACKSKY_VERIFICATION_RQKEY, 17 + } from '../blacksky-verification' 18 + import {asUri, asyncGenFind, type ConstellationLink} from '../constellation' 9 19 10 20 export function useVerificationCreateMutation() { 11 21 const agent = useAgent() 12 22 const {currentAccount} = useSession() 13 23 const updateProfileVerificationCache = useUpdateProfileVerificationCache() 24 + 25 + const qc = useQueryClient() 26 + const blackskyVerificationEnabled = useBlackskyVerificationEnabled() 27 + const blackskyVerificationTrusted = useBlackskyVerificationTrusted( 28 + currentAccount?.did, 29 + ) 30 + const constellationInstance = useConstellationInstance() 14 31 15 32 return useMutation({ 16 33 async mutationFn({profile}: {profile: bsky.profile.AnyProfileView}) { ··· 28 45 }, 29 46 ) 30 47 31 - await until( 32 - 5, 33 - 1e3, 34 - ({data: profile}: AppBskyActorGetProfile.Response) => { 35 - if ( 36 - profile.verification && 37 - profile.verification.verifications.find(v => v.uri === uri) 38 - ) { 39 - return true 40 - } 41 - return false 42 - }, 43 - () => { 44 - return agent.getProfile({actor: profile.did ?? ''}) 45 - }, 46 - ) 48 + if (blackskyVerificationEnabled) { 49 + await until( 50 + 10, 51 + 2e3, 52 + (link: ConstellationLink | undefined) => { 53 + return link !== undefined 54 + }, 55 + () => { 56 + return asyncGenFind( 57 + getTrustedConstellationVerifications( 58 + constellationInstance, 59 + profile.did, 60 + blackskyVerificationTrusted, 61 + ), 62 + link => asUri(link) === uri, 63 + ) 64 + }, 65 + ) 66 + } else { 67 + await until( 68 + 5, 69 + 1e3, 70 + ({data: profile}: AppBskyActorGetProfile.Response) => { 71 + if ( 72 + profile.verification && 73 + profile.verification.verifications.find(v => v.uri === uri) 74 + ) { 75 + return true 76 + } 77 + return false 78 + }, 79 + () => { 80 + return agent.getProfile({actor: profile.did ?? ''}) 81 + }, 82 + ) 83 + } 47 84 }, 48 85 async onSuccess(_, {profile}) { 49 86 logger.metric('verification:create', {}, {statsig: true}) 50 87 await updateProfileVerificationCache({profile}) 88 + qc.invalidateQueries({ 89 + queryKey: BLACKSKY_VERIFICATION_RQKEY( 90 + profile.did, 91 + blackskyVerificationTrusted, 92 + ), 93 + }) 51 94 }, 52 95 }) 53 96 }
+67 -18
src/state/queries/verification/useVerificationsRemoveMutation.tsx
··· 3 3 type AppBskyActorGetProfile, 4 4 AtUri, 5 5 } from '@atproto/api' 6 - import {useMutation} from '@tanstack/react-query' 6 + import {useMutation, useQueryClient} from '@tanstack/react-query' 7 7 8 8 import {until} from '#/lib/async/until' 9 9 import {logger} from '#/logger' 10 + import { 11 + useBlackskyVerificationEnabled, 12 + useBlackskyVerificationTrusted, 13 + } from '#/state/preferences/blacksky-verification' 14 + import {useConstellationInstance} from '#/state/preferences/constellation-instance' 10 15 import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' 11 16 import {useAgent, useSession} from '#/state/session' 12 17 import type * as bsky from '#/types/bsky' 18 + import { 19 + getTrustedConstellationVerifications, 20 + RQKEY as BLACKSKY_VERIFICATION_RQKEY, 21 + } from '../blacksky-verification' 22 + import { 23 + asUri, 24 + asyncGenCollect, 25 + asyncGenFilter, 26 + type ConstellationLink, 27 + } from '../constellation' 13 28 14 29 export function useVerificationsRemoveMutation() { 15 30 const agent = useAgent() 16 31 const {currentAccount} = useSession() 17 32 const updateProfileVerificationCache = useUpdateProfileVerificationCache() 18 33 34 + const qc = useQueryClient() 35 + const blackskyVerificationEnabled = useBlackskyVerificationEnabled() 36 + const blackskyVerificationTrusted = useBlackskyVerificationTrusted( 37 + currentAccount?.did, 38 + ) 39 + const constellationInstance = useConstellationInstance() 40 + 19 41 return useMutation({ 20 42 async mutationFn({ 21 43 profile, ··· 28 50 throw new Error('User not logged in') 29 51 } 30 52 31 - const uris = verifications.map(v => v.uri) 53 + const uris = new Set(verifications.map(v => v.uri)) 32 54 33 55 await Promise.all( 34 - uris.map(uri => { 56 + Array.from(uris).map(uri => { 35 57 return agent.app.bsky.graph.verification.delete({ 36 58 repo: currentAccount.did, 37 59 rkey: new AtUri(uri).rkey, ··· 39 61 }), 40 62 ) 41 63 42 - await until( 43 - 5, 44 - 1e3, 45 - ({data: profile}: AppBskyActorGetProfile.Response) => { 46 - if ( 47 - !profile.verification?.verifications.some(v => uris.includes(v.uri)) 48 - ) { 49 - return true 50 - } 51 - return false 52 - }, 53 - () => { 54 - return agent.getProfile({actor: profile.did ?? ''}) 55 - }, 56 - ) 64 + if (blackskyVerificationEnabled) { 65 + await until( 66 + 10, 67 + 2e3, 68 + (link: ConstellationLink[]) => { 69 + return link.length === 0 70 + }, 71 + () => 72 + asyncGenCollect( 73 + asyncGenFilter( 74 + getTrustedConstellationVerifications( 75 + constellationInstance, 76 + profile.did, 77 + blackskyVerificationTrusted, 78 + ), 79 + link => uris.has(asUri(link)), 80 + ), 81 + ), 82 + ) 83 + } else { 84 + await until( 85 + 5, 86 + 1e3, 87 + ({data: profile}: AppBskyActorGetProfile.Response) => { 88 + if ( 89 + !profile.verification?.verifications.some(v => uris.has(v.uri)) 90 + ) { 91 + return true 92 + } 93 + return false 94 + }, 95 + () => { 96 + return agent.getProfile({actor: profile.did ?? ''}) 97 + }, 98 + ) 99 + } 57 100 }, 58 101 async onSuccess(_, {profile}) { 59 102 logger.metric('verification:revoke', {}, {statsig: true}) 60 103 await updateProfileVerificationCache({profile}) 104 + qc.invalidateQueries({ 105 + queryKey: BLACKSKY_VERIFICATION_RQKEY( 106 + profile.did, 107 + blackskyVerificationTrusted, 108 + ), 109 + }) 61 110 }, 62 111 }) 63 112 }
+5 -2
src/state/session/moderation.ts
··· 1 1 import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api' 2 2 3 3 import {IS_TEST_USER} from '#/lib/constants' 4 + import {getNoAppLabelers} from '../preferences/no-app-labelers' 4 5 import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities' 5 6 import {readLabelers} from './agent-config' 6 - import {SessionAccount} from './types' 7 + import {type SessionAccount} from './types' 7 8 8 9 export function configureModerationForGuest() { 9 10 // This global mutation is *only* OK because this code is only relevant for testing. ··· 38 39 } 39 40 40 41 function switchToBskyAppLabeler() { 41 - BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) 42 + BskyAgent.configure({ 43 + appLabelers: getNoAppLabelers() ? [] : [BSKY_LABELER_DID], 44 + }) 42 45 } 43 46 44 47 async function trySwitchToTestAppLabeler(agent: BskyAgent) {
+2 -2
src/style.css
··· 109 109 pointer-events: none; 110 110 } 111 111 .ProseMirror .mention { 112 - color: #0085ff; 112 + color: rgb(75, 155, 108); 113 113 } 114 114 .ProseMirror a, 115 115 .ProseMirror .autolink { 116 - color: #0085ff; 116 + color: rgb(75, 155, 108); 117 117 } 118 118 /* OLLIE: TODO -- this is not accessible */ 119 119 /* Remove focus state on inputs */
+4
src/view/com/auth/LoggedOut.tsx
··· 115 115 onPressBack={() => 116 116 setScreenState(ScreenState.S_LoginOrCreateAccount) 117 117 } 118 + onPressSignIn={() => { 119 + setScreenState(ScreenState.S_Login) 120 + logEvent('splash:signInPressed', {}) 121 + }} 118 122 /> 119 123 ) : undefined} 120 124 </ErrorBoundary>
+3 -15
src/view/com/auth/SplashScreen.web.tsx
··· 165 165 t.atoms.border_contrast_medium, 166 166 ]}> 167 167 <InlineLinkText 168 - label={_(msg`Learn more about Bluesky`)} 169 - to="https://bsky.social"> 170 - <Trans>Business</Trans> 171 - </InlineLinkText> 172 - <InlineLinkText 173 - label={_(msg`Read the Bluesky blog`)} 174 - to="https://bsky.social/about/blog"> 175 - <Trans>Blog</Trans> 176 - </InlineLinkText> 177 - <InlineLinkText 178 - label={_(msg`See jobs at Bluesky`)} 179 - to="https://bsky.social/about/join"> 180 - <Trans comment="Link to a page with job openings at Bluesky"> 181 - Jobs 182 - </Trans> 168 + label={_(msg`Read the patches and contribute`)} 169 + to="https://github.com/blacksky-algorithms/blacksky.community"> 170 + <Trans>Github</Trans> 183 171 </InlineLinkText> 184 172 185 173 <View style={a.flex_1} />
+38
src/view/com/profile/ProfileMenu.tsx
··· 16 16 import {type Shadow} from '#/state/cache/types' 17 17 import {useModalControls} from '#/state/modals' 18 18 import { 19 + useBlackskyVerificationEnabled, 20 + useBlackskyVerificationTrusted, 21 + useSetBlackskyVerificationTrust, 22 + } from '#/state/preferences/blacksky-verification' 23 + import { 19 24 RQKEY as profileQueryKey, 20 25 useProfileBlockMutationQueue, 21 26 useProfileFollowMutationQueue, ··· 79 84 const [devModeEnabled] = useDevMode() 80 85 const verification = useFullVerificationState({profile}) 81 86 const canGoLive = useCanGoLive(currentAccount?.did) 87 + 88 + const blackskyVerificationEnabled = useBlackskyVerificationEnabled() 89 + const blackskyVerificationTrusted = useBlackskyVerificationTrusted().has( 90 + profile.did, 91 + ) 92 + const setBlackskyVerificationTrust = useSetBlackskyVerificationTrust() 82 93 83 94 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 84 95 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) ··· 340 351 <Menu.ItemIcon icon={LiveIcon} /> 341 352 </Menu.Item> 342 353 )} 354 + {!isSelf && 355 + blackskyVerificationEnabled && 356 + (blackskyVerificationTrusted ? ( 357 + <Menu.Item 358 + testID="profileHeaderDropdownVerificationTrustRemoveButton" 359 + label={_(msg`Remove trust`)} 360 + onPress={() => 361 + setBlackskyVerificationTrust.remove(profile.did) 362 + }> 363 + <Menu.ItemText> 364 + <Trans>Remove trust</Trans> 365 + </Menu.ItemText> 366 + <Menu.ItemIcon icon={CircleXIcon} /> 367 + </Menu.Item> 368 + ) : ( 369 + <Menu.Item 370 + testID="profileHeaderDropdownVerificationTrustAddButton" 371 + label={_(msg`Trust verifier`)} 372 + onPress={() => 373 + setBlackskyVerificationTrust.add(profile.did) 374 + }> 375 + <Menu.ItemText> 376 + <Trans>Trust verifier</Trans> 377 + </Menu.ItemText> 378 + <Menu.ItemIcon icon={CircleCheckIcon} /> 379 + </Menu.Item> 380 + ))} 343 381 {verification.viewer.role === 'verifier' && 344 382 !verification.profile.isViewer && 345 383 (verification.viewer.hasIssuedVerification ? (
+1 -1
src/view/com/util/PostMeta.tsx
··· 61 61 return ( 62 62 <View 63 63 style={[ 64 - a.flex_1, 64 + isAndroid ? a.flex_1 : a.flex_shrink, 65 65 a.flex_row, 66 66 a.align_center, 67 67 a.pb_xs,
+25 -26
src/view/icons/Logo.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, TextProps} from 'react-native' 2 + import {StyleSheet, type TextProps} from 'react-native' 3 3 import Svg, { 4 4 Defs, 5 5 LinearGradient, 6 6 Path, 7 - PathProps, 7 + type PathProps, 8 8 Stop, 9 - SvgProps, 9 + type SvgProps, 10 10 } from 'react-native-svg' 11 - import {Image} from 'expo-image' 12 11 13 12 import {colors} from '#/lib/styles' 14 - import {useKawaiiMode} from '#/state/preferences/kawaii' 15 13 16 - const ratio = 57 / 64 14 + const ratio = 512 / 512 17 15 18 16 type Props = { 19 17 fill?: PathProps['fill'] ··· 28 26 // @ts-ignore it's fiiiiine 29 27 const size = parseInt(rest.width || 32) 30 28 31 - const isKawaii = useKawaiiMode() 29 + // const isKawaii = useKawaiiMode() 32 30 33 - if (isKawaii) { 34 - return ( 35 - <Image 36 - source={ 37 - size > 100 38 - ? require('../../../assets/kawaii.png') 39 - : require('../../../assets/kawaii_smol.png') 40 - } 41 - accessibilityLabel="Bluesky" 42 - accessibilityHint="" 43 - accessibilityIgnoresInvertColors 44 - style={[{height: size, aspectRatio: 1.4}]} 45 - /> 46 - ) 47 - } 31 + // if (isKawaii) { 32 + // return ( 33 + // <Image 34 + // source={ 35 + // size > 100 36 + // ? require('../../../assets/kawaii.png') 37 + // : require('../../../assets/kawaii_smol.png') 38 + // } 39 + // accessibilityLabel="Bluesky" 40 + // accessibilityHint="" 41 + // accessibilityIgnoresInvertColors 42 + // style={[{height: size, aspectRatio: 1.4}]} 43 + // /> 44 + // ) 45 + // } 48 46 49 47 return ( 50 48 <Svg 51 49 fill="none" 52 50 // @ts-ignore it's fiiiiine 53 51 ref={ref} 54 - viewBox="0 0 64 57" 52 + viewBox="0 0 512 512" 55 53 {...rest} 56 54 style={[{width: size, height: size * ratio}, styles]}> 57 55 {gradient && ( 58 56 <Defs> 59 57 <LinearGradient id="sky" x1="0" y1="0" x2="0" y2="1"> 60 - <Stop offset="0" stopColor="#0A7AFF" stopOpacity="1" /> 61 - <Stop offset="1" stopColor="#59B9FF" stopOpacity="1" /> 58 + <Stop offset="0" stopColor="#a3b18a" stopOpacity="1" /> 59 + <Stop offset="1" stopColor="#344e41" stopOpacity="1" /> 62 60 </LinearGradient> 63 61 </Defs> 64 62 )} 65 63 66 64 <Path 67 65 fill={_fill} 68 - d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z" 66 + d="m 149.96484,186.56641 46.09766,152.95898 c 0,0 -6.30222,-9.61174 -15.60547,-17.47656 -8.87322,-7.50128 -28.4082,-4.04492 -28.4082,-4.04492 0,0 6.14721,39.88867 15.53125,44.39843 10.71251,5.1482 22.19726,0.16993 22.19726,0.16993 0,0 11.7613,-4.87282 22.82032,31.82421 5.26534,17.47196 15.33258,50.877 20.9707,69.58594 2.16717,7.1913 8.83789,7.25781 8.83789,7.25781 0,0 6.67072,-0.0665 8.83789,-7.25781 5.63812,-18.70894 15.70536,-52.11398 20.9707,-69.58594 11.05902,-36.69703 22.82032,-31.82421 22.82032,-31.82421 0,0 11.48475,4.97827 22.19726,-0.16993 9.38404,-4.50976 15.5332,-44.39843 15.5332,-44.39843 0,0 -19.53693,-3.45636 -28.41015,4.04492 -9.30325,7.86482 -15.60547,17.47656 -15.60547,17.47656 l 46.09766,-152.95898 -49.32618,83.84179 -20.34375,-31.1914 6.35547,54.96875 -23.1582,39.36132 c 0,0 -2.97595,5.06226 -5.94336,4.68946 -0.009,-0.001 -0.0169,0.003 -0.0254,0.01 -0.008,-0.007 -0.0167,-0.0109 -0.0254,-0.01 -2.96741,0.3728 -5.94336,-4.68946 -5.94336,-4.68946 l -23.1582,-39.36132 6.35547,-54.96875 -20.34375,31.1914 z" 67 + transform="matrix(2.6921023,0,0,1.7145911,-396.58283,-308.01527)" 69 68 /> 70 69 </Svg> 71 70 )
+6 -5
src/view/icons/Logotype.tsx
··· 1 - import Svg, {Path, PathProps, SvgProps} from 'react-native-svg' 1 + import Svg, {Path, type PathProps, type SvgProps} from 'react-native-svg' 2 2 3 3 import {usePalette} from '#/lib/hooks/usePalette' 4 4 5 - const ratio = 17 / 64 5 + const ratio = 16 / 100.3335 6 6 7 7 export function Logotype({ 8 8 fill, ··· 10 10 }: {fill?: PathProps['fill']} & SvgProps) { 11 11 const pal = usePalette('default') 12 12 // @ts-ignore it's fiiiiine 13 - const size = parseInt(rest.width || 32) 13 + const size = parseInt(rest.width || 32) * 1.5 14 14 15 15 return ( 16 16 <Svg 17 17 fill="none" 18 - viewBox="0 0 64 17" 18 + viewBox="0 0 100.333 16" 19 19 {...rest} 20 20 width={size} 21 21 height={Number(size) * ratio}> 22 22 <Path 23 23 fill={fill || pal.text.color} 24 - d="M8.478 6.252c1.503.538 2.3 1.78 2.3 3.172 0 2.356-1.576 3.785-4.6 3.785H0V0h5.974c2.875 0 4.267 1.466 4.267 3.413 0 1.3-.594 2.245-1.763 2.839Zm-2.69-4.193H2.504v3.45h3.284c1.28 0 1.967-.667 1.967-1.78 0-1.02-.705-1.67-1.967-1.67Zm-3.284 9.072h3.544c1.41 0 2.17-.65 2.17-1.818 0-1.224-.723-1.837-2.17-1.837H2.504v3.655ZM14.251 13.209h-2.337V0h2.337v13.209ZM22.001 8.998V3.636h2.338v9.573h-2.263v-1.392c-.724 1.076-1.726 1.614-3.006 1.614-2.022 0-3.34-1.224-3.34-3.45V3.636h2.338v5.955c0 1.206.594 1.818 1.8 1.818 1.132 0 2.133-.835 2.133-2.411ZM34.979 8.59v.556h-7.161c.167 1.651 1.076 2.467 2.486 2.467 1.076 0 1.8-.463 2.189-1.372h2.244c-.5 1.947-2.17 3.19-4.452 3.19-1.428 0-2.579-.463-3.45-1.372-.872-.91-1.318-2.115-1.318-3.637 0-1.502.427-2.708 1.299-3.636.872-.909 2.004-1.372 3.432-1.372 1.447 0 2.597.482 3.45 1.428.854.946 1.28 2.208 1.28 3.747Zm-4.75-3.358c-1.28 0-2.17.742-2.393 2.281h4.805c-.204-1.391-1.057-2.281-2.411-2.281ZM40.16 13.469c-2.783 0-4.249-1.095-4.379-3.303h2.282c.13 1.188.724 1.633 2.134 1.633 1.261 0 1.892-.39 1.892-1.15 0-.687-.445-1.02-1.874-1.262l-1.094-.185c-2.097-.353-3.136-1.318-3.136-2.894 0-1.8 1.429-2.894 3.97-2.894 2.728 0 4.138 1.075 4.23 3.246h-2.207c-.056-1.169-.742-1.577-2.023-1.577-1.113 0-1.67.371-1.67 1.113 0 .668.483.965 1.596 1.169l1.206.186c2.32.426 3.32 1.28 3.32 2.912 0 1.93-1.557 3.006-4.247 3.006ZM54.667 13.209h-2.671l-2.783-4.453-1.447 1.447v3.006h-2.3V0h2.3v7.606l3.896-3.97h2.783l-3.618 3.618 3.84 5.955ZM60.772 6.048l.78-2.412H64l-3.692 10.352c-.39 1.057-.872 1.818-1.484 2.245-.612.426-1.484.63-2.634.63-.39 0-.724-.018-1.02-.055V14.97h.89c1.057 0 1.577-.65 1.577-1.54 0-.445-.149-1.094-.446-1.929l-2.746-7.866h2.487l.779 2.393c.575 1.8 1.076 3.58 1.521 5.343.408-1.521.928-3.302 1.54-5.324Z" 24 + transform="translate(-2.4999044,-3.333312)" 25 + d="M 8.333249,3.333312 V 7.999987 H 7.1665795 V 6.833318 H 3.6665735 V 7.999987 H 2.4999045 V 15 h 1.166669 V 7.999987 h 3.500006 V 9.166656 H 8.333249 v 4.666675 H 7.1665795 V 15 h -3.500006 v 1.166668 h 3.500006 V 15 H 8.333249 v 1.166668 H 9.499918 V 3.333312 Z m 4.666674,3.500006 v 1.166669 h 4.666675 v 2.333338 H 12.999923 V 7.999987 H 11.833256 V 15 h 1.166667 v 1.166668 h 4.666675 V 15 h 1.166669 V 13.833331 H 17.666598 V 15 h -4.666675 v -3.500007 h 5.833344 V 7.999987 H 17.666598 V 6.833318 Z m 9.33335,0 v 1.166669 h 4.666676 v 2.333338 H 22.333273 V 7.999987 H 21.166605 V 15 h 1.166668 v 1.166668 h 4.666676 V 15 h 1.166668 V 13.833331 H 26.999949 V 15 h -4.666676 v -3.500007 h 5.833344 V 7.999987 H 26.999949 V 6.833318 Z m 8.166682,0 v 9.33335 h 1.166668 V 9.166656 h 1.166669 V 7.999987 h 3.500006 v 2.333338 h 1.166669 V 7.999987 H 36.333298 V 6.833318 H 32.833292 V 7.999987 H 31.666623 V 6.833318 Z m 11.666688,7.000013 v 2.333337 h 2.333337 v -2.333337 z m 8.166682,-7.000013 v 1.166669 h -1.166668 v 2.333338 h 1.166668 v 1.166668 h 2.333338 v 1.166669 H 55 V 15 H 50.333325 V 13.833331 H 49.166657 V 15 h 1.166668 v 1.166668 H 55 V 15 h 1.166669 V 12.666662 H 55 V 11.499993 H 52.666663 V 10.333325 H 50.333325 V 7.999987 H 55 v 1.166669 h 1.166669 V 7.999987 H 55 V 6.833318 Z m 9.333348,0 V 7.999987 H 58.500005 V 15 h 1.166668 v 1.166668 h 4.666675 V 15 H 59.666673 V 7.999987 h 4.666675 V 15 h 1.166668 V 7.999987 H 64.333348 V 6.833318 Z m 9.33335,0 V 7.999987 H 67.833355 V 15 h 1.166668 v 1.166668 h 4.666676 V 15 h 1.166668 V 13.833331 H 73.666699 V 15 H 69.000023 V 7.999987 h 4.666676 v 1.166669 h 1.166668 V 7.999987 H 73.666699 V 6.833318 Z m 11.66669,-3.500006 v 2.333337 h 1.166669 V 3.333312 Z m -1.166669,3.500006 v 1.166669 h 1.166669 V 15 h -2.333337 v 1.166668 h 5.833343 V 15 H 81.833382 V 6.833318 Z m 8.166683,0 v 1.166669 h -1.166669 v 1.166669 h 1.166669 V 7.999987 H 92.3334 v 2.333338 h -4.666674 v 1.166668 H 86.500057 V 15 h 1.166669 v 1.166668 H 91.16673 V 15 H 87.666726 V 11.499993 H 92.3334 v 2.333338 H 91.16673 V 15 h 1.16667 v 1.166668 h 1.16667 V 7.999987 H 92.3334 V 6.833318 Z M 98.166743,3.333312 v 1.166669 h 1.16667 V 15 H 97.00007 v 1.166668 h 5.83335 V 15 h -2.33334 V 3.333312 Z" 25 26 /> 26 27 </Svg> 27 28 )
+9 -1
src/view/screens/DebugMod.tsx
··· 24 24 type CommonNavigatorParams, 25 25 type NativeStackScreenProps, 26 26 } from '#/lib/routes/types' 27 + import {useHideFollowNotifications} from '#/state/preferences/hide-follow-notifications' 27 28 import {useModerationOpts} from '#/state/preferences/moderation-opts' 28 29 import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts' 29 30 import {type FeedNotification} from '#/state/queries/notifications/types' ··· 877 878 moderationOpts: ModerationOpts 878 879 }) { 879 880 const t = useTheme() 880 - if (shouldFilterNotif(notif.notification, moderationOpts)) { 881 + const hideFollowNotifications = useHideFollowNotifications() 882 + if ( 883 + shouldFilterNotif( 884 + notif.notification, 885 + moderationOpts, 886 + hideFollowNotifications, 887 + ) 888 + ) { 881 889 return ( 882 890 <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}> 883 891 Filtered from the feed
+6 -3
src/view/screens/PrivacyPolicy.tsx
··· 5 5 import {useFocusEffect} from '@react-navigation/native' 6 6 7 7 import {usePalette} from '#/lib/hooks/usePalette' 8 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 8 + import { 9 + type CommonNavigatorParams, 10 + type NativeStackScreenProps, 11 + } from '#/lib/routes/types' 9 12 import {s} from '#/lib/styles' 10 13 import {useSetMinimalShellMode} from '#/state/shell' 11 14 import {TextLink} from '#/view/com/util/Link' ··· 36 39 The Privacy Policy has been moved to{' '} 37 40 <TextLink 38 41 style={pal.link} 39 - href="https://bsky.social/about/support/privacy-policy" 40 - text="bsky.social/about/support/privacy-policy" 42 + href="https://blacksky.community/about/privacy" 43 + text="blacksky.community/about/privacy" 41 44 /> 42 45 </Trans> 43 46 </Text>
+6 -3
src/view/screens/TermsOfService.tsx
··· 5 5 import {useFocusEffect} from '@react-navigation/native' 6 6 7 7 import {usePalette} from '#/lib/hooks/usePalette' 8 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 8 + import { 9 + type CommonNavigatorParams, 10 + type NativeStackScreenProps, 11 + } from '#/lib/routes/types' 9 12 import {s} from '#/lib/styles' 10 13 import {useSetMinimalShellMode} from '#/state/shell' 11 14 import {TextLink} from '#/view/com/util/Link' ··· 35 38 <Trans>The Terms of Service have been moved to</Trans>{' '} 36 39 <TextLink 37 40 style={pal.link} 38 - href="https://bsky.social/about/support/tos" 39 - text="bsky.social/about/support/tos" 41 + href="https://blacksky.community/about/tos" 42 + text="blacksky.community/about/tos" 40 43 /> 41 44 </Text> 42 45 </View>
+2 -2
src/view/shell/Drawer.tsx
··· 695 695 <InlineLinkText 696 696 style={[a.text_md]} 697 697 label={_(msg`Terms of Service`)} 698 - to="https://bsky.social/about/support/tos"> 698 + to="https://blacksky.community/about/tos"> 699 699 <Trans>Terms of Service</Trans> 700 700 </InlineLinkText> 701 701 <InlineLinkText 702 702 style={[a.text_md]} 703 - to="https://bsky.social/about/support/privacy-policy" 703 + to="https://blacksky.community/about/privacy" 704 704 label={_(msg`Privacy Policy`)}> 705 705 <Trans>Privacy Policy</Trans> 706 706 </InlineLinkText>
+4 -2
src/view/shell/desktop/LeftNav.tsx
··· 388 388 let isCurrent = 389 389 currentRouteInfo.name === 'Profile' 390 390 ? isTab(currentRouteInfo.name, pathName) && 391 - (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === 392 - currentAccount?.handle 391 + ((currentRouteInfo.params as CommonNavigatorParams['Profile']).name === 392 + currentAccount?.handle || 393 + (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === 394 + currentAccount?.did) 393 395 : isTab(currentRouteInfo.name, pathName) 394 396 const navigation = useNavigation<NavigationProp>() 395 397 const onPressWrapped = useCallback(
+2 -2
src/view/shell/desktop/RightNav.tsx
··· 110 110 </> 111 111 )} 112 112 <InlineLinkText 113 - to="https://bsky.social/about/support/privacy-policy" 113 + to="https://blacksky.community/about/privacy" 114 114 label={_(msg`Privacy`)}> 115 115 {_(msg`Privacy`)} 116 116 </InlineLinkText> 117 117 {' • '} 118 118 <InlineLinkText 119 - to="https://bsky.social/about/support/tos" 119 + to="https://blacksky.community/about/tos" 120 120 label={_(msg`Terms`)}> 121 121 {_(msg`Terms`)} 122 122 </InlineLinkText>
+27 -52
web/index.html
··· 2 2 <html> 3 3 <head> 4 4 <meta charset="utf-8"> 5 - <meta name="theme-color"> 5 + <meta name="theme-color" content="#4b9b6c"> 6 6 <!-- 7 7 This viewport works for phones with notches. 8 8 It's optimized for gestures by disabling global zoom. ··· 74 74 } 75 75 #splash { 76 76 position: fixed; 77 - width: 100px; 77 + width: 125px; 78 78 left: 50%; 79 79 top: 50%; 80 80 transform: translateX(-50%) translateY(-50%) translateY(-50px); ··· 93 93 </head> 94 94 95 95 <body> 96 - <!-- 97 - A generic no script element with a reload button and a message. 98 - Feel free to customize this however you'd like. 99 - --> 100 - <noscript> 101 - <form 102 - action="" 103 - style=" 104 - background-color: #fff; 105 - position: fixed; 106 - top: 0; 107 - left: 0; 108 - right: 0; 109 - bottom: 0; 110 - z-index: 9999; 111 - " 112 - > 113 - <div 114 - style=" 115 - font-size: 18px; 116 - font-family: Helvetica, sans-serif; 117 - line-height: 24px; 118 - margin: 10%; 119 - width: 80%; 120 - " 121 - > 122 - <p lang="en">Oh no! It looks like JavaScript is not enabled in your browser.</p> 123 - <p lang="en" style="margin: 20px 0;"> 124 - <button 125 - type="submit" 126 - style=" 127 - background-color: #4630eb; 128 - border-radius: 100px; 129 - border: none; 130 - box-shadow: none; 131 - color: #fff; 132 - cursor: pointer; 133 - font-weight: bold; 134 - line-height: 20px; 135 - padding: 6px 16px; 136 - " 137 - > 138 - Reload 139 - </button> 140 - </p> 141 - </div> 142 - </form> 143 - </noscript> 96 + <noscript style=" 97 + background-color: #fff; 98 + position: fixed; 99 + top: 0; 100 + left: 0; 101 + right: 0; 102 + bottom: 0; 103 + z-index: 9999; 104 + margin: 1em; 105 + "> 106 + <h1 lang="en">JavaScript Required</h1> 107 + <p lang="en">This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is. 108 + <p lang="en">Learn more about Bluesky at <a href="https://bsky.social">bsky.social</a> and <a href="https://atproto.com">atproto.com</a>, or this fork at <a href="https://github.com/blacksky-algorithms/blacksky.community">github.com/a-viv-a/blacksky.community</a>. 109 + </noscript> 144 110 145 111 <!-- The root element for your Expo app. --> 146 112 <div id="root"> 147 113 <div id="splash"> 148 - <!-- Bluesky SVG --> 149 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320"><path fill="#0085ff" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/></svg> 114 + <!-- blacksky SVG --> 115 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> 116 + 117 + <defs> 118 + <linearGradient x1="0" y1="0" x2="0" y2="1" id="index_sky"><stop offset="0" stop-color="#a3b18a" stop-opacity="1"></stop><stop offset="1" stop-color="#344e41" stop-opacity="1"></stop></linearGradient> 119 + </defs> 120 + <path fill="url(#index_sky)" 121 + d="m 149.96484,186.56641 46.09766,152.95898 c 0,0 -6.30222,-9.61174 -15.60547,-17.47656 -8.87322,-7.50128 -28.4082,-4.04492 -28.4082,-4.04492 0,0 6.14721,39.88867 15.53125,44.39843 10.71251,5.1482 22.19726,0.16993 22.19726,0.16993 0,0 11.7613,-4.87282 22.82032,31.82421 5.26534,17.47196 15.33258,50.877 20.9707,69.58594 2.16717,7.1913 8.83789,7.25781 8.83789,7.25781 0,0 6.67072,-0.0665 8.83789,-7.25781 5.63812,-18.70894 15.70536,-52.11398 20.9707,-69.58594 11.05902,-36.69703 22.82032,-31.82421 22.82032,-31.82421 0,0 11.48475,4.97827 22.19726,-0.16993 9.38404,-4.50976 15.5332,-44.39843 15.5332,-44.39843 0,0 -19.53693,-3.45636 -28.41015,4.04492 -9.30325,7.86482 -15.60547,17.47656 -15.60547,17.47656 l 46.09766,-152.95898 -49.32618,83.84179 -20.34375,-31.1914 6.35547,54.96875 -23.1582,39.36132 c 0,0 -2.97595,5.06226 -5.94336,4.68946 -0.009,-0.001 -0.0169,0.003 -0.0254,0.01 -0.008,-0.007 -0.0167,-0.0109 -0.0254,-0.01 -2.96741,0.3728 -5.94336,-4.68946 -5.94336,-4.68946 l -23.1582,-39.36132 6.35547,-54.96875 -20.34375,31.1914 z" 122 + transform="matrix(2.6921023,0,0,1.7145911,-396.58283,-308.01527)" 123 + /> 124 + </svg> 150 125 </div> 151 126 </div> 152 127 </body>
+1
wrangler.toml
··· 1 + compatibility_date = "2025-04-16"