Skip to content

Commit

Permalink
Mark messages unseen when fully visible
Browse files Browse the repository at this point in the history
- Also fix selectors unseen check: check all messages, not just last
  message
  • Loading branch information
rottabonus committed Apr 21, 2024
1 parent ababe10 commit 6c4d038
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 63 deletions.
15 changes: 13 additions & 2 deletions src/Screens/Main/Chat/MessageList/MemoizedRenderItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import * as RN from 'react-native';

import Message from './Message';
import Message, { MessageProps } from './Message';
import DateBubble from './DateBubble';
import Spinner from 'src/Screens/components/Spinner';
import { Renderable } from '.';
Expand All @@ -27,7 +27,18 @@ const RenderItem: React.FC<Props> = ({ item }) => {
const equalProps = (
prevProps: React.ComponentProps<typeof RenderItem>,
nextProps: React.ComponentProps<typeof RenderItem>,
) => prevProps.item.id === nextProps.item.id;
) => {
if (
Object.prototype.hasOwnProperty.call(nextProps.item, 'isSeen') &&
Object.prototype.hasOwnProperty.call(prevProps.item, 'isSeen')
) {
const prev = prevProps.item as MessageProps;
const next = nextProps.item as MessageProps;
return prev.id === next.id && prev.isSeen === next.isSeen;

Check failure on line 37 in src/Screens/Main/Chat/MessageList/MemoizedRenderItem.tsx

View workflow job for this annotation

GitHub Actions / Lint

Expected blank line before this statement
}

return prevProps.item.id === nextProps.item.id;
};

export const MemoizedRenderItem = React.memo(RenderItem, equalProps);

Expand Down
19 changes: 7 additions & 12 deletions src/Screens/Main/Chat/MessageList/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import React from 'react';
import RN from 'react-native';
import * as redux from 'redux';
import { useDispatch } from 'react-redux';

import { markSeen } from '../../../../state/reducers/markSeen';
import * as actions from '../../../../state/actions';

import colors from '../../../components/colors';
import fonts from '../../../components/fonts';
Expand All @@ -16,28 +11,28 @@ export type MessageProps = {
type: 'Message';
value: messageApi.Message;
id: string;
isSeen: boolean;
};

const Message = ({ value: message }: MessageProps) => {
const { content, sentTime, type } = message;
const dispatch = useDispatch<redux.Dispatch<actions.Action>>();
React.useEffect(() => {
if (!message.isSeen && message.type === 'Received') {
dispatch(markSeen({ message }));
}
}, []);

const bubbleStyle =
type === 'Received' ? styles.leftBubble : styles.rightBubble;

const isSeenTemp =
type === 'Received' && !message.isSeen
? { borderWidth: 2, borderColor: 'red' }
: { borderWidth: 2, borderColor: 'green' };

const addZero = (n: number) => (n < 10 ? `0${n}` : `${n}`);
const date = new Date(sentTime);
const hours = addZero(date.getHours());
const minutes = addZero(date.getMinutes());
const timeText = `${hours}:${minutes}`;

return (
<RN.View style={[bubbleStyle, styles.bubble]}>
<RN.View style={[bubbleStyle, isSeenTemp, styles.bubble]}>
<RN.View>
<RN.Text style={styles.text}>{content}</RN.Text>
</RN.View>
Expand Down
43 changes: 33 additions & 10 deletions src/Screens/Main/Chat/MessageList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import RN from 'react-native';
import { useDispatch } from 'react-redux';

import * as localization from '../../../../localization';

import { markSeen } from '../../../../state/reducers/markSeen';
import * as messageApi from '../../../../api/messages';

import { MessageProps } from './Message';
Expand Down Expand Up @@ -55,6 +57,7 @@ export function toRenderable(
type: 'Message' as const,
value: m,
id: m.messageId,
isSeen: m.isSeen,
};
const date = getDate(next.value.sentTime);
const nextDate = { type: 'Date' as const, value: date, id: date };
Expand All @@ -81,13 +84,14 @@ export function toRenderable(
: messageList;
}

const MessageList = ({
export const MessageList = ({
messageList,
getPreviousMessages,
isLoading,
}: Props) => {
const messages = toRenderable(messageList, isLoading);
const previousItem = messageList.length > 0 ? messageList[0].messageId : '0';
const dispatch = useDispatch();

const getPreviousMessagesIfNotLoading = () => {
if (isLoading) {
Expand All @@ -97,11 +101,39 @@ const MessageList = ({
getPreviousMessages(previousItem);
};

type ViewArgs = {
viewableItems: Array<RN.ViewToken>;
changed: Array<RN.ViewToken>;
};
const handleViewableChanged = ({ changed }: ViewArgs) => {
// TODO: Helper here, decode the changed maybe
const unSeenMessagesOnScreen = changed
.filter(item => item.isViewable)
.filter(
({ item }) =>
!item.isSeen && item.value && item.value.type === 'Received',
)
.map<messageApi.Message>(({ item }) => item.value);

console.log('changed', unSeenMessagesOnScreen);

dispatch(markSeen({ messages: unSeenMessagesOnScreen }));
};

const viewabilityConfig: RN.ViewabilityConfig = {
itemVisiblePercentThreshold: 100,
minimumViewTime: 1000,
};
const viewabilityConfigCallbackPairs = React.useRef([
{ viewabilityConfig, onViewableItemsChanged: handleViewableChanged },
]);

return (
<RN.FlatList
contentContainerStyle={styles.scrollContent}
data={messages}
renderItem={({ item }) => <MemoizedRenderItem item={item} />}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
keyExtractor={item => item.id}
inverted={true}
onEndReachedThreshold={0.01}
Expand All @@ -110,15 +142,6 @@ const MessageList = ({
);
};

const equalProps = (
prevProps: React.ComponentProps<typeof MessageList>,
nextProps: React.ComponentProps<typeof MessageList>,
) =>
prevProps.messageList.length === nextProps.messageList.length &&
prevProps.isLoading === nextProps.isLoading;

export const MemoizedMessageList = React.memo(MessageList, equalProps);

const styles = RN.StyleSheet.create({
scrollContent: {
paddingHorizontal: 24,
Expand Down
4 changes: 2 additions & 2 deletions src/Screens/Main/Chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { isDevice } from '../../../lib/isDevice';

import Title from './Title';
import Input from './Input';
import { MemoizedMessageList } from './MessageList';
import { MessageList } from './MessageList';
import DropDown, { DropDownItem } from '../../components/DropDownMenu';
import Modal from '../../components/Modal';
import { dialogProperties, changeChatStatusOptions } from './chatProperties';
Expand Down Expand Up @@ -185,7 +185,7 @@ const Chat = ({ navigation, route }: Props) => {
messageId="main.chat.send.failure"
/>
)}
<MemoizedMessageList
<MessageList
messageList={sortedMessageList}
getPreviousMessages={getPreviousMessages}
isLoading={isLoading}
Expand Down
2 changes: 1 addition & 1 deletion src/state/actions/regular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type RegularActions = {
'messages/setPollingParams': messages.PollingParams;
'messages/get/completed': E.Either<string, messageApi.MessageResponse>;

'messages/markSeen': { message: messageApi.Message };
'messages/markSeen': { messages: Array<messageApi.Message> };
'messages/markSeen/end': undefined;

'buddies/completed': Result<typeof buddyApi.fetchBuddies>;
Expand Down
41 changes: 28 additions & 13 deletions src/state/reducers/markSeen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,40 @@ export const reducer: automaton.Reducer<State, actions.Action> = (
action,
) => {
switch (action.type) {
case 'messages/markSeen':
const message = action.payload.message;
const id = message.messageId;

if (id in state || message.isSeen || message.type === 'Sent') {
return state;
case 'messages/markSeen': {
const hasMessages = action.payload.messages.length > 0;
if (!hasMessages) {
return automaton.loop(state, {
type: 'messages/markSeen/end',
payload: undefined,
});
}

const markSeenTask = api.markSeen(action.payload.message);
const [first, ...messages] = action.payload.messages;

console.log('markSeen action for message', first.content);
const nextState =
first.messageId in state || first.isSeen || first.type === 'Sent'
? state
: { ...state, [first.messageId]: true };

const markSeenTask = api.markSeen(first);

return automaton.loop(
{ ...state, [id]: true },
withToken(markSeenTask, () => ({
type: 'messages/markSeen/end',
payload: undefined,
})),
nextState,
// withToken(markSeenTask, () => ({
// type: 'messages/markSeen',
// payload: { messages },
// })),
{
type: 'messages/markSeen',
payload: { messages },
},
);
}

default:
default: {
return state;
}
}
};
38 changes: 15 additions & 23 deletions src/state/reducers/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,14 @@ export const reducer: automaton.Reducer<State, actions.Action> = (
}

case 'messages/markSeen': {
const { messageId, buddyId, type, isSeen } = action.payload.message;
const hasMessages = action.payload.messages.length > 0;
if (!hasMessages) {
return state;
}

const [first, ..._rest] = action.payload.messages;

if (type === 'Sent' || isSeen === true) {
if (first.type === 'Sent' || first.isSeen === true) {
return state;
}

Expand All @@ -170,14 +175,14 @@ export const reducer: automaton.Reducer<State, actions.Action> = (
: {};

const updatedMessage = {
...action.payload.message,
...first,
isSeen: true,
};

const updatedMessageRecord = {
[buddyId]: {
...oldMessages[buddyId],
[messageId]: updatedMessage,
[first.buddyId]: {
...oldMessages[first.buddyId],
[first.messageId]: updatedMessage,
},
};

Expand Down Expand Up @@ -225,17 +230,8 @@ export const ordMessage: ord.Ord<messageApi.Message> = ord.fromCompare((a, b) =>
export const hasUnseen: (
buddyId: string,
) => (appState: types.AppState) => boolean = buddyId => appState =>
pipe(
getMessagesByBuddyId(buddyId)(appState),
array.sort(ordMessage),
array.last,
O.map(
({ type, isSeen }) =>
type === 'Received' &&
isSeen === false &&
!getIsBanned(buddyId)(appState),
),
O.fold(() => false, identity),
pipe(getMessagesByBuddyId(buddyId)(appState), messages =>
messages.some(({ type, isSeen }) => type === 'Received' && !isSeen),
);

export const isAnyMessageUnseen = (appState: types.AppState) =>
Expand All @@ -259,17 +255,13 @@ export const hasUnseenMessagesOfStatus =
export const hasUnseenMessagesByType =
(buddyId: string, chatType: buddyApi.ChatStatus) =>
(appState: types.AppState) =>
pipe(
getMessagesByBuddyId(buddyId)(appState),
array.sort(ordMessage),
array.last,
O.map(
pipe(getMessagesByBuddyId(buddyId)(appState), messages =>
messages.some(
({ type, isSeen }) =>
type === 'Received' &&
!isSeen &&
getBuddyStatus(buddyId)(appState) === chatType,
),
O.fold(() => false, identity),
);
export const getMessage = (
{ messages: messageState }: types.AppState,
Expand Down

0 comments on commit 6c4d038

Please sign in to comment.