Skip to content

Commit

Permalink
Merge pull request #540 from ephemeraHQ/ar/pinned-unread
Browse files Browse the repository at this point in the history
feat: Unread indicators on Pinned Chats
  • Loading branch information
alexrisch authored Aug 22, 2024
2 parents 5c1e81f + 6d1e4fd commit 231126f
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 84 deletions.
31 changes: 21 additions & 10 deletions components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { actionSecondaryColor, textSecondaryColor } from "@styles/colors";
import { AvatarSizes } from "@styles/sizes";
import { getFirstLetterForAvatar } from "@utils/getFirstLetterForAvatar";
import { Image } from "expo-image";
import { ImageBackground } from "expo-image";
import React, { useCallback, useState } from "react";
import {
ColorSchemeName,
Expand All @@ -13,12 +13,15 @@ import {
View,
} from "react-native";

import { Indicator } from "./Indicator";

type Props = {
uri?: string | undefined;
size?: number | undefined;
style?: StyleProp<ImageStyle>;
color?: boolean;
name?: string | undefined;
showIndicator?: boolean;
};

function Avatar({
Expand All @@ -27,6 +30,7 @@ function Avatar({
style,
color,
name,
showIndicator,
}: Props) {
const colorScheme = useColorScheme();
const styles = getStyles(colorScheme, size);
Expand All @@ -40,15 +44,19 @@ function Avatar({
const handleImageLoad = useCallback(() => {
setDidError(false);
}, []);

return uri && !didError ? (
<Image
onLoad={handleImageLoad}
onError={handleImageError}
key={`${uri}-${color}-${colorScheme}`}
source={{ uri }}
style={[styles.image, style]}
/>
<>
<ImageBackground
onLoad={handleImageLoad}
onError={handleImageError}
key={`${uri}-${color}-${colorScheme}`}
source={{ uri }}
style={[styles.imageContainer, style]}
imageStyle={styles.image}
>
{showIndicator && <Indicator size={size} />}
</ImageBackground>
</>
) : (
<View
style={StyleSheet.flatten([
Expand All @@ -58,16 +66,19 @@ function Avatar({
])}
>
<Text style={styles.text}>{firstLetter}</Text>
{showIndicator && <Indicator size={size} />}
</View>
);
}

const getStyles = (colorScheme: ColorSchemeName, size: number) =>
StyleSheet.create({
image: {
borderRadius: size,
},
imageContainer: {
width: size,
height: size,
borderRadius: size,
},
placeholder: {
width: size,
Expand Down
21 changes: 8 additions & 13 deletions components/ConversationFlashList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { FlashList } from "@shopify/flash-list";
import { backgroundColor } from "@styles/colors";
import { showUnreadOnConversation } from "@utils/conversation/showUnreadOnConversation";
import { useCallback, useEffect, useRef } from "react";
import { Platform, StyleSheet, View, useColorScheme } from "react-native";

Expand Down Expand Up @@ -129,19 +130,13 @@ export default function ConversationFlashList({
lastMessagePreview?.message?.sent || conversation.createdAt
}
conversationName={conversationName(conversation)}
showUnread={(() => {
if (!initialLoadDoneOnce) return false;
if (!lastMessagePreview) return false;
// Manually marked as unread
if (topicsData[conversation.topic]?.status === "unread")
return true;
// If not manually markes as unread, we only show badge if last message
// not from me
if (lastMessagePreview.message.senderAddress === userAddress)
return false;
const readUntil = topicsData[conversation.topic]?.readUntil || 0;
return readUntil < lastMessagePreview.message.sent;
})()}
showUnread={showUnreadOnConversation(
initialLoadDoneOnce,
lastMessagePreview,
topicsData,
conversation,
userAddress
)}
lastMessagePreview={
conversation.peerAddress &&
peersStatus[conversation.peerAddress.toLowerCase()] === "blocked"
Expand Down
23 changes: 8 additions & 15 deletions components/ConversationList/GroupConversationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useGroupNameQuery } from "@queries/useGroupNameQuery";
import { useGroupPhotoQuery } from "@queries/useGroupPhotoQuery";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { actionSheetColors } from "@styles/colors";
import { showUnreadOnConversation } from "@utils/conversation/showUnreadOnConversation";
import { FC, useCallback } from "react";
import { useColorScheme } from "react-native";

Expand Down Expand Up @@ -158,21 +159,13 @@ export const GroupConversationItem: FC<GroupConversationItemProps> = ({
lastMessagePreview?.message?.sent || conversation.createdAt
}
conversationName={groupName ? groupName : conversationName(conversation)}
showUnread={(() => {
if (!initialLoadDoneOnce) return false;
if (!lastMessagePreview) return false;
// Manually marked as unread
if (topicsData[conversation.topic]?.status === "unread") return true;
// If not manually markes as unread, we only show badge if last message
// not from me
if (
lastMessagePreview.message.senderAddress.toLowerCase() ===
userAddress.toLowerCase()
)
return false;
const readUntil = topicsData[conversation.topic]?.readUntil || 0;
return readUntil < lastMessagePreview.message.sent;
})()}
showUnread={showUnreadOnConversation(
initialLoadDoneOnce,
lastMessagePreview,
topicsData,
conversation,
userAddress
)}
lastMessagePreview={
lastMessagePreview ? lastMessagePreview.contentPreview : ""
}
Expand Down
48 changes: 5 additions & 43 deletions components/GroupAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "react-native";

import Avatar from "./Avatar";
import { Indicator } from "./Indicator";
import {
useProfilesStore,
useCurrentAccount,
Expand All @@ -42,6 +43,7 @@ type GroupAvatarProps = {
// Converstion List should not make requests across the bridge
// Use data from the initial sync, and as the query gets updated
onConversationListScreen?: boolean;
showIndicator?: boolean;
};

const calculatePositions = (
Expand Down Expand Up @@ -79,48 +81,6 @@ const calculatePositions = (
);
};

const PlaceholderAvatar: React.FC<{
pos: Position;
firstLetter: string;
colorScheme: ColorSchemeType;
size: number;
}> = ({ pos, firstLetter, colorScheme, size }) => {
const styles = getStyles(colorScheme);
return (
<View
style={[
styles.placeholderAvatar,
{
left: (pos.x / 100) * size,
top: (pos.y / 100) * size,
width: (pos.size / 100) * size,
height: (pos.size / 100) * size,
borderRadius: ((pos.size / 100) * size) / 2,
backgroundColor:
colorScheme === "dark"
? textSecondaryColor(colorScheme)
: actionSecondaryColor(colorScheme),
},
]}
>
<Text
style={[
styles.placeholderText,
{
color:
colorScheme === "dark"
? textPrimaryColor(colorScheme)
: inversePrimaryColor(colorScheme),
fontSize: ((pos.size / 100) * size) / 2,
},
]}
>
{firstLetter}
</Text>
</View>
);
};

const ExtraMembersIndicator: React.FC<{
pos: Position;
extraMembersCount: number;
Expand Down Expand Up @@ -171,6 +131,7 @@ const GroupAvatar: React.FC<GroupAvatarProps> = ({
pendingGroupMembers,
excludeSelf = true,
onConversationListScreen = false,
showIndicator = false,
}) => {
const colorScheme = useColorScheme();
const styles = getStyles(colorScheme);
Expand Down Expand Up @@ -224,7 +185,7 @@ const GroupAvatar: React.FC<GroupAvatarProps> = ({
return (
<View style={[styles.container, { width: size, height: size }, style]}>
{uri ? (
<Avatar size={size} uri={uri} />
<Avatar size={size} uri={uri} showIndicator={showIndicator} />
) : (
<View style={[styles.container, { width: size, height: size }]}>
<View style={styles.overlay} />
Expand Down Expand Up @@ -261,6 +222,7 @@ const GroupAvatar: React.FC<GroupAvatarProps> = ({
return null;
})}
</View>
{showIndicator && <Indicator size={size} />}
</View>
)}
</View>
Expand Down
43 changes: 43 additions & 0 deletions components/Indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { backgroundColor, badgeColor } from "@styles/colors";
import React, { memo } from "react";
import {
useColorScheme,
ColorSchemeName,
StyleSheet,
View,
} from "react-native";

const IndicatorInner = ({ size }: { size: number }) => {
const styles = getStyles(useColorScheme(), size);

return (
<View style={styles.indicator}>
<View style={styles.indicatorInner} />
</View>
);
};

export const Indicator = memo(IndicatorInner);

const getStyles = (colorScheme: ColorSchemeName, size: number) =>
StyleSheet.create({
indicator: {
position: "absolute",
right: size / 20,
bottom: size / 20,
height: size / 5,
width: size / 5,
backgroundColor: backgroundColor(colorScheme),
alignItems: "center",
justifyContent: "center",
borderRadius: size / 5,
// borderWidth: 2,
// borderColor: backgroundColor(colorScheme),
},
indicatorInner: {
height: size / 5 - 4,
width: size / 5 - 4,
backgroundColor: badgeColor(colorScheme),
borderRadius: size / 5,
},
});
25 changes: 22 additions & 3 deletions components/PinnedConversations/PinnedConversation.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useSelect } from "@data/store/storeHelpers";
import { useGroupNameQuery } from "@queries/useGroupNameQuery";
import { useGroupPhotoQuery } from "@queries/useGroupPhotoQuery";
import { backgroundColor, textSecondaryColor } from "@styles/colors";
import { AvatarSizes } from "@styles/sizes";
import { FC, useCallback } from "react";
import { ConversationWithLastMessagePreview } from "@utils/conversation";
import { showUnreadOnConversation } from "@utils/conversation/showUnreadOnConversation";
import { FC, useCallback, useMemo } from "react";
import {
StyleSheet,
Text,
Expand All @@ -16,13 +19,12 @@ import {
useCurrentAccount,
useProfilesStore,
} from "../../data/store/accountsStore";
import { XmtpConversation } from "../../data/store/chatStore";
import { navigate } from "../../utils/navigation";
import { getPreferredAvatar, getPreferredName } from "../../utils/profile";
import GroupAvatar from "../GroupAvatar";

interface Props {
conversation: XmtpConversation;
conversation: ConversationWithLastMessagePreview;
}

export const PinnedConversation: FC<Props> = ({ conversation }) => {
Expand Down Expand Up @@ -50,6 +52,21 @@ export const PinnedConversation: FC<Props> = ({ conversation }) => {
const onLongPress = useCallback(() => {
setPinnedConversations([conversation]);
}, [conversation, setPinnedConversations]);
const { initialLoadDoneOnce, topicsData } = useChatStore(
useSelect(["initialLoadDoneOnce", "topicsData"])
);

const showUnread = useMemo(
() =>
showUnreadOnConversation(
initialLoadDoneOnce,
conversation.lastMessagePreview,
topicsData,
conversation,
account
),
[account, conversation, initialLoadDoneOnce, topicsData]
);

const avatarComponent = isGroup ? (
<GroupAvatar
Expand All @@ -58,6 +75,7 @@ export const PinnedConversation: FC<Props> = ({ conversation }) => {
size={AvatarSizes.pinnedConversation}
style={styles.avatar}
topic={conversation.topic}
showIndicator={showUnread}
/>
) : (
<Avatar
Expand All @@ -66,6 +84,7 @@ export const PinnedConversation: FC<Props> = ({ conversation }) => {
size={AvatarSizes.pinnedConversation}
style={styles.avatar}
name={getPreferredName(socials, conversation.peerAddress || "")}
showIndicator={showUnread}
/>
);

Expand Down
23 changes: 23 additions & 0 deletions utils/conversation/showUnreadOnConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TopicsData } from "@data/store/chatStore";
import {
ConversationWithLastMessagePreview,
LastMessagePreview,
} from "@utils/conversation";

export const showUnreadOnConversation = (
initialLoadDoneOnce: boolean,
lastMessagePreview: LastMessagePreview | undefined,
topicsData: TopicsData,
conversation: ConversationWithLastMessagePreview,
userAddress: string
) => {
if (!initialLoadDoneOnce) return false;
if (!lastMessagePreview) return false;
// Manually marked as unread
if (topicsData[conversation.topic]?.status === "unread") return true;
// If not manually markes as unread, we only show badge if last message
// not from me
if (lastMessagePreview.message.senderAddress === userAddress) return false;
const readUntil = topicsData[conversation.topic]?.readUntil || 0;
return readUntil < lastMessagePreview.message.sent;
};

0 comments on commit 231126f

Please sign in to comment.