Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import EditProfileScreen from './screens/EditProfileScreen';
import WriterScreen from './screens/WriterScreen';
import MessageScreen from './screens/MessagesScreen';
import ThreadScreen from './screens/ThreadScreen';
import UserProfileScreen from './screens/UserProfileScreen';
import { storage, hasMigratedFromAsyncStorage, migrateFromAsyncStorage } from './utils/mmkv';
import { useMMKVBoolean, useMMKVString } from 'react-native-mmkv';

Expand Down Expand Up @@ -101,6 +102,7 @@ export default function App() {
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
<Stack.Screen name="MyProfile" component={MyProfileScreen} />
<Stack.Screen name="UserProfile" component={UserProfileScreen} />
<Stack.Screen name="EditProfile" component={EditProfileScreen} />
<Stack.Screen name="Messages" component={MessageScreen} />
<Stack.Screen name="Thread" component={ThreadScreen} />
Expand Down
31 changes: 27 additions & 4 deletions src/components/Comment.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '../types/OffsidesTypes.js';
import React from 'react';
import { View, Alert, Linking } from 'react-native';
import { View, Alert, Linking, Pressable } from 'react-native';
import { Badge, Button, Card, Chip, IconButton, Text, useTheme } from 'react-native-paper';
import { setStringAsync as copyToClipboard } from 'expo-clipboard';
import timesago from 'timesago';
Expand Down Expand Up @@ -38,6 +38,20 @@ function Comment({ comment, nav, isolated = false }) {
});
};

const canOpenProfile =
!!nav &&
!!comment?.identity?.posted_with_username &&
!!comment?.identity?.name &&
comment.identity.name != 'Anonymous';
const openProfile = () => {
if (!canOpenProfile) return;
if (comment.authored_by_user) {
nav.push('MyProfile');
} else {
nav.push('UserProfile', { username: comment.identity.name });
}
};

const deleteComment = () => {
Alert.alert('Are you sure?', 'This will permanently delete this comment.', [
{
Expand Down Expand Up @@ -68,7 +82,11 @@ function Comment({ comment, nav, isolated = false }) {
mode="contained">
<Card.Content>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ position: 'relative' }}>
<Pressable
style={{ position: 'relative' }}
onPress={openProfile}
disabled={!canOpenProfile}
hitSlop={4}>
<UserAvatar
group={comment.group}
conversationIcon={comment?.identity?.conversation_icon}
Expand All @@ -84,7 +102,7 @@ function Comment({ comment, nav, isolated = false }) {
{comment.authored_by_user && (
<Badge mode="outlined" compact={true} style={{ position: 'absolute', bottom: -8, right: -8, color: colors.onPrimary, backgroundColor: colors.primary }}>YOU</Badge>
)}
</View>
</Pressable>
<View
style={{
justifyContent: 'center',
Expand All @@ -95,7 +113,12 @@ function Comment({ comment, nav, isolated = false }) {
comment?.identity?.posted_with_username && (
<Text
variant="labelMedium"
style={{ marginLeft: 10, opacity: 0.75 }}>
onPress={canOpenProfile ? openProfile : undefined}
style={{
marginLeft: 10,
opacity: 0.75,
color: canOpenProfile ? colors.primary : undefined,
}}>
@{comment.identity.name}
</Text>
)}
Expand Down
39 changes: 31 additions & 8 deletions src/components/Post.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SidechatPostOrComment } from 'sidechat.js/src/types/SidechatTypes.js';
import React, { useState } from 'react';
import { Alert, Linking, View } from 'react-native';
import { Alert, Linking, Pressable, View } from 'react-native';
import { Card, Chip, IconButton, Text, useTheme } from 'react-native-paper';
import { setStringAsync as copyToClipboard } from 'expo-clipboard';
import timesago from 'timesago';
Expand All @@ -26,6 +26,7 @@ function Post({
cardMode = repost ? 'outlined' : 'elevated',
apiInstance = null,
themeColors = {},
profileLink = true,
}) {
const colors = themeColors;
const API = apiInstance;
Expand All @@ -39,6 +40,21 @@ function Post({
const [identity, setIdentity] = useRecyclingState(post?.identity, [post]);
const postID = post.id;

// Posts made with a username can be tapped through to that user's public profile.
const hasUsername =
!!identity?.name &&
identity.name != 'Anonymous' &&
identity.posted_with_username !== false;
const canOpenProfile = profileLink && hasUsername && !!nav;
const openProfile = React.useCallback(() => {
if (!canOpenProfile) return;
if (post.authored_by_user) {
nav.push('MyProfile');
} else {
nav.push('UserProfile', { username: identity.name });
}
}, [canOpenProfile, post.authored_by_user, identity?.name, nav]);

const upvote = React.useCallback(() => {
const action = vote == 'upvote' ? 'none' : 'upvote';
API.setVote(postID, action).then(res => {
Expand Down Expand Up @@ -100,12 +116,14 @@ function Post({
style={repost ? { marginBottom: 10 } : {}}>
<Card.Content>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<UserAvatar
group={group}
conversationIcon={identity?.conversation_icon}
size={46}
borderRadius={BORDER_RADIUS}
/>
<Pressable onPress={openProfile} disabled={!canOpenProfile} hitSlop={4}>
<UserAvatar
group={group}
conversationIcon={identity?.conversation_icon}
size={46}
borderRadius={BORDER_RADIUS}
/>
</Pressable>
<View
style={{
justifyContent: 'center',
Expand All @@ -118,7 +136,12 @@ function Post({
{post.identity.name != 'Anonymous' && (
<Text
variant="labelMedium"
style={{ marginLeft: 10, opacity: 0.75 }}>
onPress={canOpenProfile ? openProfile : undefined}
style={{
marginLeft: 10,
opacity: 0.75,
color: canOpenProfile ? colors.primary : undefined,
}}>
@{post.identity.name}
</Text>
)}
Expand Down
39 changes: 38 additions & 1 deletion src/screens/EditProfileScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ function EditProfileScreen({ navigation }) {
secondary: null,
});
const [username, setUsername] = React.useState('');
const [bio, setBio] = React.useState('');
const [initialBio, setInitialBio] = React.useState('');
const [usernameError, setUsernameError] = React.useState(false);
const [error, setError] = React.useState(false);
const [emoji, setEmoji] = React.useState();
Expand Down Expand Up @@ -64,11 +66,33 @@ function EditProfileScreen({ navigation }) {
if (u.user?.username) {
setUsername(u.user.username);
}
// The bio lives on the public profile object; fall back to any field on the user object.
let currentBio = '';
if (typeof u.user?.bio === 'string') currentBio = u.user.bio;
else if (typeof u.user?.description === 'string') currentBio = u.user.description;
else if (u.user?.username) {
try {
const p = await API.getUserProfile(u.user.username);
if (typeof p?.description === 'string') currentBio = p.description;
} catch (e) {
// no public profile yet; leave the bio empty
}
}
setBio(currentBio);
setInitialBio(currentBio);
setLoading(false);
};
const saveIcon = async () => {
const uname = await API.setUsername(appState.userID, username);
await uname;
if (bio.trim() !== initialBio.trim()) {
const bioRes = await API.setUserBio(appState.userID, bio.trim());
if (bioRes?.message) {
setError(true);
loadCurrent();
return;
}
}
const res = await API.setUserIcon(
appState.userID,
emoji,
Expand Down Expand Up @@ -113,6 +137,19 @@ function EditProfileScreen({ navigation }) {
<HelperText visible={usernameError} type="error">
You can't set that as your username.
</HelperText>
<TextInput
label="bio"
mode="outlined"
multiline={true}
numberOfLines={3}
maxLength={200}
style={{ width: '100%', minWidth: 200, maxWidth: 320 }}
value={bio}
onChangeText={setBio}
/>
<HelperText visible={true} type="info" style={{ alignSelf: 'flex-end' }}>
{bio.length}/200
</HelperText>
<TouchableRipple
borderless={true}
style={{
Expand Down Expand Up @@ -227,7 +264,7 @@ function EditProfileScreen({ navigation }) {
</View>
)}
<Snackbar visible={error} onDismiss={() => setError(false)}>
Sorry, you can't set that as your icon.
Sorry, that couldn't be saved.
</Snackbar>
</View>
);
Expand Down
21 changes: 17 additions & 4 deletions src/screens/MyProfileScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ function MyProfileScreen({ navigation }) {
header: "Comment Karma",
value: karmaObj?.comment || 0
});
karmaObj?.groups.forEach(group => {
const g = groupList.find((item => item.id == group.group_id));
(karmaObj?.groups || []).forEach(group => {
const g = (groupList || []).find((item => item.id == group.group_id));
karmaObjects.push({
header: g.name,
value: group.post + group.comment
header: g?.name || 'Group',
value: (group.post || 0) + (group.comment || 0)
})
});
return karmaObjects;
Expand All @@ -67,6 +67,13 @@ function MyProfileScreen({ navigation }) {
crashlytics().log('Fetching profile');
const u = await API.getUpdates(currentGroup?.id);
crashlytics().log('Profile fetched successfully');
// Pull the bio from the public profile if the user object doesn't carry one.
if (u?.user?.username && typeof u.user.bio !== 'string' && typeof u.user.description !== 'string') {
try {
const p = await API.getUserProfile(u.user.username);
if (typeof p?.description === 'string') u.user.bio = p.description;
} catch (e) { /* no public profile */ }
}
setUpdates(u);
setLoading(false);
};
Expand Down Expand Up @@ -160,6 +167,12 @@ function MyProfileScreen({ navigation }) {
</Text>
</View>
</View>
{(() => {
const b = typeof updates.user?.bio === 'string' ? updates.user.bio : typeof updates.user?.description === 'string' ? updates.user.description : '';
return b.trim() ? (
<Text variant="bodyMedium" style={{ paddingHorizontal: 10 }}>{b.trim()}</Text>
) : null;
})()}
<ScrollView horizontal={true} style={{ maxHeight: 100, flexDirection: 'row', marginTop: 10 }} contentContainerStyle={{ gap: 10, paddingBottom: 10, paddingHorizontal: 10 }} showsHorizontalScrollIndicator={false}>
{karmaInfo.map((item) =>
<Card key={item.header}>
Expand Down
Loading