Merge branch 'devel'

This commit is contained in:
or-else
2020-10-11 10:23:33 -07:00
29 changed files with 1732 additions and 1155 deletions
+3 -3
View File
@@ -32,10 +32,10 @@
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16/umd/react.development.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.development.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.development.js"></script>
<!-- React-Intl for translations --> <!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3/dist/react-intl.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@4/dist/react-intl.js"></script>
<!-- Firebase for push notifications --> <!-- Firebase for push notifications -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-app.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-messaging.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-messaging.js"></script>
</head> </head>
<body> <body>
+3 -3
View File
@@ -24,9 +24,9 @@
<link rel="stylesheet" href="css/base.min.css" /> <link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3.12.1/dist/react-intl.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@4.7.6/dist/react-intl.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-app.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-messaging.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-messaging.js"></script>
</head> </head>
<body> <body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div> <noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+1231 -923
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -1,7 +1,7 @@
{ {
"name": "tinode-webapp", "name": "tinode-webapp",
"description": "Tinode messenger for the web", "description": "Tinode messenger for the web",
"version": "0.16.7", "version": "0.16.8",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/example-react-js.git" "url": "https://github.com/tinode/example-react-js.git"
@@ -68,25 +68,25 @@
"author": "Tinode Authors <info@tinode.co>", "author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"firebase": "^7.16.1", "firebase": "^7.20.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-intl": "^4.7.6", "react-intl": "^4.7.6",
"tinode-sdk": "^0.16.7" "tinode-sdk": "^0.16.8"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.5", "@babel/core": "^7.11.6",
"@babel/preset-env": "^7.10.4", "@babel/preset-env": "^7.11.5",
"@babel/preset-react": "^7.10.4", "@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-plugin-react-intl": "^7.8.2", "babel-plugin-react-intl": "^7.9.4",
"browserslist": "^4.13.0", "browserslist": "^4.14.2",
"copy-webpack-plugin": "^6.0.3", "copy-webpack-plugin": "^6.1.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"postcss": "^7.0.32", "postcss": "^7.0.32",
"postcss-cli": "^7.1.1", "postcss-cli": "^7.1.2",
"terser-webpack-plugin": "^3.0.7", "terser-webpack-plugin": "^3.1.0",
"webpack": "^4.43.0", "webpack": "^4.44.1",
"webpack-cli": "^3.3.12" "webpack-cli": "^3.3.12"
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
// Must be located at the root. // Must be located at the root.
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-app.js'); importScripts('https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-app.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-messaging.js'); importScripts('https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-messaging.js');
importScripts('firebase-init.js'); importScripts('firebase-init.js');
firebase.initializeApp(FIREBASE_INIT); firebase.initializeApp(FIREBASE_INIT);
+2
View File
@@ -31,6 +31,8 @@ export const MAX_TAG_COUNT = 16;
export const DEFAULT_P2P_ACCESS_MODE = 'JRWPS'; export const DEFAULT_P2P_ACCESS_MODE = 'JRWPS';
// Access mode for new group topics created by the current user. // Access mode for new group topics created by the current user.
export const NEW_GRP_ACCESS_MODE = 'JRWPSAO'; export const NEW_GRP_ACCESS_MODE = 'JRWPSAO';
// Access mode for a channel.
export const CHANNEL_ACCESS_MODE = 'JR';
// Access mode for no access. // Access mode for no access.
export const NO_ACCESS_MODE = 'N'; export const NO_ACCESS_MODE = 'N';
+2 -1
View File
@@ -171,5 +171,6 @@
"tags_not_found": "Keine Schlagworte definiert. Erstellen Sie welche.", "tags_not_found": "Keine Schlagworte definiert. Erstellen Sie welche.",
"tags_editor_no_tags": "Schlagworte hinzufügen", "tags_editor_no_tags": "Schlagworte hinzufügen",
"title_manage_tags": "Verwalten", "title_manage_tags": "Verwalten",
"channel_prompt": "Dies ist ein Kanal" "channel_prompt": "Dies ist ein Sender",
"channel": "Sender"
} }
+1
View File
@@ -65,6 +65,7 @@
"online_now": "online now", "online_now": "online now",
"last_seen_timestamp": "Last seen", "last_seen_timestamp": "Last seen",
"title_not_found": "Not found", "title_not_found": "Not found",
"channel": "channel",
"unnamed_topic": "Unnamed", "unnamed_topic": "Unnamed",
"messages_not_readable": "no access to messages", "messages_not_readable": "no access to messages",
"peers_messaging_disabled": "Peer's messaging is disabled.", "peers_messaging_disabled": "Peer's messaging is disabled.",
+2 -1
View File
@@ -171,5 +171,6 @@
"tags_not_found": "No hay etiquetas definidas. Añade unas.", "tags_not_found": "No hay etiquetas definidas. Añade unas.",
"tags_editor_no_tags": "Añadir etiquetas", "tags_editor_no_tags": "Añadir etiquetas",
"title_manage_tags": "Gestionar", "title_manage_tags": "Gestionar",
"channel_prompt": "Este es un canal" "channel_prompt": "Este es un canal",
"channel": "canal"
} }
+2 -1
View File
@@ -174,5 +174,6 @@
"granted_permissions": "승낙하다", "granted_permissions": "승낙하다",
"messages_not_readable": "메시지에 액세스 할 수 없습니다", "messages_not_readable": "메시지에 액세스 할 수 없습니다",
"deleted_content": "내용이 삭제되었습니다", "deleted_content": "내용이 삭제되었습니다",
"channel_prompt": "이 채널입니다" "channel_prompt": "이 채널입니다",
"channel": "채널"
} }
+2 -1
View File
@@ -171,5 +171,6 @@
"invalid_security_token": "Токен некорректен", "invalid_security_token": "Токен некорректен",
"no_connection": "Нет связи", "no_connection": "Нет связи",
"code_doesnot_match": "Код не совпадает", "code_doesnot_match": "Код не совпадает",
"channel_prompt": "Создать канал" "channel_prompt": "Создать канал",
"channel": "канал"
} }
+2 -1
View File
@@ -171,5 +171,6 @@
"invalid_security_token": "无效的安全令牌", "invalid_security_token": "无效的安全令牌",
"no_connection": "无连接", "no_connection": "无连接",
"code_doesnot_match": "代码不匹配", "code_doesnot_match": "代码不匹配",
"channel_prompt": "这是一个频道" "channel_prompt": "这是一个频道",
"channel": "频道"
} }
+1
View File
@@ -1,5 +1,6 @@
// File and image helper functions. // File and image helper functions.
import { MAX_INBAND_ATTACHMENT_SIZE, MAX_IMAGE_DIM } from '../config.js'; import { MAX_INBAND_ATTACHMENT_SIZE, MAX_IMAGE_DIM } from '../config.js';
import { bytesToHumanSize } from './strformat.js'
// Supported image MIME types and corresponding file extensions. // Supported image MIME types and corresponding file extensions.
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml']; export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
+9 -9
View File
@@ -5,8 +5,8 @@
export default class HashNavigation { export default class HashNavigation {
static parseUrlHash(hash) { static parseUrlHash(hash) {
// Split path from args, path -> parts[0], args->path[1] // Split path from args, path -> parts[0], args->path[1]
let parts = hash.split('?', 2); const parts = hash.split('?', 2);
let params = {}; const params = {};
let path = []; let path = [];
if (parts[0]) { if (parts[0]) {
path = parts[0].substr(1).split('/'); path = parts[0].substr(1).split('/');
@@ -27,9 +27,9 @@ export default class HashNavigation {
} }
static composeUrlHash(path, params) { static composeUrlHash(path, params) {
var url = path.join('/'); let url = path.join('/');
var args = []; const args = [];
for (var key in params) { for (const key in params) {
if (params.hasOwnProperty(key)) { if (params.hasOwnProperty(key)) {
args.push(key + '=' + params[key]); args.push(key + '=' + params[key]);
} }
@@ -41,25 +41,25 @@ export default class HashNavigation {
} }
static addUrlParam(hash, key, value) { static addUrlParam(hash, key, value) {
var parsed = this.parseUrlHash(hash); const parsed = this.parseUrlHash(hash);
parsed.params[key] = value; parsed.params[key] = value;
return this.composeUrlHash(parsed.path, parsed.params); return this.composeUrlHash(parsed.path, parsed.params);
} }
static removeUrlParam(hash, key) { static removeUrlParam(hash, key) {
var parsed = this.parseUrlHash(hash); const parsed = this.parseUrlHash(hash);
delete parsed.params[key]; delete parsed.params[key];
return this.composeUrlHash(parsed.path, parsed.params); return this.composeUrlHash(parsed.path, parsed.params);
} }
static setUrlSidePanel(hash, sidepanel) { static setUrlSidePanel(hash, sidepanel) {
var parsed = this.parseUrlHash(hash); const parsed = this.parseUrlHash(hash);
parsed.path[0] = sidepanel; parsed.path[0] = sidepanel;
return this.composeUrlHash(parsed.path, parsed.params); return this.composeUrlHash(parsed.path, parsed.params);
} }
static setUrlTopic(hash, topic) { static setUrlTopic(hash, topic) {
var parsed = this.parseUrlHash(hash); const parsed = this.parseUrlHash(hash);
parsed.path[1] = topic; parsed.path[1] = topic;
// Close InfoView on topic change. // Close InfoView on topic change.
delete parsed.params.info; delete parsed.params.info;
+11 -5
View File
@@ -172,7 +172,8 @@
"tags_not_found": "Keine Schlagworte definiert. Erstellen Sie welche.", "tags_not_found": "Keine Schlagworte definiert. Erstellen Sie welche.",
"tags_editor_no_tags": "Schlagworte hinzufügen", "tags_editor_no_tags": "Schlagworte hinzufügen",
"title_manage_tags": "Verwalten", "title_manage_tags": "Verwalten",
"channel_prompt": "Dies ist ein Kanal" "channel_prompt": "Dies ist ein Sender",
"channel": "Sender"
}, },
"en": { "en": {
"validate_credential_action": "confirm", "validate_credential_action": "confirm",
@@ -241,6 +242,7 @@
"online_now": "online now", "online_now": "online now",
"last_seen_timestamp": "Last seen", "last_seen_timestamp": "Last seen",
"title_not_found": "Not found", "title_not_found": "Not found",
"channel": "channel",
"unnamed_topic": "Unnamed", "unnamed_topic": "Unnamed",
"messages_not_readable": "no access to messages", "messages_not_readable": "no access to messages",
"peers_messaging_disabled": "Peer's messaging is disabled.", "peers_messaging_disabled": "Peer's messaging is disabled.",
@@ -522,7 +524,8 @@
"tags_not_found": "No hay etiquetas definidas. Añade unas.", "tags_not_found": "No hay etiquetas definidas. Añade unas.",
"tags_editor_no_tags": "Añadir etiquetas", "tags_editor_no_tags": "Añadir etiquetas",
"title_manage_tags": "Gestionar", "title_manage_tags": "Gestionar",
"channel_prompt": "Este es un canal" "channel_prompt": "Este es un canal",
"channel": "canal"
}, },
"ko": { "ko": {
"validate_credential_action": "확인", "validate_credential_action": "확인",
@@ -697,7 +700,8 @@
"granted_permissions": "승낙하다", "granted_permissions": "승낙하다",
"messages_not_readable": "메시지에 액세스 할 수 없습니다", "messages_not_readable": "메시지에 액세스 할 수 없습니다",
"deleted_content": "내용이 삭제되었습니다", "deleted_content": "내용이 삭제되었습니다",
"channel_prompt": "이 채널입니다" "channel_prompt": "이 채널입니다",
"channel": "채널"
}, },
"ru": { "ru": {
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯", "contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
@@ -872,7 +876,8 @@
"invalid_security_token": "Токен некорректен", "invalid_security_token": "Токен некорректен",
"no_connection": "Нет связи", "no_connection": "Нет связи",
"code_doesnot_match": "Код не совпадает", "code_doesnot_match": "Код не совпадает",
"channel_prompt": "Создать канал" "channel_prompt": "Создать канал",
"channel": "канал"
}, },
"zh": { "zh": {
"archived_contacts": "已归档联系人 ({count})", "archived_contacts": "已归档联系人 ({count})",
@@ -1047,6 +1052,7 @@
"invalid_security_token": "无效的安全令牌", "invalid_security_token": "无效的安全令牌",
"no_connection": "无连接", "no_connection": "无连接",
"code_doesnot_match": "代码不匹配", "code_doesnot_match": "代码不匹配",
"channel_prompt": "这是一个频道" "channel_prompt": "这是一个频道",
"channel": "频道"
} }
} }
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit. // This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.16.7"; export const PACKAGE_VERSION = "0.16.8";
+28 -16
View File
@@ -106,6 +106,7 @@ class InfoView extends React.Component {
muted: false, muted: false,
address: null, address: null,
groupTopic: undefined, groupTopic: undefined,
channel: undefined,
fullName: undefined, fullName: undefined,
avatar: null, avatar: null,
private: null, private: null,
@@ -230,6 +231,7 @@ class InfoView extends React.Component {
private: topic.private ? topic.private.comment : null, private: topic.private ? topic.private.comment : null,
address: topic.name, address: topic.name,
groupTopic: (topic.getType() == 'grp'), groupTopic: (topic.getType() == 'grp'),
channel: topic.isChannel(),
showMemberPanel: false, showMemberPanel: false,
access: acs ? acs.getMode() : undefined, access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : undefined, modeGiven: acs ? acs.getGiven() : undefined,
@@ -335,9 +337,17 @@ class InfoView extends React.Component {
case 'want': case 'want':
toEdit = this.state.modeWant; toEdit = this.state.modeWant;
toCompare = this.state.modeGiven; toCompare = this.state.modeGiven;
toSkip = this.state.owner ? 'O' : // Don't allow owner to unset 'O' permission. if (this.state.owner) {
// Don't allow owner to unset 'O' permission. All other permissions are OK.
toSkip = 'O';
} else {
// Allow accepting any of 'ASDO' permissions but don't allow asking for them. // Allow accepting any of 'ASDO' permissions but don't allow asking for them.
Tinode.AccessMode.encode(Tinode.AccessMode.diff('ASDO', this.state.modeGiven)); toSkip = Tinode.AccessMode.encode(Tinode.AccessMode.diff('ASDO', this.state.modeGiven));
if (this.state.channel) {
// Channels are read-only.
toSkip += 'W';
}
}
titleEdit = formatMessage(messages.requested); titleEdit = formatMessage(messages.requested);
titleCompare = formatMessage(messages.granted); titleCompare = formatMessage(messages.granted);
break; break;
@@ -357,11 +367,11 @@ class InfoView extends React.Component {
toSkip = 'O'; toSkip = 'O';
break; break;
case 'user': { case 'user': {
let topic = this.props.tinode.getTopic(this.props.topic); const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) { if (!topic) {
return; return;
} }
var user = topic.subscriber(uid); const user = topic.subscriber(uid);
if (!user || !user.acs) { if (!user || !user.acs) {
return; return;
} }
@@ -695,11 +705,15 @@ class InfoView extends React.Component {
null null
} }
<div className="panel-form-column"> <div className="panel-form-column">
<a href="#" className="flat-button" onClick={this.handleDeleteMessages}> {!this.state.channel ?
<i className="material-icons">delete_outline</i> &nbsp;{ <a href="#" className="flat-button" onClick={this.handleDeleteMessages}>
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages) <i className="material-icons">delete_outline</i> &nbsp;{
} formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages)
</a> }
</a>
:
null
}
<a href="#" className="red flat-button" onClick={this.handleLeave}> <a href="#" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> &nbsp;{formatMessage(messages.leave_chat)} <i className="material-icons">exit_to_app</i> &nbsp;{formatMessage(messages.leave_chat)}
</a> </a>
@@ -718,7 +732,7 @@ class InfoView extends React.Component {
null null
} }
</div> </div>
{this.state.groupTopic ? {this.state.groupTopic && this.state.sharer ?
<> <>
<div className="hr" /> <div className="hr" />
<div className="panel-form-column"> <div className="panel-form-column">
@@ -729,12 +743,10 @@ class InfoView extends React.Component {
</label> </label>
</div> </div>
<div className="panel-form-row"> <div className="panel-form-row">
{this.state.sharer ? <a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
<a href="#" className="flat-button" onClick={this.handleShowAddMembers}> <i className="material-icons">person_add</i> &nbsp;<FormattedMessage id="button_add_members"
<i className="material-icons">person_add</i> &nbsp;<FormattedMessage id="button_add_members" defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
defaultMessage="Add members" description="Flat button [Add members] (to topic)" /> </a>
</a>
: null}
</div> </div>
<FormattedMessage id="group_has_no_members" defaultMessage="No members" <FormattedMessage id="group_has_no_members" defaultMessage="No members"
description="Shown in place of group members">{ description="Shown in place of group members">{
+71 -30
View File
@@ -19,18 +19,19 @@ import { DEFAULT_P2P_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTA
MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js'; MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, filePasted, fileToBase64, imageFileToBase64, import { SUPPORTED_IMAGE_FORMATS, filePasted, fileToBase64, imageFileToBase64,
imageFileScaledToBase64, makeImageUrl } from '../lib/blob-helpers.js'; imageFileScaledToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js';
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js'; import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
// Run timer with this frequency (ms) for checking notification queue. // Run timer with this frequency (ms) for checking notification queue.
const NOTIFICATION_EXEC_INTERVAL = 300; const NOTIFICATION_EXEC_INTERVAL = 300;
const messages = defineMessages({ const messages = defineMessages({
online_now: { 'online_now': {
id: 'online_now', id: 'online_now',
defaultMessage: 'online now', defaultMessage: 'online now',
description: 'Indicator that the user or topic is currently online', description: 'Indicator that the user or topic is currently online',
}, },
last_seen: { 'last_seen': {
id: 'last_seen_timestamp', id: 'last_seen_timestamp',
defaultMessage: 'Last seen', defaultMessage: 'Last seen',
description: 'Label for the timestamp of when the user or topic was last online' description: 'Label for the timestamp of when the user or topic was last online'
@@ -39,6 +40,11 @@ const messages = defineMessages({
id: 'title_not_found', id: 'title_not_found',
defaultMessage: 'Not found', defaultMessage: 'Not found',
description: 'Title shown when topic is not found' description: 'Title shown when topic is not found'
},
'channel': {
id: 'channel',
defaultMessage: 'channel',
description: 'Subtitle shown for channels in MessagesView instead of last seen'
} }
}); });
@@ -152,9 +158,13 @@ class MessagesView extends React.Component {
// Don't request the tags. They are useless unless the user // Don't request the tags. They are useless unless the user
// is the owner and is editing the topic. // is the owner and is editing the topic.
let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub(); let getQuery = topic.startMetaQuery().withLaterDesc();
if (this.state.isSharer || (newTopic && !topic.isChannel())) {
// Request subscriptions only if one of S,O,A is given or it's a new non-channel topic.
getQuery = getQuery.withLaterSub();
}
if (this.state.isReader || newTopic) { if (this.state.isReader || newTopic) {
// If reading is either permitted or we don't know because it's a new topic. Ask for messages. // Reading is either permitted or we don't know because it's a new topic. Ask for messages.
getQuery = getQuery.withLaterData(MESSAGES_PAGE); getQuery = getQuery.withLaterData(MESSAGES_PAGE);
if (this.state.isReader) { if (this.state.isReader) {
getQuery = getQuery.withLaterDel(); getQuery = getQuery.withLaterDel();
@@ -165,6 +175,11 @@ class MessagesView extends React.Component {
const setQuery = newTopic ? this.props.newTopicParams : undefined; const setQuery = newTopic ? this.props.newTopicParams : undefined;
topic.subscribe(getQuery.build(), setQuery) topic.subscribe(getQuery.build(), setQuery)
.then((ctrl) => { .then((ctrl) => {
if (ctrl.code == 303) {
// Redirect to another topic requested.
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
return;
}
if (this.state.topic != ctrl.topic) { if (this.state.topic != ctrl.topic) {
this.setState({topic: ctrl.topic}); this.setState({topic: ctrl.topic});
} }
@@ -202,7 +217,8 @@ class MessagesView extends React.Component {
typingIndicator: false, typingIndicator: false,
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false, fetchingMessages: false,
peerMessagingDisabled: false peerMessagingDisabled: false,
channel: false
}; };
} else if (nextProps.topic != prevState.topic) { } else if (nextProps.topic != prevState.topic) {
const topic = nextProps.tinode.getTopic(nextProps.topic); const topic = nextProps.tinode.getTopic(nextProps.topic);
@@ -262,6 +278,9 @@ class MessagesView extends React.Component {
peerMessagingDisabled: false peerMessagingDisabled: false
}); });
} }
Object.assign(nextState, {
channel: topic.isChannel()
});
} else { } else {
// Invalid topic. // Invalid topic.
Object.assign(nextState, { Object.assign(nextState, {
@@ -269,7 +288,8 @@ class MessagesView extends React.Component {
onlineSubs: [], onlineSubs: [],
title: '', title: '',
avatar: null, avatar: null,
peerMessagingDisabled: false peerMessagingDisabled: false,
channel: false
}); });
} }
} }
@@ -284,6 +304,9 @@ class MessagesView extends React.Component {
if (!nextProps.acs.isReader('given') != prevState.readingBlocked) { if (!nextProps.acs.isReader('given') != prevState.readingBlocked) {
nextState.readingBlocked = !prevState.readingBlocked; nextState.readingBlocked = !prevState.readingBlocked;
} }
if (nextProps.acs.isSharer() != prevState.isSharer) {
nextState.isSharer = !prevState.isSharer;
}
} else { } else {
if (prevState.isWriter) { if (prevState.isWriter) {
nextState.isWriter = false; nextState.isWriter = false;
@@ -294,6 +317,9 @@ class MessagesView extends React.Component {
if (!prevState.readingBlocked) { if (!prevState.readingBlocked) {
prevState.readingBlocked = true; prevState.readingBlocked = true;
} }
if (prevState.isSharer) {
nextState.isSharer = false;
}
} }
if (isUnconfirmed(nextProps.acs) == !prevState.unconformed) { if (isUnconfirmed(nextProps.acs) == !prevState.unconformed) {
@@ -569,9 +595,11 @@ class MessagesView extends React.Component {
handleShowContextMenuMessage(params, messageSpecificMenuItems) { handleShowContextMenuMessage(params, messageSpecificMenuItems) {
params.topicName = this.state.topic; params.topicName = this.state.topic;
const menuItems = messageSpecificMenuItems || []; const menuItems = messageSpecificMenuItems || [];
menuItems.push('message_delete');
const topic = this.props.tinode.getTopic(params.topicName); const topic = this.props.tinode.getTopic(params.topicName);
if (topic) { if (topic) {
if (!topic.isChannel()) {
menuItems.push('message_delete');
}
const acs = topic.getAccessMode(); const acs = topic.getAccessMode();
if (acs && acs.isDeleter()) { if (acs && acs.isDeleter()) {
menuItems.push('message_delete_hard'); menuItems.push('message_delete_hard');
@@ -726,7 +754,8 @@ class MessagesView extends React.Component {
); );
} else { } else {
const topic = this.props.tinode.getTopic(this.state.topic); const topic = this.props.tinode.getTopic(this.state.topic);
const groupTopic = topic.getType() == 'grp'; const isChannel = topic.isChannel();
const groupTopic = topic.getType() == 'grp' && !isChannel;
let messageNodes = []; let messageNodes = [];
let previousFrom = null; let previousFrom = null;
let chatBoxClass = null; let chatBoxClass = null;
@@ -735,32 +764,33 @@ class MessagesView extends React.Component {
let nextFrom = null; let nextFrom = null;
if (i + 1 < this.state.messages.length) { if (i + 1 < this.state.messages.length) {
nextFrom = this.state.messages[i+1].from nextFrom = this.state.messages[i+1].from || 'chan';
} }
let sequence = 'single'; let sequence = 'single';
if (msg.from == previousFrom) { let thisFrom = msg.from || 'chan';
if (msg.from == nextFrom) { if (thisFrom == previousFrom) {
if (thisFrom == nextFrom) {
sequence = 'middle'; sequence = 'middle';
} else { } else {
sequence = 'last'; sequence = 'last';
} }
} else if (msg.from == nextFrom) { } else if (thisFrom == nextFrom) {
sequence = 'first'; sequence = 'first';
} }
previousFrom = msg.from; previousFrom = thisFrom;
const isReply = !(msg.from == this.props.myUserId); const isReply = !(thisFrom == this.props.myUserId);
const deliveryStatus = topic.msgStatus(msg); const deliveryStatus = topic.msgStatus(msg);
let userName, userAvatar, userFrom; let userName, userAvatar, userFrom;
if (groupTopic) { if (groupTopic) {
const user = topic.userDesc(msg.from); const user = topic.userDesc(thisFrom);
if (user && user.public) { if (user && user.public) {
userName = user.public.fn; userName = user.public.fn;
userAvatar = makeImageUrl(user.public.photo); userAvatar = makeImageUrl(user.public.photo);
} }
userFrom = msg.from; userFrom = thisFrom;
chatBoxClass='chat-box group'; chatBoxClass='chat-box group';
} else { } else {
chatBoxClass='chat-box'; chatBoxClass='chat-box';
@@ -769,13 +799,20 @@ class MessagesView extends React.Component {
messageNodes.push( messageNodes.push(
<ChatMessage <ChatMessage
tinode={this.props.tinode} tinode={this.props.tinode}
content={msg.content} deleted={msg.hi} content={msg.content}
deleted={msg.hi}
mimeType={msg.head ? msg.head.mime : null} mimeType={msg.head ? msg.head.mime : null}
timestamp={msg.ts} response={isReply} seq={msg.seq} timestamp={msg.ts}
userFrom={userFrom} userName={userName} userAvatar={userAvatar} response={isReply}
sequence={sequence} received={deliveryStatus} uploader={msg._uploader} seq={msg.seq}
userFrom={userFrom}
userName={userName}
userAvatar={userAvatar}
sequence={sequence}
received={deliveryStatus}
uploader={msg._uploader}
viewportWidth={this.props.viewportWidth} viewportWidth={this.props.viewportWidth}
showContextMenu={this.handleShowContextMenuMessage} showContextMenu={this.state.channel? false : this.handleShowContextMenuMessage}
onImagePreview={this.handleImagePostview} onImagePreview={this.handleImagePostview}
onFormResponse={this.handleFormResponse} onFormResponse={this.handleFormResponse}
onError={this.props.onError} onError={this.props.onError}
@@ -784,14 +821,18 @@ class MessagesView extends React.Component {
} }
let lastSeen = null; let lastSeen = null;
const cont = this.props.tinode.getMeTopic().getContact(this.state.topic); if (isChannel) {
if (cont && Tinode.topicType(cont.topic) == 'p2p') { lastSeen = formatMessage(messages.channel);
if (cont.online) { } else {
lastSeen = formatMessage(messages.online_now); const cont = this.props.tinode.getMeTopic().getContact(this.state.topic);
} else if (cont.seen) { if (cont && Tinode.isP2PTopicName(cont.topic)) {
lastSeen = formatMessage(messages.last_seen) + ": " + if (cont.online) {
shortDateFormat(cont.seen.when, this.props.intl.locale); lastSeen = formatMessage(messages.online_now);
// TODO: also handle user agent in c.seen.ua } else if (cont.seen) {
lastSeen = formatMessage(messages.last_seen) + ": " +
shortDateFormat(cont.seen.when, this.props.intl.locale);
// TODO: also handle user agent in c.seen.ua
}
} }
} }
const avatar = this.state.avatar || true; const avatar = this.state.avatar || true;
@@ -811,7 +852,7 @@ class MessagesView extends React.Component {
avatar={avatar} avatar={avatar}
topic={this.state.topic} topic={this.state.topic}
title={this.state.title} /> title={this.state.title} />
<span className={online} /> {!isChannel ? <span className={online} /> : null}
</div> </div>
<div id="topic-title-group"> <div id="topic-title-group">
<div id="topic-title" className="panel-title">{ <div id="topic-title" className="panel-title">{
+1 -1
View File
@@ -59,7 +59,7 @@ class NewTopicView extends React.Component {
handleContactSelected(sel) { handleContactSelected(sel) {
if (this.state.tabSelected === 'p2p') { if (this.state.tabSelected === 'p2p') {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(sel, undefined); this.props.onCreateTopic(sel);
} }
} }
+18 -14
View File
@@ -108,7 +108,7 @@ class TinodeWeb extends React.Component {
this.togglePushToken = this.togglePushToken.bind(this); this.togglePushToken = this.togglePushToken.bind(this);
this.requestPushToken = this.requestPushToken.bind(this); this.requestPushToken = this.requestPushToken.bind(this);
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this); this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
this.handleNewTopicRequest = this.handleNewTopicRequest.bind(this); this.handleStartTopicRequest = this.handleStartTopicRequest.bind(this);
this.handleNewTopicCreated = this.handleNewTopicCreated.bind(this); this.handleNewTopicCreated = this.handleNewTopicCreated.bind(this);
this.handleTopicUpdateRequest = this.handleTopicUpdateRequest.bind(this); this.handleTopicUpdateRequest = this.handleTopicUpdateRequest.bind(this);
this.handleChangePermissions = this.handleChangePermissions.bind(this); this.handleChangePermissions = this.handleChangePermissions.bind(this);
@@ -634,7 +634,7 @@ class TinodeWeb extends React.Component {
// For chatList topics merge only p2p topics and convert them to the // For chatList topics merge only p2p topics and convert them to the
// same format as foundContacts. // same format as foundContacts.
for (const c of chatList) { for (const c of chatList) {
if (Tinode.topicType(c.topic) == 'p2p') { if (Tinode.isP2PTopicName(c.topic)) {
merged[c.topic] = { merged[c.topic] = {
user: c.topic, user: c.topic,
updated: c.updated, updated: c.updated,
@@ -726,7 +726,7 @@ class TinodeWeb extends React.Component {
} }
// User clicked on a contact in the side panel or deleted a contact. // User clicked on a contact in the side panel or deleted a contact.
handleTopicSelected(topicName, unused_index, online, acs) { handleTopicSelected(topicName) {
// Clear newTopicParams after use. // Clear newTopicParams after use.
if (this.state.newTopicParams && this.state.newTopicParams._topicName != topicName) { if (this.state.newTopicParams && this.state.newTopicParams._topicName != topicName) {
this.setState({ this.setState({
@@ -744,8 +744,8 @@ class TinodeWeb extends React.Component {
// Different contact selected. // Different contact selected.
if (this.state.topicSelected != topicName) { if (this.state.topicSelected != topicName) {
this.setState({ this.setState({
topicSelectedOnline: online, topicSelectedOnline: this.tinode.isTopicOnline(topicName),
topicSelectedAcs: acs topicSelectedAcs: this.tinode.getTopicAccessMode(topicName)
}); });
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', topicName)); HashNavigation.navigateTo(HashNavigation.setUrlTopic('', topicName));
} }
@@ -813,7 +813,7 @@ class TinodeWeb extends React.Component {
// Accept given permissions. // Accept given permissions.
const mode = topic.getAccessMode().getGiven(); const mode = topic.getAccessMode().getGiven();
response = topic.setMeta({sub: {mode: mode}}); response = topic.setMeta({sub: {mode: mode}});
if (topic.getType() == 'p2p') { if (topic.isP2P()) {
// For P2P topics change 'given' permission of the peer too. // For P2P topics change 'given' permission of the peer too.
// In p2p topics the other user has the same name as the topic. // In p2p topics the other user has the same name as the topic.
response = response.then((ctrl) => { response = response.then((ctrl) => {
@@ -1060,22 +1060,26 @@ class TinodeWeb extends React.Component {
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, hash)); HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, hash));
} }
// Request to start a new topic. New P2P topic requires peer's name. // Request to start a topic, new or selected from search results.
handleNewTopicRequest(peerName, pub, priv, tags, isChannel) { handleStartTopicRequest(topicName, pub, priv, tags, isChannel) {
// Check if topic is indeed new. If not, launch it.
if (topicName && this.tinode.getTopic(topicName)) {
this.handleTopicSelected(topicName);
return;
}
const topicName = peerName || this.tinode.newGroupTopicName(isChannel); const params = {};
const params = { if (Tinode.isP2PTopicName(topicName)) {
_topicName: topicName,
};
if (peerName) {
// Because we are initialing the subscription, set 'want' to all permissions. // Because we are initialing the subscription, set 'want' to all permissions.
params.sub = {mode: DEFAULT_P2P_ACCESS_MODE}; params.sub = {mode: DEFAULT_P2P_ACCESS_MODE};
// Give the other user all permissions too. // Give the other user all permissions too.
params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}}; params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}};
} else { } else {
topicName = topicName || this.tinode.newGroupTopicName(isChannel);
params.desc = {public: pub, private: {comment: priv}}; params.desc = {public: pub, private: {comment: priv}};
params.tags = tags; params.tags = tags;
} }
params._topicName = topicName;
this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)}); this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)});
} }
@@ -1458,7 +1462,7 @@ class TinodeWeb extends React.Component {
onCredDelete={this.handleCredDelete} onCredDelete={this.handleCredDelete}
onCredConfirm={this.handleCredConfirm} onCredConfirm={this.handleCredConfirm}
onTopicSelected={this.handleTopicSelected} onTopicSelected={this.handleTopicSelected}
onCreateTopic={this.handleNewTopicRequest} onCreateTopic={this.handleStartTopicRequest}
onLogout={this.handleLogout} onLogout={this.handleLogout}
onDeleteAccount={this.handleDeleteAccount} onDeleteAccount={this.handleDeleteAccount}
onShowAlert={this.handleShowAlert} onShowAlert={this.handleShowAlert}
+1 -1
View File
@@ -136,7 +136,7 @@ export default class ChatMessage extends React.Component {
received={this.props.received} /> received={this.props.received} />
: null} : null}
</div> </div>
{this.props.deleted ? {this.props.deleted || !this.props.showContextMenu ?
null : null :
<span className="menuTrigger"> <span className="menuTrigger">
<a href="#" onClick={this.handleContextClick}> <a href="#" onClick={this.handleContextClick}>
+4 -1
View File
@@ -3,6 +3,8 @@
import React from 'react'; import React from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Tinode from 'tinode-sdk';
import Contact from './contact.jsx'; import Contact from './contact.jsx';
import ContactAction from './contact-action.jsx'; import ContactAction from './contact-action.jsx';
@@ -55,6 +57,7 @@ class ContactList extends React.Component {
} }
} }
const isChannel = Tinode.isChannelTopicName(key);
const selected = showCheckmark ? const selected = showCheckmark ?
(this.props.topicSelected.indexOf(key) > -1) : (this.props.topicSelected.indexOf(key) > -1) :
(this.props.topicSelected === key); (this.props.topicSelected === key);
@@ -82,7 +85,7 @@ class ContactList extends React.Component {
badges={badges} badges={badges}
showCheckmark={showCheckmark} showCheckmark={showCheckmark}
selected={selected} selected={selected}
showOnline={this.props.showOnline} showOnline={this.props.showOnline && !isChannel}
onSelected={this.props.onTopicSelected} onSelected={this.props.onTopicSelected}
showContextMenu={this.props.showContextMenu} showContextMenu={this.props.showContextMenu}
item={key} item={key}
+2 -1
View File
@@ -18,7 +18,8 @@ export default class Contact extends React.Component {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (this.props.onSelected) { if (this.props.onSelected) {
this.props.onSelected(this.props.item, this.props.index, this.props.now, this.props.acs); // Index this.props.index is used by GroupManager.
this.props.onSelected(this.props.item, this.props.index);
} }
} }
+141 -78
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+177 -47
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long