about summary refs log tree commit diff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/constants.ts36
-rw-r--r--src/lib/hooks/useSetTitle.ts20
-rw-r--r--src/lib/icons.tsx55
-rw-r--r--src/lib/labeling/helpers.ts33
-rw-r--r--src/lib/labeling/types.ts4
-rw-r--r--src/lib/media/picker.tsx49
-rw-r--r--src/lib/routes/types.ts9
-rw-r--r--src/lib/strings/display-names.ts15
-rw-r--r--src/lib/strings/headings.ts4
-rw-r--r--src/lib/strings/url-helpers.ts9
10 files changed, 188 insertions, 46 deletions
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index df325b059..6d0d4797b 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -33,42 +33,50 @@ export function TEAM_HANDLES(serviceUrl: string) {
   }
 }
 
+// NOTE
+// this is a temporary list that we periodically update
+// it is used in the search interface if the user doesn't follow anybody
+// -prf
 export const PROD_SUGGESTED_FOLLOWS = [
-  'faithlove.art',
-  'danielkoeth.bsky.social',
   'bsky.app',
   'jay.bsky.team',
   'pfrazee.com',
   'why.bsky.team',
-  'support.bsky.team',
+  'dholms.xyz',
+  'emily.bsky.team',
+  'rose.bsky.team',
   'jack.bsky.social',
-  'earthquake.bsky.social',
+  'faithlove.art',
+  'annaghughes.bsky.social',
+  'astrokatie.com',
+  'whysharksmatter.bsky.social',
   'jamesgunn.bsky.social',
   'seangunn.bsky.social',
   'kumail.bsky.social',
   'craignewmark.bsky.social',
-  'grimes.bsky.social',
   'xychelsea.tv',
+  'catsofyore.bsky.social',
   'mcq.bsky.social',
   'mmasnick.bsky.social',
-  'nitasha.bsky.social',
-  'kenklippenstein.bsky.social',
-  'jaypeters.bsky.social',
-  'miyagawa.bsky.social',
-  'anildash.com',
-  'tiffani.bsky.social',
   'kelseyhightower.com',
   'aliafonzy.bsky.social',
-  'tszzl.bsky.social',
+  'bradfitz.com',
   'danabramov.bsky.social',
   'shinyakato.dev',
   'karpathy.bsky.social',
   'lookitup.baby',
+  'pariss.blacktechpipeline.com',
+  'swiftonsecurity.com',
+  'ericajoy.astrel.la',
+  'b0rk.jvns.ca',
+  'vickiboykis.com',
   'brooke.vibe.camp',
-  'mollywhite.net',
   'amir.blue',
-  'zoink.bsky.social',
   'moskov.bsky.social',
+  'neilhimself.bsky.social',
+  'kylierobison.com',
+  'carnage4life.bsky.social',
+  'lolennui.bsky.social',
 ]
 export const STAGING_SUGGESTED_FOLLOWS = ['arcalinea', 'paul', 'paul2'].map(
   handle => `${handle}.staging.bsky.dev`,
diff --git a/src/lib/hooks/useSetTitle.ts b/src/lib/hooks/useSetTitle.ts
new file mode 100644
index 000000000..c5c7a5ca1
--- /dev/null
+++ b/src/lib/hooks/useSetTitle.ts
@@ -0,0 +1,20 @@
+import {useEffect} from 'react'
+import {useNavigation} from '@react-navigation/native'
+
+import {NavigationProp} from 'lib/routes/types'
+import {bskyTitle} from 'lib/strings/headings'
+import {useStores} from 'state/index'
+
+/**
+ * Requires consuming component to be wrapped in `observer`:
+ * https://stackoverflow.com/a/71488009
+ */
+export function useSetTitle(title?: string) {
+  const navigation = useNavigation<NavigationProp>()
+  const {unreadCountLabel} = useStores().me.notifications
+  useEffect(() => {
+    if (title) {
+      navigation.setOptions({title: bskyTitle(title, unreadCountLabel)})
+    }
+  }, [title, navigation, unreadCountLabel])
+}
diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx
index 960090ad7..0c7b7512a 100644
--- a/src/lib/icons.tsx
+++ b/src/lib/icons.tsx
@@ -322,6 +322,35 @@ export function MoonIcon({
 
 // Copyright (c) 2020 Refactoring UI Inc.
 // https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
+export function SunIcon({
+  style,
+  size,
+  strokeWidth = 1.5,
+}: {
+  style?: StyleProp<ViewStyle>
+  size?: string | number
+  strokeWidth?: number
+}) {
+  return (
+    <Svg
+      fill="none"
+      viewBox="0 0 24 24"
+      width={size || 32}
+      height={size || 32}
+      strokeWidth={strokeWidth}
+      stroke="currentColor"
+      style={style}>
+      <Path
+        d="M12 3V5.25M18.364 5.63604L16.773 7.22703M21 12H18.75M18.364 18.364L16.773 16.773M12 18.75V21M7.22703 16.773L5.63604 18.364M5.25 12H3M7.22703 7.22703L5.63604 5.63604M15.75 12C15.75 14.0711 14.0711 15.75 12 15.75C9.92893 15.75 8.25 14.0711 8.25 12C8.25 9.92893 9.92893 8.25 12 8.25C14.0711 8.25 15.75 9.92893 15.75 12Z"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+    </Svg>
+  )
+}
+
+// Copyright (c) 2020 Refactoring UI Inc.
+// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
 export function UserIcon({
   style,
   size,
@@ -828,3 +857,29 @@ export function InfoCircleIcon({
     </Svg>
   )
 }
+
+export function HandIcon({
+  style,
+  size,
+  strokeWidth = 1.5,
+}: {
+  style?: StyleProp<TextStyle>
+  size?: string | number
+  strokeWidth?: number
+}) {
+  return (
+    <Svg
+      width={size}
+      height={size}
+      viewBox="0 0 76 76"
+      stroke="currentColor"
+      strokeWidth={strokeWidth}
+      strokeLinecap="round"
+      fill="none"
+      style={style}>
+      <Path d="M33.5 37V11.5C33.5 8.46243 31.0376 6 28 6V6C24.9624 6 22.5 8.46243 22.5 11.5V48V48C22.5 48.5802 21.8139 48.8874 21.3811 48.501L13.2252 41.2189C10.72 38.9821 6.81945 39.4562 4.92296 42.228L4.77978 42.4372C3.17708 44.7796 3.50863 47.9385 5.56275 49.897L16.0965 59.9409C20.9825 64.5996 26.7533 68.231 33.0675 70.6201V70.6201C38.8234 72.798 45.1766 72.798 50.9325 70.6201L51.9256 70.2444C57.4044 68.1713 61.8038 63.9579 64.1113 58.5735V58.5735C65.6874 54.8962 66.5 50.937 66.5 46.9362V22.5C66.5 19.4624 64.0376 17 61 17V17C57.9624 17 55.5 19.4624 55.5 22.5V36.5" />
+      <Path d="M55.5 37V11.5C55.5 8.46243 53.0376 6 50 6V6C46.9624 6 44.5 8.46243 44.5 11.5V37" />
+      <Path d="M44.5 37V8.5C44.5 5.46243 42.0376 3 39 3V3C35.9624 3 33.5 5.46243 33.5 8.5V37" />
+    </Svg>
+  )
+}
diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts
index baac0ed5a..447b0a99a 100644
--- a/src/lib/labeling/helpers.ts
+++ b/src/lib/labeling/helpers.ts
@@ -1,5 +1,6 @@
 import {
   AppBskyActorDefs,
+  AppBskyGraphDefs,
   AppBskyEmbedRecordWithMedia,
   AppBskyEmbedRecord,
   AppBskyEmbedImages,
@@ -16,6 +17,7 @@ import {
   Label,
   LabelValGroup,
   ModerationBehaviorCode,
+  ModerationBehavior,
   PostModeration,
   ProfileModeration,
   PostLabelInfo,
@@ -127,11 +129,15 @@ export function getPostModeration(
 
   // muting
   if (postInfo.isMuted) {
+    let msg = 'Post from an account you muted.'
+    if (postInfo.mutedByList) {
+      msg = `Muted by ${postInfo.mutedByList.name}`
+    }
     return {
       avatar,
-      list: hide('Post from an account you muted.'),
-      thread: warn('Post from an account you muted.'),
-      view: warn('Post from an account you muted.'),
+      list: isMute(hide(msg)),
+      thread: isMute(warn(msg)),
+      view: isMute(warn(msg)),
     }
   }
 
@@ -273,6 +279,7 @@ export function getProfileViewBasicLabelInfo(
     profileLabels: filterProfileLabels(profile.labels),
     isMuted: profile.viewer?.muted || false,
     isBlocking: !!profile.viewer?.blocking || false,
+    isBlockedBy: !!profile.viewer?.blockedBy || false,
   }
 }
 
@@ -302,6 +309,21 @@ export function getEmbedMuted(embed?: Embed): boolean {
   return false
 }
 
+export function getEmbedMutedByList(
+  embed?: Embed,
+): AppBskyGraphDefs.ListViewBasic | undefined {
+  if (!embed) {
+    return undefined
+  }
+  if (
+    AppBskyEmbedRecord.isView(embed) &&
+    AppBskyEmbedRecord.isViewRecord(embed.record)
+  ) {
+    return embed.record.author.viewer?.mutedByList
+  }
+  return undefined
+}
+
 export function getEmbedBlocking(embed?: Embed): boolean {
   if (!embed) {
     return false
@@ -401,6 +423,11 @@ function warnContent(reason: string) {
   }
 }
 
+function isMute(behavior: ModerationBehavior): ModerationBehavior {
+  behavior.isMute = true
+  return behavior
+}
+
 function warnImages(reason: string) {
   return {
     behavior: ModerationBehaviorCode.WarnImages,
diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts
index 078043076..1ee058024 100644
--- a/src/lib/labeling/types.ts
+++ b/src/lib/labeling/types.ts
@@ -1,4 +1,4 @@
-import {ComAtprotoLabelDefs} from '@atproto/api'
+import {ComAtprotoLabelDefs, AppBskyGraphDefs} from '@atproto/api'
 import {LabelPreferencesModel} from 'state/models/ui/preferences'
 
 export type Label = ComAtprotoLabelDefs.Label
@@ -22,6 +22,7 @@ export interface PostLabelInfo {
   accountLabels: Label[]
   profileLabels: Label[]
   isMuted: boolean
+  mutedByList?: AppBskyGraphDefs.ListViewBasic
   isBlocking: boolean
   isBlockedBy: boolean
 }
@@ -44,6 +45,7 @@ export enum ModerationBehaviorCode {
 
 export interface ModerationBehavior {
   behavior: ModerationBehaviorCode
+  isMute?: boolean
   noOverride?: boolean
   reason?: string
 }
diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx
index af4a3e4d3..7c6dfcc44 100644
--- a/src/lib/media/picker.tsx
+++ b/src/lib/media/picker.tsx
@@ -1,12 +1,16 @@
 import {
-  openPicker as openPickerFn,
   openCamera as openCameraFn,
   openCropper as openCropperFn,
-  ImageOrVideo,
+  Image as RNImage,
 } from 'react-native-image-crop-picker'
 import {RootStoreModel} from 'state/index'
-import {PickerOpts, CameraOpts, CropperOptions} from './types'
-import {Image as RNImage} from 'react-native-image-crop-picker'
+import {CameraOpts, CropperOptions} from './types'
+import {
+  ImagePickerOptions,
+  launchImageLibraryAsync,
+  MediaTypeOptions,
+} from 'expo-image-picker'
+import {getDataUriSize} from './util'
 
 /**
  * NOTE
@@ -19,27 +23,22 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
 
 export async function openPicker(
   _store: RootStoreModel,
-  opts?: PickerOpts,
-): Promise<RNImage[]> {
-  const items = await openPickerFn({
-    mediaType: 'photo', // TODO: eventually add other media types
-    multiple: opts?.multiple,
-    maxFiles: opts?.maxFiles,
-    forceJpg: true, // ios only
-    compressImageQuality: 0.8,
+  opts?: ImagePickerOptions,
+) {
+  const response = await launchImageLibraryAsync({
+    exif: false,
+    mediaTypes: MediaTypeOptions.Images,
+    quality: 1,
+    ...opts,
   })
 
-  const toMedia = (item: ImageOrVideo) => ({
-    path: item.path,
-    mime: item.mime,
-    size: item.size,
-    width: item.width,
-    height: item.height,
-  })
-  if (Array.isArray(items)) {
-    return items.map(toMedia)
-  }
-  return [toMedia(items)]
+  return (response.assets ?? []).map(image => ({
+    mime: 'image/jpeg',
+    height: image.height,
+    width: image.width,
+    path: image.uri,
+    size: getDataUriSize(image.uri),
+  }))
 }
 
 export async function openCamera(
@@ -55,6 +54,7 @@ export async function openCamera(
     forceJpg: true, // ios only
     compressImageQuality: 0.8,
   })
+
   return {
     path: item.path,
     mime: item.mime,
@@ -67,11 +67,10 @@ export async function openCamera(
 export async function openCropper(
   _store: RootStoreModel,
   opts: CropperOptions,
-): Promise<RNImage> {
+) {
   const item = await openCropperFn({
     ...opts,
     forceJpg: true, // ios only
-    compressImageQuality: 0.8,
   })
 
   return {
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index b1dcbb999..8b96aaad7 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -5,13 +5,19 @@ export type {NativeStackScreenProps} from '@react-navigation/native-stack'
 
 export type CommonNavigatorParams = {
   NotFound: undefined
+  Moderation: undefined
+  ModerationMuteLists: undefined
+  ModerationMutedAccounts: undefined
+  ModerationBlockedAccounts: undefined
   Settings: undefined
   Profile: {name: string; hideBackButton?: boolean}
   ProfileFollowers: {name: string}
   ProfileFollows: {name: string}
+  ProfileList: {name: string; rkey: string}
   PostThread: {name: string; rkey: string}
   PostLikedBy: {name: string; rkey: string}
   PostRepostedBy: {name: string; rkey: string}
+  CustomFeed: {name: string; rkey: string; displayName?: string}
   Debug: undefined
   Log: undefined
   Support: undefined
@@ -22,9 +28,6 @@ export type CommonNavigatorParams = {
   AppPasswords: undefined
   SavedFeeds: undefined
   PinnedFeeds: undefined
-  CustomFeed: {name: string; rkey: string; displayName?: string}
-  MutedAccounts: undefined
-  BlockedAccounts: undefined
 }
 
 export type BottomTabNavigatorParams = CommonNavigatorParams & {
diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts
index 555151b55..b98153732 100644
--- a/src/lib/strings/display-names.ts
+++ b/src/lib/strings/display-names.ts
@@ -10,3 +10,18 @@ export function sanitizeDisplayName(str: string): string {
   }
   return ''
 }
+
+export function combinedDisplayName({
+  handle,
+  displayName,
+}: {
+  handle?: string
+  displayName?: string
+}): string {
+  if (!handle) {
+    return ''
+  }
+  return displayName
+    ? `${sanitizeDisplayName(displayName)} (@${handle})`
+    : `@${handle}`
+}
diff --git a/src/lib/strings/headings.ts b/src/lib/strings/headings.ts
new file mode 100644
index 000000000..a88a69645
--- /dev/null
+++ b/src/lib/strings/headings.ts
@@ -0,0 +1,4 @@
+export function bskyTitle(page: string, unreadCountLabel?: string) {
+  const unreadPrefix = unreadCountLabel ? `(${unreadCountLabel}) ` : ''
+  return `${unreadPrefix}${page} - Bluesky`
+}
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index 549587f74..a5412920e 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -94,6 +94,15 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
   return url
 }
 
+export function listUriToHref(url: string): string {
+  try {
+    const {hostname, rkey} = new AtUri(url)
+    return `/profile/${hostname}/lists/${rkey}`
+  } catch {
+    return ''
+  }
+}
+
 export function getYoutubeVideoId(link: string): string | undefined {
   let url
   try {