diff --git a/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx b/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx
index dace16dad6..8821915642 100644
--- a/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx
+++ b/apps/tlon-mobile/src/fixtures/GroupList.fixture.tsx
@@ -100,6 +100,8 @@ export default {
basic: (
{}}
pinned={[groupWithLongTitle, groupWithImage].map((g) =>
makeChannelSummary({ group: g })
)}
@@ -116,6 +118,8 @@ export default {
emptyPinned: (
{}}
pinned={[dmSummary, groupDmSummary]}
unpinned={[
groupWithColorAndNoImage,
@@ -129,7 +133,13 @@ export default {
),
loading: (
-
+ {}}
+ pinned={[]}
+ unpinned={[]}
+ pendingChats={[]}
+ />
),
};
diff --git a/apps/tlon-mobile/src/fixtures/ProfileWidget.fixture.tsx b/apps/tlon-mobile/src/fixtures/ProfileWidget.fixture.tsx
index 87ab509823..293a2419f2 100644
--- a/apps/tlon-mobile/src/fixtures/ProfileWidget.fixture.tsx
+++ b/apps/tlon-mobile/src/fixtures/ProfileWidget.fixture.tsx
@@ -12,7 +12,6 @@ export default {
>
@@ -26,11 +25,7 @@ export default {
verticalAlign="center"
>
-
+
),
@@ -44,7 +39,6 @@ export default {
diff --git a/apps/tlon-mobile/src/hooks/useHandleLogout.ts b/apps/tlon-mobile/src/hooks/useHandleLogout.ts
new file mode 100644
index 0000000000..6d757ac6c2
--- /dev/null
+++ b/apps/tlon-mobile/src/hooks/useHandleLogout.ts
@@ -0,0 +1,23 @@
+import * as api from '@tloncorp/shared/dist/api';
+import { useCallback } from 'react';
+
+import { clearShipInfo, useShip } from '../contexts/ship';
+import { resetDb } from '../lib/nativeDb';
+import { removeHostingToken, removeHostingUserId } from '../utils/hosting';
+
+export function useHandleLogout() {
+ const { clearShip } = useShip();
+
+ const handleLogout = useCallback(async () => {
+ api.queryClient.clear();
+ api.removeUrbitClient();
+ clearShip();
+ clearShipInfo();
+ removeHostingToken();
+ removeHostingUserId();
+ // delay DB reset to next tick to avoid race conditions
+ setTimeout(() => resetDb());
+ }, [clearShip]);
+
+ return handleLogout;
+}
diff --git a/apps/tlon-mobile/src/hooks/useLogout.ts b/apps/tlon-mobile/src/hooks/useLogout.ts
deleted file mode 100644
index 1c9dba1b34..0000000000
--- a/apps/tlon-mobile/src/hooks/useLogout.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { removeUrbitClient } from '@tloncorp/shared/dist/api';
-import { useCallback } from 'react';
-
-import { useShip } from '../contexts/ship';
-import { removeHostingToken, removeHostingUserId } from '../utils/hosting';
-
-export function useLogout() {
- const { clearShip } = useShip();
- const handleLogout = useCallback(() => {
- clearShip();
- removeUrbitClient();
- removeHostingToken();
- removeHostingUserId();
- }, [clearShip]);
-
- return { handleLogout };
-}
diff --git a/apps/tlon-mobile/src/hooks/useNotificationListener.ts b/apps/tlon-mobile/src/hooks/useNotificationListener.ts
index 35748d8016..5902367a19 100644
--- a/apps/tlon-mobile/src/hooks/useNotificationListener.ts
+++ b/apps/tlon-mobile/src/hooks/useNotificationListener.ts
@@ -1,9 +1,11 @@
+import crashlytics from '@react-native-firebase/crashlytics';
import type { NavigationProp } from '@react-navigation/native';
import { CommonActions, useNavigation } from '@react-navigation/native';
import { syncDms, syncGroups } from '@tloncorp/shared';
import { markChatRead } from '@tloncorp/shared/dist/api';
import * as api from '@tloncorp/shared/dist/api';
import * as db from '@tloncorp/shared/dist/db';
+import * as store from '@tloncorp/shared/dist/store';
import { whomIsDm, whomIsMultiDm } from '@tloncorp/shared/dist/urbit';
import { addNotificationResponseReceivedListener } from 'expo-notifications';
import { useEffect, useState } from 'react';
@@ -21,10 +23,12 @@ export default function useNotificationListener({
notificationChannelId,
}: Props) {
const navigation = useNavigation>();
- const [{ postId, channelId, isDm }, setGotoData] = useState<{
+ const { data: isTlonEmployee } = store.useIsTlonEmployee();
+
+ const [{ postInfo, channelId, isDm }, setGotoData] = useState<{
path?: string;
isDm?: boolean;
- postId?: string | null;
+ postInfo?: { id: string; authorId: string } | null;
channelId?: string;
}>({
path: notificationPath,
@@ -32,7 +36,7 @@ export default function useNotificationListener({
});
const resetGotoData = () =>
- setGotoData({ path: undefined, channelId: undefined });
+ setGotoData({ path: undefined, channelId: undefined, postInfo: undefined });
// Start notifications prompt
useEffect(() => {
@@ -53,7 +57,7 @@ export default function useNotificationListener({
},
},
} = response;
- const postId = api.getPostIdFromWer(data.wer);
+ const postInfo = api.getPostInfoFromWer(data.wer);
const isDm = api.getIsDmFromWer(data.wer);
if (actionIdentifier === 'markAsRead' && data.channelId) {
markChatRead(data.channelId);
@@ -63,7 +67,7 @@ export default function useNotificationListener({
setGotoData({
path: data.wer,
isDm,
- postId,
+ postInfo,
channelId: data.channelId,
});
}
@@ -86,42 +90,33 @@ export default function useNotificationListener({
}
// if we have a post id, try to navigate to the thread
- if (postId) {
- let postToNavigateTo: db.Post | null = null;
- if (isDm) {
- // for DMs, we get the backend ID (seal time) of the thread reply itself. So first we have to try to find that,
- // then we grab the parent
- const post = await db.getPostByBackendTime({ backendTime: postId });
- if (post && post.parentId) {
- const parentPost = await db.getPost({ postId: post.parentId });
- if (parentPost) {
- postToNavigateTo = parentPost;
- }
- }
- } else {
- // for group posts, we get the correct post ID and can just try to grab it
- const post = await db.getPost({ postId });
- if (post) {
- postToNavigateTo = post;
- }
- }
+ if (postInfo) {
+ let postToNavigateTo: {
+ id: string;
+ authorId: string;
+ channelId: string;
+ } | null = null;
- // if we found the post, navigate to it. Otherwise fallback to channel
- if (postToNavigateTo) {
- navigation.dispatch(
- CommonActions.reset({
- index: 1,
- routes: [
- { name: 'ChatList' },
- { name: 'Channel', params: { channel } },
- { name: 'Post', params: { post: postToNavigateTo } },
- ],
- })
- );
+ const post = await db.getPost({ postId: postInfo.id });
- resetGotoData();
- return true;
+ if (post) {
+ postToNavigateTo = post;
+ } else {
+ postToNavigateTo = { ...postInfo, channelId };
}
+
+ navigation.dispatch(
+ CommonActions.reset({
+ index: 1,
+ routes: [
+ { name: 'ChatList' },
+ { name: 'Channel', params: { channel } },
+ { name: 'Post', params: { post: postToNavigateTo } },
+ ],
+ })
+ );
+ resetGotoData();
+ return true;
}
navigation.navigate('Channel', { channel });
@@ -145,10 +140,19 @@ export default function useNotificationListener({
// If still not found, clear out the requested channel ID
if (!didNavigate) {
+ if (isTlonEmployee) {
+ crashlytics().log(`failed channel ID: ${channelId}`);
+ crashlytics().log(`failed post ID: ${postInfo?.id}`);
+ }
+ crashlytics().recordError(
+ new Error(
+ `Notification listener: failed to navigate to ${isDm ? 'DM ' : ''}channel ${postInfo?.id ? ' thread' : ''}`
+ )
+ );
resetGotoData();
}
}
})();
}
- }, [channelId, postId, navigation, isDm]);
+ }, [channelId, postInfo, navigation, isDm]);
}
diff --git a/apps/tlon-mobile/src/lib/nativeDb.ts b/apps/tlon-mobile/src/lib/nativeDb.ts
index 0bf46f7961..e7b3b8c6f5 100644
--- a/apps/tlon-mobile/src/lib/nativeDb.ts
+++ b/apps/tlon-mobile/src/lib/nativeDb.ts
@@ -99,3 +99,8 @@ async function runMigrations() {
await migrate(client!, migrations);
logger.log("migrations succeeded after purge, shouldn't happen often");
}
+
+export async function resetDb() {
+ await purgeDb();
+ await migrate(client!, migrations);
+}
diff --git a/apps/tlon-mobile/src/navigation/RootStack.tsx b/apps/tlon-mobile/src/navigation/RootStack.tsx
index 1c67e9a81a..227178ca7f 100644
--- a/apps/tlon-mobile/src/navigation/RootStack.tsx
+++ b/apps/tlon-mobile/src/navigation/RootStack.tsx
@@ -5,12 +5,19 @@ import { Platform, StatusBar } from 'react-native';
import { useIsDarkMode } from '../hooks/useIsDarkMode';
import { ActivityScreen } from '../screens/ActivityScreen';
+import { AppInfoScreen } from '../screens/AppInfo';
+import { AppSettingsScreen } from '../screens/AppSettingsScreen';
+import { BlockedUsersScreen } from '../screens/BlockedUsersScreen';
import ChannelScreen from '../screens/ChannelScreen';
import ChannelSearch from '../screens/ChannelSearchScreen';
import ChatListScreen from '../screens/ChatListScreen';
+import { EditProfileScreen } from '../screens/EditProfileScreen';
+import { FeatureFlagScreen } from '../screens/FeatureFlagScreen';
import { GroupChannelsScreen } from '../screens/GroupChannelsScreen';
import ImageViewerScreen from '../screens/ImageViewerScreen';
+import { ManageAccountScreen } from '../screens/ManageAccountScreen';
import PostScreen from '../screens/PostScreen';
+import { PushNotificationSettingsScreen } from '../screens/PushNotificationSettingsScreen';
import type { RootStackParamList } from '../types';
import { GroupSettingsStack } from './GroupSettingsStack';
import { SettingsStack } from './SettingsStack';
@@ -66,6 +73,17 @@ export function RootStack() {
component={ImageViewerScreen}
options={{ animation: 'fade' }}
/>
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/tlon-mobile/src/screens/AppInfo.tsx b/apps/tlon-mobile/src/screens/AppInfo.tsx
new file mode 100644
index 0000000000..28e2749220
--- /dev/null
+++ b/apps/tlon-mobile/src/screens/AppInfo.tsx
@@ -0,0 +1,91 @@
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import * as store from '@tloncorp/shared/dist/store';
+import {
+ AppSetting,
+ GenericHeader,
+ ListItem,
+ SizableText,
+ Stack,
+ View,
+ YStack,
+} from '@tloncorp/ui';
+import { preSig } from '@urbit/aura';
+import * as Application from 'expo-application';
+import { useCallback } from 'react';
+import { Platform } from 'react-native';
+import { ScrollView } from 'react-native-gesture-handler';
+
+import { NOTIFY_PROVIDER, NOTIFY_SERVICE } from '../constants';
+import { RootStackParamList } from '../types';
+
+type Props = NativeStackScreenProps;
+
+const BUILD_VERSION = `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${Application.nativeBuildVersion}`;
+
+export function AppInfoScreen(props: Props) {
+ const { data: appInfo } = store.useAppInfo();
+
+ const onPressPreviewFeatures = useCallback(() => {
+ props.navigation.navigate('FeatureFlags');
+ }, [props.navigation]);
+
+ return (
+
+ props.navigation.goBack()}
+ />
+
+
+
+
+
+ {appInfo ? (
+ <>
+
+
+
+ >
+ ) : (
+
+
+ Cannot load app info settings
+
+
+ )}
+
+
+
+
+
+ Feature previews
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/tlon-mobile/src/screens/AppSettingsScreen.tsx b/apps/tlon-mobile/src/screens/AppSettingsScreen.tsx
new file mode 100644
index 0000000000..2395606d65
--- /dev/null
+++ b/apps/tlon-mobile/src/screens/AppSettingsScreen.tsx
@@ -0,0 +1,101 @@
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { GenericHeader, IconType, ListItem, View, YStack } from '@tloncorp/ui';
+import { useCallback, useEffect, useState } from 'react';
+import { ScrollView } from 'react-native-gesture-handler';
+
+import { RootStackParamList } from '../types';
+import { getHostingToken, getHostingUserId } from '../utils/hosting';
+
+type Props = NativeStackScreenProps;
+
+export function AppSettingsScreen(props: Props) {
+ const [hasHostedAuth, setHasHostedAuth] = useState(false);
+
+ useEffect(() => {
+ async function getHostingInfo() {
+ const [cookie, userId] = await Promise.all([
+ getHostingToken(),
+ getHostingUserId(),
+ ]);
+ if (cookie && userId) {
+ setHasHostedAuth(true);
+ }
+ }
+ getHostingInfo();
+ }, []);
+
+ const onManageAccountPressed = useCallback(() => {
+ props.navigation.navigate('ManageAccount');
+ }, [props.navigation]);
+
+ const onAppInfoPressed = useCallback(() => {
+ props.navigation.navigate('AppInfo');
+ }, [props.navigation]);
+
+ const onPushNotifPressed = useCallback(() => {
+ props.navigation.navigate('PushNotificationSettings');
+ }, [props.navigation]);
+
+ const onBlockedUsersPressed = useCallback(() => {
+ props.navigation.navigate('BlockedUsers');
+ }, [props.navigation]);
+
+ return (
+
+ props.navigation.goBack()}
+ />
+
+
+
+
+
+ {hasHostedAuth && (
+
+ )}
+
+
+
+ );
+}
+
+function AppInfoListItem({
+ onPress,
+ title,
+ icon,
+}: {
+ onPress: () => void;
+ title: string;
+ icon: IconType;
+}) {
+ return (
+
+
+
+ {title}
+
+
+
+
+ );
+}
diff --git a/apps/tlon-mobile/src/screens/BlockedUsersScreen.tsx b/apps/tlon-mobile/src/screens/BlockedUsersScreen.tsx
new file mode 100644
index 0000000000..5e8fc3ea46
--- /dev/null
+++ b/apps/tlon-mobile/src/screens/BlockedUsersScreen.tsx
@@ -0,0 +1,60 @@
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import * as db from '@tloncorp/shared/dist/db';
+import * as store from '@tloncorp/shared/dist/store';
+import {
+ BlockedContactsWidget,
+ ContactsProvider,
+ ScreenHeader,
+ View,
+} from '@tloncorp/ui';
+import { useCallback } from 'react';
+import { Alert } from 'react-native';
+
+import { useCurrentUserId } from '../hooks/useCurrentUser';
+import { RootStackParamList } from '../types';
+
+type Props = NativeStackScreenProps;
+
+export function BlockedUsersScreen(props: Props) {
+ const currentUserId = useCurrentUserId();
+ const { data: calm } = store.useCalmSettings({ userId: currentUserId });
+ const { data: allContacts } = store.useContacts();
+ const { data: blockedContacts } = store.useBlockedContacts();
+
+ const onBlockedContactPress = useCallback(
+ (contact: db.Contact) => {
+ Alert.alert(
+ `${calm?.disableNicknames && contact.nickname ? contact.nickname : contact.id}`,
+ `Are you sure you want to unblock this user?`,
+ [
+ {
+ text: 'Cancel',
+ style: 'cancel',
+ },
+ {
+ text: 'Unblock',
+ onPress: () => store.unblockUser(contact.id),
+ },
+ ]
+ );
+ },
+ [calm?.disableNicknames]
+ );
+
+ return (
+
+
+
+ props.navigation.goBack()} />
+ Blocked Users
+
+
+
+
+
+
+ );
+}
diff --git a/apps/tlon-mobile/src/screens/ChatListScreen.tsx b/apps/tlon-mobile/src/screens/ChatListScreen.tsx
index 84ff1ccf22..af20ca58c3 100644
--- a/apps/tlon-mobile/src/screens/ChatListScreen.tsx
+++ b/apps/tlon-mobile/src/screens/ChatListScreen.tsx
@@ -48,6 +48,9 @@ export default function ChatListScreen(
const [longPressedGroup, setLongPressedGroup] = useState(
null
);
+ const [activeTab, setActiveTab] = useState<'all' | 'groups' | 'messages'>(
+ 'all'
+ );
const [selectedGroup, setSelectedGroup] = useState(null);
const [startDmOpen, setStartDmOpen] = useState(false);
const [addGroupOpen, setAddGroupOpen] = useState(false);
@@ -239,9 +242,24 @@ export default function ChatListScreen(
const { calmSettings } = useCalmSettings();
- const handleSectionChange = useCallback((title: string) => {
- setScreenTitle(title);
- }, []);
+ const handleSectionChange = useCallback(
+ (title: string) => {
+ if (activeTab === 'all') {
+ setScreenTitle(title);
+ }
+ },
+ [activeTab]
+ );
+
+ useEffect(() => {
+ if (activeTab === 'all') {
+ setScreenTitle('Home');
+ } else if (activeTab === 'groups') {
+ setScreenTitle('Groups');
+ } else if (activeTab === 'messages') {
+ setScreenTitle('Messages');
+ }
+ }, [activeTab]);
const [splashVisible, setSplashVisible] = useState(true);
@@ -274,6 +292,8 @@ export default function ChatListScreen(
/>
{chats && chats.unpinned.length ? (
;
+
+export function EditProfileScreen(props: Props) {
+ const currentUserId = useCurrentUserId();
+ const { data: contacts } = store.useContacts();
+ const uploadInfo = useImageUpload({
+ uploaderKey: 'profile-edit',
+ });
+
+ const onGoBack = useCallback(() => {
+ props.navigation.goBack();
+ }, [props.navigation]);
+
+ const onSaveProfile = useCallback(
+ (update: api.ProfileUpdate) => {
+ store.updateCurrentUserProfile(update);
+ props.navigation.goBack();
+ },
+ [props.navigation]
+ );
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/apps/tlon-mobile/src/screens/FeatureFlagScreen.tsx b/apps/tlon-mobile/src/screens/FeatureFlagScreen.tsx
index 9cd752a1e8..59975db07a 100644
--- a/apps/tlon-mobile/src/screens/FeatureFlagScreen.tsx
+++ b/apps/tlon-mobile/src/screens/FeatureFlagScreen.tsx
@@ -3,10 +3,10 @@ import { FeatureFlagScreenView } from '@tloncorp/ui';
import { useCallback, useState } from 'react';
import * as featureFlags from '../lib/featureFlags';
-import type { SettingsStackParamList } from '../types';
+import type { RootStackParamList } from '../types';
type FeatureFlagScreenProps = NativeStackScreenProps<
- SettingsStackParamList,
+ RootStackParamList,
'FeatureFlags'
>;
diff --git a/apps/tlon-mobile/src/screens/ManageAccountScreen.tsx b/apps/tlon-mobile/src/screens/ManageAccountScreen.tsx
new file mode 100644
index 0000000000..c33992570e
--- /dev/null
+++ b/apps/tlon-mobile/src/screens/ManageAccountScreen.tsx
@@ -0,0 +1,94 @@
+import { useFocusEffect } from '@react-navigation/native';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { LoadingSpinner, ScreenHeader, View, YStack } from '@tloncorp/ui';
+import { useCallback, useEffect, useState } from 'react';
+import { WebView } from 'react-native-webview';
+
+import { useHandleLogout } from '../hooks/useHandleLogout';
+import { useWebView } from '../hooks/useWebView';
+import { getHostingUser } from '../lib/hostingApi';
+import { RootStackParamList } from '../types';
+import { getHostingToken, getHostingUserId } from '../utils/hosting';
+
+const MANAGE_ACCOUNT_URL = 'https://tlon.network/account';
+
+type Props = NativeStackScreenProps;
+
+interface HostingSession {
+ cookie: string;
+ userId: string;
+}
+
+export function ManageAccountScreen(props: Props) {
+ const handleLogout = useHandleLogout();
+ const webview = useWebView();
+ const [hostingSession, setHostingSession] = useState(
+ null
+ );
+
+ useFocusEffect(
+ useCallback(() => {
+ // check if the user deleted their account when navigating away
+ return async () => {
+ const hostingUserId = await getHostingUserId();
+ if (hostingUserId) {
+ try {
+ const user = await getHostingUser(hostingUserId);
+ if (!user.verified) {
+ handleLogout();
+ }
+ } catch (err) {
+ handleLogout();
+ }
+ }
+ };
+ }, [handleLogout])
+ );
+
+ useEffect(() => {
+ async function initialize() {
+ const [cookie, userId] = await Promise.all([
+ getHostingToken(),
+ getHostingUserId(),
+ ]);
+ if (cookie && userId) {
+ // we need to strip HttpOnly from the cookie or it won't get sent along with the request
+ const modifiedCookie = cookie.replace(' HttpOnly;', '');
+ setHostingSession({ cookie: modifiedCookie, userId });
+ } else {
+ throw new Error(
+ 'Cannot manage account, failed to get hosting token or user ID.'
+ );
+ }
+ }
+ initialize();
+ }, []);
+
+ return (
+
+
+ props.navigation.goBack()} />
+ Manage Account
+
+ {hostingSession ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/tlon-mobile/src/screens/ProfileScreen.tsx b/apps/tlon-mobile/src/screens/ProfileScreen.tsx
index 2d6c37463a..544244f727 100644
--- a/apps/tlon-mobile/src/screens/ProfileScreen.tsx
+++ b/apps/tlon-mobile/src/screens/ProfileScreen.tsx
@@ -1,55 +1,36 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack';
-import * as api from '@tloncorp/shared/dist/api';
import * as store from '@tloncorp/shared/dist/store';
import { ProfileScreenView, View } from '@tloncorp/ui';
-import * as Application from 'expo-application';
import { useCallback } from 'react';
-import { Platform } from 'react-native';
-import { NOTIFY_PROVIDER, NOTIFY_SERVICE } from '../constants';
-import { clearShipInfo, useShip } from '../contexts/ship';
import { useCurrentUserId } from '../hooks/useCurrentUser';
-import { purgeDb } from '../lib/nativeDb';
+import { useHandleLogout } from '../hooks/useHandleLogout';
import NavBar from '../navigation/NavBarView';
-import { SettingsStackParamList } from '../types';
-import { removeHostingToken, removeHostingUserId } from '../utils/hosting';
+import { RootStackParamList } from '../types';
-type Props = NativeStackScreenProps;
-
-const DEBUG_MESSAGE = `
- Version:
- ${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${Application.nativeBuildVersion}
-
- Notify Provider:
- ${NOTIFY_PROVIDER}
-
- Notify Service:
- ${NOTIFY_SERVICE}
-`;
+type Props = NativeStackScreenProps;
export default function ProfileScreen(props: Props) {
- const { clearShip } = useShip();
const currentUserId = useCurrentUserId();
const { data: contacts } = store.useContacts();
+ const handleLogout = useHandleLogout();
+
+ const onAppSettingsPressed = useCallback(() => {
+ props.navigation.navigate('AppSettings');
+ }, [props.navigation]);
- const handleLogout = useCallback(async () => {
- await purgeDb();
- api.queryClient.clear();
- api.removeUrbitClient();
- clearShip();
- clearShipInfo();
- removeHostingToken();
- removeHostingUserId();
- }, [clearShip]);
+ const onEditProfilePressed = useCallback(() => {
+ props.navigation.navigate('EditProfile');
+ }, [props.navigation]);
return (
props.navigation.navigate('FeatureFlags')}
- handleLogout={handleLogout}
+ onAppSettingsPressed={onAppSettingsPressed}
+ onEditProfilePressed={onEditProfilePressed}
+ onLogoutPressed={handleLogout}
/>
diff --git a/apps/tlon-mobile/src/screens/PushNotificationSettingsScreen.tsx b/apps/tlon-mobile/src/screens/PushNotificationSettingsScreen.tsx
new file mode 100644
index 0000000000..51c7796d3c
--- /dev/null
+++ b/apps/tlon-mobile/src/screens/PushNotificationSettingsScreen.tsx
@@ -0,0 +1,98 @@
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import * as store from '@tloncorp/shared/dist/store';
+import * as ub from '@tloncorp/shared/dist/urbit';
+import {
+ GenericHeader,
+ Icon,
+ SizableText,
+ View,
+ XStack,
+ YStack,
+} from '@tloncorp/ui';
+import { useCallback } from 'react';
+
+import { RootStackParamList } from '../types';
+
+type Props = NativeStackScreenProps;
+
+export function PushNotificationSettingsScreen(props: Props) {
+ const { data: pushNotificationsSetting } =
+ store.usePushNotificationsSetting();
+
+ const setLevel = useCallback(
+ async (level: ub.PushNotificationsSetting) => {
+ if (level === pushNotificationsSetting) return;
+ await store.setDefaultNotificationLevel(level);
+ },
+ [pushNotificationsSetting]
+ );
+
+ const LevelIndicator = useCallback(
+ (props: { level: ub.PushNotificationsSetting }) => {
+ if (pushNotificationsSetting === props.level) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+ },
+ [pushNotificationsSetting]
+ );
+
+ return (
+
+ props.navigation.goBack()}
+ />
+
+
+ Configure what kinds of messages will send you notifications.
+
+
+
+ setLevel('all')}>
+
+ All group activity
+
+
+ setLevel('some')}>
+
+
+ Mentions and replies only
+
+ Direct messages will still notify unless you mute them.
+
+
+
+
+ setLevel('none')}>
+
+ Nothing
+
+
+
+
+ );
+}
diff --git a/apps/tlon-mobile/src/screens/ShipLoginScreen.tsx b/apps/tlon-mobile/src/screens/ShipLoginScreen.tsx
index b4172c475a..4ed2bd4bf4 100644
--- a/apps/tlon-mobile/src/screens/ShipLoginScreen.tsx
+++ b/apps/tlon-mobile/src/screens/ShipLoginScreen.tsx
@@ -1,6 +1,6 @@
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { getLandscapeAuthCookie } from '@tloncorp/shared/dist/api';
-import { useEffect, useLayoutEffect, useState } from 'react';
+import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Text, TextInput, View } from 'react-native';
import { useTailwind } from 'tailwind-rn';
@@ -46,13 +46,29 @@ export const ShipLoginScreen = ({ navigation }: Props) => {
});
const { setShip } = useShip();
+ const isValidUrl = useCallback((url: string) => {
+ const urlPattern =
+ /^(https?:\/\/)?(localhost|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|[\w.-]+\.([a-z]{2,}))(:\d+)?$/i;
+ const hostedPattern = /tlon\.network/i;
+ if (!urlPattern.test(url)) {
+ return false;
+ }
+ if (hostedPattern.test(url)) {
+ return 'hosted';
+ }
+ return true;
+ }, []);
+
const onSubmit = handleSubmit(async ({ shipUrl: rawShipUrl, accessCode }) => {
setIsSubmitting(true);
const shipUrl = transformShipURL(rawShipUrl);
setFormattedShipUrl(shipUrl);
try {
- const authCookie = await getLandscapeAuthCookie(shipUrl, accessCode);
+ const authCookie = await getLandscapeAuthCookie(
+ shipUrl,
+ accessCode.trim()
+ );
if (authCookie) {
const shipId = getShipFromCookie(authCookie);
if (await isEulaAgreed()) {
@@ -65,7 +81,9 @@ export const ShipLoginScreen = ({ navigation }: Props) => {
navigation.navigate('EULA', { shipId, shipUrl, authCookie });
}
} else {
- setRemoteError("Sorry, we couldn't log you into your Urbit ID.");
+ setRemoteError(
+ "Sorry, we couldn't log in to your ship. It may be busy or offline."
+ );
}
} catch (err) {
setRemoteError((err as Error).message);
@@ -76,6 +94,9 @@ export const ShipLoginScreen = ({ navigation }: Props) => {
useLayoutEffect(() => {
navigation.setOptions({
+ headerLeft: () => (
+ navigation.goBack()} />
+ ),
headerRight: () =>
isSubmitting ? (
@@ -101,7 +122,7 @@ export const ShipLoginScreen = ({ navigation }: Props) => {
'text-lg font-medium text-tlon-black-80 dark:text-white'
)}
>
- Connect an unhosted ship by entering its URL and access code.
+ Connect a self-hosted ship by entering its URL and access code.
{remoteError ? (
{remoteError}
@@ -118,13 +139,23 @@ export const ShipLoginScreen = ({ navigation }: Props) => {
control={control}
rules={{
required: 'Please enter a valid URL.',
+ validate: (value) => {
+ const urlValidation = isValidUrl(value);
+ if (urlValidation === false) {
+ return 'Please enter a valid URL.';
+ }
+ if (urlValidation === 'hosted') {
+ return 'Please log in to your hosted Tlon ship using email and password.';
+ }
+ return true;
+ },
}}
render={({ field: { onChange, onBlur, value } }) => (
{
navigation.setOptions({
+ headerLeft: () => (
+ navigation.goBack()} />
+ ),
headerRight: () =>
isSubmitting ? (
diff --git a/apps/tlon-mobile/src/screens/TlonLoginScreen.tsx b/apps/tlon-mobile/src/screens/TlonLoginScreen.tsx
index fd67682387..03ae9f26cc 100644
--- a/apps/tlon-mobile/src/screens/TlonLoginScreen.tsx
+++ b/apps/tlon-mobile/src/screens/TlonLoginScreen.tsx
@@ -124,6 +124,9 @@ export const TlonLoginScreen = ({ navigation }: Props) => {
useLayoutEffect(() => {
navigation.setOptions({
+ headerLeft: () => (
+ navigation.goBack()} />
+ ),
headerRight: () =>
isSubmitting ? (
diff --git a/apps/tlon-mobile/src/types.ts b/apps/tlon-mobile/src/types.ts
index fe5d9181ae..353cb0fb0f 100644
--- a/apps/tlon-mobile/src/types.ts
+++ b/apps/tlon-mobile/src/types.ts
@@ -21,7 +21,7 @@ export type WebViewStackParamList = {
export type RootStackParamList = {
ChatList: undefined;
Activity: undefined;
- Profile: NavigatorScreenParams;
+ Profile: undefined;
Channel: {
channel: db.Channel;
selectedPostId?: string | null;
@@ -33,7 +33,11 @@ export type RootStackParamList = {
channel: db.Channel;
};
Post: {
- post: db.Post;
+ post: {
+ id: string;
+ channelId: string;
+ authorId: string;
+ };
};
ImageViewer: {
post: db.Post;
@@ -42,6 +46,13 @@ export type RootStackParamList = {
GroupSettings: {
group: db.Group;
};
+ AppSettings: undefined;
+ FeatureFlags: undefined;
+ ManageAccount: undefined;
+ BlockedUsers: undefined;
+ AppInfo: undefined;
+ PushNotificationSettings: undefined;
+ EditProfile: undefined;
};
export type GroupSettingsStackParamList = {
diff --git a/apps/tlon-mobile/src/utils/posthog.ts b/apps/tlon-mobile/src/utils/posthog.ts
index a18c78c4f6..7dc269d26b 100644
--- a/apps/tlon-mobile/src/utils/posthog.ts
+++ b/apps/tlon-mobile/src/utils/posthog.ts
@@ -1,4 +1,5 @@
import crashlytics from '@react-native-firebase/crashlytics';
+import * as db from '@tloncorp/shared/dist/db';
import PostHog from 'posthog-react-native';
import { POST_HOG_API_KEY } from '../constants';
@@ -54,4 +55,5 @@ export const identifyTlonEmployee = () => {
const UUID = posthog.getDistinctId();
posthog.identify(UUID, { isTlonEmployee: true });
+ db.setIsTlonEmployee(true);
};
diff --git a/apps/tlon-web/package.json b/apps/tlon-web/package.json
index d597c8f914..3f94dcea9b 100644
--- a/apps/tlon-web/package.json
+++ b/apps/tlon-web/package.json
@@ -90,7 +90,7 @@
"@types/marked": "^4.3.0",
"@urbit/api": "^2.2.0",
"@urbit/aura": "^1.0.0",
- "@urbit/http-api": "^3.1.0-dev-2",
+ "@urbit/http-api": "3.2.0-dev",
"@urbit/sigil-js": "^2.2.0",
"any-ascii": "^0.3.1",
"big-integer": "^1.6.51",
diff --git a/apps/tlon-web/src/api.ts b/apps/tlon-web/src/api.ts
index e4a69c8099..ca64c731c4 100644
--- a/apps/tlon-web/src/api.ts
+++ b/apps/tlon-web/src/api.ts
@@ -299,11 +299,6 @@ class API {
...params,
event: eventListener(params.event),
quit: () => {
- this.client!.subscribe({
- ...params,
- event: eventListener(params.event),
- });
-
// should only happen once since we call this each invocation
// and onReconnect will set the lastReconnect time
const { lastReconnect, onReconnect } = useLocalState.getState();
diff --git a/desk/desk.docket-0 b/desk/desk.docket-0
index 654f91ff85..0aaf6f0ae8 100644
--- a/desk/desk.docket-0
+++ b/desk/desk.docket-0
@@ -2,7 +2,7 @@
info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.'
color+0xde.dede
image+'https://bootstrap.urbit.org/tlon.svg?v=1'
- glob-http+['https://bootstrap.urbit.org/glob-0v1.0i4g9.237vl.7ddqh.elnhk.njlbc.glob' 0v1.0i4g9.237vl.7ddqh.elnhk.njlbc]
+ glob-http+['https://bootstrap.urbit.org/glob-0v4.2d6l8.46eg8.cg9i7.jmtfb.qccv3.glob' 0v4.2d6l8.46eg8.cg9i7.jmtfb.qccv3]
base+'groups'
version+[6 1 0]
website+'https://tlon.io'
diff --git a/packages/shared/src/api/chatApi.ts b/packages/shared/src/api/chatApi.ts
index 33f23e4605..928aa74bec 100644
--- a/packages/shared/src/api/chatApi.ts
+++ b/packages/shared/src/api/chatApi.ts
@@ -66,6 +66,8 @@ export const respondToDMInvite = ({
};
export type ChatEvent =
+ | { type: 'showPost'; postId: string }
+ | { type: 'hidePost'; postId: string }
| { type: 'addDmInvites'; channels: db.Channel[] }
| { type: 'groupDmsUpdate' }
| { type: 'addPost'; post: db.Post; replyMeta?: db.ReplyMeta | null }
@@ -84,6 +86,20 @@ export function subscribeToChatUpdates(
(event: ub.WritResponse | ub.ClubAction | string[]) => {
logger.log('raw chat sub event', event);
+ if ('show' in event) {
+ // show/unhide post event
+ logger.log('show/unhide post', event.show);
+ const postId = getCanonicalPostId(event.show as string);
+ return eventHandler({ type: 'showPost', postId });
+ }
+
+ if ('hide' in event) {
+ // hide post event
+ logger.log('hide post', event.hide);
+ const postId = getCanonicalPostId(event.hide as string);
+ return eventHandler({ type: 'hidePost', postId });
+ }
+
// check for DM invites
if (Array.isArray(event)) {
// dm invites
@@ -197,6 +213,10 @@ export function subscribeToChatUpdates(
);
}
+export function getBlockedUsers() {
+ return scry({ app: 'chat', path: '/blocked' });
+}
+
export function blockUser(userId: string) {
return poke({
app: 'chat',
@@ -248,6 +268,7 @@ export const toClientDm = (id: string, isInvite?: boolean): db.Channel => {
title: '',
description: '',
isDmInvite: !!isInvite,
+ contactId: id,
members: [{ chatId: id, contactId: id, membershipType: 'channel' }],
};
};
diff --git a/packages/shared/src/api/contactsApi.ts b/packages/shared/src/api/contactsApi.ts
index e3764c93e4..b5a602f6df 100644
--- a/packages/shared/src/api/contactsApi.ts
+++ b/packages/shared/src/api/contactsApi.ts
@@ -19,6 +19,41 @@ export const addContacts = async (contactIds: string[]) => {
});
};
+export interface ProfileUpdate {
+ nickname?: string;
+ bio?: string;
+ avatarImage?: string;
+ coverImage?: string;
+}
+export const updateCurrentUserProfile = async (update: ProfileUpdate) => {
+ const editedFields = [];
+ if (update.nickname !== undefined) {
+ editedFields.push({ nickname: update.nickname });
+ }
+
+ if (update.bio !== undefined) {
+ editedFields.push({ bio: update.bio });
+ }
+
+ if (update.avatarImage !== undefined) {
+ editedFields.push({ avatar: update.avatarImage });
+ }
+
+ if (update.coverImage !== undefined) {
+ editedFields.push({ cover: update.coverImage });
+ }
+
+ const action: ub.ContactEdit = {
+ edit: editedFields,
+ };
+
+ return poke({
+ app: 'contacts',
+ mark: 'contact-action',
+ json: action,
+ });
+};
+
export type ContactsUpdate =
| { type: 'add'; contact: db.Contact }
| { type: 'delete'; contactId: string };
diff --git a/packages/shared/src/api/harkApi.ts b/packages/shared/src/api/harkApi.ts
index 00e8354023..ab34b631a1 100644
--- a/packages/shared/src/api/harkApi.ts
+++ b/packages/shared/src/api/harkApi.ts
@@ -1,6 +1,8 @@
import { getCanonicalPostId } from './apiUtils';
-export const getPostIdFromWer = (wer: string): string | null => {
+export const getPostInfoFromWer = (
+ wer: string
+): { id: string; authorId: string } | null => {
const isDm = getIsDmFromWer(wer);
const isChannelPost = getIsChannelPostFromWer(wer);
const parts = wer.split('/');
@@ -10,12 +12,18 @@ export const getPostIdFromWer = (wer: string): string | null => {
return null;
}
- if (isDm && parts[3]) {
- return getCanonicalPostId(parts[3]);
+ if (isDm && parts[5]) {
+ return {
+ id: getCanonicalPostId(parts[5]),
+ authorId: parts[4],
+ };
}
if (isChannelPost && isGroupChannelReply && parts[9]) {
- return getCanonicalPostId(parts[9]);
+ return {
+ id: getCanonicalPostId(parts[9]),
+ authorId: '',
+ };
}
return null;
diff --git a/packages/shared/src/api/postsApi.ts b/packages/shared/src/api/postsApi.ts
index 4a6bb56ca9..83344cb741 100644
--- a/packages/shared/src/api/postsApi.ts
+++ b/packages/shared/src/api/postsApi.ts
@@ -6,6 +6,8 @@ import * as ub from '../urbit';
import {
ClubAction,
DmAction,
+ HiddenMessages,
+ HiddenPosts,
KindData,
KindDataChat,
Story,
@@ -381,12 +383,14 @@ export const getLatestPosts = async ({
count
),
});
+
return response.map((head) => {
const channelId = 'nest' in head ? head.nest : head.whom;
+ const latestPost = toPostData(channelId, head.latest);
return {
channelId: channelId,
updatedAt: head.recency,
- latestPost: toPostData(channelId, head.latest),
+ latestPost,
};
});
};
@@ -572,34 +576,79 @@ export async function removeReaction({
});
}
-export async function showPost(channelId: string, postId: string) {
+export async function showPost(post: db.Post) {
+ if (isGroupChannelId(post.channelId)) {
+ const action = {
+ app: 'channels',
+ mark: 'channel-action',
+ json: {
+ 'toggle-post': {
+ show: post.id,
+ },
+ },
+ };
+
+ return poke(action);
+ }
+
+ const writId = `${post.authorId}/${post.id}`;
+
const action = {
- app: 'channels',
- mark: 'channel-action',
+ app: 'chat',
+ mark: 'chat-toggle-message',
json: {
- 'toggle-post': {
- show: postId,
- },
+ show: writId,
},
};
- return await poke(action);
+ return poke(action);
}
-export async function hidePost(channelId: string, postId: string) {
+export async function hidePost(post: db.Post) {
+ if (isGroupChannelId(post.channelId)) {
+ const action = {
+ app: 'channels',
+ mark: 'channel-action',
+ json: {
+ 'toggle-post': {
+ hide: post.id,
+ },
+ },
+ };
+
+ return poke(action);
+ }
+
+ const writId = `${post.authorId}/${post.id}`;
const action = {
- app: 'channels',
- mark: 'channel-action',
+ app: 'chat',
+ mark: 'chat-toggle-message',
json: {
- 'toggle-post': {
- hide: postId,
- },
+ hide: writId,
},
};
- return await poke(action);
+ return poke(action);
}
+export const getHiddenPosts = async () => {
+ const hiddenPosts = await scry({
+ app: 'channels',
+ path: '/hidden-posts',
+ });
+
+ return hiddenPosts.map((postId) => getCanonicalPostId(postId));
+};
+
+export const getHiddenDMPosts = async () => {
+ const hiddenDMPosts = await scry({
+ app: 'chat',
+ path: '/hidden-messages',
+ });
+
+ return hiddenDMPosts.map((postId) => getCanonicalPostId(postId));
+};
+
export async function deletePost(channelId: string, postId: string) {
const action = channelAction(channelId, {
post: {
@@ -687,35 +736,36 @@ export function toPagedPostsData(
data: ub.PagedPosts | ub.PagedWrits
): GetChannelPostsResponse {
const posts = 'writs' in data ? data.writs : data.posts;
+ const postsData = toPostsData(channelId, posts);
return {
older: data.older ? formatUd(data.older) : null,
newer: data.newer ? formatUd(data.newer) : null,
totalPosts: data.total,
- ...toPostsData(channelId, posts),
+ ...postsData,
};
}
export function toPostsData(
channelId: string,
posts: ub.Posts | ub.Writs | Record
-) {
- const [deletedPosts, otherPosts] = Object.entries(posts).reduce<
- [string[], db.Post[]]
- >(
- (memo, [id, post]) => {
- if (post === null) {
- memo[0].push(id);
- } else {
- memo[1].push(toPostData(channelId, post));
- }
- return memo;
- },
- [[], []]
- );
+): { posts: db.Post[]; deletedPosts: string[] } {
+ const entries = Object.entries(posts);
+ const deletedPosts: string[] = [];
+ const otherPosts: db.Post[] = [];
+
+ for (const [id, post] of entries) {
+ if (post === null) {
+ deletedPosts.push(id);
+ } else {
+ const postData = toPostData(channelId, post);
+ otherPosts.push(postData);
+ }
+ }
+
+ otherPosts.sort((a, b) => (a.receivedAt ?? 0) - (b.receivedAt ?? 0));
+
return {
- posts: otherPosts.sort((a, b) => {
- return (a.receivedAt ?? 0) - (b.receivedAt ?? 0);
- }),
+ posts: otherPosts,
deletedPosts,
};
}
@@ -754,6 +804,10 @@ export function toPostData(
? getCanonicalPostId(post.seal.time.toString())
: null;
+ const replyData = isPostDataResponse(post)
+ ? getReplyData(id, channelId, post)
+ : null;
+
return {
id,
channelId,
@@ -773,9 +827,7 @@ export function toPostData(
replyContactIds: post?.seal.meta.lastRepliers,
images: getContentImages(id, post.essay?.content),
reactions: toReactionsData(post?.seal.reacts ?? {}, id),
- replies: isPostDataResponse(post)
- ? getReplyData(id, channelId, post)
- : null,
+ replies: replyData,
deliveryStatus: null,
syncedAt: Date.now(),
...flags,
diff --git a/packages/shared/src/api/settingsApi.ts b/packages/shared/src/api/settingsApi.ts
index 164e61728c..c9c18fbbde 100644
--- a/packages/shared/src/api/settingsApi.ts
+++ b/packages/shared/src/api/settingsApi.ts
@@ -1,3 +1,5 @@
+import { ChargeUpdateInitial, Pikes, getPikes, scryCharges } from '@urbit/api';
+
import * as db from '../db';
import * as ub from '../urbit';
import { client, scry } from './urbit';
@@ -57,3 +59,17 @@ export const toClientSettings = (
notebookSettings: JSON.stringify(settings.desk.diary),
};
};
+
+export async function getAppInfo(): Promise {
+ const pikes = await scry(getPikes);
+ const charges = (await scry(scryCharges)).initial;
+
+ const groupsPike = pikes?.['groups'] ?? {};
+ const groupsCharge = charges?.['groups'] ?? {};
+
+ return {
+ groupsVersion: groupsCharge.version ?? 'n/a',
+ groupsHash: groupsPike.hash ?? 'n/a',
+ groupsSyncNode: groupsPike.sync?.ship ?? 'n/a',
+ };
+}
diff --git a/packages/shared/src/api/urbit.ts b/packages/shared/src/api/urbit.ts
index a757c369e0..d05f0c8766 100644
--- a/packages/shared/src/api/urbit.ts
+++ b/packages/shared/src/api/urbit.ts
@@ -44,6 +44,14 @@ export const getCurrentUserId = () => {
return client.our;
};
+export const getCurrentUserIsHosted = () => {
+ if (!client.our) {
+ throw new Error('Client not initialized');
+ }
+
+ return client.url.endsWith('tlon.network');
+};
+
export function configureClient({
shipName,
shipUrl,
diff --git a/packages/shared/src/db/keyValue.ts b/packages/shared/src/db/keyValue.ts
index 78d11cbc62..1d317b4796 100644
--- a/packages/shared/src/db/keyValue.ts
+++ b/packages/shared/src/db/keyValue.ts
@@ -16,6 +16,9 @@ export const PUSH_NOTIFICATIONS_SETTING_QUERY_KEY = [
'pushNotifications',
];
+export const IS_TLON_EMPLOYEE_QUERY_KEY = ['settings', 'isTlonEmployee'];
+export const APP_INFO_QUERY_KEY = ['settings', 'appInfo'];
+
export type ChannelSortPreference = 'recency' | 'arranged';
export async function storeChannelSortPreference(
sortPreference: ChannelSortPreference
@@ -63,3 +66,31 @@ export async function getPushNotificationsSetting(): Promise {
+ const storedAppInfo = await AsyncStorage.getItem(`settings:appInfo`);
+ const appInfo = storedAppInfo ? (JSON.parse(storedAppInfo) as AppInfo) : null;
+ return appInfo;
+}
diff --git a/packages/shared/src/db/migrations/0000_thankful_thing.sql b/packages/shared/src/db/migrations/0000_furry_king_cobra.sql
similarity index 99%
rename from packages/shared/src/db/migrations/0000_thankful_thing.sql
rename to packages/shared/src/db/migrations/0000_furry_king_cobra.sql
index d27076f354..bb707399cc 100644
--- a/packages/shared/src/db/migrations/0000_thankful_thing.sql
+++ b/packages/shared/src/db/migrations/0000_furry_king_cobra.sql
@@ -49,6 +49,7 @@ CREATE TABLE `channels` (
`cover_image_color` text,
`title` text,
`description` text,
+ `contact_id` text,
`added_to_group_at` integer,
`current_user_is_member` integer,
`post_count` integer,
diff --git a/packages/shared/src/db/migrations/meta/0000_snapshot.json b/packages/shared/src/db/migrations/meta/0000_snapshot.json
index 57a006dfae..6ae8cfc902 100644
--- a/packages/shared/src/db/migrations/meta/0000_snapshot.json
+++ b/packages/shared/src/db/migrations/meta/0000_snapshot.json
@@ -1,7 +1,7 @@
{
"version": "5",
"dialect": "sqlite",
- "id": "c25041ca-efef-491c-9869-b3763f16f213",
+ "id": "5fa6c36d-ce08-4b92-ae6d-305c6fbc307b",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"activity_events": {
@@ -312,6 +312,13 @@
"notNull": false,
"autoincrement": false
},
+ "contact_id": {
+ "name": "contact_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
"added_to_group_at": {
"name": "added_to_group_at",
"type": "integer",
diff --git a/packages/shared/src/db/migrations/meta/_journal.json b/packages/shared/src/db/migrations/meta/_journal.json
index 8338004b81..11c2302214 100644
--- a/packages/shared/src/db/migrations/meta/_journal.json
+++ b/packages/shared/src/db/migrations/meta/_journal.json
@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "5",
- "when": 1720021644437,
- "tag": "0000_thankful_thing",
+ "when": 1720805158211,
+ "tag": "0000_furry_king_cobra",
"breakpoints": true
}
]
diff --git a/packages/shared/src/db/migrations/migrations.js b/packages/shared/src/db/migrations/migrations.js
index 935ababcbc..55bd039368 100644
--- a/packages/shared/src/db/migrations/migrations.js
+++ b/packages/shared/src/db/migrations/migrations.js
@@ -1,7 +1,7 @@
// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo
import journal from './meta/_journal.json';
-import m0000 from './0000_thankful_thing.sql';
+import m0000 from './0000_furry_king_cobra.sql';
export default {
journal,
diff --git a/packages/shared/src/db/queries.ts b/packages/shared/src/db/queries.ts
index 4195342187..19d2fc4f01 100644
--- a/packages/shared/src/db/queries.ts
+++ b/packages/shared/src/db/queries.ts
@@ -1806,11 +1806,11 @@ async function insertPosts(posts: Post[], ctx: QueryCtx) {
)
.onConflictDoUpdate({
target: $posts.id,
- set: conflictUpdateSetAll($posts),
+ set: conflictUpdateSetAll($posts, ['hidden']),
})
.onConflictDoUpdate({
target: [$posts.authorId, $posts.sentAt],
- set: conflictUpdateSetAll($posts),
+ set: conflictUpdateSetAll($posts, ['hidden']),
});
logger.log('inserted posts');
await setLastPosts(posts, ctx);
@@ -1822,6 +1822,31 @@ async function insertPosts(posts: Post[], ctx: QueryCtx) {
logger.log('clear matched pending');
}
+export const insertHiddenPosts = createWriteQuery(
+ 'insertHiddenPosts',
+ async (postIds: string[], ctx: QueryCtx) => {
+ if (postIds.length === 0) return;
+
+ logger.log('insertHiddenPosts', postIds);
+
+ await ctx.db
+ .update($posts)
+ .set({ hidden: true })
+ .where(inArray($posts.id, postIds));
+ },
+ ['posts']
+);
+
+export const getHiddenPosts = createReadQuery(
+ 'getHiddenPosts',
+ async (ctx: QueryCtx) => {
+ return ctx.db.query.posts.findMany({
+ where: eq($posts.hidden, true),
+ });
+ },
+ ['posts']
+);
+
async function setLastPosts(newPosts: Post[] | null, ctx: QueryCtx) {
const channelIds = newPosts?.map((p) => p.channelId) ?? [];
@@ -2270,6 +2295,37 @@ export const getGroupByChannel = createReadQuery(
['channels', 'groups']
);
+export const insertBlockedContacts = createWriteQuery(
+ 'insertBlockedContacts',
+ async ({ blockedIds }: { blockedIds: string[] }, ctx: QueryCtx) => {
+ if (blockedIds.length === 0) return;
+
+ const blockedContacts: Contact[] = blockedIds.map((id) => ({
+ id,
+ isBlocked: true,
+ }));
+
+ return ctx.db
+ .insert($contacts)
+ .values(blockedContacts)
+ .onConflictDoUpdate({
+ target: $contacts.id,
+ set: conflictUpdateSet($contacts.isBlocked),
+ });
+ },
+ ['contacts']
+);
+
+export const getBlockedUsers = createReadQuery(
+ 'getBlockedUsers',
+ async (ctx: QueryCtx) => {
+ return ctx.db.query.contacts.findMany({
+ where: eq($contacts.isBlocked, true),
+ });
+ },
+ ['contacts']
+);
+
export const getContacts = createReadQuery(
'getContacts',
async (ctx: QueryCtx) => {
@@ -2929,16 +2985,24 @@ function conflictUpdateSetAll(table: Table, exclude?: string[]) {
function conflictUpdateSet(...columns: Column[]) {
return Object.fromEntries(
- columns.map((c) => [toCamelCase(c.name), sql.raw(`excluded.${c.name}`)])
+ columns.map((c) => {
+ return [getColumnTsName(c), sql.raw(`excluded.${c.name}`)];
+ })
);
}
-function ascNullsLast(column: SQLWrapper | AnyColumn) {
- return sql`${column} ASC NULLS LAST`;
+function getColumnTsName(c: Column) {
+ const name = Object.keys(c.table).find(
+ (k) => c.table[k as keyof typeof c.table] === c
+ );
+ if (!name) {
+ throw new Error('unable to find column name');
+ }
+ return name;
}
-function toCamelCase(str: string) {
- return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
+function ascNullsLast(column: SQLWrapper | AnyColumn) {
+ return sql`${column} ASC NULLS LAST`;
}
function returnNullIfUndefined(input: T | undefined): T | null {
diff --git a/packages/shared/src/db/schema.ts b/packages/shared/src/db/schema.ts
index cff0a0a805..1e830d4e1a 100644
--- a/packages/shared/src/db/schema.ts
+++ b/packages/shared/src/db/schema.ts
@@ -628,6 +628,7 @@ export const channels = sqliteTable(
onDelete: 'cascade',
}),
...metaFields,
+ contactId: text('contact_id'),
addedToGroupAt: timestamp('added_to_group_at'),
currentUserIsMember: boolean('current_user_is_member'),
postCount: integer('post_count'),
@@ -660,6 +661,10 @@ export const channelRelations = relations(channels, ({ one, many }) => ({
fields: [channels.groupId],
references: [groups.id],
}),
+ contact: one(contacts, {
+ fields: [channels.contactId],
+ references: [contacts.id],
+ }),
posts: many(posts),
lastPost: one(posts, {
fields: [channels.lastPostId],
diff --git a/packages/shared/src/logic/utils.ts b/packages/shared/src/logic/utils.ts
index be3dcf8704..38a75e6a0f 100644
--- a/packages/shared/src/logic/utils.ts
+++ b/packages/shared/src/logic/utils.ts
@@ -4,16 +4,23 @@ import {
differenceInDays,
endOfToday,
format,
+ getDate,
} from 'date-fns';
import emojiRegex from 'emoji-regex';
import { backOff } from 'exponential-backoff';
import { useMemo } from 'react';
import * as api from '../api';
-import { isDmChannelId, isGroupDmChannelId } from '../api/apiUtils';
+import {
+ isDmChannelId,
+ isGroupChannelId,
+ isGroupDmChannelId,
+} from '../api/apiUtils';
import * as db from '../db';
import * as ub from '../urbit';
+export { isDmChannelId, isGroupDmChannelId, isGroupChannelId };
+
export const IMAGE_REGEX =
/(\.jpg|\.img|\.png|\.gif|\.tiff|\.jpeg|\.webp|\.svg)(?:\?.*)?$/i;
export const AUDIO_REGEX = /(\.mp3|\.wav|\.ogg|\.m4a)(?:\?.*)?$/i;
@@ -98,7 +105,7 @@ export function makePrettyDay(date: Date) {
}
export function makePrettyShortDate(date: Date) {
- return format(date, 'MMM dd, yyyy');
+ return format(date, `MMMM do, yyyy`);
}
export function makeShortDate(date: Date) {
@@ -363,6 +370,33 @@ export const textPostIsLinkedImage = (post: db.Post): boolean => {
return false;
};
+export const textPostIsLink = (post: db.Post): boolean => {
+ const postIsJustText = isTextPost(post);
+ if (!postIsJustText) {
+ return false;
+ }
+
+ const postIsImage = textPostIsLinkedImage(post);
+ if (postIsImage) {
+ return false;
+ }
+
+ const { inlines } = extractContentTypesFromPost(post);
+
+ if (inlines.length <= 2) {
+ const [first] = inlines;
+ if (typeof first === 'object' && 'link' in first) {
+ const link = first as ub.Link;
+ const { href } = link.link;
+ const isLink = URL_REGEX.test(href);
+
+ return isLink;
+ }
+ }
+
+ return false;
+};
+
export const textPostIsReference = (post: db.Post): boolean => {
const { inlines, references } = extractContentTypesFromPost(post);
if (references.length === 0) {
@@ -413,6 +447,7 @@ export const usePostMeta = (post: db.Post) => {
);
const isText = useMemo(() => isTextPost(post), [post]);
const isImage = useMemo(() => isImagePost(post), [post]);
+ const isLink = useMemo(() => textPostIsLink(post), [post]);
const isReference = useMemo(() => isReferencePost(post), [post]);
const isLinkedImage = useMemo(() => textPostIsLinkedImage(post), [post]);
const isRefInText = useMemo(() => textPostIsReference(post), [post]);
@@ -428,6 +463,7 @@ export const usePostMeta = (post: db.Post) => {
return {
isText,
isImage,
+ isLink,
isReference,
isLinkedImage,
isRefInText,
diff --git a/packages/shared/src/store/contactActions.ts b/packages/shared/src/store/contactActions.ts
new file mode 100644
index 0000000000..70ee9c5310
--- /dev/null
+++ b/packages/shared/src/store/contactActions.ts
@@ -0,0 +1,27 @@
+import * as api from '../api';
+import * as db from '../db';
+
+export async function updateCurrentUserProfile(update: api.ProfileUpdate) {
+ const currentUserId = api.getCurrentUserId();
+ const currentUserContact = await db.getContact({ id: currentUserId });
+ const startingValues: Partial = {};
+ if (currentUserContact) {
+ for (const key in update) {
+ if (key in currentUserContact) {
+ startingValues[key as keyof api.ProfileUpdate] =
+ currentUserContact[key as keyof api.ProfileUpdate];
+ }
+ }
+ }
+
+ // Optimistic update
+ await db.updateContact({ id: currentUserId, ...update });
+
+ try {
+ await api.updateCurrentUserProfile(update);
+ } catch (e) {
+ console.error('Error updating profile', e);
+ // Rollback the update
+ await db.updateContact({ id: currentUserId, ...startingValues });
+ }
+}
diff --git a/packages/shared/src/store/dbHooks.ts b/packages/shared/src/store/dbHooks.ts
index 5d31a40d31..fcd921ae06 100644
--- a/packages/shared/src/store/dbHooks.ts
+++ b/packages/shared/src/store/dbHooks.ts
@@ -70,6 +70,13 @@ export const useCalmSettings = (options: { userId: string }) => {
});
};
+export const useAppInfo = () => {
+ return useQuery({
+ queryKey: db.APP_INFO_QUERY_KEY,
+ queryFn: db.getAppInfoSettings,
+ });
+};
+
export const useActivitySeenMarker = () => {
return useQuery({
queryKey: db.ACTIVITY_SEEN_MARKER_QUERY_KEY,
@@ -84,6 +91,13 @@ export const usePushNotificationsSetting = () => {
});
};
+export const useIsTlonEmployee = () => {
+ return useQuery({
+ queryKey: db.IS_TLON_EMPLOYEE_QUERY_KEY,
+ queryFn: db.getIsTlonEmployee,
+ });
+};
+
export const useContact = (options: { id: string }) => {
const deps = useKeyFromQueryDeps(db.getContact);
return useQuery({
@@ -92,6 +106,14 @@ export const useContact = (options: { id: string }) => {
});
};
+export const useBlockedContacts = () => {
+ const depsKey = useKeyFromQueryDeps(db.getBlockedUsers);
+ return useQuery({
+ queryKey: ['blockedContacts', depsKey],
+ queryFn: () => db.getBlockedUsers(),
+ });
+};
+
export const useContacts = () => {
const deps = useKeyFromQueryDeps(db.getContacts);
return useQuery({
diff --git a/packages/shared/src/store/dmActions.ts b/packages/shared/src/store/dmActions.ts
index d3ffe783d3..2ae0be717d 100644
--- a/packages/shared/src/store/dmActions.ts
+++ b/packages/shared/src/store/dmActions.ts
@@ -60,3 +60,25 @@ export async function blockUser(userId: string) {
}
}
}
+
+export async function unblockUser(userId: string) {
+ logger.log(`unblocking user`, userId);
+ // optimistic update
+ const existingContact = await db.getContact({ id: userId });
+ if (existingContact) {
+ await db.updateContact({ id: userId, isBlocked: false });
+ }
+
+ try {
+ await api.unblockUser(userId);
+ } catch (e) {
+ console.error('Failed to unblock user', e);
+ // rollback optimistic update
+ if (existingContact) {
+ await db.updateContact({
+ id: userId,
+ isBlocked: existingContact.isBlocked,
+ });
+ }
+ }
+}
diff --git a/packages/shared/src/store/index.ts b/packages/shared/src/store/index.ts
index 7902a3ee15..0b52bf99f6 100644
--- a/packages/shared/src/store/index.ts
+++ b/packages/shared/src/store/index.ts
@@ -13,3 +13,4 @@ export * from './useNegotiation';
export * from './activityActions';
export * from './useActivityFetchers';
export * from './session';
+export * from './contactActions';
diff --git a/packages/shared/src/store/postActions.ts b/packages/shared/src/store/postActions.ts
index 1202c0ef04..6bb8b76de1 100644
--- a/packages/shared/src/store/postActions.ts
+++ b/packages/shared/src/store/postActions.ts
@@ -131,7 +131,7 @@ export async function hidePost({ post }: { post: db.Post }) {
await db.updatePost({ id: post.id, hidden: true });
try {
- await api.hidePost(post.channelId, post.id);
+ await api.hidePost(post);
} catch (e) {
console.error('Failed to hide post', e);
@@ -145,7 +145,7 @@ export async function showPost({ post }: { post: db.Post }) {
await db.updatePost({ id: post.id, hidden: false });
try {
- await api.showPost(post.channelId, post.id);
+ await api.showPost(post);
} catch (e) {
console.error('Failed to show post', e);
diff --git a/packages/shared/src/store/sync.test.ts b/packages/shared/src/store/sync.test.ts
index 2d9f4ac279..8d0b526e7f 100644
--- a/packages/shared/src/store/sync.test.ts
+++ b/packages/shared/src/store/sync.test.ts
@@ -138,6 +138,7 @@ test('syncs dms', async () => {
id: '~solfer-magfed',
type: 'dm',
groupId: null,
+ contactId: '~solfer-magfed',
iconImage: null,
iconImageColor: null,
coverImage: null,
@@ -174,6 +175,7 @@ test('syncs dms', async () => {
id: '0v4.00000.qd4p2.it253.qs53q.s53qs',
type: 'groupDm',
groupId: null,
+ contactId: null,
iconImage: null,
iconImageColor: '#f0ebbd',
coverImage: null,
diff --git a/packages/shared/src/store/sync.ts b/packages/shared/src/store/sync.ts
index d0abb1c74e..d376c8faaa 100644
--- a/packages/shared/src/store/sync.ts
+++ b/packages/shared/src/store/sync.ts
@@ -117,6 +117,11 @@ function checkForNewlyJoined({
}
}
+export const syncBlockedUsers = async () => {
+ const blockedIds = await api.getBlockedUsers();
+ await db.insertBlockedContacts({ blockedIds });
+};
+
export const syncChannelHeads = async (
reporter?: ErrorReporter,
priority = SyncPriority.High
@@ -142,6 +147,13 @@ export const syncSettings = async (priority = SyncPriority.Medium) => {
return db.insertSettings(settings);
};
+export const syncAppInfo = async (priority = SyncPriority.Medium) => {
+ const appInfo = await syncQueue.add('appInfo', priority, () =>
+ api.getAppInfo()
+ );
+ return db.setAppInfoSettings(appInfo);
+};
+
export const syncVolumeSettings = async (priority = SyncPriority.Medium) => {
const volumeSettings = await syncQueue.add('volumeSettings', priority, () =>
api.getVolumeSettings()
@@ -604,6 +616,12 @@ export const handleChatUpdate = async (update: api.ChatEvent) => {
logger.log('event: chat update', update);
switch (update.type) {
+ case 'showPost':
+ await db.updatePost({ id: update.postId, hidden: false });
+ break;
+ case 'hidePost':
+ await db.updatePost({ id: update.postId, hidden: true });
+ break;
case 'addPost':
await handleAddPost(update.post, update.replyMeta);
break;
@@ -681,6 +699,38 @@ export async function handleAddPost(
}
}
+export async function syncHiddenPosts(reporter: ErrorReporter) {
+ const hiddenPosts = await syncQueue.add(
+ 'hiddenPosts',
+ SyncPriority.High,
+ () => api.getHiddenPosts()
+ );
+ reporter?.log('got hidden channel posts data from api');
+ const hiddenDMPosts = await syncQueue.add(
+ 'hiddenDMPosts',
+ SyncPriority.High,
+ () => api.getHiddenDMPosts()
+ );
+ reporter?.log('got hidden dm posts data from api');
+
+ const currentHiddenPosts = await db.getHiddenPosts();
+
+ // if the user deleted the posts from another client while we were offline,
+ // we should remove them from our hidden posts list
+ currentHiddenPosts.forEach(async (hiddenPost) => {
+ if (
+ !hiddenPosts.some((postId) => postId === hiddenPost.id) &&
+ !hiddenDMPosts.some((postId) => postId === hiddenPost.id)
+ ) {
+ reporter?.log(`deleting hidden post ${hiddenPost.id}`);
+ await db.updatePost({ id: hiddenPost.id, hidden: false });
+ }
+ });
+
+ await db.insertHiddenPosts([...hiddenPosts, ...hiddenDMPosts]);
+ reporter?.log('inserted hidden posts');
+}
+
export async function syncPosts(
options: api.GetChannelPostsOptions,
priority = SyncPriority.Medium
@@ -706,6 +756,7 @@ export async function syncPosts(
syncedAt: Date.now(),
});
}
+
return response;
}
@@ -797,6 +848,8 @@ export const syncStart = async (alreadySubscribed?: boolean) => {
// highest priority, do immediately
await withRetry(() => syncInitData(reporter));
reporter.log(`finished syncing init data`);
+ await withRetry(() => syncHiddenPosts(reporter));
+ reporter.log(`finished syncing hidden posts`);
await withRetry(() => syncChannelHeads(reporter));
reporter.log(`finished syncing latest posts`);
@@ -830,6 +883,12 @@ export const syncStart = async (alreadySubscribed?: boolean) => {
syncPushNotificationsSetting().then(() =>
reporter.log(`finished syncing push notifications setting`)
),
+ syncBlockedUsers().then(() => {
+ reporter.log(`finished syncing blocked users`);
+ }),
+ syncAppInfo().then(() => {
+ reporter.log(`finished syncing app info`);
+ }),
])
);
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 926092c8b7..1c74487791 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -37,6 +37,7 @@
"expo-blur": "^12.9.2",
"expo-haptics": "^12.8.1",
"expo-image-picker": "~14.7.1",
+ "fuzzy": "^0.1.3",
"lodash": "^4.17.21",
"moti": "^0.28.1",
"react-hook-form": "^7.52.0",
diff --git a/packages/ui/src/assets/icons/Copy.svg b/packages/ui/src/assets/icons/Copy.svg
new file mode 100644
index 0000000000..b96f8682cb
--- /dev/null
+++ b/packages/ui/src/assets/icons/Copy.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/packages/ui/src/assets/icons/index.ts b/packages/ui/src/assets/icons/index.ts
index 950eda1e85..aae1642b7c 100644
--- a/packages/ui/src/assets/icons/index.ts
+++ b/packages/ui/src/assets/icons/index.ts
@@ -43,6 +43,7 @@ export { default as ChevronRight } from './ChevronRight.svg';
export { default as ChevronUp } from './ChevronUp.svg';
export { default as Clock } from './Clock.svg';
export { default as Close } from './Close.svg';
+export { default as Copy } from './Copy.svg';
export { default as Discover } from './Discover.svg';
export { default as Dragger } from './Dragger.svg';
export { default as Draw } from './Draw.svg';
diff --git a/packages/ui/src/components/Activity/ActivityHeader.tsx b/packages/ui/src/components/Activity/ActivityHeader.tsx
index 4165b0f7b3..be35d1f507 100644
--- a/packages/ui/src/components/Activity/ActivityHeader.tsx
+++ b/packages/ui/src/components/Activity/ActivityHeader.tsx
@@ -1,9 +1,9 @@
import * as db from '@tloncorp/shared/dist/db';
import React from 'react';
-import { SizableText, View, XStack } from '../../core';
-import Pressable from '../Pressable';
+import { View } from '../../core';
import { ScreenHeader } from '../ScreenHeader';
+import { Tabs } from '../Tabs';
export type ActivityTab = 'all' | 'threads' | 'mentions';
@@ -21,72 +21,29 @@ function ActivityHeaderRaw({
Activity
-
-
+ onTabPress('all')}
+ name="all"
>
- onTabPress('all')}
- >
-
- All
-
-
-
- All
+
+ onTabPress('mentions')}
+ name="mentions"
>
- onTabPress('mentions')}
- >
-
- Mentions
-
-
-
- Mentions
+
+ onTabPress('replies')}
+ name="replies"
>
- onTabPress('replies')}
- >
-
- Replies
-
-
-
-
+ Replies
+
+
);
}
diff --git a/packages/ui/src/components/Activity/ActivitySummaryMessage.tsx b/packages/ui/src/components/Activity/ActivitySummaryMessage.tsx
index 6138ec43a3..1236cc2098 100644
--- a/packages/ui/src/components/Activity/ActivitySummaryMessage.tsx
+++ b/packages/ui/src/components/Activity/ActivitySummaryMessage.tsx
@@ -149,7 +149,7 @@ function SummaryMessageRaw({
{` ${postVerb(newest.channel?.type ?? 'chat')} ${count} ${postName(newest, count > 1)}`}
diff --git a/packages/ui/src/components/AddChats/CreateGroupWidget.tsx b/packages/ui/src/components/AddChats/CreateGroupWidget.tsx
index cf6fed617a..a47abb0af8 100644
--- a/packages/ui/src/components/AddChats/CreateGroupWidget.tsx
+++ b/packages/ui/src/components/AddChats/CreateGroupWidget.tsx
@@ -50,7 +50,7 @@ export function CreateGroupWidget(props: {
props.goBack()} />
- Start a New Group
+ Start a New Group
What is your group about?
diff --git a/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx b/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx
index 430dc4b64e..f160e8ad02 100644
--- a/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx
+++ b/packages/ui/src/components/AddChats/ViewUserGroupsWidget.tsx
@@ -49,7 +49,7 @@ export function ViewUserGroupsWidget({
Loading groups hosted by{' '}
-
+
) : isError ? (
@@ -59,7 +59,7 @@ export function ViewUserGroupsWidget({
@@ -69,7 +69,7 @@ export function ViewUserGroupsWidget({
Groups hosted by{' '}
- :
+ :
{
+ if (didCopy) return;
+ Clipboard.setString(value);
+ setDidCopy(true);
+ setTimeout(() => {
+ setDidCopy(false);
+ }, 3000);
+ }, [didCopy, value]);
+
+ return (
+
+
+ {title}
+ {value}
+
+ {copyable && (
+
+
+
+ )}
+
+ );
+}
diff --git a/packages/ui/src/components/AuthorRow.tsx b/packages/ui/src/components/AuthorRow.tsx
index 793a5d1a4b..44b54e230a 100644
--- a/packages/ui/src/components/AuthorRow.tsx
+++ b/packages/ui/src/components/AuthorRow.tsx
@@ -89,7 +89,7 @@ function ChatAuthorRow({ authorId, sent, roles, ...props }: AuthorRowProps) {
return (
-
+
{timeDisplay}
diff --git a/packages/ui/src/components/Avatar.tsx b/packages/ui/src/components/Avatar.tsx
index b620202def..9c81e074a4 100644
--- a/packages/ui/src/components/Avatar.tsx
+++ b/packages/ui/src/components/Avatar.tsx
@@ -50,6 +50,11 @@ const AvatarFrame = styled(View, {
width: '$5xl',
borderRadius: '$m',
},
+ $9xl: {
+ height: '$9xl',
+ width: '$9xl',
+ borderRadius: '$xl',
+ },
custom: {},
},
} as const,
@@ -61,16 +66,18 @@ export type AvatarProps = ComponentProps & {
export const ContactAvatar = React.memo(function ContactAvatComponent({
contactId,
+ overrideUrl,
innerSigilSize,
...props
}: {
contactId: string;
+ overrideUrl?: string;
innerSigilSize?: number;
} & AvatarProps) {
const contact = useContact(contactId);
return (
void;
+}) {
+ if (blockedContacts.length === 0) {
+ return (
+
+ No blocked users
+
+ );
+ }
+
+ return (
+
+ {blockedContacts.map((contact) => (
+ onBlockedContactPress(contact)}
+ showNickname
+ showEndContent
+ endContent={}
+ />
+ ))}
+
+ );
+}
diff --git a/packages/ui/src/components/ChannelSearch/SearchResults.tsx b/packages/ui/src/components/ChannelSearch/SearchResults.tsx
index 995badc081..9c1306c9dd 100644
--- a/packages/ui/src/components/ChannelSearch/SearchResults.tsx
+++ b/packages/ui/src/components/ChannelSearch/SearchResults.tsx
@@ -53,14 +53,14 @@ export function SearchResults({
Results for "
-
+
{search.query}
"
Sorted by:{' '}
-
+
most recent
diff --git a/packages/ui/src/components/ChannelSearch/SearchStatus.tsx b/packages/ui/src/components/ChannelSearch/SearchStatus.tsx
index 7a4ea68b89..5c02bac88c 100644
--- a/packages/ui/src/components/ChannelSearch/SearchStatus.tsx
+++ b/packages/ui/src/components/ChannelSearch/SearchStatus.tsx
@@ -29,7 +29,7 @@ export function SearchStatus({
)}
{numResults > 0 && (
-
+
{numResults}
{` results · `}
diff --git a/packages/ui/src/components/ChannelSwitcherSheet.tsx b/packages/ui/src/components/ChannelSwitcherSheet.tsx
index 99f37e011a..b3e48ea6a6 100644
--- a/packages/ui/src/components/ChannelSwitcherSheet.tsx
+++ b/packages/ui/src/components/ChannelSwitcherSheet.tsx
@@ -69,7 +69,7 @@ export function ChannelSwitcherSheet({
diff --git a/packages/ui/src/components/ChatList.tsx b/packages/ui/src/components/ChatList.tsx
index 4082d9269c..f51588e2cb 100644
--- a/packages/ui/src/components/ChatList.tsx
+++ b/packages/ui/src/components/ChatList.tsx
@@ -1,7 +1,9 @@
import * as db from '@tloncorp/shared/dist/db';
import * as logic from '@tloncorp/shared/dist/logic';
import * as store from '@tloncorp/shared/dist/store';
-import { useCallback, useMemo, useRef } from 'react';
+import fuzzy from 'fuzzy';
+import { debounce } from 'lodash';
+import { useCallback, useMemo, useRef, useState } from 'react';
import {
NativeScrollEvent,
NativeSyntheticEvent,
@@ -12,13 +14,31 @@ import {
ViewStyle,
ViewToken,
} from 'react-native';
+import Animated, {
+ Extrapolation,
+ interpolate,
+ useAnimatedScrollHandler,
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { getTokenValue } from 'tamagui';
-import { useStyle } from '../core';
+import { Text, View, YStack, useStyle } from '../core';
+import { Icon } from './Icon';
+import { Input } from './Input';
import { ChatListItem, SwipableChatListItem } from './ListItem';
+import Pressable from './Pressable';
import { SectionListHeader } from './SectionList';
+import { Tabs } from './Tabs';
+
+const DEBOUNCE_DELAY = 200;
export type Chat = db.Channel | db.Group;
+const AnimatedSectionList = Animated.createAnimatedComponent(SectionList);
+
export function ChatList({
pinned,
unpinned,
@@ -26,21 +46,102 @@ export function ChatList({
onLongPressItem,
onPressItem,
onSectionChange,
+ activeTab,
+ setActiveTab,
}: store.CurrentChats & {
onPressItem?: (chat: Chat) => void;
onLongPressItem?: (chat: Chat) => void;
onSectionChange?: (title: string) => void;
+ activeTab: 'all' | 'groups' | 'messages';
+ setActiveTab: (tab: 'all' | 'groups' | 'messages') => void;
}) {
- const data = useMemo(() => {
- if (pinned.length === 0) {
- return [{ title: 'All', data: [...pendingChats, ...unpinned] }];
+ const [searchQuery, setSearchQuery] = useState('');
+ const scrollY = useSharedValue(0);
+ const filterVisible = useSharedValue(false);
+ const filteredData = useMemo(() => {
+ const filteredPinned = pinned.filter((c) => {
+ if (logic.isGroupChannelId(c.id)) {
+ return activeTab === 'all' || activeTab === 'groups';
+ }
+ return activeTab === 'all' || activeTab === 'messages';
+ });
+
+ const filteredUnpinned = unpinned.filter((c) => {
+ if (logic.isGroupChannelId(c.id)) {
+ return activeTab === 'all' || activeTab === 'groups';
+ }
+ return activeTab === 'all' || activeTab === 'messages';
+ });
+
+ return {
+ filteredPinned,
+ filteredUnpinned,
+ };
+ }, [activeTab, pinned, unpinned]);
+
+ const sectionedData = useMemo(() => {
+ const { filteredPinned, filteredUnpinned } = filteredData;
+ if (filteredPinned.length === 0) {
+ return [{ title: 'All', data: [...pendingChats, ...filteredUnpinned] }];
}
return [
- { title: 'Pinned', data: pinned },
- { title: 'All', data: [...pendingChats, ...unpinned] },
+ { title: 'Pinned', data: filteredPinned },
+ { title: 'All', data: [...pendingChats, ...filteredUnpinned] },
];
- }, [pinned, unpinned, pendingChats]);
+ }, [filteredData, pendingChats]);
+
+ const extractForFuzzy = useCallback((item: db.Channel) => {
+ if (logic.isGroupChannelId(item.id)) {
+ return item.group?.title || '';
+ }
+ if (item.type === 'dm' && item.contact) {
+ return item.contact.nickname
+ ? `${item.contact.nickname}-${item.id}`
+ : item.id;
+ }
+ if (item.type === 'groupDm') {
+ return (item.members || [])
+ .map((m) =>
+ m.contact
+ ? m.contact.nickname
+ ? `${m.contact.nickname}-${m.contact.id}`
+ : m.contact.id
+ : ''
+ )
+ .join(' ');
+ }
+ return item.id;
+ }, []);
+
+ const debouncedFuzzySearch = useMemo(
+ () =>
+ debounce((query) => {
+ const results = fuzzy.filter(
+ query.trim(),
+ [...filteredData.filteredPinned, ...filteredData.filteredUnpinned],
+ { extract: extractForFuzzy }
+ );
+ return results.map((result) => result.original);
+ }, DEBOUNCE_DELAY),
+ [filteredData, extractForFuzzy]
+ );
+
+ const searchResults = useMemo(() => {
+ if (searchQuery.trim() === '') {
+ return sectionedData;
+ }
+ const results = debouncedFuzzySearch(searchQuery);
+
+ return results && results.length > 0
+ ? [
+ {
+ title: 'Search',
+ data: results,
+ },
+ ]
+ : [{ title: 'Search', data: [] }];
+ }, [searchQuery, sectionedData, debouncedFuzzySearch]);
const contentContainerStyle = useStyle(
{
@@ -51,16 +152,19 @@ export function ChatList({
) as StyleProp;
const renderItem = useCallback(
- ({ item }: SectionListRenderItemInfo) => {
+ ({ item }: SectionListRenderItemInfo) => {
+ const itemModel = item as Chat;
const baseListItem = (
);
- return logic.isChannel(item) ? (
- {baseListItem}
+ return logic.isChannel(itemModel) ? (
+
+ {baseListItem}
+
) : (
baseListItem
);
@@ -69,10 +173,11 @@ export function ChatList({
);
const renderSectionHeader = useCallback(
- ({ section }: { section: SectionListData }) => {
+ ({ section }: { section: SectionListData }) => {
+ const sectionItem = section as SectionListData;
return (
- {section.title}
+ {sectionItem.title}
);
},
@@ -114,31 +219,156 @@ export function ChatList({
}
).current;
- const getChannelKey = useCallback((item: Chat) => {
- if (!item || typeof item !== 'object' || !item.id) {
+ const FILTER_HEIGHT =
+ getTokenValue('$6xl', 'size') + getTokenValue('$4xl', 'size');
+ const HEADER_HEIGHT = getTokenValue('$4xl', 'size');
+ const SNAP_THRESHOLD = FILTER_HEIGHT / 2;
+
+ const { top } = useSafeAreaInsets();
+ const filterStyle = useAnimatedStyle(() => {
+ const translateY = interpolate(
+ scrollY.value,
+ [-FILTER_HEIGHT, 0],
+ [0, -FILTER_HEIGHT],
+ Extrapolation.CLAMP
+ );
+
+ return {
+ height: FILTER_HEIGHT,
+ transform: [{ translateY: filterVisible.value ? 0 : translateY }],
+ position: 'absolute',
+ top: top + HEADER_HEIGHT,
+ left: 0,
+ right: 0,
+ zIndex: 40,
+ };
+ });
+
+ const listStyle = useAnimatedStyle(() => {
+ return {
+ transform: [{ translateY: filterVisible.value ? FILTER_HEIGHT : 0 }],
+ };
+ });
+
+ const scrollHandler = useAnimatedScrollHandler({
+ onScroll: (event) => {
+ if (!filterVisible.value) {
+ scrollY.value = event.contentOffset.y;
+ }
+ },
+ onEndDrag: (event) => {
+ if (event.contentOffset.y < -SNAP_THRESHOLD && !filterVisible.value) {
+ filterVisible.value = true;
+ scrollY.value = withSpring(-FILTER_HEIGHT);
+ } else if (
+ event.contentOffset.y > -SNAP_THRESHOLD &&
+ filterVisible.value
+ ) {
+ filterVisible.value = false;
+ scrollY.value = withSpring(0);
+ }
+ },
+ });
+
+ const getChannelKey = useCallback((item: unknown) => {
+ const chatItem = item as Chat;
+
+ if (!chatItem || typeof chatItem !== 'object' || !chatItem.id) {
return 'invalid-item';
}
- if (logic.isGroup(item)) {
- return item.id;
+ if (logic.isGroup(chatItem)) {
+ return chatItem.id;
}
- return `${item.id}-${item.pin?.itemId ?? ''}`;
+ return `${chatItem.id}-${chatItem.pin?.itemId ?? ''}`;
}, []);
return (
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ setActiveTab('all')}
+ >
+ All
+
+ setActiveTab('groups')}
+ >
+ Groups
+
+ setActiveTab('messages')}
+ >
+
+ Messages
+
+
+
+
+
+
+ {searchQuery !== '' && searchResults.length === 0 ? (
+
+ No results found.
+ {activeTab !== 'all' && (
+ setActiveTab('all')}>
+ Try in All?
+
+ )}
+ setSearchQuery('')}>
+ Clear search
+
+
+ ) : (
+
+ )}
+
+ >
);
}
diff --git a/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx b/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx
index 928629c7ca..677def23c5 100644
--- a/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx
+++ b/packages/ui/src/components/ChatMessage/ChatMessageActions/MessageActions.tsx
@@ -100,7 +100,7 @@ export function getPostActions({
{ id: 'muteThread', label: isMuted ? 'Unmute thread' : 'Mute thread' },
{ id: 'copyRef', label: 'Copy link to post' },
{ id: 'edit', label: 'Edit message' },
- { id: 'visibility', label: 'Hide' },
+ { id: 'visibility', label: post?.hidden ? 'Show post' : 'Hide post' },
{ id: 'delete', label: 'Delete message', actionType: 'destructive' },
];
case 'notebook':
@@ -110,7 +110,7 @@ export function getPostActions({
{ id: 'pin', label: 'Pin post' },
{ id: 'copyRef', label: 'Copy link to post' },
{ id: 'edit', label: 'Edit message' },
- { id: 'visibility', label: 'Hide' },
+ { id: 'visibility', label: post?.hidden ? 'Show post' : 'Hide post' },
{ id: 'delete', label: 'Delete message', actionType: 'destructive' },
];
case 'dm':
@@ -120,7 +120,7 @@ export function getPostActions({
{ id: 'startThread', label: 'Start thread' },
{ id: 'muteThread', label: isMuted ? 'Unmute thread' : 'Mute thread' },
{ id: 'copyText', label: 'Copy message text' },
- { id: 'visibility', label: 'Hide' },
+ { id: 'visibility', label: post?.hidden ? 'Show post' : 'Hide post' },
{ id: 'delete', label: 'Delete message', actionType: 'destructive' },
];
case 'chat':
diff --git a/packages/ui/src/components/ChatMessage/ChatMessageReplySummary.tsx b/packages/ui/src/components/ChatMessage/ChatMessageReplySummary.tsx
index 56ee7cb377..64788709d4 100644
--- a/packages/ui/src/components/ChatMessage/ChatMessageReplySummary.tsx
+++ b/packages/ui/src/components/ChatMessage/ChatMessageReplySummary.tsx
@@ -10,9 +10,13 @@ export const ChatMessageReplySummary = React.memo(
function ChatMessageReplySummary({
post,
onPress,
+ paddingLeft = true,
}: {
post: db.Post;
onPress?: () => void;
+ // Since this component is used in places other than a chat log, we need to
+ // be able to toggle the Chat message padding on and off
+ paddingLeft?: boolean;
}) {
const { replyCount, replyTime, replyContactIds, threadUnread } = post;
@@ -21,7 +25,7 @@ export const ChatMessageReplySummary = React.memo(
}, [replyTime]);
return replyCount && replyContactIds && replyTime ? (
-
+
{replyContactIds?.map((c, i) => (
{
- return {children};
+const ContactListFrameComponent = ({
+ children,
+ ...rest
+}: PropsWithChildren>) => {
+ return {children};
};
export const ContactList = withStaticProperties(ContactListFrameComponent, {
diff --git a/packages/ui/src/components/ContentReference/ChannelReference.tsx b/packages/ui/src/components/ContentReference/ChannelReference.tsx
index d30d9c8ca1..ed5db00fdc 100644
--- a/packages/ui/src/components/ContentReference/ChannelReference.tsx
+++ b/packages/ui/src/components/ContentReference/ChannelReference.tsx
@@ -45,7 +45,6 @@ export default function ChannelReference({
}
if (channelType === 'gallery') {
- // TODO: Implement gallery reference
return (
diff --git a/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx b/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx
index d0d0f73d86..03a51f3c9f 100644
--- a/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx
+++ b/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx
@@ -35,16 +35,18 @@ export default function ChatReferenceWrapper({
);
}
if (!post) {
if (isLoading) {
- return ;
+ return ;
}
return (
diff --git a/packages/ui/src/components/ContentReference/GroupReference.tsx b/packages/ui/src/components/ContentReference/GroupReference.tsx
index 29df5642c0..7423f96e63 100644
--- a/packages/ui/src/components/ContentReference/GroupReference.tsx
+++ b/packages/ui/src/components/ContentReference/GroupReference.tsx
@@ -6,11 +6,18 @@ import { useRequests } from '../../contexts/requests';
import { getGroupHost } from '../../utils';
import { ContactAvatar } from '../Avatar';
import ContactName from '../ContactName';
+import { PostViewMode } from '../ContentRenderer';
import { ListItem } from '../ListItem';
import { LoadingSpinner } from '../LoadingSpinner';
-import { Reference } from './Reference';
+import { REF_AUTHOR_WIDTH, Reference } from './Reference';
-export function GroupReference({ groupId }: { groupId: string }) {
+export function GroupReference({
+ groupId,
+ viewMode = 'chat',
+}: {
+ groupId: string;
+ viewMode?: PostViewMode;
+}) {
const { useGroup } = useRequests();
const { onPressGroupRef } = useNavigation();
const { data: group, isLoading, isError } = useGroup(groupId);
@@ -24,7 +31,7 @@ export function GroupReference({ groupId }: { groupId: string }) {
}, [group, onPressGroupRef]);
return (
-
+
@@ -32,6 +39,7 @@ export function GroupReference({ groupId }: { groupId: string }) {
color="$tertiaryText"
size="$s"
userId={host}
+ maxWidth={REF_AUTHOR_WIDTH}
showNickname
/>
diff --git a/packages/ui/src/components/ContentReference/Reference.tsx b/packages/ui/src/components/ContentReference/Reference.tsx
index cf3f5d9b68..8396a8fb3c 100644
--- a/packages/ui/src/components/ContentReference/Reference.tsx
+++ b/packages/ui/src/components/ContentReference/Reference.tsx
@@ -7,6 +7,8 @@ import { PostViewMode } from '../ContentRenderer';
import { Icon } from '../Icon';
import Pressable from '../Pressable';
+export const REF_AUTHOR_WIDTH = 230;
+
export type ReferenceProps = {
onPress: () => void;
};
@@ -40,6 +42,9 @@ const ReferenceFrame = styled(YStack, {
},
block: {
backgroundColor: '$secondaryBackground',
+ borderWidth: 0,
+ borderRadius: 0,
+ marginBottom: 0,
},
note: {
marginLeft: 0,
diff --git a/packages/ui/src/components/ContentReference/ReferenceSkeleton.tsx b/packages/ui/src/components/ContentReference/ReferenceSkeleton.tsx
index da01747f3c..294f6aaa11 100644
--- a/packages/ui/src/components/ContentReference/ReferenceSkeleton.tsx
+++ b/packages/ui/src/components/ContentReference/ReferenceSkeleton.tsx
@@ -1,23 +1,26 @@
import { ComponentProps } from 'react';
import { Text, XStack, YStack } from '../../core';
+import { PostViewMode } from '../ContentRenderer';
import { Icon } from '../Icon';
import { LoadingSpinner } from '../LoadingSpinner';
export default function ReferenceSkeleton({
message = 'Loading',
messageType = 'loading',
+ viewMode = 'chat',
...props
}: {
message?: string;
messageType?: 'loading' | 'error' | 'not-found';
+ viewMode?: PostViewMode;
} & ComponentProps) {
return (
diff --git a/packages/ui/src/components/ContentReference/index.tsx b/packages/ui/src/components/ContentReference/index.tsx
index bba0bbdb5b..13e3c315fd 100644
--- a/packages/ui/src/components/ContentReference/index.tsx
+++ b/packages/ui/src/components/ContentReference/index.tsx
@@ -27,12 +27,13 @@ export default function ContentReference({
}
if (reference.referenceType === 'group') {
- return ;
+ return ;
}
if (reference.referenceType === 'app') {
return (
diff --git a/packages/ui/src/components/ContentRenderer.tsx b/packages/ui/src/components/ContentRenderer.tsx
index a52c4d5909..1de7023486 100644
--- a/packages/ui/src/components/ContentRenderer.tsx
+++ b/packages/ui/src/components/ContentRenderer.tsx
@@ -705,14 +705,12 @@ const LineRenderer = memo(
) : (
// not clear if this is necessary
)}
@@ -742,7 +740,6 @@ const LineRenderer = memo(
color={color}
onPressImage={onPressImage}
onLongPress={onLongPress}
- serif={serif}
/>
);
}
@@ -774,7 +771,6 @@ const LineRenderer = memo(
}
key={`line-${index}`}
flexWrap="wrap"
- fontFamily={serif ? '$serif' : '$body'}
>
{line}
@@ -792,6 +788,7 @@ export type PostViewMode = 'chat' | 'block' | 'note' | 'activity';
export default function ContentRenderer({
post,
shortened = false,
+ shortenedTextOnly = false,
isNotice = false,
deliveryStatus,
onPressImage,
@@ -801,6 +798,7 @@ export default function ContentRenderer({
}: {
post: Post | { type: 'chat' | 'diary' | 'gallery'; id: string; content: any };
shortened?: boolean;
+ shortenedTextOnly?: boolean;
isNotice?: boolean;
deliveryStatus?: PostDeliveryStatus | null;
onPressImage?: (src: string) => void;
@@ -861,7 +859,6 @@ export default function ContentRenderer({
) : null}
{post.type === 'note' && post.title ? (
+
+ );
+ }
+
+ if (shortenedTextOnly) {
+ return (
+
+
);
@@ -886,13 +896,7 @@ export default function ContentRenderer({
{story.map((s, k) => {
if ('block' in s) {
- return (
-
- );
+ return ;
}
if ('type' in s && s.type === 'reference') {
@@ -908,7 +912,6 @@ export default function ContentRenderer({
onPressImage={onPressImage}
onLongPress={onLongPress}
viewMode={viewMode}
- serif={post.type === 'note'}
/>
);
}
diff --git a/packages/ui/src/components/DetailView/DetailView.tsx b/packages/ui/src/components/DetailView/DetailView.tsx
index 3618642be0..4977f021cd 100644
--- a/packages/ui/src/components/DetailView/DetailView.tsx
+++ b/packages/ui/src/components/DetailView/DetailView.tsx
@@ -46,8 +46,11 @@ const DetailViewMetaDataComponent = ({
return makePrettyShortDate(date);
}, [post.sentAt]);
+ const hasReplies = post.replyCount! > 0;
+
return (
-
+
+ {dateDisplay}
-
- {dateDisplay}
-
- {showReplyCount && (
+ {showReplyCount && hasReplies && (
- {post.replyCount} replies
+ {post.replyCount} {post.replyCount === 1 ? 'reply' : 'replies'}
)}
@@ -83,16 +83,6 @@ const DetailViewHeaderComponentFrame = ({
>
{children}
-
-
- {replyCount} replies
-
-
);
};
@@ -172,7 +162,7 @@ const DetailViewFrameComponent = ({
getDraft={getDraft}
backgroundColor="$background"
showAttachmentButton={false}
- placeholder="Reply to post"
+ placeholder="Reply"
setHeight={setMessageInputHeight}
// TODO: add back in when we switch to bottom nav
// goBack={goBack}
diff --git a/packages/ui/src/components/DetailView/GalleryDetailView.tsx b/packages/ui/src/components/DetailView/GalleryDetailView.tsx
index 52567dbc7f..c4b7b86e33 100644
--- a/packages/ui/src/components/DetailView/GalleryDetailView.tsx
+++ b/packages/ui/src/components/DetailView/GalleryDetailView.tsx
@@ -4,6 +4,7 @@ import { Dimensions } from 'react-native';
import { Image, Text, View, YStack } from '../../core';
import ContentReference from '../ContentReference';
import ContentRenderer from '../ContentRenderer';
+import { Icon } from '../Icon';
import { DetailView, DetailViewProps } from './DetailView';
export default function GalleryDetailView({
@@ -32,6 +33,7 @@ export default function GalleryDetailView({
references,
isText,
isImage,
+ isLink,
isReference,
isLinkedImage,
isRefInText,
@@ -92,44 +94,45 @@ export default function GalleryDetailView({
)}
{isText && !isLinkedImage && !isRefInText && (
+ {isLink && }
)}
{(isReference || isRefInText) && (
-
-
+
+
)}
-
+
+
+
);
diff --git a/packages/ui/src/components/DetailView/NotebookDetailView.tsx b/packages/ui/src/components/DetailView/NotebookDetailView.tsx
index ed39b37011..b7d6d6a545 100644
--- a/packages/ui/src/components/DetailView/NotebookDetailView.tsx
+++ b/packages/ui/src/components/DetailView/NotebookDetailView.tsx
@@ -66,20 +66,12 @@ export default function NotebookDetailView({
)}
{post.title && (
-
+
{post.title}
)}
-
-
-
-
+
+
);
diff --git a/packages/ui/src/components/EditProfileScreenView.tsx b/packages/ui/src/components/EditProfileScreenView.tsx
new file mode 100644
index 0000000000..26d36d62c7
--- /dev/null
+++ b/packages/ui/src/components/EditProfileScreenView.tsx
@@ -0,0 +1,110 @@
+import * as api from '@tloncorp/shared/dist/api';
+import * as db from '@tloncorp/shared/dist/db';
+import { useForm } from 'react-hook-form';
+import { Keyboard } from 'react-native';
+
+import { useContact } from '../contexts';
+import { ScrollView, View, YStack } from '../core';
+import { EditablePofileImages } from './EditableProfileImages';
+import { FormTextInput } from './FormInput';
+import { GenericHeader } from './GenericHeader';
+import { SaveButton } from './GroupMetaScreenView';
+import KeyboardAvoidingView from './KeyboardAvoidingView';
+
+interface Props {
+ currentUserId: string;
+ uploadInfo: api.UploadInfo;
+ onGoBack: () => void;
+ onSaveProfile: (update: api.ProfileUpdate) => void;
+}
+
+export function EditProfileScreenView(props: Props) {
+ const userContact = useContact(props.currentUserId);
+ const {
+ control,
+ handleSubmit,
+ formState: { errors },
+ setValue,
+ } = useForm({
+ defaultValues: {
+ nickname: userContact?.nickname ?? '',
+ bio: userContact?.bio ?? '',
+ avatarImage: userContact?.avatarImage ?? '',
+ coverImage: userContact?.coverImage ?? '',
+ },
+ });
+
+ return (
+
+
+ }
+ />
+
+
+
+ setValue('coverImage', url)}
+ onSetIconUrl={(url) => setValue('avatarImage', url)}
+ />
+
+
+ Nickname
+
+
+
+
+ Bio
+
+
+
+
+
+
+ );
+}
diff --git a/packages/ui/src/components/EditableProfileImages.tsx b/packages/ui/src/components/EditableProfileImages.tsx
new file mode 100644
index 0000000000..d2a7063d8b
--- /dev/null
+++ b/packages/ui/src/components/EditableProfileImages.tsx
@@ -0,0 +1,188 @@
+import * as api from '@tloncorp/shared/dist/api';
+import * as db from '@tloncorp/shared/dist/db';
+import { ImageBackground } from 'expo-image';
+import {
+ ComponentProps,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { TouchableOpacity } from 'react-native';
+import { Circle, Stack, ZStack, useTheme } from 'tamagui';
+
+import { View } from '../core';
+import AttachmentSheet from './AttachmentSheet';
+import { AvatarProps, ContactAvatar, GroupAvatar } from './Avatar';
+import { Icon } from './Icon';
+import { LoadingSpinner } from './LoadingSpinner';
+
+interface Props {
+ contact?: db.Contact;
+ group?: db.Group;
+ iconProps?: AvatarProps;
+ uploadInfo: api.UploadInfo;
+ onSetCoverUrl: (url: string) => void;
+ onSetIconUrl: (url: string) => void;
+}
+
+export function EditablePofileImages(props: Props) {
+ const theme = useTheme();
+ const [showAttachmentSheet, setShowAttachmentSheet] = useState(false);
+ const [attachingTo, setAttachingTo] = useState(null);
+ const [iconUrl, setIconUrl] = useState(
+ props.contact?.avatarImage ?? props.group?.iconImage ?? ''
+ );
+ const [coverUrl, setCoverUrl] = useState(
+ props.contact?.coverImage ?? props.group?.coverImage ?? ''
+ );
+
+ useEffect(() => {
+ if (
+ props.uploadInfo.imageAttachment &&
+ props.uploadInfo.uploadedImage &&
+ !props.uploadInfo.uploading &&
+ props.uploadInfo.uploadedImage?.url !== '' &&
+ props.uploadInfo !== null
+ ) {
+ const uploadedFile = props.uploadInfo.uploadedImage as api.UploadedFile;
+ if (attachingTo === 'cover') {
+ setCoverUrl(uploadedFile.url);
+ props.onSetCoverUrl(uploadedFile.url);
+ } else if (attachingTo === 'icon') {
+ setIconUrl(uploadedFile.url);
+ props.onSetIconUrl(uploadedFile.url);
+ }
+
+ setAttachingTo(null);
+ props.uploadInfo.resetImageAttachment();
+ }
+ }, [props.uploadInfo, attachingTo, props]);
+
+ const coverIsUploading = useMemo(() => {
+ return props.uploadInfo.uploading && attachingTo === 'cover';
+ }, [attachingTo, props.uploadInfo.uploading]);
+
+ const iconIsUploading = useMemo(() => {
+ return props.uploadInfo.uploading && attachingTo === 'icon';
+ }, [attachingTo, props.uploadInfo.uploading]);
+
+ const handleCoverPress = useCallback(() => {
+ if (props.uploadInfo.canUpload) {
+ setShowAttachmentSheet(true);
+ setAttachingTo('cover');
+ }
+ }, [props.uploadInfo]);
+
+ const handleIconPress = useCallback(() => {
+ if (props.uploadInfo.canUpload) {
+ setShowAttachmentSheet(true);
+ setAttachingTo('icon');
+ }
+ }, [props.uploadInfo]);
+
+ return (
+
+
+ {/* Cover Image */}
+
+
+ {coverIsUploading && (
+
+
+
+ )}
+
+
+
+
+ {/* Profile Icon */}
+
+
+ {props.contact && (
+
+ )}
+ {props.group && (
+
+ )}
+
+
+
+
+
+ {
+ props.uploadInfo.setAttachments(attachments);
+ }}
+ />
+
+ );
+}
+
+export function EditableImageIndicator(
+ props: ComponentProps & { loading?: boolean }
+) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/ui/src/components/FeatureFlagScreenView.tsx b/packages/ui/src/components/FeatureFlagScreenView.tsx
index be06830d07..c6e88bf6ac 100644
--- a/packages/ui/src/components/FeatureFlagScreenView.tsx
+++ b/packages/ui/src/components/FeatureFlagScreenView.tsx
@@ -18,7 +18,7 @@ export function FeatureFlagScreenView({
return (
-
+
,
'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
>;
+ frameProps?: ComponentProps;
+ areaProps?: ComponentProps;
placeholder?: string;
}) {
return (
@@ -35,13 +41,14 @@ export function FormInput({
rules={rules}
render={({ field: { onChange, onBlur, value } }) => (
-
+
@@ -51,3 +58,40 @@ export function FormInput({
);
}
+
+export const FormInputContext = createStyledContext<{
+ name: string;
+ control: Control;
+}>();
+
+const FormInputFrame = styled(YStack, {
+ context: FormInputContext,
+ padding: '$m',
+});
+
+const FormLabel = styled(SizableText, {
+ context: FormInputContext,
+ color: '$secondaryText',
+ fontSize: '$s',
+});
+
+const FormErrors = (props: { errors: DeepMap }) => {
+ const context = FormInputContext.useStyledContext();
+ if (!props.errors[context.name]) {
+ return null;
+ }
+
+ return (
+
+
+ {props.errors[context.name].message}
+
+
+ );
+};
+
+export const FormTextInput = withStaticProperties(FormInputFrame, {
+ Input: FormInput,
+ Label: FormLabel,
+ Error: FormErrors,
+});
diff --git a/packages/ui/src/components/GalleryPost/GalleryPost.tsx b/packages/ui/src/components/GalleryPost/GalleryPost.tsx
index 0563df3dd7..b391bdf66f 100644
--- a/packages/ui/src/components/GalleryPost/GalleryPost.tsx
+++ b/packages/ui/src/components/GalleryPost/GalleryPost.tsx
@@ -7,6 +7,7 @@ import { ImageWithFallback, View } from '../../core';
import AuthorRow from '../AuthorRow';
import ContentReference from '../ContentReference';
import ContentRenderer from '../ContentRenderer';
+import { Icon } from '../Icon';
import { useBoundHandler } from '../ListItem/listItemUtils';
const GalleryPostFrame = styled(View, {
@@ -24,6 +25,10 @@ const GalleryPostFrame = styled(View, {
borderWidth: 0,
},
text: {},
+ link: {
+ borderWidth: 0,
+ backgroundColor: '$secondaryBackground',
+ },
reference: {
borderWidth: 0,
backgroundColor: '$secondaryBackground',
@@ -51,6 +56,7 @@ export default function GalleryPost({
references,
isText,
isImage,
+ isLink,
isReference,
isLinkedImage,
isRefInText,
@@ -61,14 +67,21 @@ export default function GalleryPost({
const handlePress = useBoundHandler(post, onPress);
const handleLongPress = useBoundHandler(post, onLongPress);
- const previewType =
- isImage || isLinkedImage
- ? 'image'
- : isText && !isLinkedImage && !isRefInText
- ? 'text'
- : isReference || isRefInText
- ? 'reference'
- : 'unsupported';
+ const previewType = (() => {
+ if (isImage || isLinkedImage) {
+ return 'image';
+ }
+ if (isText && !isLinkedImage && !isRefInText && !isLink) {
+ return 'text';
+ }
+ if (isReference || isRefInText) {
+ return 'reference';
+ }
+ if (isLink) {
+ return 'link';
+ }
+ return 'unsupported';
+ })();
return (
-
- {/** Image post */}
- {(isImage || isLinkedImage) && (
-
- Failed to load {isImage ? image!.src : linkedImage}
-
- }
- source={{ uri: isImage ? image!.src : linkedImage }}
- />
- )}
+ {post.hidden ? (
+
+
+ You have hidden or flagged this post.
+
+
+ ) : (
+
+ {/** Image post */}
+ {(isImage || isLinkedImage) && (
+
+ Failed to load {isImage ? image!.src : linkedImage}
+
+ }
+ source={{ uri: isImage ? image!.src : linkedImage }}
+ />
+ )}
- {/** Text post */}
- {isText && !isLinkedImage && !isRefInText && (
-
-
-
- )}
+ {/** Text post */}
+ {isText && !isLinkedImage && !isRefInText && (
+
+ {isLink && }
+
+
+ )}
- {/** Reference post */}
- {(isReference || isRefInText) && (
-
-
-
- )}
+ {/** Reference post */}
+ {(isReference || isRefInText) && (
+
+
+
+ )}
- {/** Unsupported post */}
- {!isImage && !isText && !isReference && !isRefInText ? (
- Unable to parse content
- ) : null}
+ {/** Unsupported post */}
+ {!isImage && !isText && !isReference && !isRefInText ? (
+ Unable to parse content
+ ) : null}
- {viewMode !== 'activity' && (
-
-
-
- )}
-
+ {viewMode !== 'activity' && (
+
+
+
+ {/** Text post */}
+ {isText && !isLinkedImage && !isRefInText && (
+
+
+
+ )}
+
+ {/** Reference post */}
+ {(isReference || isRefInText) && (
+
+
+
+ )}
+
+ {/** Unsupported post */}
+ {!isImage && !isText && !isReference && !isRefInText ? (
+ Unable to parse content
+ ) : null}
+
+ {viewMode !== 'activity' && (
+
+
+
+ )}
+
+ )}
+
+ )}
);
}
diff --git a/packages/ui/src/components/GenericHeader.tsx b/packages/ui/src/components/GenericHeader.tsx
index f2dfde1685..59dd2951cd 100644
--- a/packages/ui/src/components/GenericHeader.tsx
+++ b/packages/ui/src/components/GenericHeader.tsx
@@ -49,7 +49,7 @@ export function GenericHeader({
numberOfLines={1}
color="$primaryText"
size="$m"
- fontWeight="500"
+ fontWeight="$xl"
>
{showSpinner ? 'Loading…' : title}
diff --git a/packages/ui/src/components/GroupMetaScreenView.tsx b/packages/ui/src/components/GroupMetaScreenView.tsx
index 98ed2d34cc..c49b36bf9f 100644
--- a/packages/ui/src/components/GroupMetaScreenView.tsx
+++ b/packages/ui/src/components/GroupMetaScreenView.tsx
@@ -1,24 +1,17 @@
-import {
- MessageAttachments,
- UploadInfo,
- UploadedFile,
-} from '@tloncorp/shared/dist/api';
+import { MessageAttachments, UploadInfo } from '@tloncorp/shared/dist/api';
import * as db from '@tloncorp/shared/dist/db';
-import { ImageBackground } from 'expo-image';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
-import { getTokenValue } from 'tamagui';
-import { Text, View, YStack } from '../core';
+import { View, YStack } from '../core';
import AttachmentSheet from './AttachmentSheet';
-import { GroupAvatar } from './Avatar';
import { Button } from './Button';
import { DeleteSheet } from './DeleteSheet';
+import { EditablePofileImages } from './EditableProfileImages';
import { FormInput } from './FormInput';
import { GenericHeader } from './GenericHeader';
import KeyboardAvoidingView from './KeyboardAvoidingView';
import { LoadingSpinner } from './LoadingSpinner';
-import Pressable from './Pressable';
interface GroupMetaScreenViewProps {
group: db.Group | null;
@@ -33,81 +26,12 @@ interface GroupMetaScreenViewProps {
export function SaveButton({ onPress }: { onPress: () => void }) {
return (
-
diff --git a/packages/ui/src/components/ProfileScreenView.tsx b/packages/ui/src/components/ProfileScreenView.tsx
index f84f1603f2..e28643f82f 100644
--- a/packages/ui/src/components/ProfileScreenView.tsx
+++ b/packages/ui/src/components/ProfileScreenView.tsx
@@ -1,24 +1,20 @@
import * as db from '@tloncorp/shared/dist/db';
-import * as store from '@tloncorp/shared/dist/store';
-import * as ub from '@tloncorp/shared/dist/urbit';
-import { useCallback, useState } from 'react';
-import { Dimensions } from 'react-native';
+import { Alert, Dimensions, TouchableOpacity } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { ScrollView, SizableText, XStack, getTokens } from 'tamagui';
+import { ScrollView, SizableText, getTokens } from 'tamagui';
import { ContactsProvider, useContact } from '../contexts';
-import { View, YStack } from '../core';
-import { Icon, IconType } from './Icon';
+import { Stack, View, YStack } from '../core';
+import { IconType } from './Icon';
import { ListItem } from './ListItem';
-import { LoadingSpinner } from './LoadingSpinner';
import ProfileCover from './ProfileCover';
import ProfileRow from './ProfileRow';
interface Props {
currentUserId: string;
- debugMessage: string;
onAppSettingsPressed?: () => void;
- handleLogout: () => void;
+ onEditProfilePressed?: () => void;
+ onLogoutPressed: () => void;
}
export function ProfileScreenView({
@@ -32,163 +28,70 @@ export function ProfileScreenView({
);
}
-type NotificationState = { open: boolean; setting: 1 | 2 | 3 };
-
export function Wrapped(props: Props) {
- const [loading, setLoading] = useState(null);
- const { data: pushNotificationsSetting } =
- store.usePushNotificationsSetting();
const { top } = useSafeAreaInsets();
const contact = useContact(props.currentUserId);
- const [notifState, setNotifState] = useState({
- open: false,
- setting: 1,
- });
-
- const setLevel = useCallback(
- async (level: ub.PushNotificationsSetting) => {
- if (level === pushNotificationsSetting) return;
- setLoading(level);
- await store.setDefaultNotificationLevel(level);
- setLoading(null);
- },
- [pushNotificationsSetting]
- );
-
- const LevelIndicator = useCallback(
- (props: { level: ub.PushNotificationsSetting }) => {
- if (loading === props.level) {
- return ;
- }
-
- if (pushNotificationsSetting === props.level) {
- return (
-
-
-
- );
- }
-
- return (
-
- );
- },
- [pushNotificationsSetting, loading]
- );
// TODO: Add logout back in when we figure out TLON-2098.
- // const onLogoutPress = () => {
- // Alert.alert('Log out', 'Are you sure you want to log out?', [
- // {
- // text: 'Cancel',
- // style: 'cancel',
- // },
- // {
- // text: 'Log out',
- // onPress: props.handleLogout,
- // },
- // ]);
- // };
+ const onLogoutPress = () => {
+ Alert.alert('Log out', 'Are you sure you want to log out?', [
+ {
+ text: 'Cancel',
+ style: 'cancel',
+ },
+ {
+ text: 'Log out',
+ onPress: props.onLogoutPressed,
+ },
+ ]);
+ };
return (
- {!notifState.open && (
- <>
-
- {contact ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
- setNotifState((prev) => ({ ...prev, open: true }))
- }
- />
- {/* */}
+
+ {contact ? (
+
+ ) : (
+
+
- >
- )}
- {notifState.open && (
-
-
- setNotifState({ open: false, setting: 1 })}
- />
-
- Push Notification Settings
-
-
-
-
- Configure what kinds of messages will send you notifications.
-
-
-
- setLevel('all')}>
-
- All group activity
-
-
- setLevel('some')}>
-
-
- Mentions and replies only
-
- Direct messages will still notify unless you mute them.
-
-
-
-
- setLevel('none')}>
-
- Nothing
-
-
+ )}
+
+
+
+
+ Edit
+
+
+
- )}
+
+
+
+
+
);
@@ -197,11 +100,9 @@ export function Wrapped(props: Props) {
export function ProfileDisplayWidget({
contact,
contactId,
- debugMessage,
}: {
contact: db.Contact;
contactId: string;
- debugMessage: string;
}) {
const coverSize =
Dimensions.get('window').width - getTokens().space.$xl.val * 2;
@@ -209,24 +110,13 @@ export function ProfileDisplayWidget({
return (
-
+
);
}
- return (
-
- );
+ return ;
}
function ProfileAction({
diff --git a/packages/ui/src/components/ProfileSheet.tsx b/packages/ui/src/components/ProfileSheet.tsx
index 0feaec7fa1..33b1a114ae 100644
--- a/packages/ui/src/components/ProfileSheet.tsx
+++ b/packages/ui/src/components/ProfileSheet.tsx
@@ -1,5 +1,6 @@
import Clipboard from '@react-native-clipboard/clipboard';
import * as db from '@tloncorp/shared/dist/db';
+import * as store from '@tloncorp/shared/dist/store';
import { useCallback } from 'react';
import { Dimensions } from 'react-native';
import { getTokens } from 'tamagui';
@@ -51,9 +52,13 @@ export function ProfileSheet({
const { onPressGoToDm } = useNavigation();
const handleBlock = useCallback(() => {
- console.log('block not yet implemented', contactId);
+ if (contact && contact.isBlocked) {
+ store.unblockUser(contactId);
+ } else {
+ store.blockUser(contactId);
+ }
onOpenChange(false);
- }, [contactId, onOpenChange]);
+ }, [contact, contactId, onOpenChange]);
const handleGoToDm = useCallback(async () => {
onPressGoToDm([contactId]);
@@ -89,8 +94,7 @@ export function ProfileSheet({
diff --git a/packages/ui/src/components/Tabs.tsx b/packages/ui/src/components/Tabs.tsx
new file mode 100644
index 0000000000..89abca8e12
--- /dev/null
+++ b/packages/ui/src/components/Tabs.tsx
@@ -0,0 +1,53 @@
+import { ReactNode } from 'react';
+import { styled, withStaticProperties } from 'tamagui';
+
+import { SizableText, XStack } from '../core';
+import Pressable from './Pressable';
+
+const TabsWrapper = styled(XStack, {
+ width: '100%',
+});
+
+const TabTitleComponent = styled(SizableText, {
+ width: 100,
+ textAlign: 'center',
+ paddingVertical: '$m',
+ variants: {
+ active: {
+ true: {
+ color: '$primaryText',
+ },
+ false: {
+ color: '$secondaryText',
+ },
+ },
+ } as const,
+});
+
+const TabComponent = ({
+ onTabPress,
+ children,
+ name,
+ activeTab,
+}: {
+ onTabPress: () => void;
+ children: ReactNode;
+ name: string;
+ activeTab: string;
+}) => {
+ return (
+
+ onTabPress()}>{children}
+
+ );
+};
+
+export const Tabs = withStaticProperties(TabsWrapper, {
+ Tab: TabComponent,
+ Title: TabTitleComponent,
+});
diff --git a/packages/ui/src/components/WelcomeSheet.tsx b/packages/ui/src/components/WelcomeSheet.tsx
index debc1eb807..d2ff13a320 100644
--- a/packages/ui/src/components/WelcomeSheet.tsx
+++ b/packages/ui/src/components/WelcomeSheet.tsx
@@ -35,7 +35,7 @@ export function WelcomeSheet({
paddingHorizontal="$2xl"
>
-
+
Welcome to Tlon
- A messenger you can actually trust.
+ A messenger you can finally trust.
@@ -61,10 +61,9 @@ export function WelcomeSheet({
- Message privately
+ Control every bit
- When you chat, it’s just your computer talking to my
- computer. No middlemen in between.
+ Whatever you do, say, and make on Tlon is yours to keep
@@ -75,14 +74,14 @@ export function WelcomeSheet({
borderRadius={'$3xl'}
padding="$m"
>
-
+
- Start a community
+ From now until forever
- Get a group together for a shared purpose; stay because
- it’s free from spying.
+ With Tlon you can always take your data with you and continue
+ using it elsewhere
@@ -93,14 +92,14 @@ export function WelcomeSheet({
borderRadius={'$3xl'}
padding="$m"
>
-
+
- Grow with confidence
+ Connect with calm
- Tlon is built for longevity: your community won’t
- disappear and can’t be disappeared.
+ Tlon is designed to maximize genuine connection, not addictive
+ engagement
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 3321dec31d..3f4c281666 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -36,18 +36,22 @@ export * from './components/SectionList';
export * from './components/ContactRow';
export * from './components/Button';
export * from './components/ScreenHeader';
+export * from './components/GenericHeader';
export * from './components/IconButton';
export * from './components/Channel/ChannelHeader';
export * from './components/Channel/BaubleHeader';
export * from './components/ChatMessage';
export * from './components/GalleryPost';
export * from './components/ContactList';
+export * from './components/BlockedContactsWidget';
export * from './components/Channel/ChannelDivider';
export * from './components/Embed';
export * from './components/AddChats';
export * from './components/ContactBook';
export * from './components/Buttons';
export * from './components/Avatar';
+export * from './components/EditProfileScreenView';
+export * from './components/AppSetting';
export * from './components/GroupChannelsScreenView';
export * from './components/FeatureFlagScreenView';
export * from './tamagui.config';
diff --git a/packages/ui/src/tamagui.config.ts b/packages/ui/src/tamagui.config.ts
index 1f6e0248fc..1ff064f155 100644
--- a/packages/ui/src/tamagui.config.ts
+++ b/packages/ui/src/tamagui.config.ts
@@ -68,6 +68,7 @@ export const tokens = createTokens({
'4xl': 48,
'5xl': 64,
'6xl': 72,
+ '9xl': 96, // TODO: worth leaving room between?
},
radius: {
'2xs': 2,
@@ -258,6 +259,7 @@ export const systemFont = createFont({
m: 'regular',
true: 'regular',
l: 'medium',
+ xl: '500',
},
letterSpacing: {
s: 0,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a3b662ecdc..6aafed48d1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -73,10 +73,10 @@ importers:
version: 15.2.0
tsup:
specifier: ^8.0.1
- version: 8.0.1(@swc/core@1.4.16(@swc/helpers@0.5.10))(postcss@8.4.35)(typescript@5.4.5)
+ version: 8.0.1(@swc/core@1.4.16)(postcss@8.4.35)(typescript@5.4.5)
vitest:
specifier: ^1.2.2
- version: 1.2.2(@types/node@20.10.8)(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1)
+ version: 1.2.2(@types/node@20.14.10)(jsdom@23.2.0)(terser@5.19.1)
apps/tlon-mobile:
dependencies:
@@ -229,7 +229,7 @@ importers:
version: 4.17.21
posthog-react-native:
specifier: ^2.7.1
- version: 2.11.3(@react-native-async-storage/async-storage@1.21.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)))(@react-navigation/native@6.1.10(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(expo-application@5.8.3(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(expo-device@5.9.3(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(expo-file-system@16.0.9(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(expo-localization@14.8.3(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(react-native-device-info@10.12.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)))
+ version: 2.11.3(oi5hx7o6oxzkjy7oghrlxekpbi)
react:
specifier: ^18.2.0
version: 18.2.0
@@ -305,7 +305,7 @@ importers:
devDependencies:
'@react-native/metro-config':
specifier: ^0.73.5
- version: 0.73.5(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)
+ version: 0.73.5(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))
'@tamagui/babel-plugin':
specifier: 1.101.3
version: 1.101.3(encoding@0.1.13)(react@18.2.0)
@@ -368,7 +368,7 @@ importers:
version: 3.4.1
vitest:
specifier: ^1.0.4
- version: 1.2.2(@types/node@20.10.8)(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1)
+ version: 1.2.2(@types/node@20.14.10)(jsdom@23.2.0)(terser@5.19.1)
apps/tlon-web:
dependencies:
@@ -422,13 +422,13 @@ importers:
version: 1.101.3(encoding@0.1.13)(react@18.2.0)
'@tanstack/react-query':
specifier: ^4.28.0
- version: 4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
+ version: 4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
'@tanstack/react-query-devtools':
specifier: ^4.28.0
- version: 4.29.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ version: 4.29.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@tanstack/react-query-persist-client':
specifier: ^4.28.0
- version: 4.28.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))
+ version: 4.28.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0))
'@tanstack/react-virtual':
specifier: ^3.0.0-beta.60
version: 3.0.0-beta.65(react@18.2.0)
@@ -644,7 +644,7 @@ importers:
version: 18.2.0
react-beautiful-dnd:
specifier: ^13.1.1
- version: 13.1.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
+ version: 13.1.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
react-colorful:
specifier: ^5.5.1
version: 5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -680,7 +680,7 @@ importers:
version: https://codeload.github.com/stefkampen/react-oembed-container/tar.gz/802eee0dba7986faa9c931b1c016acba5369d5f9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-qr-code:
specifier: ^2.0.12
- version: 2.0.12(react-native-svg@15.0.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react@18.2.0)
+ version: 2.0.12(react-native-svg@15.0.0(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react@18.2.0)
react-router:
specifier: ^6.22.1
version: 6.22.2(react@18.2.0)
@@ -737,7 +737,7 @@ importers:
version: 0.16.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
vite-plugin-svgr:
specifier: ^4.2.0
- version: 4.2.0(rollup@4.13.0)(typescript@5.4.5)(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))
+ version: 4.2.0(rollup@2.79.1)(typescript@5.4.5)(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))
workbox-precaching:
specifier: ^6.5.4
version: 6.6.0
@@ -828,10 +828,10 @@ importers:
version: 2.0.1
'@vitejs/plugin-basic-ssl':
specifier: ^1.1.0
- version: 1.1.0(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))
+ version: 1.1.0(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))
'@vitejs/plugin-react':
specifier: ^4.2.1
- version: 4.2.1(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))
+ version: 4.2.1(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))
'@welldone-software/why-did-you-render':
specifier: ^7.0.1
version: 7.0.1(react@18.2.0)
@@ -873,7 +873,7 @@ importers:
version: 6.1.1
react-cosmos-plugin-vite:
specifier: 6.1.1
- version: 6.1.1(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))
+ version: 6.1.1(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))
react-test-renderer:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
@@ -882,7 +882,7 @@ importers:
version: 4.0.0
rollup-plugin-visualizer:
specifier: ^5.6.0
- version: 5.12.0(rollup@4.13.0)
+ version: 5.12.0(rollup@2.79.1)
tailwindcss:
specifier: ^3.2.7
version: 3.4.1
@@ -897,13 +897,13 @@ importers:
version: 1.1.4(typescript@5.4.5)
vite:
specifier: ^5.1.6
- version: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ version: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
vite-plugin-pwa:
specifier: ^0.17.5
- version: 0.17.5(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))(workbox-build@7.0.0(@types/babel__core@7.20.5))(workbox-window@7.0.0)
+ version: 0.17.5(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))(workbox-build@7.0.0(@types/babel__core@7.20.5))(workbox-window@7.0.0)
vitest:
specifier: ^0.34.1
- version: 0.34.6(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1)
+ version: 0.34.6(jsdom@23.2.0)(terser@5.19.1)
workbox-window:
specifier: ^7.0.0
version: 7.0.0
@@ -937,7 +937,7 @@ importers:
version: 6.21.0(eslint@8.56.0)(typescript@5.4.5)
'@vitejs/plugin-react':
specifier: ^4.2.1
- version: 4.2.1(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))
+ version: 4.2.1(vite@5.1.6(@types/node@20.14.10)(terser@5.19.1))
eslint:
specifier: ^8.50.0
version: 8.56.0
@@ -949,13 +949,13 @@ importers:
version: 5.4.5
vite:
specifier: ^5.1.6
- version: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ version: 5.1.6(@types/node@20.14.10)(terser@5.19.1)
vite-plugin-singlefile:
specifier: ^2.0.1
- version: 2.0.1(rollup@4.13.0)(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))
+ version: 2.0.1(rollup@4.13.0)(vite@5.1.6(@types/node@20.14.10)(terser@5.19.1))
vitest:
specifier: ^1.0.4
- version: 1.5.0(@types/node@20.10.8)(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1)
+ version: 1.5.0(@types/node@20.14.10)(jsdom@23.2.0)(terser@5.19.1)
packages/shared:
dependencies:
@@ -1017,13 +1017,13 @@ importers:
version: 0.20.17
tsup:
specifier: ^8.0.1
- version: 8.0.1(@swc/core@1.4.16(@swc/helpers@0.5.10))(postcss@8.4.35)(typescript@5.4.5)
+ version: 8.0.1(@swc/core@1.4.16)(postcss@8.4.35)(typescript@5.4.5)
typescript:
specifier: 5.4.5
version: 5.4.5
vitest:
specifier: ^1.4.0
- version: 1.5.0(@types/node@20.10.8)(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1)
+ version: 1.5.0(@types/node@20.14.10)(jsdom@23.2.0)(terser@5.19.1)
packages/ui:
dependencies:
@@ -1078,6 +1078,9 @@ importers:
expo-image-picker:
specifier: ~14.7.1
version: 14.7.1(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13))
+ fuzzy:
+ specifier: ^0.1.3
+ version: 0.1.3
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -5226,6 +5229,9 @@ packages:
'@types/node@20.10.8':
resolution: {integrity: sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==}
+ '@types/node@20.14.10':
+ resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
+
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -16167,7 +16173,7 @@ snapshots:
- '@babel/preset-env'
- supports-color
- '@react-native/metro-config@0.73.5(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)':
+ '@react-native/metro-config@0.73.5(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))':
dependencies:
'@react-native/js-polyfills': 0.73.1
'@react-native/metro-babel-transformer': 0.73.15(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))
@@ -16176,10 +16182,7 @@ snapshots:
transitivePeerDependencies:
- '@babel/core'
- '@babel/preset-env'
- - bufferutil
- - encoding
- supports-color
- - utf-8-validate
'@react-native/normalize-color@2.1.0': {}
@@ -16193,6 +16196,13 @@ snapshots:
nullthrows: 1.1.1
react-native: 0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)
+ '@react-native/virtualized-lists@0.73.4(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))':
+ dependencies:
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ react-native: 0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0)
+ optional: true
+
'@react-navigation/bottom-tabs@6.5.12(@react-navigation/native@6.1.10(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.8.2(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-native-screens@3.29.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)':
dependencies:
'@react-navigation/elements': 1.3.22(@react-navigation/native@6.1.10(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.8.2(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
@@ -16315,13 +16325,13 @@ snapshots:
picomatch: 2.3.1
rollup: 2.79.1
- '@rollup/pluginutils@5.1.0(rollup@4.13.0)':
+ '@rollup/pluginutils@5.1.0(rollup@2.79.1)':
dependencies:
'@types/estree': 1.0.5
estree-walker: 2.0.2
picomatch: 2.3.1
optionalDependencies:
- rollup: 4.13.0
+ rollup: 2.79.1
'@rollup/rollup-android-arm-eabi@4.13.0':
optional: true
@@ -17406,28 +17416,28 @@ snapshots:
dependencies:
'@tanstack/query-core': 4.27.0
- '@tanstack/react-query-devtools@4.29.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
+ '@tanstack/react-query-devtools@4.29.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@tanstack/match-sorter-utils': 8.8.4
- '@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
+ '@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
superjson: 1.12.2
use-sync-external-store: 1.2.0(react@18.2.0)
- '@tanstack/react-query-persist-client@4.28.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))':
+ '@tanstack/react-query-persist-client@4.28.0(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0))':
dependencies:
'@tanstack/query-persist-client-core': 4.27.0
- '@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
+ '@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
- '@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)':
+ '@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0)':
dependencies:
'@tanstack/query-core': 4.36.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
optionalDependencies:
react-dom: 18.2.0(react@18.2.0)
- react-native: 0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)
+ react-native: 0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0)
'@tanstack/react-query@5.32.1(react@18.2.0)':
dependencies:
@@ -18070,6 +18080,10 @@ snapshots:
dependencies:
undici-types: 5.26.5
+ '@types/node@20.14.10':
+ dependencies:
+ undici-types: 5.26.5
+
'@types/normalize-package-data@2.4.4': {}
'@types/object.omit@3.0.0': {}
@@ -18127,7 +18141,7 @@ snapshots:
'@types/resolve@1.17.1':
dependencies:
- '@types/node': 20.10.8
+ '@types/node': 20.14.10
'@types/scheduler@0.16.2': {}
@@ -18402,18 +18416,29 @@ snapshots:
graphql: 15.8.0
wonka: 4.0.15
- '@vitejs/plugin-basic-ssl@1.1.0(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))':
+ '@vitejs/plugin-basic-ssl@1.1.0(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))':
dependencies:
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
+
+ '@vitejs/plugin-react@4.2.1(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))':
+ dependencies:
+ '@babel/core': 7.23.7
+ '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.7)
+ '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.7)
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.14.0
+ vite: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
+ transitivePeerDependencies:
+ - supports-color
- '@vitejs/plugin-react@4.2.1(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))':
+ '@vitejs/plugin-react@4.2.1(vite@5.1.6(@types/node@20.14.10)(terser@5.19.1))':
dependencies:
'@babel/core': 7.23.7
'@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.7)
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.7)
'@types/babel__core': 7.20.5
react-refresh: 0.14.0
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.14.10)(terser@5.19.1)
transitivePeerDependencies:
- supports-color
@@ -21876,7 +21901,7 @@ snapshots:
jest-worker@26.6.2:
dependencies:
- '@types/node': 20.10.8
+ '@types/node': 20.14.10
merge-stream: 2.0.0
supports-color: 7.2.0
@@ -23233,8 +23258,8 @@ snapshots:
dependencies:
fflate: 0.4.8
- ? posthog-react-native@2.11.3(@react-native-async-storage/async-storage@1.21.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)))(@react-navigation/native@6.1.10(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(expo-application@5.8.3(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(expo-device@5.9.3(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(expo-file-system@16.0.9(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(expo-localization@14.8.3(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13)))(react-native-device-info@10.12.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)))
- : optionalDependencies:
+ posthog-react-native@2.11.3(oi5hx7o6oxzkjy7oghrlxekpbi):
+ optionalDependencies:
'@react-native-async-storage/async-storage': 1.21.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))
'@react-navigation/native': 6.1.10(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
expo-application: 5.8.3(expo@50.0.6(@babel/core@7.23.7)(@react-native/babel-preset@0.73.21(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7)))(encoding@0.1.13))
@@ -23594,7 +23619,7 @@ snapshots:
minimist: 1.2.6
strip-json-comments: 2.0.1
- react-beautiful-dnd@13.1.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0):
+ react-beautiful-dnd@13.1.1(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.23.8
css-box-model: 1.2.1
@@ -23602,7 +23627,7 @@ snapshots:
raf-schd: 4.0.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- react-redux: 7.2.8(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
+ react-redux: 7.2.8(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
redux: 4.2.0
use-memo-one: 1.1.3(react@18.2.0)
transitivePeerDependencies:
@@ -23630,11 +23655,11 @@ snapshots:
react-cosmos-core: 6.1.1
react-cosmos-renderer: 6.1.1
- react-cosmos-plugin-vite@6.1.1(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)):
+ react-cosmos-plugin-vite@6.1.1(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1)):
dependencies:
react-cosmos-core: 6.1.1
react-cosmos-dom: 6.1.1
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
react-cosmos-renderer@6.1.1:
dependencies:
@@ -23909,6 +23934,14 @@ snapshots:
react: 18.2.0
react-native: 0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)
+ react-native-svg@15.0.0(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0):
+ dependencies:
+ css-select: 5.1.0
+ css-tree: 1.1.3
+ react: 18.2.0
+ react-native: 0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0)
+ optional: true
+
react-native-url-polyfill@2.0.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)):
dependencies:
react-native: 0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)
@@ -24058,21 +24091,71 @@ snapshots:
- supports-color
- utf-8-validate
+ react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0):
+ dependencies:
+ '@jest/create-cache-key-function': 29.7.0
+ '@react-native-community/cli': 12.3.2(encoding@0.1.13)
+ '@react-native-community/cli-platform-android': 12.3.2(encoding@0.1.13)
+ '@react-native-community/cli-platform-ios': 12.3.2(encoding@0.1.13)
+ '@react-native/assets-registry': 0.73.1
+ '@react-native/codegen': 0.73.3(@babel/preset-env@7.23.7(@babel/core@7.23.7))
+ '@react-native/community-cli-plugin': 0.73.16(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)
+ '@react-native/gradle-plugin': 0.73.4
+ '@react-native/js-polyfills': 0.73.1
+ '@react-native/normalize-colors': 0.73.2
+ '@react-native/virtualized-lists': 0.73.4(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))
+ abort-controller: 3.0.0
+ anser: 1.4.10
+ ansi-regex: 5.0.1
+ base64-js: 1.5.1
+ chalk: 4.1.2
+ deprecated-react-native-prop-types: 5.0.0
+ event-target-shim: 5.0.1
+ flow-enums-runtime: 0.0.6
+ invariant: 2.2.4
+ jest-environment-node: 29.7.0
+ jsc-android: 250231.0.0
+ memoize-one: 5.2.1
+ metro-runtime: 0.80.5
+ metro-source-map: 0.80.5
+ mkdirp: 0.5.6
+ nullthrows: 1.1.1
+ pretty-format: 26.6.2
+ promise: 8.3.0
+ react: 18.2.0
+ react-devtools-core: 4.28.5
+ react-refresh: 0.14.0
+ react-shallow-renderer: 16.15.0(react@18.2.0)
+ regenerator-runtime: 0.13.11
+ scheduler: 0.24.0-canary-efb381bbf-20230505
+ stacktrace-parser: 0.1.10
+ whatwg-fetch: 3.6.20
+ ws: 6.2.2
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - '@babel/core'
+ - '@babel/preset-env'
+ - bufferutil
+ - encoding
+ - supports-color
+ - utf-8-validate
+ optional: true
+
react-oembed-container@https://codeload.github.com/stefkampen/react-oembed-container/tar.gz/802eee0dba7986faa9c931b1c016acba5369d5f9(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- react-qr-code@2.0.12(react-native-svg@15.0.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react@18.2.0):
+ react-qr-code@2.0.12(react-native-svg@15.0.0(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0))(react@18.2.0):
dependencies:
prop-types: 15.8.1
qr.js: 0.0.0
react: 18.2.0
optionalDependencies:
- react-native-svg: 15.0.0(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
+ react-native-svg: 15.0.0(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0)
- react-redux@7.2.8(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0))(react@18.2.0):
+ react-redux@7.2.8(react-dom@18.2.0(react@18.2.0))(react-native@0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.23.8
'@types/react-redux': 7.1.24
@@ -24083,7 +24166,7 @@ snapshots:
react-is: 17.0.2
optionalDependencies:
react-dom: 18.2.0(react@18.2.0)
- react-native: 0.73.4(@babel/core@7.23.7)(@babel/preset-env@7.23.7(@babel/core@7.23.7))(encoding@0.1.13)(react@18.2.0)
+ react-native: 0.73.4(@babel/core@7.23.7)(encoding@0.1.13)(react@18.2.0)
react-refresh@0.14.0: {}
@@ -24416,14 +24499,14 @@ snapshots:
serialize-javascript: 4.0.0
terser: 5.19.1
- rollup-plugin-visualizer@5.12.0(rollup@4.13.0):
+ rollup-plugin-visualizer@5.12.0(rollup@2.79.1):
dependencies:
open: 8.4.2
picomatch: 2.3.1
source-map: 0.7.4
yargs: 17.7.2
optionalDependencies:
- rollup: 4.13.0
+ rollup: 2.79.1
rollup@2.79.1:
optionalDependencies:
@@ -25347,7 +25430,7 @@ snapshots:
tslib@2.6.2: {}
- tsup@8.0.1(@swc/core@1.4.16(@swc/helpers@0.5.10))(postcss@8.4.35)(typescript@5.4.5):
+ tsup@8.0.1(@swc/core@1.4.16)(postcss@8.4.35)(typescript@5.4.5):
dependencies:
bundle-require: 4.0.2(esbuild@0.19.12)
cac: 6.7.14
@@ -25660,14 +25743,14 @@ snapshots:
react-dom: 18.2.0(react@18.2.0)
redux: 4.2.0
- vite-node@0.34.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1):
+ vite-node@0.34.6(@types/node@20.10.8)(terser@5.19.1):
dependencies:
cac: 6.7.14
debug: 4.3.4
mlly: 1.5.0
pathe: 1.1.2
picocolors: 1.0.0
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
transitivePeerDependencies:
- '@types/node'
- less
@@ -25678,13 +25761,13 @@ snapshots:
- supports-color
- terser
- vite-node@1.2.2(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1):
+ vite-node@1.2.2(@types/node@20.14.10)(terser@5.19.1):
dependencies:
cac: 6.7.14
debug: 4.3.4
pathe: 1.1.2
picocolors: 1.0.0
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.14.10)(terser@5.19.1)
transitivePeerDependencies:
- '@types/node'
- less
@@ -25695,13 +25778,13 @@ snapshots:
- supports-color
- terser
- vite-node@1.5.0(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1):
+ vite-node@1.5.0(@types/node@20.14.10)(terser@5.19.1):
dependencies:
cac: 6.7.14
debug: 4.3.4
pathe: 1.1.2
picocolors: 1.0.0
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.14.10)(terser@5.19.1)
transitivePeerDependencies:
- '@types/node'
- less
@@ -25712,35 +25795,35 @@ snapshots:
- supports-color
- terser
- vite-plugin-pwa@0.17.5(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1))(workbox-build@7.0.0(@types/babel__core@7.20.5))(workbox-window@7.0.0):
+ vite-plugin-pwa@0.17.5(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1))(workbox-build@7.0.0(@types/babel__core@7.20.5))(workbox-window@7.0.0):
dependencies:
debug: 4.3.4
fast-glob: 3.3.2
pretty-bytes: 6.1.1
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
workbox-build: 7.0.0(@types/babel__core@7.20.5)
workbox-window: 7.0.0
transitivePeerDependencies:
- supports-color
- vite-plugin-singlefile@2.0.1(rollup@4.13.0)(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)):
+ vite-plugin-singlefile@2.0.1(rollup@4.13.0)(vite@5.1.6(@types/node@20.14.10)(terser@5.19.1)):
dependencies:
micromatch: 4.0.5
rollup: 4.13.0
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.14.10)(terser@5.19.1)
- vite-plugin-svgr@4.2.0(rollup@4.13.0)(typescript@5.4.5)(vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)):
+ vite-plugin-svgr@4.2.0(rollup@2.79.1)(typescript@5.4.5)(vite@5.1.6(@types/node@20.10.8)(terser@5.19.1)):
dependencies:
- '@rollup/pluginutils': 5.1.0(rollup@4.13.0)
+ '@rollup/pluginutils': 5.1.0(rollup@2.79.1)
'@svgr/core': 8.1.0(typescript@5.4.5)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.4.5))
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
transitivePeerDependencies:
- rollup
- supports-color
- typescript
- vite@5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1):
+ vite@5.1.6(@types/node@20.10.8)(terser@5.19.1):
dependencies:
esbuild: 0.19.12
postcss: 8.4.35
@@ -25748,10 +25831,19 @@ snapshots:
optionalDependencies:
'@types/node': 20.10.8
fsevents: 2.3.3
- lightningcss: 1.19.0
terser: 5.19.1
- vitest@0.34.6(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1):
+ vite@5.1.6(@types/node@20.14.10)(terser@5.19.1):
+ dependencies:
+ esbuild: 0.19.12
+ postcss: 8.4.35
+ rollup: 4.13.0
+ optionalDependencies:
+ '@types/node': 20.14.10
+ fsevents: 2.3.3
+ terser: 5.19.1
+
+ vitest@0.34.6(jsdom@23.2.0)(terser@5.19.1):
dependencies:
'@types/chai': 4.3.11
'@types/chai-subset': 1.3.5
@@ -25774,8 +25866,8 @@ snapshots:
strip-literal: 1.3.0
tinybench: 2.6.0
tinypool: 0.7.0
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
- vite-node: 0.34.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.10.8)(terser@5.19.1)
+ vite-node: 0.34.6(@types/node@20.10.8)(terser@5.19.1)
why-is-node-running: 2.2.2
optionalDependencies:
jsdom: 23.2.0
@@ -25788,7 +25880,7 @@ snapshots:
- supports-color
- terser
- vitest@1.2.2(@types/node@20.10.8)(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1):
+ vitest@1.2.2(@types/node@20.14.10)(jsdom@23.2.0)(terser@5.19.1):
dependencies:
'@vitest/expect': 1.2.2
'@vitest/runner': 1.2.2
@@ -25808,11 +25900,11 @@ snapshots:
strip-literal: 1.3.0
tinybench: 2.6.0
tinypool: 0.8.2
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
- vite-node: 1.2.2(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.14.10)(terser@5.19.1)
+ vite-node: 1.2.2(@types/node@20.14.10)(terser@5.19.1)
why-is-node-running: 2.2.2
optionalDependencies:
- '@types/node': 20.10.8
+ '@types/node': 20.14.10
jsdom: 23.2.0
transitivePeerDependencies:
- less
@@ -25823,7 +25915,7 @@ snapshots:
- supports-color
- terser
- vitest@1.5.0(@types/node@20.10.8)(jsdom@23.2.0)(lightningcss@1.19.0)(terser@5.19.1):
+ vitest@1.5.0(@types/node@20.14.10)(jsdom@23.2.0)(terser@5.19.1):
dependencies:
'@vitest/expect': 1.5.0
'@vitest/runner': 1.5.0
@@ -25842,11 +25934,11 @@ snapshots:
strip-literal: 2.1.0
tinybench: 2.6.0
tinypool: 0.8.4
- vite: 5.1.6(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
- vite-node: 1.5.0(@types/node@20.10.8)(lightningcss@1.19.0)(terser@5.19.1)
+ vite: 5.1.6(@types/node@20.14.10)(terser@5.19.1)
+ vite-node: 1.5.0(@types/node@20.14.10)(terser@5.19.1)
why-is-node-running: 2.2.2
optionalDependencies:
- '@types/node': 20.10.8
+ '@types/node': 20.14.10
jsdom: 23.2.0
transitivePeerDependencies:
- less