Merge remote-tracking branch 'origin/main' into eric/app-864-integrate-post-tags-into-app
* origin/main: (40 commits) 1.52 README: tweaks to high-level context (#1625) Fix stuck lightbox header after double tap (#1627) Fix: add padding to the spinner bottom while loading threads (#1626) Rewrite Android lightbox (#1624) Dont trim before posting (close #1621) (#1622) Only listen to back button on android (#1623) Improve typeahead search with inclusion of followed users (temporary solution) (#1612) Slightly smaller highlighted post text (#1608) Pull upstream bugfixes to bottom-sheet (#1606) Fix animations and gestures getting reset on state updates in the lightbox (#1618) Remove unused lightbox options (#1616) Profile UI tweaks (#1607) Fix invite codes flash on desktop, use loading placeholder (#1591) Update to react-native@0.72.5 (#1599) Fixed a typo on the onboarding recommended screen (#1604) Onboarding & feed fixes (#1602) Improve time to content in the search page (#1603) Fix a potential reference error in bottombarweb (#1600) Fix: only use scroll-positioning control on thread when looking at replies (#1587) ...
This commit is contained in:
@@ -95,7 +95,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||
| undefined
|
||||
let reply
|
||||
let rt = new RichText(
|
||||
{text: opts.rawText.trim()},
|
||||
{text: opts.rawText.trimEnd()},
|
||||
{
|
||||
cleanNewlines: true,
|
||||
},
|
||||
|
||||
+3
-10
@@ -79,6 +79,7 @@ export async function DEFAULT_FEEDS(
|
||||
serviceUrl: string,
|
||||
resolveHandle: (name: string) => Promise<string>,
|
||||
) {
|
||||
// TODO: remove this when the test suite no longer relies on it
|
||||
if (IS_LOCAL_DEV(serviceUrl)) {
|
||||
// local dev
|
||||
const aliceDid = await resolveHandle('alice.test')
|
||||
@@ -106,16 +107,8 @@ export async function DEFAULT_FEEDS(
|
||||
} else {
|
||||
// production
|
||||
return {
|
||||
pinned: [
|
||||
PROD_DEFAULT_FEED('whats-hot'),
|
||||
PROD_DEFAULT_FEED('with-friends'),
|
||||
],
|
||||
saved: [
|
||||
PROD_DEFAULT_FEED('bsky-team'),
|
||||
PROD_DEFAULT_FEED('with-friends'),
|
||||
PROD_DEFAULT_FEED('whats-hot'),
|
||||
PROD_DEFAULT_FEED('hot-classic'),
|
||||
],
|
||||
pinned: [PROD_DEFAULT_FEED('whats-hot')],
|
||||
saved: [PROD_DEFAULT_FEED('whats-hot')],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {useStores} from 'state/index'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {AccountData} from 'state/models/session'
|
||||
import {reset as resetNavigation} from '../../Navigation'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
|
||||
export function useAccountSwitcher(): [
|
||||
boolean,
|
||||
(v: boolean) => void,
|
||||
(acct: AccountData) => Promise<void>,
|
||||
] {
|
||||
const {track} = useAnalytics()
|
||||
|
||||
const store = useStores()
|
||||
const [isSwitching, setIsSwitching] = useState(false)
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onPressSwitchAccount = useCallback(
|
||||
async (acct: AccountData) => {
|
||||
track('Settings:SwitchAccountButtonClicked')
|
||||
setIsSwitching(true)
|
||||
const success = await store.session.resumeSession(acct)
|
||||
store.shell.closeAllActiveElements()
|
||||
if (success) {
|
||||
resetNavigation()
|
||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||
} else {
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
store.session.clear()
|
||||
}
|
||||
},
|
||||
[track, setIsSwitching, navigation, store],
|
||||
)
|
||||
|
||||
return [isSwitching, setIsSwitching, onPressSwitchAccount]
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {useStores} from 'state/index'
|
||||
import {FollowState} from 'state/models/cache/my-follows'
|
||||
|
||||
export function useFollowDid({did}: {did: string}) {
|
||||
export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
|
||||
const store = useStores()
|
||||
const state = store.me.follows.getFollowState(did)
|
||||
const state = store.me.follows.getFollowState(profile.did)
|
||||
|
||||
return {
|
||||
state,
|
||||
@@ -13,8 +13,10 @@ export function useFollowDid({did}: {did: string}) {
|
||||
toggle: React.useCallback(async () => {
|
||||
if (state === FollowState.Following) {
|
||||
try {
|
||||
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
|
||||
store.me.follows.removeFollow(did)
|
||||
await store.agent.deleteFollow(
|
||||
store.me.follows.getFollowUri(profile.did),
|
||||
)
|
||||
store.me.follows.removeFollow(profile.did)
|
||||
return {
|
||||
state: FollowState.NotFollowing,
|
||||
following: false,
|
||||
@@ -25,8 +27,14 @@ export function useFollowDid({did}: {did: string}) {
|
||||
}
|
||||
} else if (state === FollowState.NotFollowing) {
|
||||
try {
|
||||
const res = await store.agent.follow(did)
|
||||
store.me.follows.addFollow(did, res.uri)
|
||||
const res = await store.agent.follow(profile.did)
|
||||
store.me.follows.addFollow(profile.did, {
|
||||
followRecordUri: res.uri,
|
||||
did: profile.did,
|
||||
handle: profile.handle,
|
||||
displayName: profile.displayName,
|
||||
avatar: profile.avatar,
|
||||
})
|
||||
return {
|
||||
state: FollowState.Following,
|
||||
following: true,
|
||||
@@ -41,6 +49,6 @@ export function useFollowDid({did}: {did: string}) {
|
||||
state: FollowState.Unknown,
|
||||
following: false,
|
||||
}
|
||||
}, [store, did, state]),
|
||||
}, [store, profile, state]),
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,18 @@ import {useState, useCallback, useRef} from 'react'
|
||||
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
import {s} from 'lib/styles'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from './useWebMediaQueries'
|
||||
|
||||
const DY_LIMIT_UP = isDesktopWeb ? 30 : 10
|
||||
const DY_LIMIT_DOWN = isDesktopWeb ? 150 : 10
|
||||
const Y_LIMIT = 10
|
||||
|
||||
const useDeviceLimits = () => {
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
return {
|
||||
dyLimitUp: isDesktop ? 30 : 10,
|
||||
dyLimitDown: isDesktop ? 150 : 10,
|
||||
}
|
||||
}
|
||||
|
||||
export type OnScrollCb = (
|
||||
event: NativeSyntheticEvent<NativeScrollEvent>,
|
||||
) => void
|
||||
@@ -18,6 +24,8 @@ export function useOnMainScroll(
|
||||
): [OnScrollCb, boolean, ResetCb] {
|
||||
let lastY = useRef(0)
|
||||
let [isScrolledDown, setIsScrolledDown] = useState(false)
|
||||
const {dyLimitUp, dyLimitDown} = useDeviceLimits()
|
||||
|
||||
return [
|
||||
useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
@@ -25,15 +33,11 @@ export function useOnMainScroll(
|
||||
const dy = y - (lastY.current || 0)
|
||||
lastY.current = y
|
||||
|
||||
if (
|
||||
!store.shell.minimalShellMode &&
|
||||
dy > DY_LIMIT_DOWN &&
|
||||
y > Y_LIMIT
|
||||
) {
|
||||
if (!store.shell.minimalShellMode && dy > dyLimitDown && y > Y_LIMIT) {
|
||||
store.shell.setMinimalShellMode(true)
|
||||
} else if (
|
||||
store.shell.minimalShellMode &&
|
||||
(dy < DY_LIMIT_UP * -1 || y <= Y_LIMIT)
|
||||
(dy < dyLimitUp * -1 || y <= Y_LIMIT)
|
||||
) {
|
||||
store.shell.setMinimalShellMode(false)
|
||||
}
|
||||
@@ -50,7 +54,7 @@ export function useOnMainScroll(
|
||||
setIsScrolledDown(false)
|
||||
}
|
||||
},
|
||||
[store, isScrolledDown],
|
||||
[store.shell, dyLimitDown, dyLimitUp, isScrolledDown],
|
||||
),
|
||||
isScrolledDown,
|
||||
useCallback(() => {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import {isAndroid} from 'platform/detection'
|
||||
import {BackHandler} from 'react-native'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
|
||||
export function init(store: RootStoreModel) {
|
||||
BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
return store.shell.closeAnyActiveElement()
|
||||
})
|
||||
// only register back handler on android, otherwise it throws an error
|
||||
if (isAndroid) {
|
||||
const backHandler = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
() => {
|
||||
return store.shell.closeAnyActiveElement()
|
||||
},
|
||||
)
|
||||
return () => {
|
||||
backHandler.remove()
|
||||
}
|
||||
}
|
||||
return () => {}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,20 @@ export function enforceLen(str: string, len: number, ellipsis = false): string {
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/52171480
|
||||
export function toHashCode(str: string, seed = 0): number {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
h2 = 0x41c6ce57 ^ seed
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i)
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761)
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677)
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
||||
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {PROD_SERVICE} from 'state/index'
|
||||
import TLDs from 'tlds'
|
||||
import psl from 'psl'
|
||||
|
||||
export function isValidDomain(str: string): boolean {
|
||||
return !!TLDs.find(tld => {
|
||||
@@ -166,3 +167,53 @@ export function getYoutubeVideoId(link: string): string | undefined {
|
||||
}
|
||||
return videoId
|
||||
}
|
||||
|
||||
export function linkRequiresWarning(uri: string, label: string) {
|
||||
const labelDomain = labelToDomain(label)
|
||||
if (!labelDomain) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const urip = new URL(uri)
|
||||
return labelDomain !== urip.hostname
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function labelToDomain(label: string): string | undefined {
|
||||
// any spaces just immediately consider the label a non-url
|
||||
if (/\s/.test(label)) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
return new URL(label).hostname
|
||||
} catch {}
|
||||
try {
|
||||
return new URL('https://' + label).hostname
|
||||
} catch {}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function isPossiblyAUrl(str: string): boolean {
|
||||
str = str.trim()
|
||||
if (str.startsWith('http://')) {
|
||||
return true
|
||||
}
|
||||
if (str.startsWith('https://')) {
|
||||
return true
|
||||
}
|
||||
const [firstWord] = str.split(/[\s\/]/)
|
||||
return isValidDomain(firstWord)
|
||||
}
|
||||
|
||||
export function splitApexDomain(hostname: string): [string, string] {
|
||||
const hostnamep = psl.parse(hostname)
|
||||
if (hostnamep.error || !hostnamep.listed || !hostnamep.domain) {
|
||||
return ['', hostname]
|
||||
}
|
||||
return [
|
||||
hostnamep.subdomain ? `${hostnamep.subdomain}.` : '',
|
||||
hostnamep.domain,
|
||||
]
|
||||
}
|
||||
|
||||
+2
-2
@@ -264,8 +264,8 @@ export const defaultTheme: Theme = {
|
||||
fontWeight: '400',
|
||||
},
|
||||
'post-text-lg': {
|
||||
fontSize: 22,
|
||||
letterSpacing: 0.4,
|
||||
fontSize: 20,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: '400',
|
||||
},
|
||||
'button-lg': {
|
||||
|
||||
Reference in New Issue
Block a user