Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd8dac0154 | |||
| b9268d503a | |||
| 9c72fcc9b4 | |||
| 4977a5d2a3 | |||
| 2b6fe4cb50 | |||
| 9b5bd53f84 | |||
| 9e8b14f710 | |||
| 0090371011 | |||
| 21371081c6 | |||
| f8c611118e | |||
| 977f9228f8 | |||
| 57c10cbcab | |||
| 27cb1bd276 | |||
| bcd63e710c | |||
| 01775e5bff | |||
| 731d84faaa | |||
| 775aa87540 | |||
| 6d73ed96e1 | |||
| adc5b75b2b | |||
| 44a00e1b7e |
@@ -175,6 +175,7 @@ describe('ago', () => {
|
||||
1671461038,
|
||||
'04 Dec 1995 00:12:00 GMT',
|
||||
new Date(),
|
||||
new Date().setSeconds(new Date().getSeconds() - 10),
|
||||
new Date().setMinutes(new Date().getMinutes() - 10),
|
||||
new Date().setHours(new Date().getHours() - 1),
|
||||
new Date().setDate(new Date().getDate() - 1),
|
||||
@@ -183,7 +184,8 @@ describe('ago', () => {
|
||||
const outputs = [
|
||||
new Date(1671461038).toLocaleDateString(),
|
||||
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
|
||||
'0s',
|
||||
'now',
|
||||
'10s',
|
||||
'10m',
|
||||
'1h',
|
||||
'1d',
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
module.exports = function () {
|
||||
const hasSentryToken = !!process.env.SENTRY_AUTH_TOKEN
|
||||
return {
|
||||
expo: {
|
||||
name: 'Bluesky',
|
||||
slug: 'bluesky',
|
||||
scheme: 'bluesky',
|
||||
owner: 'blueskysocial',
|
||||
version: '1.50.0',
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
orientation: 'portrait',
|
||||
icon: './assets/icon.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
splash: {
|
||||
image: './assets/cloud-splash.png',
|
||||
resizeMode: 'cover',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
supportsTablet: false,
|
||||
bundleIdentifier: 'xyz.blueskyweb.app',
|
||||
config: {
|
||||
usesNonExemptEncryption: false,
|
||||
},
|
||||
infoPlist: {
|
||||
UIBackgroundModes: ['remote-notification'],
|
||||
NSCameraUsageDescription:
|
||||
'Used for profile pictures, posts, and other kinds of content.',
|
||||
NSMicrophoneUsageDescription:
|
||||
'Used for posts and other kinds of content.',
|
||||
NSPhotoLibraryAddUsageDescription:
|
||||
'Used to save images to your library.',
|
||||
NSPhotoLibraryUsageDescription:
|
||||
'Used for profile pictures, posts, and other kinds of content',
|
||||
},
|
||||
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
|
||||
},
|
||||
androidStatusBar: {
|
||||
barStyle: 'dark-content',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
android: {
|
||||
versionCode: 37,
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/adaptive-icon.png',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
googleServicesFile: './google-services.json',
|
||||
package: 'xyz.blueskyweb.app',
|
||||
intentFilters: [
|
||||
{
|
||||
action: 'VIEW',
|
||||
autoVerify: true,
|
||||
data: [
|
||||
{
|
||||
scheme: 'https',
|
||||
host: 'bsky.app',
|
||||
},
|
||||
],
|
||||
category: ['BROWSABLE', 'DEFAULT'],
|
||||
},
|
||||
],
|
||||
},
|
||||
web: {
|
||||
favicon: './assets/favicon.png',
|
||||
},
|
||||
updates: {
|
||||
enabled: true,
|
||||
fallbackToCacheTimeout: 1000,
|
||||
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
|
||||
},
|
||||
plugins: [
|
||||
'expo-localization',
|
||||
hasSentryToken && 'sentry-expo',
|
||||
[
|
||||
'expo-build-properties',
|
||||
{
|
||||
android: {
|
||||
compileSdkVersion: 34,
|
||||
targetSdkVersion: 34,
|
||||
buildToolsVersion: '34.0.0',
|
||||
kotlinVersion: '1.8.0',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-updates',
|
||||
{
|
||||
username: 'blueskysocial',
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: '55bd077a-d905-4184-9c7f-94789ba0f302',
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
postPublish: [
|
||||
{
|
||||
file: 'sentry-expo/upload-sourcemaps',
|
||||
config: {
|
||||
organization: 'blueskyweb',
|
||||
project: 'react-native',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Bluesky",
|
||||
"slug": "bluesky",
|
||||
"scheme": "bluesky",
|
||||
"owner": "blueskysocial",
|
||||
"version": "1.49.0",
|
||||
"runtimeVersion": {
|
||||
"policy": "appVersion"
|
||||
},
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"splash": {
|
||||
"image": "./assets/cloud-splash.png",
|
||||
"resizeMode": "cover",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"buildNumber": "1",
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "xyz.blueskyweb.app",
|
||||
"config": {
|
||||
"usesNonExemptEncryption": false
|
||||
},
|
||||
"infoPlist": {
|
||||
"UIBackgroundModes": [
|
||||
"remote-notification"
|
||||
],
|
||||
"NSCameraUsageDescription": "Used for profile pictures, posts, and other kinds of content.",
|
||||
"NSMicrophoneUsageDescription": "Used for posts and other kinds of content.",
|
||||
"NSPhotoLibraryAddUsageDescription": "Used to save images to your library.",
|
||||
"NSPhotoLibraryUsageDescription": "Used for profile pictures, posts, and other kinds of content"
|
||||
},
|
||||
"associatedDomains": ["applinks:bsky.app", "applinks:staging.bsky.app"]
|
||||
},
|
||||
"androidStatusBar": {
|
||||
"barStyle": "dark-content",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"android": {
|
||||
"versionCode": 36,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"googleServicesFile": "./google-services.json",
|
||||
"package": "xyz.blueskyweb.app",
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "VIEW",
|
||||
"autoVerify": true,
|
||||
"data": [
|
||||
{
|
||||
"scheme": "https",
|
||||
"host": "bsky.app"
|
||||
}
|
||||
],
|
||||
"category": ["BROWSABLE", "DEFAULT"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
},
|
||||
"updates": {
|
||||
"enabled": true,
|
||||
"fallbackToCacheTimeout": 1000,
|
||||
"url": "https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-localization",
|
||||
"sentry-expo",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"compileSdkVersion": 34,
|
||||
"targetSdkVersion": 34,
|
||||
"buildToolsVersion": "34.0.0",
|
||||
"kotlinVersion": "1.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-updates",
|
||||
{
|
||||
"username": "blueskysocial"
|
||||
}
|
||||
]
|
||||
],
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "55bd077a-d905-4184-9c7f-94789ba0f302"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"postPublish": [
|
||||
{
|
||||
"file": "sentry-expo/upload-sourcemaps",
|
||||
"config": {
|
||||
"organization": "blueskyweb",
|
||||
"project": "react-native"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -26,20 +26,20 @@
|
||||
- Run once: `yarn e2e:build`
|
||||
- Each test run: `yarn e2e:run`
|
||||
- Tips
|
||||
- Make sure you copy the `.env.example` to `.env` and add the appropriate tokens (e.g. `SENTRY_AUTH_TOKEN` can be created on the Sentry dashboard using [these instructions](https://docs.expo.dev/guides/using-sentry/#sign-up-for-a-sentry-account-and-create-a-project)). If this is not required, you can remove it from `eas.json` and `package.json`, as well as any mentions in the code. Please check the section below on how to remove Sentry from the codebase
|
||||
- Make sure to copy the `.env.example` to `.env` and fill in any necessary tokens. (The Sentry token is NOT required; see instructions below if you want to enable Sentry.)
|
||||
- If you want to use Expo EAS on your own builds without ejecting from Expo, make sure to change the `owner` as well as `extra.eas.projectId` properties. If you do not have an Expo account, you may remove these properties.
|
||||
- `npx react-native info` Checks what has been installed.
|
||||
- The android simulator won't be able to access localhost services unless you run `adb reverse tcp:{PORT} tcp:{PORT}`
|
||||
- For instance, the locally-hosted dev-wallet will need `adb reverse tcp:3001 tcp:3001`
|
||||
- For some reason, the typescript compiler chokes on platform-specific files (e.g. `foo.native.ts`) but only when compiling for Web thus far. Therefore we always have one version of the file which doesn't use a platform specifier, and that should be the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).)
|
||||
|
||||
### Removing Sentry
|
||||
If you are part of the Bluesky team, you should have access to our Sentry dashboard, and you shouldn't need to remove Sentry. Even if you are not part of the Bluesky team, you can create your own Sentry account and add the `SENTRY_AUTH_TOKEN` env var and add your sentry account detials to `app.json` to make the app build and run successfully. However, if that is not possible, follow these steps to remove Sentry from the project (please don't commit this code in any PR):
|
||||
- `yarn remove sentry-expo @sentry/react-native`
|
||||
- Remove `sentry-expo` plugin in `app.json` and also remove the `postPublish` hook in `app.json`
|
||||
- Remove any mentions of `sentry` from the `App.native.tsx`, `App.web.tsx` and `Navigation.tsx` files. Also, delete `sentry.ts`
|
||||
- Run `rm -rf ios android` or delete the existing `android` and `ios` folders in the project (don't worry! `yarn prebuild` gets these back)
|
||||
- Run `yarn prebuild` and `yarn ios` and build the app!
|
||||
### Adding Sentry
|
||||
|
||||
Adding Sentry is NOT required. You can keep `SENTRY_AUTH_TOKEN=` in `.env` which will build the app without Sentry.
|
||||
|
||||
However, if you're a part of the Bluesky team and want to enable Sentry, fill in `SENTRY_AUTH_TOKEN` in your `.env`. It can be created on the Sentry dashboard using [these instructions](https://docs.expo.dev/guides/using-sentry/#sign-up-for-a-sentry-account-and-create-a-project).
|
||||
|
||||
If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again.
|
||||
|
||||
## Go-Server Build
|
||||
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.49.0",
|
||||
"version": "1.50.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prepare": "is-ci || husky install",
|
||||
@@ -24,7 +24,7 @@
|
||||
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.6.8",
|
||||
"@atproto/api": "^0.6.12",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -79,8 +79,8 @@
|
||||
"expo-dev-client": "2.4.7",
|
||||
"expo-device": "~5.4.0",
|
||||
"expo-image": "~1.3.2",
|
||||
"expo-image-manipulator": "~11.3.0",
|
||||
"expo-image-picker": "~14.3.2",
|
||||
"expo-image-manipulator": "~11.5.0",
|
||||
"expo-image-picker": "~14.5.0",
|
||||
"expo-localization": "~14.3.0",
|
||||
"expo-media-library": "~15.4.1",
|
||||
"expo-notifications": "~0.20.1",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, {ReactNode, createContext, useContext, useMemo} from 'react'
|
||||
import {TextStyle, useColorScheme, ViewStyle} from 'react-native'
|
||||
import React, {ReactNode, createContext, useContext} from 'react'
|
||||
import {
|
||||
AppState,
|
||||
TextStyle,
|
||||
useColorScheme,
|
||||
ViewStyle,
|
||||
ColorSchemeName,
|
||||
} from 'react-native'
|
||||
import {darkTheme, defaultTheme} from './themes'
|
||||
|
||||
export type ColorScheme = 'light' | 'dark'
|
||||
@@ -91,14 +97,28 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const colorSchemeFromRN = useColorScheme()
|
||||
const [nativeColorScheme, setNativeColorScheme] =
|
||||
React.useState<ColorSchemeName>(colorSchemeFromRN)
|
||||
|
||||
// if theme is 'system', use the device's configured color scheme
|
||||
let colorScheme = theme === 'system' ? colorSchemeFromRN : theme
|
||||
React.useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', state => {
|
||||
const isActive = state === 'active'
|
||||
|
||||
const value = useMemo(
|
||||
() => (colorScheme === 'dark' ? darkTheme : defaultTheme),
|
||||
[colorScheme],
|
||||
)
|
||||
if (!isActive) return
|
||||
|
||||
setNativeColorScheme(colorSchemeFromRN)
|
||||
})
|
||||
return () => subscription.remove()
|
||||
}, [colorSchemeFromRN])
|
||||
|
||||
const value =
|
||||
theme === 'system'
|
||||
? nativeColorScheme === 'dark'
|
||||
? darkTheme
|
||||
: defaultTheme
|
||||
: theme === 'dark'
|
||||
? darkTheme
|
||||
: defaultTheme
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {ScreenEvent, TrackEvent} from './types'
|
||||
const segmentClient = createClient({
|
||||
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
|
||||
trackAppLifecycleEvents: false,
|
||||
proxy: 'https://api.events.bsky.app/v1',
|
||||
})
|
||||
|
||||
export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent
|
||||
|
||||
@@ -15,7 +15,7 @@ const segmentClient = createClient(
|
||||
{
|
||||
integrations: {
|
||||
'Segment.io': {
|
||||
apiHost: 'api.evt.bsky.app/v1',
|
||||
apiHost: 'api.events.bsky.app/v1',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -56,8 +56,12 @@ interface TrackPropertiesMap {
|
||||
}
|
||||
// PROFILE HEADER events
|
||||
'ProfileHeader:EditProfileButtonClicked': {}
|
||||
'ProfileHeader:FollowersButtonClicked': {}
|
||||
'ProfileHeader:FollowsButtonClicked': {}
|
||||
'ProfileHeader:FollowersButtonClicked': {
|
||||
handle: string
|
||||
}
|
||||
'ProfileHeader:FollowsButtonClicked': {
|
||||
handle: string
|
||||
}
|
||||
'ProfileHeader:ShareButtonClicked': {}
|
||||
'ProfileHeader:MuteAccountButtonClicked': {}
|
||||
'ProfileHeader:UnmuteAccountButtonClicked': {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const NOW = 5
|
||||
const MINUTE = 60
|
||||
const HOUR = MINUTE * 60
|
||||
const DAY = HOUR * 24
|
||||
@@ -13,7 +14,9 @@ export function ago(date: number | string | Date): string {
|
||||
ts = date
|
||||
}
|
||||
const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
|
||||
if (diffSeconds < MINUTE) {
|
||||
if (diffSeconds < NOW) {
|
||||
return `now`
|
||||
} else if (diffSeconds < MINUTE) {
|
||||
return `${diffSeconds}s`
|
||||
} else if (diffSeconds < HOUR) {
|
||||
return `${Math.floor(diffSeconds / MINUTE)}m`
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AppBskyGraphListitem,
|
||||
} from '@atproto/api'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import chunk from 'lodash.chunk'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import * as apilib from 'lib/api/index'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
@@ -102,6 +103,10 @@ export class ListModel {
|
||||
return this.list?.viewer?.muted
|
||||
}
|
||||
|
||||
get creatorDid() {
|
||||
return this.list?.creator.did
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
@@ -211,12 +216,17 @@ export class ListModel {
|
||||
rkey: urip.rkey,
|
||||
}
|
||||
}
|
||||
await this.rootStore.agent.com.atproto.repo.applyWrites({
|
||||
repo: this.rootStore.me.did,
|
||||
writes: [createDel(this.uri)].concat(
|
||||
records.map(record => createDel(record.uri)),
|
||||
),
|
||||
})
|
||||
const writes = records
|
||||
.map(record => createDel(record.uri))
|
||||
.concat([createDel(this.uri)])
|
||||
|
||||
// apply in chunks
|
||||
for (const writesChunk of chunk(writes, 10)) {
|
||||
await this.rootStore.agent.com.atproto.repo.applyWrites({
|
||||
repo: this.rootStore.me.did,
|
||||
writes: writesChunk,
|
||||
})
|
||||
}
|
||||
|
||||
this.rootStore.emitListDeleted(this.uri)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export class ImageModel implements Omit<RNImage, 'size'> {
|
||||
// Web manipulation
|
||||
prev?: RNImage
|
||||
attributes: ImageManipulationAttributes = {
|
||||
aspectRatio: '1:1',
|
||||
aspectRatio: 'None',
|
||||
scale: 1,
|
||||
flipHorizontal: false,
|
||||
flipVertical: false,
|
||||
|
||||
@@ -118,6 +118,7 @@ export class CreateAccountModel {
|
||||
password: this.password,
|
||||
inviteCode: this.inviteCode,
|
||||
})
|
||||
/* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate)
|
||||
track('Create Account')
|
||||
} catch (e: any) {
|
||||
this.rootStore.onboarding.skip() // undo starting the onboard
|
||||
|
||||
@@ -7,6 +7,7 @@ import {RootStoreModel} from '../root-store'
|
||||
import {ModerationOpts} from '@atproto/api'
|
||||
import {DEFAULT_FEEDS} from 'lib/constants'
|
||||
import {deviceLocales} from 'platform/detection'
|
||||
import {getAge} from 'lib/strings/time'
|
||||
import {LANGUAGES} from '../../../locale/languages'
|
||||
|
||||
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
||||
@@ -47,6 +48,7 @@ export class PreferencesModel {
|
||||
contentLabels = new LabelPreferencesModel()
|
||||
savedFeeds: string[] = []
|
||||
pinnedFeeds: string[] = []
|
||||
birthDate: Date | undefined = undefined
|
||||
homeFeedRepliesEnabled: boolean = true
|
||||
homeFeedRepliesThreshold: number = 2
|
||||
homeFeedRepostsEnabled: boolean = true
|
||||
@@ -60,6 +62,13 @@ export class PreferencesModel {
|
||||
makeAutoObservable(this, {lock: false}, {autoBind: true})
|
||||
}
|
||||
|
||||
get userAge(): number | undefined {
|
||||
if (!this.birthDate) {
|
||||
return undefined
|
||||
}
|
||||
return getAge(this.birthDate)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
contentLanguages: this.contentLanguages,
|
||||
@@ -199,6 +208,7 @@ export class PreferencesModel {
|
||||
) {
|
||||
this.pinnedFeeds = prefs.feeds.pinned
|
||||
}
|
||||
this.birthDate = prefs.birthDate
|
||||
})
|
||||
|
||||
// set defaults on missing items
|
||||
@@ -430,6 +440,11 @@ export class PreferencesModel {
|
||||
)
|
||||
}
|
||||
|
||||
async setBirthDate(birthDate: Date) {
|
||||
this.birthDate = birthDate
|
||||
await this.rootStore.agent.setPersonalDetails({birthDate})
|
||||
}
|
||||
|
||||
toggleHomeFeedRepliesEnabled() {
|
||||
this.homeFeedRepliesEnabled = !this.homeFeedRepliesEnabled
|
||||
}
|
||||
|
||||
@@ -136,6 +136,10 @@ export interface PostLanguagesSettingsModal {
|
||||
name: 'post-languages-settings'
|
||||
}
|
||||
|
||||
export interface BirthDateSettingsModal {
|
||||
name: 'birth-date-settings'
|
||||
}
|
||||
|
||||
export type Modal =
|
||||
// Account
|
||||
| AddAppPasswordModal
|
||||
@@ -143,6 +147,7 @@ export type Modal =
|
||||
| DeleteAccountModal
|
||||
| EditProfileModal
|
||||
| ProfilePreviewModal
|
||||
| BirthDateSettingsModal
|
||||
|
||||
// Curation
|
||||
| ContentFilteringSettingsModal
|
||||
|
||||
@@ -103,6 +103,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
handleKeyDown: (_, event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
|
||||
textInputWebEmitter.emit('publish')
|
||||
return true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -93,7 +93,7 @@ export const Autocomplete = observer(function AutocompleteImpl({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginLeft: -54,
|
||||
marginLeft: -50, // Composer avatar width
|
||||
top: 10,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
|
||||
@@ -58,21 +58,27 @@ export function EmojiPicker({close}: {close: () => void}) {
|
||||
// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors
|
||||
<TouchableWithoutFeedback onPress={close} accessibilityViewIsModal>
|
||||
<View style={styles.mask}>
|
||||
<View
|
||||
style={[
|
||||
styles.picker,
|
||||
{
|
||||
paddingTop: noPadding ? 0 : reducedPadding ? 150 : 325,
|
||||
display: noPicker ? 'none' : 'flex',
|
||||
},
|
||||
]}>
|
||||
<Picker
|
||||
// @ts-ignore we set emojiMartData in `emoji-mart-data.js` file
|
||||
data={window.emojiMartData}
|
||||
onEmojiSelect={onInsert}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</View>
|
||||
{/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={e => {
|
||||
e.stopPropagation() // prevent event from bubbling up to the mask
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
styles.picker,
|
||||
{
|
||||
paddingTop: noPadding ? 0 : reducedPadding ? 150 : 325,
|
||||
display: noPicker ? 'none' : 'flex',
|
||||
},
|
||||
]}>
|
||||
<Picker
|
||||
// @ts-ignore we set emojiMartData in `emoji-mart-data.js` file
|
||||
data={window.emojiMartData}
|
||||
onEmojiSelect={onInsert}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import React, {useState} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {DateInput} from '../util/forms/DateInput'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {useStores} from 'state/index'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
|
||||
export const snapPoints = ['50%']
|
||||
|
||||
export const Component = observer(function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [date, setDate] = useState<Date>(
|
||||
store.preferences.birthDate || new Date(),
|
||||
)
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
const onSave = async () => {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await store.preferences.setBirthDate(date)
|
||||
store.shell.closeModal()
|
||||
} catch (e) {
|
||||
setError(cleanError(String(e)))
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="birthDateSettingsModal"
|
||||
style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
My Birthday
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
||||
This information is not shared with other users.
|
||||
</Text>
|
||||
|
||||
<View>
|
||||
<DateInput
|
||||
testID="birthdayInput"
|
||||
value={date}
|
||||
onChange={setDate}
|
||||
buttonType="default-light"
|
||||
buttonStyle={[pal.border, styles.dateInputButton]}
|
||||
buttonLabelType="lg"
|
||||
accessibilityLabel="Birthday"
|
||||
accessibilityHint="Enter your birth date"
|
||||
accessibilityLabelledBy="birthDate"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{error ? (
|
||||
<ErrorMessage message={error} style={styles.error} />
|
||||
) : undefined}
|
||||
|
||||
<View style={[styles.btnContainer, pal.borderDark]}>
|
||||
{isProcessing ? (
|
||||
<View style={styles.btn}>
|
||||
<ActivityIndicator color="#fff" />
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="confirmBtn"
|
||||
onPress={onSave}
|
||||
style={styles.btn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Save"
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: isWeb ? 0 : 40,
|
||||
},
|
||||
titleSection: {
|
||||
paddingTop: isWeb ? 0 : 4,
|
||||
paddingBottom: isWeb ? 14 : 10,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
marginBottom: 5,
|
||||
},
|
||||
error: {
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
dateInputButton: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
btnContainer: {
|
||||
paddingTop: 20,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
})
|
||||
@@ -9,6 +9,7 @@ import {s, colors, gradients} from 'lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {ToggleButton} from '../util/forms/ToggleButton'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {CONFIGURABLE_LABEL_GROUPS} from 'lib/labeling/const'
|
||||
import {isIOS} from 'platform/detection'
|
||||
@@ -27,22 +28,6 @@ export const Component = observer(
|
||||
store.preferences.sync()
|
||||
}, [store])
|
||||
|
||||
const onToggleAdultContent = React.useCallback(async () => {
|
||||
if (isIOS) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await store.preferences.setAdultContentEnabled(
|
||||
!store.preferences.adultContentEnabled,
|
||||
)
|
||||
} catch (e) {
|
||||
Toast.show(
|
||||
'There was an issue syncing your preferences with the server',
|
||||
)
|
||||
store.log.error('Failed to update preferences with server', {e})
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const onPressDone = React.useCallback(() => {
|
||||
store.shell.closeModal()
|
||||
}, [store])
|
||||
@@ -51,29 +36,7 @@ export const Component = observer(
|
||||
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
|
||||
<Text style={[pal.text, styles.title]}>Content Filtering</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
<View style={s.mb10}>
|
||||
{isIOS ? (
|
||||
store.preferences.adultContentEnabled ? null : (
|
||||
<Text type="md" style={pal.textLight}>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://bsky.app"
|
||||
text="bsky.app"
|
||||
/>
|
||||
.
|
||||
</Text>
|
||||
)
|
||||
) : (
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label="Enable Adult Content"
|
||||
isSelected={store.preferences.adultContentEnabled}
|
||||
onPress={onToggleAdultContent}
|
||||
style={styles.toggleBtn}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<AdultContentEnabledPref />
|
||||
<ContentLabelPref
|
||||
group="nsfw"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
@@ -121,6 +84,71 @@ export const Component = observer(
|
||||
},
|
||||
)
|
||||
|
||||
const AdultContentEnabledPref = observer(
|
||||
function AdultContentEnabledPrefImpl() {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const onSetAge = () => store.shell.openModal({name: 'birth-date-settings'})
|
||||
|
||||
const onToggleAdultContent = async () => {
|
||||
if (isIOS) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await store.preferences.setAdultContentEnabled(
|
||||
!store.preferences.adultContentEnabled,
|
||||
)
|
||||
} catch (e) {
|
||||
Toast.show(
|
||||
'There was an issue syncing your preferences with the server',
|
||||
)
|
||||
store.log.error('Failed to update preferences with server', {e})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.mb10}>
|
||||
{isIOS ? (
|
||||
store.preferences.adultContentEnabled ? null : (
|
||||
<Text type="md" style={pal.textLight}>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://bsky.app"
|
||||
text="bsky.app"
|
||||
/>
|
||||
.
|
||||
</Text>
|
||||
)
|
||||
) : typeof store.preferences.birthDate === 'undefined' ? (
|
||||
<View style={[pal.viewLight, styles.agePrompt]}>
|
||||
<Text type="md" style={[pal.text, {flex: 1}]}>
|
||||
Confirm your age to enable adult content.
|
||||
</Text>
|
||||
<Button type="primary" label="Set Age" onPress={onSetAge} />
|
||||
</View>
|
||||
) : (store.preferences.userAge || 0) >= 18 ? (
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label="Enable Adult Content"
|
||||
isSelected={store.preferences.adultContentEnabled}
|
||||
onPress={onToggleAdultContent}
|
||||
style={styles.toggleBtn}
|
||||
/>
|
||||
) : (
|
||||
<View style={[pal.viewLight, styles.agePrompt]}>
|
||||
<Text type="md" style={[pal.text, {flex: 1}]}>
|
||||
You must be 18 or older to enable adult content.
|
||||
</Text>
|
||||
<Button type="primary" label="Set Age" onPress={onSetAge} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// TODO: Refactor this component to pass labels down to each tab
|
||||
const ContentLabelPref = observer(function ContentLabelPrefImpl({
|
||||
group,
|
||||
@@ -277,6 +305,16 @@ const styles = StyleSheet.create({
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
|
||||
agePrompt: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 14,
|
||||
paddingRight: 10,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
},
|
||||
|
||||
contentLabelPref: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
@@ -29,6 +29,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
import * as BirthDateSettingsModal from './BirthDateSettings'
|
||||
|
||||
const DEFAULT_SNAPPOINTS = ['90%']
|
||||
|
||||
@@ -132,6 +133,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||
} else if (activeModal?.name === 'moderation-details') {
|
||||
snapPoints = ModerationDetailsModal.snapPoints
|
||||
element = <ModerationDetailsModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'birth-date-settings') {
|
||||
snapPoints = BirthDateSettingsModal.snapPoints
|
||||
element = <BirthDateSettingsModal.Component />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
import * as BirthDateSettingsModal from './BirthDateSettings'
|
||||
|
||||
export const ModalsContainer = observer(function ModalsContainer() {
|
||||
const store = useStores()
|
||||
@@ -107,6 +108,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||
element = <EditImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'moderation-details') {
|
||||
element = <ModerationDetailsModal.Component {...modal} />
|
||||
} else if (modal.name === 'birth-date-settings') {
|
||||
element = <BirthDateSettingsModal.Component />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {runInAction} from 'mobx'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
@@ -47,6 +48,10 @@ const CHILD_SPINNER = {
|
||||
_reactKey: '__child_spinner__',
|
||||
_isHighlightedPost: false,
|
||||
}
|
||||
const LOAD_MORE = {
|
||||
_reactKey: '__load_more__',
|
||||
_isHighlightedPost: false,
|
||||
}
|
||||
const BOTTOM_COMPONENT = {
|
||||
_reactKey: '__bottom_component__',
|
||||
_isHighlightedPost: false,
|
||||
@@ -74,10 +79,14 @@ export const PostThread = observer(function PostThread({
|
||||
const ref = useRef<FlatList>(null)
|
||||
const hasScrolledIntoView = useRef<boolean>(false)
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||
const [maxVisible, setMaxVisible] = React.useState(100)
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const posts = React.useMemo(() => {
|
||||
if (view.thread) {
|
||||
const arr = [TOP_COMPONENT].concat(Array.from(flattenThread(view.thread)))
|
||||
let arr = [TOP_COMPONENT].concat(Array.from(flattenThread(view.thread)))
|
||||
if (arr.length > maxVisible) {
|
||||
arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
|
||||
}
|
||||
if (view.isLoadingFromCache) {
|
||||
if (view.thread?.postRecord?.reply) {
|
||||
arr.unshift(PARENT_SPINNER)
|
||||
@@ -89,7 +98,7 @@ export const PostThread = observer(function PostThread({
|
||||
return arr
|
||||
}
|
||||
return []
|
||||
}, [view.isLoadingFromCache, view.thread])
|
||||
}, [view.isLoadingFromCache, view.thread, maxVisible])
|
||||
useSetTitle(
|
||||
view.thread?.postRecord &&
|
||||
`${sanitizeDisplayName(
|
||||
@@ -178,7 +187,7 @@ export const PostThread = observer(function PostThread({
|
||||
return <ComposePrompt onPressCompose={onPressReply} />
|
||||
} else if (item === DELETED) {
|
||||
return (
|
||||
<View style={[pal.border, pal.viewLight, styles.missingItem]}>
|
||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||
<Text type="lg-bold" style={pal.textLight}>
|
||||
Deleted post.
|
||||
</Text>
|
||||
@@ -186,12 +195,30 @@ export const PostThread = observer(function PostThread({
|
||||
)
|
||||
} else if (item === BLOCKED) {
|
||||
return (
|
||||
<View style={[pal.border, pal.viewLight, styles.missingItem]}>
|
||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||
<Text type="lg-bold" style={pal.textLight}>
|
||||
Blocked post.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
} else if (item === LOAD_MORE) {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => setMaxVisible(n => n + 50)}
|
||||
style={[pal.border, pal.view, styles.itemContainer]}
|
||||
accessibilityLabel="Load more posts"
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[
|
||||
pal.viewLight,
|
||||
{paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
|
||||
]}>
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
Load more posts
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
} else if (item === BOTTOM_COMPONENT) {
|
||||
// HACK
|
||||
// due to some complexities with how flatlist works, this is the easiest way
|
||||
@@ -373,8 +400,8 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 14,
|
||||
borderRadius: 6,
|
||||
},
|
||||
missingItem: {
|
||||
borderTop: 1,
|
||||
itemContainer: {
|
||||
borderTopWidth: 1,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 18,
|
||||
},
|
||||
|
||||
@@ -33,11 +33,11 @@ import {isNative} from 'platform/detection'
|
||||
import {FollowState} from 'state/models/cache/my-follows'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {formatCount} from '../util/numeric/format'
|
||||
import {navigate} from '../../../Navigation'
|
||||
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
|
||||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {isInvalidHandle} from 'lib/strings/handles'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {Link} from '../util/Link'
|
||||
|
||||
interface Props {
|
||||
view: ProfileModel
|
||||
@@ -152,21 +152,14 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||
})
|
||||
}, [track, store, view, onRefreshAll])
|
||||
|
||||
const onPressFollowers = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowersButtonClicked')
|
||||
navigate('ProfileFollowers', {
|
||||
name: isInvalidHandle(view.handle) ? view.did : view.handle,
|
||||
})
|
||||
store.shell.closeAllActiveElements() // for when used in the profile preview modal
|
||||
}, [track, view, store.shell])
|
||||
|
||||
const onPressFollows = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowsButtonClicked')
|
||||
navigate('ProfileFollows', {
|
||||
name: isInvalidHandle(view.handle) ? view.did : view.handle,
|
||||
})
|
||||
store.shell.closeAllActiveElements() // for when used in the profile preview modal
|
||||
}, [track, view, store.shell])
|
||||
const trackPress = React.useCallback(
|
||||
(f: 'Followers' | 'Follows') => {
|
||||
track(`ProfileHeader:${f}ButtonClicked`, {
|
||||
handle: view.handle,
|
||||
})
|
||||
},
|
||||
[track, view],
|
||||
)
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
track('ProfileHeader:ShareButtonClicked')
|
||||
@@ -460,11 +453,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||
{!blockHide && (
|
||||
<>
|
||||
<View style={styles.metricsLine}>
|
||||
<TouchableOpacity
|
||||
<Link
|
||||
testID="profileHeaderFollowersButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollowers}
|
||||
accessibilityRole="button"
|
||||
href={makeProfileLink(view, 'followers')}
|
||||
onPressOut={() => trackPress('Followers')}
|
||||
asAnchor
|
||||
accessibilityLabel={`${followers} ${pluralizedFollowers}`}
|
||||
accessibilityHint={'Opens followers list'}>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
@@ -473,12 +467,13 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
{pluralizedFollowers}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</Link>
|
||||
<Link
|
||||
testID="profileHeaderFollowsButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollows}
|
||||
accessibilityRole="button"
|
||||
href={makeProfileLink(view, 'follows')}
|
||||
onPressOut={() => trackPress('Follows')}
|
||||
asAnchor
|
||||
accessibilityLabel={`${following} following`}
|
||||
accessibilityHint={'Opens following list'}>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
@@ -487,7 +482,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
following
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{formatCount(view.postsCount)}{' '}
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
|
||||
@@ -224,7 +224,6 @@ const DropdownItems = ({
|
||||
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onOuterPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Toggle dropdown"
|
||||
accessibilityHint="">
|
||||
<View style={[styles.bg]} />
|
||||
|
||||
@@ -38,11 +38,11 @@ export function PostAlerts({
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<ShieldExclamation style={pal.text} size={16} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||
Learn More
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
{desc.name}{' '}
|
||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||
Learn More
|
||||
</Text>
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {AppState, FlatList, View} from 'react-native'
|
||||
import {FlatList, View} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
@@ -18,7 +18,6 @@ import {s} from 'lib/styles'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {isWeb} from 'platform/detection'
|
||||
|
||||
const NOTIFICATION_MARK_READ_TIMEOUT = 5000
|
||||
type Props = NativeStackScreenProps<
|
||||
NotificationsTabNavigatorParams,
|
||||
'Notifications'
|
||||
@@ -56,22 +55,10 @@ export const NotificationsScreen = withAuthRequired(
|
||||
const softResetSub = store.onScreenSoftReset(onPressLoadLatest)
|
||||
store.me.notifications.update()
|
||||
screen('Notifications')
|
||||
// marks notifications read if the user opens the notifications tab for x seconds
|
||||
// but doesn't open any notifications or any other tab
|
||||
const markReadTimeout = setTimeout(() => {
|
||||
store.me.notifications.markAllRead()
|
||||
}, NOTIFICATION_MARK_READ_TIMEOUT) // mark all notifications as read after 3s
|
||||
// marks notification read if the user suspends the app while in the notification tab
|
||||
// then opens the app into the notification tab and then closes the app again
|
||||
const markReadOnBlur = AppState.addEventListener('change', event => {
|
||||
if (event === 'background') store.me.notifications.markAllRead()
|
||||
})
|
||||
|
||||
return () => {
|
||||
softResetSub.remove()
|
||||
markReadOnBlur.remove()
|
||||
store.me.notifications.markAllRead()
|
||||
clearTimeout(markReadTimeout) // in case we unmount before the timeout fires
|
||||
}
|
||||
}, [store, screen, onPressLoadLatest]),
|
||||
)
|
||||
|
||||
@@ -97,9 +97,9 @@ export const ProfileListScreen = withAuthRequired(
|
||||
}, [store, list])
|
||||
|
||||
const onPressShareList = React.useCallback(() => {
|
||||
const url = toShareUrl(`/profile/${name}/lists/${rkey}`)
|
||||
const url = toShareUrl(`/profile/${list.creatorDid}/lists/${rkey}`)
|
||||
shareUrl(url)
|
||||
}, [name, rkey])
|
||||
}, [list.creatorDid, rkey])
|
||||
|
||||
const renderEmptyState = React.useCallback(() => {
|
||||
return <EmptyState icon="users-slash" message="This list is empty!" />
|
||||
|
||||
@@ -40,6 +40,7 @@ import {AccountData} from 'state/models/session'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {HandIcon} from 'lib/icons'
|
||||
import {formatCount} from 'view/com/util/numeric/format'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {reset as resetNavigation} from '../../Navigation'
|
||||
@@ -175,7 +176,7 @@ export const SettingsScreen = withAuthRequired(
|
||||
Toast.show('Copied build version to clipboard')
|
||||
}, [])
|
||||
|
||||
const openPreferencesModal = React.useCallback(() => {
|
||||
const openHomeFeedPreferences = React.useCallback(() => {
|
||||
navigation.navigate('PreferencesHomeFeed')
|
||||
}, [navigation])
|
||||
|
||||
@@ -220,6 +221,19 @@ export const SettingsScreen = withAuthRequired(
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.infoLine]}>
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
Birthday:{' '}
|
||||
</Text>
|
||||
<Link
|
||||
onPress={() =>
|
||||
store.shell.openModal({name: 'birth-date-settings'})
|
||||
}>
|
||||
<Text type="lg" style={pal.link}>
|
||||
Show
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
<View style={styles.spacer20} />
|
||||
</>
|
||||
) : null}
|
||||
@@ -387,15 +401,15 @@ export const SettingsScreen = withAuthRequired(
|
||||
<View style={styles.spacer20} />
|
||||
|
||||
<Text type="xl-bold" style={[pal.text, styles.heading]}>
|
||||
Advanced
|
||||
Basics
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
testID="preferencesHomeFeedButton"
|
||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
||||
onPress={openPreferencesModal}
|
||||
onPress={openHomeFeedPreferences}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint="Open home feed preferences modal"
|
||||
accessibilityLabel="Opens the home feed preferences modal">
|
||||
accessibilityHint=""
|
||||
accessibilityLabel="Opens the home feed preferences">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="sliders"
|
||||
@@ -406,23 +420,6 @@ export const SettingsScreen = withAuthRequired(
|
||||
Home Feed Preferences
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="appPasswordBtn"
|
||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
||||
onPress={onPressAppPasswords}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint="Open app password settings"
|
||||
accessibilityLabel="Opens the app password settings page">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
App passwords
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="savedFeedsBtn"
|
||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
||||
@@ -456,6 +453,44 @@ export const SettingsScreen = withAuthRequired(
|
||||
Content languages
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="moderationBtn"
|
||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
||||
onPress={
|
||||
isSwitching ? undefined : () => navigation.navigate('Moderation')
|
||||
}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint=""
|
||||
accessibilityLabel="Opens moderation settings">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<HandIcon style={pal.text} size={18} strokeWidth={6} />
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
Moderation
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.spacer20} />
|
||||
|
||||
<Text type="xl-bold" style={[pal.text, styles.heading]}>
|
||||
Advanced
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
testID="appPasswordBtn"
|
||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
||||
onPress={onPressAppPasswords}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint="Open app password settings"
|
||||
accessibilityLabel="Opens the app password settings page">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
App passwords
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="changeHandleBtn"
|
||||
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
|
||||
@@ -620,6 +655,8 @@ const styles = StyleSheet.create({
|
||||
paddingBottom: 6,
|
||||
},
|
||||
infoLine: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 18,
|
||||
paddingBottom: 6,
|
||||
},
|
||||
|
||||
@@ -311,7 +311,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
|
||||
icon={
|
||||
<HandIcon
|
||||
strokeWidth={5.5}
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
style={pal.text}
|
||||
size={isDesktop ? 24 : 27}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
tlds "^1.234.0"
|
||||
typed-emitter "^2.1.0"
|
||||
|
||||
"@atproto/api@^0.6.8":
|
||||
version "0.6.8"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.8.tgz#fe77f3ab2e7a815edca1357b0a89a7496be8f764"
|
||||
integrity sha512-WmXpIbO79f85UA8AzvvSqKibojBXN1HT3KEHhUOqXJRW8X1trYijgWIXwhnxhoBQXgiQfzKG7HdORvRjmRSLoQ==
|
||||
"@atproto/api@^0.6.12":
|
||||
version "0.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.12.tgz#f39ad9d225aafc5fd90f07d0011d63435c657775"
|
||||
integrity sha512-9R8F78553GI47Iq4FDVwL05LorWTQZQ6FmFsDF/+yryiA+a/VVyvYG4USSptURBZCRgZA5VgTW1We/PwAcDfEA==
|
||||
dependencies:
|
||||
"@atproto/common-web" "*"
|
||||
"@atproto/syntax" "*"
|
||||
"@atproto/xrpc" "*"
|
||||
"@atproto/common-web" "^0.2.0"
|
||||
"@atproto/lexicon" "^0.2.0"
|
||||
"@atproto/syntax" "^0.1.0"
|
||||
"@atproto/xrpc" "^0.3.0"
|
||||
multiformats "^9.9.0"
|
||||
tlds "^1.234.0"
|
||||
typed-emitter "^2.1.0"
|
||||
|
||||
@@ -104,7 +106,7 @@
|
||||
typed-emitter "^2.1.0"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/common-web@*":
|
||||
"@atproto/common-web@*", "@atproto/common-web@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.0.tgz#8420da28e89dac64ad2a11b6ff10a4023c67151f"
|
||||
integrity sha512-ugKrT8CWf6PDsZ29VOhOCs5K4z9BAFIV7SxPXA+MHC7pINqQ+wyjIq+DtUI8kmUSce1dTqc/lMN1akf/W5whVQ==
|
||||
@@ -203,7 +205,7 @@
|
||||
axios "^0.27.2"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/lexicon@*":
|
||||
"@atproto/lexicon@*", "@atproto/lexicon@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.2.0.tgz#e18590f248fa2795a67d7440adba73ab5a48dd1e"
|
||||
integrity sha512-0OpLZ4o4pbRj2e19h2fO69781wPmL0S8WJaXey3y9FLjV19lj18IEbnlE/OQG8qZNDyGmgezs00L/o7qNCJorw==
|
||||
@@ -317,7 +319,7 @@
|
||||
uint8arrays "3.0.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/syntax@*":
|
||||
"@atproto/syntax@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.0.tgz#f13b9dad8d13342cc54295ecd702ea13c82c9bf0"
|
||||
integrity sha512-Ktui0qvIXt1o1Px1KKC0eqn69MfRHQ9ok5EwjcxIEPbJ8OY5XqkeyJneFDIWRJZiR6vqPHfjFYRUpTB+jNPfRQ==
|
||||
@@ -356,6 +358,14 @@
|
||||
"@atproto/lexicon" "*"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/xrpc@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.3.0.tgz#32dd3a15fedad401b94fdf38313aebff1c0b9d0d"
|
||||
integrity sha512-PYE/4GT/sS4s/FxR5QO2YBaLU6GdhraYVfMOtfj+AN8YgZw1QoMHXkAv4v+BVH4T5HWU2XwZEBZvwfsr7YcewA==
|
||||
dependencies:
|
||||
"@atproto/lexicon" "*"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@aws-crypto/crc32@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"
|
||||
@@ -10273,24 +10283,24 @@ expo-font@~11.4.0:
|
||||
dependencies:
|
||||
fontfaceobserver "^2.1.0"
|
||||
|
||||
expo-image-loader@~4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.3.0.tgz#d897b4d4f1838faa8291c36f00b99c2b36a6ac5c"
|
||||
integrity sha512-2kqJIO+oYM8J3GbvTUHLqTSpt1dLpOn/X0eB4U4RTuzz/faj8l/TyQELsMBLlGAkweNUuG9LqznbaBz+WuSFEw==
|
||||
expo-image-loader@~4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.4.0.tgz#35909516311aa60fa4bf35aa6eb7624d7f7f788d"
|
||||
integrity sha512-dq88tiRB8aB1U+O/oJI7IY4VU9Nu2iDjEwR8QDfLtD4xWd5lNGoyic5Vh7911E1qNJFqNMSVIVzOD5l1SQ3A6g==
|
||||
|
||||
expo-image-manipulator@~11.3.0:
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-11.3.0.tgz#3bf5e1d7331c182950f73bfacf2060cc4604170e"
|
||||
integrity sha512-lw17v8VD5LqE+j3YMZNNlfM19OlkBPzLEoVyGcx9FtGC533le5dnXF+UiZTCbFIVr/xyBJTMse9G0oJ70q9MbQ==
|
||||
expo-image-manipulator@~11.5.0:
|
||||
version "11.5.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-11.5.0.tgz#2a0f4c069dc25722c2c8f0e15c0c42353f1115a4"
|
||||
integrity sha512-W/mwSIuLyolDDSzIkENvwVoQpe6HJfpcAQbeu3VX3ZcTJrgvORM/tfuCJacCTHQ0iVIB49Txv65CgbtO+3Pd6w==
|
||||
dependencies:
|
||||
expo-image-loader "~4.3.0"
|
||||
expo-image-loader "~4.4.0"
|
||||
|
||||
expo-image-picker@~14.3.2:
|
||||
version "14.3.2"
|
||||
resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.3.2.tgz#5ae2f36fe518a10c8fe82e5ff2ee735c14817cfc"
|
||||
integrity sha512-xr/YeQMIYheXecWP033F2SPwpBlBR5xVCx7YSfSCTH8Y9pw7Z886agqKGbS9QBVGlzJ5qecJktZ6ASSzeslDVg==
|
||||
expo-image-picker@~14.5.0:
|
||||
version "14.5.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.5.0.tgz#fdc14c74ad338a6a630f8fc5c81e642d6bea343c"
|
||||
integrity sha512-9icvfQuWMdNKufFqu+FxenKcfybz9ior+XYQCIl/q5+mpHPo2tKq8I30vu2BjsOJ0Cfo7V4vMeN7a9F/3ooUfQ==
|
||||
dependencies:
|
||||
expo-image-loader "~4.3.0"
|
||||
expo-image-loader "~4.4.0"
|
||||
|
||||
expo-image@~1.3.2:
|
||||
version "1.3.2"
|
||||
@@ -14637,7 +14647,7 @@ multicast-dns@^7.2.5:
|
||||
dns-packet "^5.2.2"
|
||||
thunky "^1.0.2"
|
||||
|
||||
multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6.4:
|
||||
multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6.4, multiformats@^9.9.0:
|
||||
version "9.9.0"
|
||||
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
|
||||
integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==
|
||||
|
||||
Reference in New Issue
Block a user