ability to hard-delete p2p messages, debugged pinned messages

This commit is contained in:
or-else
2025-02-22 18:55:53 +03:00
parent 30bf77d4ba
commit 5e92189b97
29 changed files with 498 additions and 137 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "Saved messages",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "message not found",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "message deleted",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+8
View File
@@ -604,10 +604,18 @@
"defaultMessage": "Video call",
"description": "Start video call"
},
"message_deleted": {
"defaultMessage": "message deleted",
"description": "Pinned message is deleted"
},
"message_edited_marker": {
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited"
},
"message_not_found": {
"defaultMessage": "message not found",
"description": "Pinned message is not found"
},
"message_sending": {
"defaultMessage": "sending...",
"description": "Message being sent, in place of time stamp"
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+14
View File
@@ -1671,5 +1671,19 @@
"translation": "",
"missing": true,
"obsolete": false
},
"message_not_found": {
"translation": "",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": true,
"obsolete": false
},
"message_deleted": {
"translation": "",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": true,
"obsolete": false
}
}
+58 -19
View File
@@ -327,6 +327,7 @@ class MessagesView extends React.Component {
showGoToLastButton: false,
dragging: false,
pins: [],
pinsLoaded: false,
selectedPin: 0,
subsVersion: 0
};
@@ -411,7 +412,8 @@ class MessagesView extends React.Component {
maxSeqId: topic.maxMsgSeq(),
latestClearId: topic.maxClearId(),
channel: topic.isChannelType(),
pins: (topic.aux('pins') || []).slice()
pins: (topic.aux('pins') || []).slice(),
pinsLoaded: false
});
if (nextProps.callTopic == topic.name && shouldPresentCallPanel(nextProps.callState)) {
@@ -428,7 +430,8 @@ class MessagesView extends React.Component {
avatar: null,
peerMessagingDisabled: false,
channel: false,
pins: []
pins: [],
pinsLoaded: false
});
}
} else {
@@ -798,8 +801,16 @@ class MessagesView extends React.Component {
}
// Load pinned message too (if any);
const topic = this.props.tinode.getTopic(this.state.topic);
topic.getPinnedMessages();
if (!this.state.pinsLoaded) {
// This needs to be guarded to avoid an infinite loop.
// The loop may happen if some pinned messages are deleted.
const topic = this.props.tinode.getTopic(this.state.topic);
this.setState({pinsLoaded: true}, _ => {
topic.getPinnedMessages();
});
} else {
this.setState({pinsLoaded: false});
}
}
handleAuxUpdate(aux) {
@@ -896,12 +907,35 @@ class MessagesView extends React.Component {
const menuItems = messageSpecificMenuItems || [];
const topic = this.props.tinode.getTopic(params.topicName);
if (topic) {
if (!topic.isChannelType()) {
menuItems.push('message_delete');
}
const acs = topic.getAccessMode();
if (acs && acs.isDeleter()) {
menuItems.push('message_delete_hard');
// Self -> (1) always hard-delete with [Delete] title.
// P2P -> (1) hard-delete if allowed and not too old, (2) always soft-delete.
// Group -> (1) hard-delete if allowed and not too old or owner, (2) always soft-delete.
if (topic.isSelfType()) {
// Hard-delete with plain [Delete] title.
menuItems.push('message_delete_generic');
} else {
if (!topic.isChannelType()) {
// P2P or normal group: always allow to soft-delete.
menuItems.push('message_delete');
}
const acs = topic.getAccessMode();
if (acs && acs.isDeleter()) {
// Owner can always hard-delete regardless of age.
let canDelete = acs.isOwner();
if (!canDelete) {
// Not owner: check message age if configured on the server.
const maxAge = this.props.tinode.getServerParam(Tinode.MSG_DELETE_AGE, 0) | 0;
if (maxAge > 0 && params.timestamp) {
// Make sure the message is not too old.
canDelete = params.timestamp.getTime() > (new Date().getTime() - maxAge * 1000);
}
}
if (canDelete) {
// [Delete for All] title.
menuItems.push('message_delete_hard');
}
}
}
}
this.props.showContextMenu(params, menuItems);
@@ -1700,9 +1734,10 @@ class MessagesView extends React.Component {
<div id="topic-last-seen">{lastSeen}</div>
</div>
<div style={{marginLeft: 'auto'}}/>
{!this.props.displayMobile ?
{!this.props.displayMobile && this.state.pins.length > 0 ?
<PinnedMessages
tinode={this.props.tinode}
pins={this.state.pins}
messages={pinnedMessages}
selected={this.state.selectedPin}
isAdmin={this.state.isAdmin}
@@ -1724,14 +1759,18 @@ class MessagesView extends React.Component {
</div>
{this.props.displayMobile ?
<>
<PinnedMessages
tinode={this.props.tinode}
messages={pinnedMessages}
selected={this.state.selectedPin}
isAdmin={this.state.isAdmin}
setSelected={index => this.setState({selectedPin: index})}
onSelected={this.handleQuoteClick}
onCancel={this.handleUnpinMessage} />
{this.state.pins.length > 0 ?
<PinnedMessages
tinode={this.props.tinode}
pins={this.state.pins}
messages={pinnedMessages}
selected={this.state.selectedPin}
isAdmin={this.state.isAdmin}
setSelected={index => this.setState({selectedPin: index})}
onSelected={this.handleQuoteClick}
onCancel={this.handleUnpinMessage} />
: null
}
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
+1
View File
@@ -129,6 +129,7 @@ class BaseChatMessage extends React.PureComponent {
x: e.pageX,
pickReply: this.props.pickReply,
editMessage: this.props.editMessage,
timestamp: this.props.timestamp,
}, menuItems);
}
-1
View File
@@ -76,7 +76,6 @@ class ContactList extends React.Component {
filterOn.push(('' + c.public.fn).toLowerCase());
}
if (!this.props.filterFunc(this.props.filter, filterOn)) {
console.log('Filtered:', key);
return;
}
}
+8
View File
@@ -189,6 +189,14 @@ class ContextMenu extends React.Component {
return this.deleteMessages(false, true, params, errorHandler);
}
},
// Hard-delete but with just 'Delete' title.
'message_delete_generic': {
id: 'message_delete_generic',
title: formatMessage(messages.delete),
handler: (params, errorHandler) => {
return this.deleteMessages(false, true, params, errorHandler);
}
},
'menu_item_send_retry': {
id: 'menu_item_send_retry',
title: formatMessage(messages.send_retry),
+45 -29
View File
@@ -1,20 +1,34 @@
// Send message form.
import React from 'react';
import { injectIntl } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import { previewFormatter } from '../lib/formatters.js';
import { MIN_SWIPE_DISTANCE } from '../config.js';
const messages = defineMessages({
message_not_found: {
id: 'message_not_found',
defaultMessage: 'message not found',
description: 'Pinned message is not found'
},
message_deleted: {
id: 'message_deleted',
defaultMessage: 'message deleted',
description: 'Pinned message is deleted'
}
});
class PinnedMessages extends React.PureComponent {
constructor(props) {
super(props);
this.touchSurface = React.createRef();
// Starting location of the touch gesture.
this.touchX = null;
this.touchY = null;
this.touchSurface = React.createRef();
this.getSelectedIndex = this.getSelectedIndex.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSelected = this.handleSelected.bind(this);
@@ -26,34 +40,29 @@ class PinnedMessages extends React.PureComponent {
}
componentDidMount() {
if (this.touchSurface.current) {
this.touchSurface.current.addEventListener('touchstart', this.handleTouchEventStart, {passive: true});
this.touchSurface.current.addEventListener('touchend', this.handleTouchEventEnd, {passive: true});
this.touchSurface.current.addEventListener('touchcancel', this.handleTouchCancel, {passive: true});
}
this.touchSurface.current.addEventListener('touchstart', this.handleTouchEventStart, {passive: true});
this.touchSurface.current.addEventListener('touchend', this.handleTouchEventEnd, {passive: true});
this.touchSurface.current.addEventListener('touchcancel', this.handleTouchCancel, {passive: true});
}
componentWillUnmount() {
if (this.touchSurface.current) {
this.touchSurface.current.removeEventListener('touchstart', this.handleTouchEventStart);
this.touchSurface.current.removeEventListener('touchend', this.handleTouchEventEnd);
this.touchSurface.current.removeEventListener('touchcancel', this.handleTouchCancel);
}
this.touchSurface.current.removeEventListener('touchstart', this.handleTouchEventStart);
this.touchSurface.current.removeEventListener('touchend', this.handleTouchEventEnd);
this.touchSurface.current.removeEventListener('touchcancel', this.handleTouchCancel);
}
getSelectedIndex() {
const list = (this.props.messages || []);
const list = (this.props.pins || []);
return list.length - this.props.selected - 1;
}
handleCancel(e) {
e.preventDefault();
this.props.onCancel(this.props.messages[this.getSelectedIndex()].seq);
this.props.onCancel(this.props.pins[this.getSelectedIndex()]);
}
handleSelected(e) {
e.preventDefault();
this.props.onSelected(this.props.messages[this.getSelectedIndex()].seq);
this.props.onSelected(this.props.pins[this.getSelectedIndex()]);
}
handleMoveNext(e, isTouch) {
@@ -70,7 +79,7 @@ class PinnedMessages extends React.PureComponent {
// Don't call preventDefault for touch events.
isTouch || e.preventDefault();
e.stopPropagation();
const idx = Math.min(this.props.selected + 1, (this.props.messages || []).length - 1);
const idx = Math.min(this.props.selected + 1, (this.props.pins || []).length - 1);
if (idx != this.props.selected) {
this.props.setSelected(idx);
}
@@ -105,26 +114,32 @@ class PinnedMessages extends React.PureComponent {
}
}
handleTouchCancel(e) {
handleTouchCancel() {
this.touchX = null;
}
render() {
const selected = this.getSelectedIndex();
let shown = (this.props.messages || [])[selected];
shown = shown ? Drafty.format(shown.content, previewFormatter, {
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
}) : null;
let messageShown = (this.props.messages || [])[selected];
messageShown = messageShown ?
messageShown._deleted ?
<i className="gray">{this.props.intl.formatMessage(messages.message_deleted)}</i>
:
Drafty.format(messageShown.content, previewFormatter, {
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
})
:
<i className="gray">{this.props.intl.formatMessage(messages.message_not_found)}</i>;
const dots = [];
this.props.messages.forEach(_ => {
this.props.pins.forEach(seq => {
const cn = dots.length == selected ? 'adot' : 'dot';
dots.push(<div key={dots.length} className={cn} />);
dots.push(<div key={seq} className={cn} />);
});
return shown ?
(<div id="pinned-wrapper" ref={this.touchSurface}>
return (
<div id="pinned-wrapper" ref={this.touchSurface}>
{this.props.isAdmin ?
<div className="cancel">
<a href="#" onClick={this.handleCancel}><i className="material-icons gray">close</i></a>
@@ -132,7 +147,7 @@ class PinnedMessages extends React.PureComponent {
<div><i className="material-icons gray">push_pin</i></div>
}
<div className="pinned-scroll">{dots}</div>
<div className="pinned" onClick={this.handleSelected}><p>{shown}</p></div>
<div className="pinned" onClick={this.handleSelected}><p>{messageShown}</p></div>
<div className="pinned-menu">
<span className="menuTrigger upper">
{selected > 0 ?
@@ -151,7 +166,8 @@ class PinnedMessages extends React.PureComponent {
}
</span>
</div>
</div>) : null;
</div>
);
}
}
+5 -3
View File
@@ -4,6 +4,8 @@ import { FormattedMessage } from 'react-intl';
import ChipInput from './chip-input.jsx';
import { Tinode } from 'tinode-sdk';
import { MAX_TAG_COUNT, MAX_TAG_LENGTH, MIN_TAG_LENGTH } from '../config.js';
import { arrayEqual } from '../lib/utils.js';
@@ -49,7 +51,7 @@ export default class TagManager extends React.Component {
}
handleAddTag(tag) {
const maxTagCount = this.props.tinode.getServerParam('maxTagCount', MAX_TAG_COUNT);
const maxTagCount = this.props.tinode.getServerParam(Tinode.MAX_TAG_COUNT, MAX_TAG_COUNT);
if (tag.length > 0 && this.state.tags.length < maxTagCount) {
const tags = this.state.tags.slice(0);
@@ -87,8 +89,8 @@ export default class TagManager extends React.Component {
}
render() {
const minTagLength = this.props.tinode.getServerParam('minTagLength', MIN_TAG_LENGTH);
const maxTagLength = this.props.tinode.getServerParam('maxTagLength', MAX_TAG_LENGTH);
const minTagLength = this.props.tinode.getServerParam(Tinode.MIN_TAG_LENGTH, MIN_TAG_LENGTH);
const maxTagLength = this.props.tinode.getServerParam(Tinode.MAX_TAG_LENGTH, MAX_TAG_LENGTH);
let tags = [];
if (this.state.activated) {