Skip to content

Commit

Permalink
feat: Unread indicators on Pinned Chats
Browse files Browse the repository at this point in the history
Added unread indicator on pinned chats
Added shared function for determining if unread should be shown
Added Indicator component
  • Loading branch information
Alex Risch committed Aug 21, 2024
1 parent 8795769 commit 6d1e4fd
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 85 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
25 changes: 9 additions & 16 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 @@ -44,7 +45,7 @@ export const GroupConversationItem: FC<GroupConversationItemProps> = ({
staleTime: Infinity,
gcTime: Infinity,
});
const { consent, blockGroup, allowGroup } = useGroupConsent(topic, {
const { consent, blockGroup, allowGroup } = useGroupConsent(topic, {
refetchOnMount: false,
staleTime: Infinity,
gcTime: Infinity,
Expand Down Expand Up @@ -161,21 +162,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 6d1e4fd

Please sign in to comment.