feat: added interaction properties to conversation modal and started UI changes in ConversationListItem based on confirming an interaction

still to to port the original modal checks to use thd convo props
pull/2789/head
William Grant 2 years ago
parent c492fad4ee
commit 8a7007e7c3

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer'; import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
import { updateConfirmModal } from '../../state/ducks/modalDialog'; import { updateConfirmModal, updateConfirmModalStatus } from '../../state/ducks/modalDialog';
import { SpacerLG } from '../basic/Text'; import { SpacerLG } from '../basic/Text';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../basic/SessionSpinner'; import { SessionSpinner } from '../basic/SessionSpinner';
@ -9,10 +9,14 @@ import { SessionWrapperModal } from '../SessionWrapperModal';
import { Dispatch } from '@reduxjs/toolkit'; import { Dispatch } from '@reduxjs/toolkit';
import { shell } from 'electron'; import { shell } from 'electron';
import { MessageInteraction } from '../../interactions'; import { MessageInteraction } from '../../interactions';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../interactions/conversationInteractions';
export type ConfirmationStatus = 'loading' | 'success' | 'error'; // NOTE could be other confirmation statuses and types in future
// TODO expand support for other confirmation actions export type ConfirmationStatus = ConversationInteractionStatus | undefined;
export type ConfirmationType = 'delete-conversation'; export type ConfirmationType = ConversationInteractionType | undefined;
export interface SessionConfirmDialogProps { export interface SessionConfirmDialogProps {
message?: string; message?: string;
@ -81,10 +85,12 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
await onClickOk(); await onClickOk();
} catch (e) { } catch (e) {
window.log.warn(e); window.log.warn(e);
window.inboxStore?.dispatch(updateConfirmModal({ ...props, status: 'error' })); window.inboxStore?.dispatch(updateConfirmModalStatus(ConversationInteractionStatus.Error));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
window.inboxStore?.dispatch(updateConfirmModal({ ...props, status: 'success' })); window.inboxStore?.dispatch(
updateConfirmModalStatus(ConversationInteractionStatus.Success)
);
} }
} }
@ -109,15 +115,22 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
onClickClose(); onClickClose();
} }
window.inboxStore?.dispatch(updateConfirmModal(null)); // We clear and close the modal but maintain the confirmation status
window.inboxStore?.dispatch(updateConfirmModal({ status: props.status }));
}; };
useEffect(() => { useEffect(() => {
if (isLoading) { if (isLoading) {
window.inboxStore?.dispatch(updateConfirmModal({ ...props, status: 'loading' })); window.inboxStore?.dispatch(updateConfirmModalStatus(ConversationInteractionStatus.Loading));
} }
}, [isLoading]); }, [isLoading]);
useEffect(() => {
window.log.debug(
`WIP: SessionConfirm updating status for ${props.conversationId} to ${props.confirmationType} ${props.status}`
);
}, [props.conversationId, props.confirmationType, props.status]);
return ( return (
<SessionWrapperModal <SessionWrapperModal
title={title} title={title}

@ -15,6 +15,7 @@ import { updateUserDetailsModal } from '../../../state/ducks/modalDialog';
import _, { isNil } from 'lodash'; import _, { isNil } from 'lodash';
import { import {
useAvatarPath, useAvatarPath,
useConfirmModalStatusAndType,
useConversationUsername, useConversationUsername,
useHasUnread, useHasUnread,
useIsBlocked, useIsBlocked,
@ -27,6 +28,7 @@ import { MemoConversationListItemContextMenu } from '../../menu/ConversationList
import { ContextConversationProvider, useConvoIdFromContext } from './ConvoIdContext'; import { ContextConversationProvider, useConvoIdFromContext } from './ConvoIdContext';
import { ConversationListItemHeaderItem } from './HeaderItem'; import { ConversationListItemHeaderItem } from './HeaderItem';
import { MessageItem } from './MessageItem'; import { MessageItem } from './MessageItem';
import { InteractionItem } from './InteractionItem';
// tslint:disable-next-line: no-empty-interface // tslint:disable-next-line: no-empty-interface
export type ConversationListItemProps = Pick<ReduxConversationType, 'id'>; export type ConversationListItemProps = Pick<ReduxConversationType, 'id'>;
@ -80,6 +82,7 @@ const ConversationListItem = (props: Props) => {
let isBlocked = useIsBlocked(conversationId); let isBlocked = useIsBlocked(conversationId);
const isSearch = useSelector(isSearching); const isSearch = useSelector(isSearching);
const selectedConvo = useSelectedConversationKey(); const selectedConvo = useSelectedConversationKey();
const confirmModal = useConfirmModalStatusAndType();
const isSelectedConvo = conversationId === selectedConvo && !isNil(selectedConvo); const isSelectedConvo = conversationId === selectedConvo && !isNil(selectedConvo);
@ -129,7 +132,11 @@ const ConversationListItem = (props: Props) => {
<AvatarItem /> <AvatarItem />
<div className="module-conversation-list-item__content"> <div className="module-conversation-list-item__content">
<ConversationListItemHeaderItem /> <ConversationListItemHeaderItem />
<MessageItem /> {confirmModal?.conversationId === conversationId && confirmModal?.type ? (
<InteractionItem {...confirmModal} />
) : (
<MessageItem />
)}
</div> </div>
</div> </div>
<Portal> <Portal>

@ -0,0 +1,60 @@
import React from 'react';
import { isEmpty } from 'lodash';
import { useIsPrivate, useIsPublic } from '../../../hooks/useParamSelector';
import { MessageBody } from '../../conversation/message/message-content/MessageBody';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { ConfirmationStatus } from '../../dialog/SessionConfirm';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../../interactions/conversationInteractions';
type InteractionItemProps = {
status: ConfirmationStatus | undefined;
type: ConversationInteractionType | undefined;
conversationId: string | undefined;
};
export const InteractionItem = (props: InteractionItemProps) => {
const { status, type, conversationId } = props;
const isGroup = !useIsPrivate(conversationId);
const isCommunity = useIsPublic(conversationId);
if (!type) {
return null;
}
let text = '';
window.log.debug(`WIP: InteractionItem updating status for ${type} ${status}`);
switch (type) {
case ConversationInteractionType.Leave:
const failText = isCommunity
? ''
: isGroup
? window.i18n('leaveGroupFailed')
: window.i18n('deleteConversationFailed');
text =
status === ConversationInteractionStatus.Error
? failText
: status === ConversationInteractionStatus.Loading
? window.i18n('leaving')
: '';
break;
default:
assertUnreachable(type, `MessageItem: Missing case error "${type}"`);
}
if (isEmpty(text)) {
return null;
}
return (
<div className="module-conversation-list-item__message">
<div className="module-conversation-list-item__message__text">
<MessageBody text={text} disableJumbomoji={true} disableLinks={true} isGroup={isGroup} />
</div>
</div>
);
};

@ -3,11 +3,9 @@ import { isEmpty } from 'lodash';
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
useConfirmModalStatusAndType,
useConversationPropsById, useConversationPropsById,
useHasUnread, useHasUnread,
useIsPrivate, useIsPrivate,
useIsPublic,
useIsTyping, useIsTyping,
} from '../../../hooks/useParamSelector'; } from '../../../hooks/useParamSelector';
import { isSearching } from '../../../state/selectors/search'; import { isSearching } from '../../../state/selectors/search';
@ -16,7 +14,6 @@ import { TypingAnimation } from '../../conversation/TypingAnimation';
import { MessageBody } from '../../conversation/message/message-content/MessageBody'; import { MessageBody } from '../../conversation/message/message-content/MessageBody';
import { OutgoingMessageStatus } from '../../conversation/message/message-content/OutgoingMessageStatus'; import { OutgoingMessageStatus } from '../../conversation/message/message-content/OutgoingMessageStatus';
import { useConvoIdFromContext } from './ConvoIdContext'; import { useConvoIdFromContext } from './ConvoIdContext';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
function useLastMessageFromConvo(convoId: string) { function useLastMessageFromConvo(convoId: string) {
const convoProps = useConversationPropsById(convoId); const convoProps = useConversationPropsById(convoId);
@ -30,7 +27,6 @@ export const MessageItem = () => {
const conversationId = useConvoIdFromContext(); const conversationId = useConvoIdFromContext();
const lastMessage = useLastMessageFromConvo(conversationId); const lastMessage = useLastMessageFromConvo(conversationId);
const isGroup = !useIsPrivate(conversationId); const isGroup = !useIsPrivate(conversationId);
const isCommunity = useIsPublic(conversationId);
const hasUnread = useHasUnread(conversationId); const hasUnread = useHasUnread(conversationId);
const isConvoTyping = useIsTyping(conversationId); const isConvoTyping = useIsTyping(conversationId);
@ -38,37 +34,11 @@ export const MessageItem = () => {
const isSearchingMode = useSelector(isSearching); const isSearchingMode = useSelector(isSearching);
const confirmModal = useConfirmModalStatusAndType();
if (!lastMessage && !isConvoTyping) { if (!lastMessage && !isConvoTyping) {
return null; return null;
} }
let text = lastMessage?.text || ''; const text = lastMessage?.text || '';
if (confirmModal?.conversationId === conversationId && confirmModal?.type) {
window.log.debug(`WIP: updating status for ${confirmModal?.type} ${confirmModal.status}`);
switch (confirmModal?.type) {
case 'delete-conversation':
const failText = isCommunity
? ''
: isGroup
? window.i18n('leaveGroupFailed')
: window.i18n('deleteConversationFailed');
text =
confirmModal.status === 'error'
? failText
: confirmModal.status === 'loading'
? window.i18n('leaving')
: '';
break;
default:
assertUnreachable(
confirmModal?.type,
`MessageItem: Missing case error "${confirmModal?.type}"`
);
}
}
if (isEmpty(text)) { if (isEmpty(text)) {
return null; return null;

@ -44,6 +44,16 @@ import { ReleasedFeatures } from '../util/releaseFeature';
import { Storage, setLastProfileUpdateTimestamp } from '../util/storage'; import { Storage, setLastProfileUpdateTimestamp } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface'; import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
export enum ConversationInteractionStatus {
Success = 'success',
Error = 'error',
Loading = 'loading',
}
export enum ConversationInteractionType {
Leave = 'leave',
}
export async function copyPublicKeyByConvoId(convoId: string) { export async function copyPublicKeyByConvoId(convoId: string) {
if (OpenGroupUtils.isOpenGroupV2(convoId)) { if (OpenGroupUtils.isOpenGroupV2(convoId)) {
const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(convoId); const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(convoId);
@ -237,6 +247,8 @@ export function showLeavePrivateConversationbyConvoId(conversationId: string, na
}; };
const onClickOk = async () => { const onClickOk = async () => {
window.log.debug(`WIP: onClickOk ran`);
return;
await getConversationController().delete1o1(conversationId, { await getConversationController().delete1o1(conversationId, {
fromSyncMessage: false, fromSyncMessage: false,
justHidePrivate: true, justHidePrivate: true,
@ -252,7 +264,7 @@ export function showLeavePrivateConversationbyConvoId(conversationId: string, na
okText: window.i18n('delete'), okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger, okTheme: SessionButtonColor.Danger,
onClickClose, onClickClose,
confirmationType: 'delete-conversation', confirmationType: ConversationInteractionType.Leave,
conversationId, conversationId,
}) })
); );
@ -278,6 +290,8 @@ export function showLeaveGroupByConvoId(conversationId: string, name?: string) {
}; };
const onClickOk = async () => { const onClickOk = async () => {
window.log.debug(`WIP: onClickOk ran`);
return;
if (isPublic) { if (isPublic) {
await getConversationController().deleteCommunity(conversation.id, { await getConversationController().deleteCommunity(conversation.id, {
fromSyncMessage: false, fromSyncMessage: false,
@ -293,7 +307,7 @@ export function showLeaveGroupByConvoId(conversationId: string, name?: string) {
// TODO Communities don't need confirmation modal and have different logic // TODO Communities don't need confirmation modal and have different logic
if (isPublic || (isClosedGroup && !isAdmin)) { if (isPublic || (isClosedGroup && !isAdmin)) {
window.inboxStore?.dispatch( window?.inboxStore?.dispatch(
updateConfirmModal({ updateConfirmModal({
title: isPublic ? window.i18n('leaveCommunity') : window.i18n('leaveGroup'), title: isPublic ? window.i18n('leaveCommunity') : window.i18n('leaveGroup'),
message: window.i18n('leaveGroupConfirmation', name ? [name] : undefined), message: window.i18n('leaveGroupConfirmation', name ? [name] : undefined),
@ -301,20 +315,20 @@ export function showLeaveGroupByConvoId(conversationId: string, name?: string) {
okText: window.i18n('delete'), okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger, okTheme: SessionButtonColor.Danger,
onClickClose, onClickClose,
confirmationType: 'delete-conversation', confirmationType: ConversationInteractionType.Leave,
conversationId,
})
);
} else {
// TODO use different admin modal from figma with add another admin option
window.inboxStore?.dispatch(
adminLeaveClosedGroup({
conversationId, conversationId,
}) })
); );
return;
} }
// TODO use different admin modal from figma with add another admin option
window.inboxStore?.dispatch(
adminLeaveClosedGroup({
conversationId,
})
);
} }
export function showInviteContactByConvoId(conversationId: string) { export function showInviteContactByConvoId(conversationId: string) {
window.inboxStore?.dispatch(updateInviteContactModal({ conversationId })); window.inboxStore?.dispatch(updateInviteContactModal({ conversationId }));
} }

@ -115,6 +115,10 @@ import {
getSubscriberCountOutsideRedux, getSubscriberCountOutsideRedux,
} from '../state/selectors/sogsRoomInfo'; } from '../state/selectors/sogsRoomInfo';
import { markAttributesAsReadIfNeeded } from './messageFactory'; import { markAttributesAsReadIfNeeded } from './messageFactory';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../interactions/conversationInteractions';
type InMemoryConvoInfos = { type InMemoryConvoInfos = {
mentionedUs: boolean; mentionedUs: boolean;
@ -368,6 +372,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
} }
if (this.get('interactionType')) {
toRet.interactionType = this.get('interactionType');
}
if (this.get('interactionStatus')) {
toRet.interactionStatus = this.get('interactionStatus');
}
// -- Handle the field stored only in memory for all types of conversation-- // -- Handle the field stored only in memory for all types of conversation--
const inMemoryConvoInfo = inMemoryConvoInfos.get(this.id); const inMemoryConvoInfo = inMemoryConvoInfos.get(this.id);
if (inMemoryConvoInfo) { if (inMemoryConvoInfo) {
@ -2173,6 +2184,26 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return []; return [];
} }
public getInteractionState() {
return { type: this.get('interactionType'), status: this.get('interactionStatus') };
}
public async setInteractionState({
type,
status,
shouldCommit,
}: {
type: ConversationInteractionType;
status: ConversationInteractionStatus;
shouldCommit: boolean;
}) {
this.set({ interactionType: type, interactionStatus: status });
if (shouldCommit) {
await this.commit();
}
}
} }
export async function commitConversationAndRefreshWrapper(id: string) { export async function commitConversationAndRefreshWrapper(id: string) {

@ -1,5 +1,9 @@
import { defaults } from 'lodash'; import { defaults } from 'lodash';
import { LastMessageStatusType } from '../state/ducks/conversations'; import { LastMessageStatusType } from '../state/ducks/conversations';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../interactions/conversationInteractions';
/** /**
* Private chats have always the type `Private` * Private chats have always the type `Private`
@ -101,6 +105,10 @@ export interface ConversationAttributes {
didApproveMe: boolean; // if our message request was approved already (or they've sent us a message request/message themselves). If isApproved & didApproveMe, a message request becomes a contact didApproveMe: boolean; // if our message request was approved already (or they've sent us a message request/message themselves). If isApproved & didApproveMe, a message request becomes a contact
markedAsUnread: boolean; // Force the conversation as unread even if all the messages are read. Used to highlight a conversation the user wants to check again later, synced. markedAsUnread: boolean; // Force the conversation as unread even if all the messages are read. Used to highlight a conversation the user wants to check again later, synced.
// the last interaction we had with this conversation e.g. failed to leave a group
interactionType?: ConversationInteractionType; // e.g. Leave
interactionStatus?: ConversationInteractionStatus; // e.g. Error
} }
/** /**

@ -75,6 +75,8 @@ const allowedKeysFormatRowOfConversation = [
'conversationIdOrigin', 'conversationIdOrigin',
'markedAsUnread', 'markedAsUnread',
'priority', 'priority',
'interactionType',
'interactionStatus',
]; ];
// tslint:disable: cyclomatic-complexity // tslint:disable: cyclomatic-complexity
export function formatRowOfConversation( export function formatRowOfConversation(
@ -192,6 +194,8 @@ const allowedKeysOfConversationAttributes = [
'conversationIdOrigin', 'conversationIdOrigin',
'markedAsUnread', 'markedAsUnread',
'priority', 'priority',
'interactionType',
'interactionStatus',
]; ];
/** /**

@ -100,6 +100,7 @@ const LOKI_SCHEMA_VERSIONS = [
updateToSessionSchemaVersion29, updateToSessionSchemaVersion29,
updateToSessionSchemaVersion30, updateToSessionSchemaVersion30,
updateToSessionSchemaVersion31, updateToSessionSchemaVersion31,
updateToSessionSchemaVersion32,
]; ];
function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) { function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@ -1832,6 +1833,25 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
})(); })();
} }
function updateToSessionSchemaVersion32(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 32;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN interactionType TEXT;`).run();
db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN interactionStatus TEXT;`).run();
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
export function printTableColumns(table: string, db: BetterSqlite3.Database) { export function printTableColumns(table: string, db: BetterSqlite3.Database) {
console.info(db.pragma(`table_info('${table}');`)); console.info(db.pragma(`table_info('${table}');`));
} }

@ -443,6 +443,8 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
conversationIdOrigin, conversationIdOrigin,
priority, priority,
markedAsUnread, markedAsUnread,
interactionType,
interactionStatus,
} = formatted; } = formatted;
const omited = omit(formatted); const omited = omit(formatted);
@ -492,6 +494,8 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
displayNameInProfile, displayNameInProfile,
conversationIdOrigin, conversationIdOrigin,
markedAsUnread: toSqliteBoolean(markedAsUnread), markedAsUnread: toSqliteBoolean(markedAsUnread),
interactionType,
interactionStatus,
}); });
return fetchConvoMemoryDetails(id); return fetchConvoMemoryDetails(id);

@ -16,6 +16,10 @@ import {
} from '../../models/messageType'; } from '../../models/messageType';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { ReactionList } from '../../types/Reaction'; import { ReactionList } from '../../types/Reaction';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../interactions/conversationInteractions';
export type CallNotificationType = 'missed-call' | 'started-call' | 'answered-a-call'; export type CallNotificationType = 'missed-call' | 'started-call' | 'answered-a-call';
export type PropsForCallNotification = { export type PropsForCallNotification = {
@ -263,6 +267,9 @@ export interface ReduxConversationType {
didApproveMe?: boolean; didApproveMe?: boolean;
isMarkedUnread?: boolean; isMarkedUnread?: boolean;
interactionType?: ConversationInteractionType;
interactionStatus?: ConversationInteractionStatus;
} }
export interface NotificationForConvoOption { export interface NotificationForConvoOption {
@ -1102,3 +1109,5 @@ export async function openConversationToSpecificMessage(args: {
}) })
); );
} }
// export async function updateActionConfirmationStatus

Loading…
Cancel
Save