路由从 hash 换成 History API(pushState/replaceState/popstate),不再依赖 location.hash

HashNavigation 改名 Navigation(src/lib/navigation.js),接口不变,内部改用真实路径:
parseUrlHash/composeUrlHash 改名 parseUrl/composeUrl;navigateTo/navigateReplace 改调
pushState/replaceState 后手动派发 popstate(pushState/replaceState 不会像赋值 location.hash
那样自动触发事件,这里复用 navigateReplace 早就在用的同一个手动 dispatch 手法);goBack()
不用改,history.back() 本来就是真实浏览器历史机制。新增 Navigation.currentUrl() 替代调用方
原本直接读的 window.location.hash(这个类从来没包装过这个读取)。

一处需要专门处理的边界情况:当 path[0] 是空字符串("不选中侧栏视图、只开一个会话"这个仓库里
的合法状态)时,拼出的真实路径开头会变成双斜杠,被浏览器当协议相对地址解析、试图跳到一个叫
会话id的host,pushState 直接抛 SecurityError。composeUrl/parseUrl 内部用占位 token "_" 处理
这种情况,对外的调用方(19个文件)完全无感。

tinode-web.jsx 的 handleHashRoute 改名 handleRouteChange,监听从 hashchange 换成 popstate;
service-worker.js 的推送通知点击跳转手动改写成同样的路径格式(这个文件在打包之外单独执行,
没法 import Navigation)。index.html/index-dev.html 加了 <base href="/">——所有静态资源引用
都是相对路径,hash 路由下页面路径永远是 /,换真实路径后深层地址刷新会按当前目录解析出错误
资源地址导致白屏,这一行是标准修法。nginx.conf 的 try_files 兜底规则本来就适用两种路由,只
更新了注释。

实机验证:地址栏不再有 #;点击会话/设置等各类视图正常跳转;浏览器前进后退和应用内"返回"
按钮都正常;收藏页内部的类型 tab 切换(navigateReplace)确认不产生新的历史记录;在会话页/
个人中心页刷新、直接用完整深层路径打开新标签页都能正确恢复到位,资源正常加载不白屏。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-07 21:00:39 +08:00
co-authored by Claude Sonnet 5
parent 97a6b88d59
commit 4e8f45d7d8
46 changed files with 398 additions and 330 deletions
+3 -2
View File
@@ -5,8 +5,9 @@ server {
root /usr/share/nginx/html;
index index.html;
# Tinode webapp uses hash-based client-side routing, so any unknown path
# can safely fall back to index.html.
# Tinode webapp uses real (History API) client-side routing, so any unknown path
# can safely fall back to index.html -- the app resolves the actual view/topic
# client-side from the path itself once index.html loads.
location / {
try_files $uri $uri/ /index.html;
}
+6
View File
@@ -4,6 +4,12 @@
<head>
<title>MeetingIM</title>
<meta charset="utf-8" />
<!-- Real (History API) routing means the address bar can show a deep, multi-segment path like
/messages-home/grpXXX -- every asset reference below is a relative path (css/base.css,
umd/index.dev.js, etc.), which would otherwise resolve against that deep path instead of the
app's actual root on a deep-linked load or a refresh, 404ing everything. This pins relative
resolution to root regardless of the current path. -->
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="application-name" content="MeetingIM, version for developers" />
<meta name="description" content="MeetingIM instant messenger, development version" />
+6
View File
@@ -3,6 +3,12 @@
<head>
<title>MeetingIM</title>
<meta charset="utf-8" />
<!-- Real (History API) routing means the address bar can show a deep, multi-segment path like
/messages-home/grpXXX -- every asset reference below is a relative path (css/base.min.css,
umd/index.prod.js, etc.), which would otherwise resolve against that deep path instead of the
app's actual root on a deep-linked load or a refresh, 404ing everything. This pins relative
resolution to root regardless of the current path. -->
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="application-name" content="MeetingIM" />
<meta name="description" content="MeetingIM instant messenger in a browser" />
+9 -4
View File
@@ -131,7 +131,12 @@ self.addEventListener('notificationclick', event => {
return;
}
const urlHash = '#/' + data.topic;
// '_' mirrors the placeholder Navigation.composeUrl (src/lib/navigation.js) substitutes for an
// empty path[0] ("no sidepanel view, just open this topic") -- a real empty first segment would
// make the path start with '//', which pushState/URL resolve as a protocol-relative reference
// (host becomes "data.topic") rather than a same-origin path. This file runs outside the webpack
// bundle (importScripts, not an ES module) so it can't import the constant; kept in sync by hand.
const urlPath = '/_/' + data.topic;
event.waitUntil(self.clients.matchAll({
type: 'window',
@@ -141,7 +146,7 @@ self.addEventListener('notificationclick', event => {
let anyClient = null;
for (let i = 0; i < windowClients.length; i++) {
const url = new URL(windowClients[i].url);
if (url.hash.includes(data.topic)) {
if (url.pathname.includes(data.topic)) {
// Found the Tinode tab with the right topic open.
return windowClients[i].focus();
} else {
@@ -154,7 +159,7 @@ self.addEventListener('notificationclick', event => {
// navigate to the right topic.
if (anyClient) {
const url = new URL(anyClient.url);
url.hash = urlHash;
url.pathname = urlPath;
return anyClient.focus().then(thisClient => {
return thisClient.navigate(url);
});
@@ -162,7 +167,7 @@ self.addEventListener('notificationclick', event => {
// Did not find a Tinode browser tab. Open one.
const url = new URL(self.location.origin);
url.hash = urlHash;
url.pathname = urlPath;
return clients.openWindow(url);
}));
});
+2 -2
View File
@@ -7,7 +7,7 @@ import { createRoot } from 'react-dom/client';
import { IntlProvider } from 'react-intl';
import TinodeWeb from './views/tinode-web.jsx';
import HashNavigation from './lib/navigation.js';
import Navigation from './lib/navigation.js';
// Insert google analytics script and tag if configured.
if (typeof FIREBASE_INIT != 'undefined' && FIREBASE_INIT && FIREBASE_INIT.measurementId) {
@@ -45,7 +45,7 @@ const rtl = ['ar', 'fa', 'he', 'ur'];
// Detect human language to use in the UI:
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
const { params } = HashNavigation.parseUrlHash(window.location.hash);
const { params } = Navigation.parseUrl(Navigation.currentUrl());
const language = (params && params.hl) ||
(navigator.languages && navigator.languages[0]) ||
navigator.language ||
+79 -44
View File
@@ -1,7 +1,7 @@
// Utility class for hash navigation.
// Utility class for client-side routing via the History API.
// Every `navigateTo` call assigns `location.hash`, which the browser already records as a normal
// history entry -- `history.back()` would land on whatever the user actually saw before, for free.
// Every `navigateTo` call pushes a real history entry -- `history.back()` would land on whatever
// the user actually saw before, for free.
// The product's "back" buttons never used that: each page hardcodes a single destination view name
// (e.g. Profile's `back={friend ? 'friend-profile' : 'chat-standard'}`), which is only ever right for
// the one entry point its author had in mind. Reach the same page from a second place (search result
@@ -12,16 +12,28 @@
// counter is in-memory and intentionally does not survive those).
let navigationCount = 0;
export default class HashNavigation {
// Parse hash as in http://www.example.com/path#hash as if it were
// path and arguments.
static parseUrlHash(hash) {
// Placeholder path segment for "no sidepanel view selected, just a topic" (path[0] === '', the last
// entry in tinode-web.jsx's own SIDE_PANEL_VIEWS whitelist). A real empty path segment can't be
// represented safely as the FIRST segment of a real URL path: joining ['', 'grpXXX'] would produce
// "/grpXXX" before the leading '/' this class adds is even accounted for, so the final string comes
// out "//grpXXX" -- and a path starting with '//' is resolved by pushState/replaceState as a
// protocol-relative reference (same scheme, host becomes "grpXXX"), not a same-origin path. Swapping
// in this token keeps composeUrl/parseUrl round-tripping path[0] === '' exactly like before, entirely
// inside this file -- no caller needs to know the placeholder exists.
const EMPTY_VIEW_PLACEHOLDER = '_';
export default class Navigation {
// Parse a route string (path + optional '?args') into path segments and params.
static parseUrl(url) {
// Split 'path/?args' into 'path/' and 'args', path -> parts[0], args->path[1].
const parts = hash.split('?', 2);
const parts = url.split('?', 2);
const params = {};
let path = [];
if (parts[0]) {
path = parts[0].replace('#', '').split('/');
path = parts[0].replace(/^\//, '').split('/');
if (path.length > 1 && path[0] === EMPTY_VIEW_PLACEHOLDER) {
path[0] = '';
}
}
if (parts[1]) {
parts[1].split('&').forEach((arg) => {
@@ -35,34 +47,45 @@ export default class HashNavigation {
return {path: path, params: params};
}
// The current route string, in the same shape `navigateTo`/`navigateReplace` consume and
// `parseUrl` expects -- callers that used to read `window.location.hash` directly read this
// instead (this class never wrapped that read itself, so every call site has to switch explicitly).
static currentUrl() {
return window.location.pathname + window.location.search;
}
static navigateTo(url) {
navigationCount++;
window.location.hash = url;
window.history.pushState(null, '', url);
// pushState never fires popstate on its own (only real back/forward/go() does) -- dispatch a
// synthetic one so tinode-web.jsx's listener re-derives state from the new URL, same trick
// navigateReplace below already uses for replaceState's identical silence.
window.dispatchEvent(new PopStateEvent('popstate'));
}
// For switching between sub-tabs of the *same* page (e.g. content-library-view.jsx's favorites/
// history type tabs), not for navigating to a different page. `navigateTo`/`location.hash =`
// always pushes a new history entry, so clicking through N tabs pushes N entries -- the system
// back button (and this class's own goBack()) then has to step back through every tab the user
// ever visited before it reaches whatever page they were actually on before this one. `replaceState`
// updates the hash (still correct for a refresh or a copied link) without adding a history entry,
// so it doesn't inflate navigationCount either -- a tab switch was never a real in-app navigation
// to begin with. replaceState doesn't fire `hashchange` on its own, so dispatch one manually for
// the existing `window.addEventListener('hashchange', ...)` listener (tinode-web.jsx's
// handleHashRoute) to pick up; that handler re-reads window.location.hash itself rather than the
// event, so a synthetic event with no detail is indistinguishable from a real one to it.
// history type tabs), not for navigating to a different page. `navigateTo` always pushes a new
// history entry, so clicking through N tabs pushes N entries -- the system back button (and this
// class's own goBack()) then has to step back through every tab the user ever visited before it
// reaches whatever page they were actually on before this one. `replaceState` updates the URL
// (still correct for a refresh or a copied link) without adding a history entry, so it doesn't
// inflate navigationCount either -- a tab switch was never a real in-app navigation to begin with.
// replaceState doesn't fire `popstate` on its own, so dispatch one manually for the existing
// `window.addEventListener('popstate', ...)` listener (tinode-web.jsx's handleRouteChange) to pick
// up; that handler re-reads the current URL itself rather than the event, so a synthetic event with
// no detail is indistinguishable from a real one to it.
static navigateReplace(url) {
window.history.replaceState(null, '', '#' + url);
window.dispatchEvent(new Event('hashchange'));
window.history.replaceState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate'));
}
// Real "back": retraces the hash the user was actually on before, via native browser history,
// Real "back": retraces the route the user was actually on before, via native browser history,
// rather than a page's own guess at where it must have been reached from. Returns whether it
// actually did that -- every call site already has its own hardcoded-destination `navigate(...)`
// fallback (a page's `back`/`backUid`/`backTopic` props) for the one case this can't cover: a
// fresh load, deep link, or refresh, where there's no in-app history yet to go back to (this
// counter is in-memory by design and does not survive those) -- callers keep using that fallback
// unchanged, just gated behind `if (!HashNavigation.goBack()) { ...existing fallback... }`.
// unchanged, just gated behind `if (!Navigation.goBack()) { ...existing fallback... }`.
static goBack() {
if (navigationCount > 0) {
navigationCount--;
@@ -72,8 +95,20 @@ export default class HashNavigation {
return false;
}
static composeUrlHash(path, params) {
let url = path.join('/');
static composeUrl(path, params) {
let segments = path;
// !segments[0] (not === '') deliberately also catches a sparse hole at index 0 -- parseUrl('')
// returns path: [] (empty, not ['']; inherited as-is from the original hash-based parseUrlHash,
// which had the identical `if (parts[0])` guard), and setUrlTopic('', topic) then does
// `parsed.path[1] = topic` on that empty array, leaving index 0 an actual hole rather than ''.
// Array#join treats a hole the same as an empty string either way, so this was always silently
// producing the same "no view, just a topic" shape -- harmless under the old scheme since a
// leading '#//' is still just inert fragment text, but a real path starting '//' is parsed by
// pushState/URL as protocol-relative (steals the topic id as the new host) and throws.
if (segments.length > 1 && !segments[0]) {
segments = [EMPTY_VIEW_PLACEHOLDER, ...segments.slice(1)];
}
let url = '/' + segments.join('/');
const args = [];
for (const key in params) {
if (params.hasOwnProperty(key)) {
@@ -88,50 +123,50 @@ export default class HashNavigation {
return url;
}
static addUrlParam(hash, key, value) {
const parsed = HashNavigation.parseUrlHash(hash);
static addUrlParam(url, key, value) {
const parsed = Navigation.parseUrl(url);
parsed.params[key] = value;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static removeUrlParam(hash, key) {
const parsed = HashNavigation.parseUrlHash(hash);
static removeUrlParam(url, key) {
const parsed = Navigation.parseUrl(url);
delete parsed.params[key];
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static removeUrlParams(hash, keys) {
static removeUrlParams(url, keys) {
if (!Array.isArray(keys)) {
keys = [keys];
}
const parsed = HashNavigation.parseUrlHash(hash);
const parsed = Navigation.parseUrl(url);
for (const key of keys) {
delete parsed.params[key];
}
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static setUrlSidePanel(hash, sidepanel) {
const parsed = HashNavigation.parseUrlHash(hash);
static setUrlSidePanel(url, sidepanel) {
const parsed = Navigation.parseUrl(url);
parsed.path[0] = sidepanel;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static setUrlInfoPanel(hash, infopanel) {
const parsed = HashNavigation.parseUrlHash(hash);
static setUrlInfoPanel(url, infopanel) {
const parsed = Navigation.parseUrl(url);
if (infopanel) {
parsed.params.info = infopanel;
} else {
delete parsed.params.info;
}
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static setUrlTopic(hash, topic) {
const parsed = HashNavigation.parseUrlHash(hash);
static setUrlTopic(url, topic) {
const parsed = Navigation.parseUrl(url);
parsed.path[1] = topic;
// Close InfoView on topic change.
delete parsed.params.info;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import React from 'react';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { BASE_APP_NAME } from '../config.js';
import {
resolveTenant, submitTenantApplication, submitTenantEntryFeedback, uploadPublicFile,
@@ -16,8 +16,8 @@ const CREDIT_CODE_RE = /^[0-9A-Z]{18}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// params is optional and additive only -- every existing call site that passes just `view` keeps
// producing the exact same bare hash as the old `HashNavigation.navigateTo(view)` did.
const navigate = (view, params) => HashNavigation.navigateTo(HashNavigation.composeUrlHash([view], params || {}));
// producing the exact same bare hash as the old `Navigation.navigateTo(view)` did.
const navigate = (view, params) => Navigation.navigateTo(Navigation.composeUrl([view], params || {}));
function Icon({children, className = ''}) {
return <i className={'material-icons ' + className} aria-hidden="true">{children}</i>;
@@ -32,7 +32,7 @@ function AppMark({tenantName}) {
function Header({title, back = 'tenant'}) {
return <header className="auth-header">
<button type="button" className="auth-icon-button" onClick={() => { if (!HashNavigation.goBack()) navigate(back); }} aria-label="返回">
<button type="button" className="auth-icon-button" onClick={() => { if (!Navigation.goBack()) navigate(back); }} aria-label="返回">
<Icon>arrow_back_ios_new</Icon>
</button>
<h1>{title}</h1>
+2 -2
View File
@@ -1,8 +1,8 @@
import React from 'react';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
const navigate = view => HashNavigation.navigateTo(view);
const navigate = view => Navigation.navigateTo(view);
export const CALL_EXPERIENCE_VIEWS = [
'call-history', 'call-incoming', 'audio-call-ringing', 'audio-call-active',
+12 -12
View File
@@ -2,7 +2,7 @@ import React from 'react';
import QRCode from 'qrcodejs';
import { pinyin } from 'pinyin-pro';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import {
getFriends, getFriendRequests, createFriendRequest, acceptFriendRequest, rejectFriendRequest,
deleteFriend, updateFriendRemark, searchUsers, getUserProfile, createMyQRCode, getBlocks,
@@ -14,9 +14,9 @@ import { readCachedData, writeCachedData, useCachedData } from '../lib/use-cache
// Carries an optional `uid` (and anything else already in the hash) across chapter-5 navigation --
// e.g. address-book -> friend-profile -> friend-settings all need to know which person this is.
const navigate = (view, uid) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
hash = uid ? HashNavigation.addUrlParam(hash, 'uid', uid) : HashNavigation.removeUrlParam(hash, 'uid');
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
hash = uid ? Navigation.addUrlParam(hash, 'uid', uid) : Navigation.removeUrlParam(hash, 'uid');
Navigation.navigateTo(hash);
};
export const CONTACT_MANAGEMENT_VIEWS = [
@@ -44,7 +44,7 @@ function alphaInitial(name) {
function Icon({children}) { return <i className="material-icons" aria-hidden="true">{children}</i>; }
function Header({title, back = 'messages-home', backUid, right, onRight}) {
return <header className="people-header"><button type="button" onClick={() => { if (!HashNavigation.goBack()) navigate(back, backUid); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>{title}</h1>{right ? <button type="button" className="text" onClick={onRight}>{right}</button> : <span />}</header>;
return <header className="people-header"><button type="button" onClick={() => { if (!Navigation.goBack()) navigate(back, backUid); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>{title}</h1>{right ? <button type="button" className="text" onClick={onRight}>{right}</button> : <span />}</header>;
}
// `person.avatar_file_id` (real API field) is loaded lazily via a cached authenticated fetch --
@@ -589,8 +589,8 @@ function Settings({uid, initialBlocked, pinBlocked, onToast}) {
};
return <div className="people-page"><Header title="聊天设置" back="friend-profile" backUid={uid} /><main className="people-settings">
<section className="people-settings-profile"><Avatar person={profile} /><strong>{(profile && profile.display_name) || ''}</strong></section>
<section><button type="button" onClick={() => HashNavigation.navigateTo(
HashNavigation.addUrlParam(HashNavigation.setUrlSidePanel(window.location.hash, 'history-search'), 'topic', uid))}>
<section><button type="button" onClick={() => Navigation.navigateTo(
Navigation.addUrlParam(Navigation.setUrlSidePanel(Navigation.currentUrl(), 'history-search'), 'topic', uid))}>
<span>查看聊天记录</span><Icon>chevron_right</Icon>
</button>
<div><span>消息传输方式</span><small>已启动加密传输</small></div></section>
@@ -622,11 +622,11 @@ function Settings({uid, initialBlocked, pinBlocked, onToast}) {
// The local navigate(view, uid) helper only carries a single `uid` param -- the report
// flow (security-support-view.jsx's ReportReasons/ReportDetails) needs target_type/
// target_id/target_name, so build the hash directly here rather than through it.
let hash = HashNavigation.setUrlSidePanel(window.location.hash, 'abuse-report-reasons');
hash = HashNavigation.addUrlParam(hash, 'target_type', 'user');
hash = HashNavigation.addUrlParam(hash, 'target_id', uid);
hash = HashNavigation.addUrlParam(hash, 'target_name', (profile && profile.display_name) || uid);
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), 'abuse-report-reasons');
hash = Navigation.addUrlParam(hash, 'target_type', 'user');
hash = Navigation.addUrlParam(hash, 'target_id', uid);
hash = Navigation.addUrlParam(hash, 'target_name', (profile && profile.display_name) || uid);
Navigation.navigateTo(hash);
}}><span>投诉与举报</span><Icon>chevron_right</Icon></button>
</section>
{blocked ? null : <button type="button" className="people-danger" onClick={() => navigate('friend-delete-confirm', uid)}>删除好友</button>}
+9 -9
View File
@@ -4,7 +4,7 @@ import { Drafty, Tinode } from 'tinode-sdk';
import AudioPlayer from '../widgets/audio-player.jsx';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { bytesToHumanSize, secondsToTime, shortDateFormat } from '../lib/strformat.js';
import {
getFavorites, getFavorite, batchDeleteFavorites, forwardFavorites, createFileDownloader,
@@ -40,15 +40,15 @@ function resolveSourceName(topic, onDone) {
}
// Generic multi-param navigate -- content-library-view.jsx's original helper was a bare
// `HashNavigation.navigateTo(view)`, replacing the whole hash and dropping any existing params on
// `Navigation.navigateTo(view)`, replacing the whole hash and dropping any existing params on
// every internal tab/link click; nothing in this file could carry a topic/favoriteId through its own
// navigation even if a caller threaded one in from outside.
const navigate = (view, params) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? HashNavigation.addUrlParam(hash, key, value) : HashNavigation.removeUrlParam(hash, key);
hash = value ? Navigation.addUrlParam(hash, key, value) : Navigation.removeUrlParam(hash, key);
});
HashNavigation.navigateTo(hash);
Navigation.navigateTo(hash);
};
// Same as navigate() above but for switching this page's own type tabs (Favorites' 全部/图片/.../链接,
@@ -56,7 +56,7 @@ const navigate = (view, params) => {
// which page the user is on, so they shouldn't push a browser-history entry the way a real navigate()
// does. Previously every tab click called navigate(), so N tab switches pushed N history entries and
// the system back button just cycled through past tabs instead of leaving the page.
const navigateReplace = (view) => HashNavigation.navigateReplace(HashNavigation.setUrlSidePanel(window.location.hash, view));
const navigateReplace = (view) => Navigation.navigateReplace(Navigation.setUrlSidePanel(Navigation.currentUrl(), view));
export const CONTENT_LIBRARY_VIEWS = [
'favorites', 'favorites-mixed', 'favorites-files', 'favorites-text', 'favorites-voice',
@@ -68,7 +68,7 @@ export const CONTENT_LIBRARY_VIEWS = [
function Icon({children}) { return <i className="material-icons" aria-hidden="true">{children}</i>; }
function Header({title, back = 'messages-home', backParams, right, onRight}) {
return <header className="library-header">
<button type="button" onClick={() => { if (!HashNavigation.goBack()) navigate(back, backParams); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<button type="button" onClick={() => { if (!Navigation.goBack()) navigate(back, backParams); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<h1>{title}</h1>
{right ? <button type="button" className="text" onClick={onRight}>{right}</button> : <span />}
</header>;
@@ -695,11 +695,11 @@ function History({view, hashParams, myUserId, onToast}) {
}).catch(() => {});
});
}, [items]);
// Mirrors tinode-web.jsx's own handleTopicSelected exactly (HashNavigation.setUrlTopic('', topicName)
// Mirrors tinode-web.jsx's own handleTopicSelected exactly (Navigation.setUrlTopic('', topicName)
// -- an empty base hash, not the current one, per its "Clear newTopicParams after use" comment) so
// this lands the same way a normal chat-list click does, then layers the jump target on as a param;
// navigate()'s own helper only ever touches path[0]+params, never path[1] (the actual topic slot).
const jump = seq => HashNavigation.navigateTo(HashNavigation.addUrlParam(HashNavigation.setUrlTopic('', topic), 'jumpseq', seq));
const jump = seq => Navigation.navigateTo(Navigation.addUrlParam(Navigation.setUrlTopic('', topic), 'jumpseq', seq));
const nameOf = from => {
if (!from) return '';
if (from === myUserId || ('usr' + from) === myUserId) return '我';
+2 -2
View File
@@ -1,8 +1,8 @@
import React from 'react';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
const navigate = view => HashNavigation.navigateTo(view);
const navigate = view => Navigation.navigateTo(view);
const VIEW_CONFIG = {
'chat-inbound-request': {relationship: 'pending_inbound', variant: 'inbound-request'},
+18 -18
View File
@@ -2,7 +2,7 @@ import React from 'react';
import QRCode from 'qrcodejs';
import { pinyin } from 'pinyin-pro';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import {
getGroups, createGroup, getGroup, updateGroup, getGroupSettings, updateGroupMemberSettings,
updateMyGroupProfile, getMyGroupProfile, leaveGroup, getGroupMembers, createGroupQRCode, getGroupJoinRequests,
@@ -17,9 +17,9 @@ import { readCachedData, writeCachedData, useDebouncedValue } from '../lib/use-c
// Carries the group `topic` (and anything else already in the hash) across navigation, the same way
// contact-management-view.jsx threads `uid` -- these pages all need to know *which* group.
const navigate = (view, topic) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
hash = topic ? HashNavigation.addUrlParam(hash, 'topic', topic) : HashNavigation.removeUrlParam(hash, 'topic');
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
hash = topic ? Navigation.addUrlParam(hash, 'topic', topic) : Navigation.removeUrlParam(hash, 'topic');
Navigation.navigateTo(hash);
};
// 'group-member-profile' is contact-management-view.jsx's Profile component (Ch.5) reused as-is for
@@ -45,8 +45,8 @@ const navigateToProfile = (topic, userId, member) => {
...(member.gender ? {gender: member.gender} : {})
});
}
HashNavigation.navigateTo(
HashNavigation.addUrlParam(HashNavigation.setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
Navigation.navigateTo(
Navigation.addUrlParam(Navigation.setUrlSidePanel(Navigation.currentUrl(), 'group-member-profile'), 'uid', userId));
// 群场景下真正该用的是 GET /groups/{topic}/members/{user_id}/profile——群上下文里权限判断更
// 宽松,实测对方哪怕不是好友也能拿到 {capabilities, relationship}(Profile 组件目标页读的
// GET /users/{id}/profile 对非好友是 403,只能回退用种子)。异步补种:请求回来后如果用户还
@@ -140,7 +140,7 @@ function Icon({children}) { return <i className="material-icons" aria-hidden="tr
// navigate 辅助函数只会写 query 参数),这类情况传一个自己的回调进来。
function Header({title, back = 'group-list', backTopic, onBack, right, onRight}) {
return <header className="group-header">
<button type="button" onClick={() => { if (!HashNavigation.goBack()) { if (onBack) onBack(); else navigate(back, backTopic); } }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<button type="button" onClick={() => { if (!Navigation.goBack()) { if (onBack) onBack(); else navigate(back, backTopic); } }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<h1>{title}</h1>
{right ? <button type="button" className="text" onClick={onRight}>{right}</button> : <span />}
</header>;
@@ -266,7 +266,7 @@ function JoinRequests({topic, fromChat, onToast}) {
// ambiguity above -- without it, returning from a click on the in-chat banner always landed on
// "group-settings" (closing the open conversation) instead of back to the chat itself.
const backToChat = React.useCallback(() => {
HashNavigation.navigateTo(HashNavigation.setUrlTopic(HashNavigation.setUrlSidePanel('', ''), topic));
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.setUrlSidePanel('', ''), topic));
}, [topic]);
const [items, setItems] = React.useState(() => (topic && readCachedData('group-join-requests:' + topic)) || null);
const [profiles, setProfiles] = React.useState({});
@@ -337,14 +337,14 @@ function Announcement({topic, myRole, fromChat, onToast}) {
const canEdit = myRole === 'owner' || myRole === 'admin';
// Coming from the announcement banner atop the chat itself (messages-view.jsx's
// handleShowAnnouncement) vs. from group-settings' "群公告" row land on the identical
// path[0]=group-announcement-detail hash -- `fromChat` is the only thing telling them apart, so
// path[0]=group-announcement-detail URL -- `fromChat` is the only thing telling them apart, so
// "back" can return to the chat (close the sidepanel) instead of always forcing group-settings.
// 从聊天页横幅进来时(messages-view.jsx:1152 用空基准拼 hash,所以 path[1] 是空的),返回应该
// 从聊天页横幅进来时(messages-view.jsx:1152 用空基准拼路径,所以 path[1] 是空的),返回应该
// 关掉侧栏回到那个会话。原来写的是 navigate('', topic)——navigate 只把 topic 写成 query 参数,
// path[1] 依旧为空,tinode-web 的 handleHashRoute 就把 topicSelected 清成 null,会话跟着一起关了。
// path[1] 依旧为空,tinode-web 的 handleRouteChange 就把 topicSelected 清成 null,会话跟着一起关了。
// 这里显式把 topic 写回 path[1]。
const backToChat = React.useCallback(() => {
HashNavigation.navigateTo(HashNavigation.setUrlTopic(HashNavigation.setUrlSidePanel('', ''), topic));
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.setUrlSidePanel('', ''), topic));
}, [topic]);
React.useEffect(() => {
if (!topic) return;
@@ -491,13 +491,13 @@ function GroupSettings({topic, myUserId, tinode, onToast}) {
{/* target_type='group' 这条链路早就是通的(tinode-web.jsx 的 handleReportTopic 就在用),
这里原来一直是个"暂未开放"的死桩。跟 contact-management-view.jsx 的好友举报入口一样,
举报页要的是 target_type/target_id/target_name 三个参数,本文件的 navigate 辅助函数
只会写 topic,所以直接拼 hash。 */}
只会写 topic,所以直接拼路径。 */}
<button type="button" onClick={() => {
let hash = HashNavigation.setUrlSidePanel('', 'abuse-report-reasons');
hash = HashNavigation.addUrlParam(hash, 'target_type', 'group');
hash = HashNavigation.addUrlParam(hash, 'target_id', topic);
hash = HashNavigation.addUrlParam(hash, 'target_name', (group && group.name) || topic);
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlSidePanel('', 'abuse-report-reasons');
hash = Navigation.addUrlParam(hash, 'target_type', 'group');
hash = Navigation.addUrlParam(hash, 'target_id', topic);
hash = Navigation.addUrlParam(hash, 'target_name', (group && group.name) || topic);
Navigation.navigateTo(hash);
}}><span>投诉/举报</span><Icon>chevron_right</Icon></button>
</section>
<button type="button" className="group-danger" onClick={leave}>退出群组</button>
+2 -2
View File
@@ -10,7 +10,7 @@ import CheckBox from '../widgets/checkbox.jsx';
import ContactList from '../widgets/contact-list.jsx';
import ErrorPanel from '../widgets/error-panel.jsx';
import GroupManager from '../widgets/group-manager.jsx';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import MenuCancel from '../widgets/menu-cancel.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
import ShowQRCode from '../widgets/show-qrcode.jsx';
@@ -455,7 +455,7 @@ class InfoView extends React.Component {
// system back button had to be pressed once per level ever visited, and each press could land back
// inside a sub-panel the user had already explicitly backed out of.
handleBackNavigate() {
if (HashNavigation.goBack()) {
if (Navigation.goBack()) {
return;
}
const args = (this.props.panel || 'info').split('/');
+6 -6
View File
@@ -1,7 +1,7 @@
import React from 'react';
import { Tinode } from 'tinode-sdk';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { BASE_APP_NAME } from '../config.js';
import { getBootstrap, getRealnameStatus, getSystemNotifications, markSystemNotificationRead } from '../lib/im-api.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
@@ -15,9 +15,9 @@ import { topicDisplayName } from '../lib/utils.js';
// group-management-view.jsx 的同名辅助函数保持一致(不带 topic 时显式清掉,避免把上一个页面
// 残留的 topic 参数带进不相关的页面)。
const navigate = (view, topic) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
hash = topic ? HashNavigation.addUrlParam(hash, 'topic', topic) : HashNavigation.removeUrlParam(hash, 'topic');
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
hash = topic ? Navigation.addUrlParam(hash, 'topic', topic) : Navigation.removeUrlParam(hash, 'topic');
Navigation.navigateTo(hash);
};
// BottomNavigation's 消息<->工作台 switch is the app's root-level tab bar, not a navigation to a
@@ -27,7 +27,7 @@ const navigate = (view, topic) => {
// two bottom tabs) made the back button just cycle through past tab switches instead of leaving the
// app, exactly like a standard bottom-tab-bar app's back button should not have to unwind every past
// tab switch to back out.
const navigateReplace = (view) => HashNavigation.navigateReplace(HashNavigation.setUrlSidePanel(window.location.hash, view));
const navigateReplace = (view) => Navigation.navigateReplace(Navigation.setUrlSidePanel(Navigation.currentUrl(), view));
// Was a hardcoded FREQUENT_CONTACTS mock (5 fake names, always clicking through to the same
// direct-message-view.jsx demo regardless of which avatar was tapped -- confirmed independently by
@@ -501,7 +501,7 @@ function SystemMessages({authenticated}) {
};
return <div className="system-messages-page">
<header className="messages-sub-header"><button onClick={() => { if (!HashNavigation.goBack()) navigate('messages-home'); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>系统消息</h1><span /></header>
<header className="messages-sub-header"><button onClick={() => { if (!Navigation.goBack()) navigate('messages-home'); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>系统消息</h1><span /></header>
<main className="system-notification-list">
{loading ? <p className="messages-empty"><Icon>hourglass_empty</Icon>加载中…</p> : null}
{error ? <p className="messages-empty"><Icon>error_outline</Icon>{error}</p> : null}
+2 -2
View File
@@ -1,8 +1,8 @@
import React from 'react';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
const navigate = view => HashNavigation.navigateTo(view);
const navigate = view => Navigation.navigateTo(view);
export const MESSAGE_COMPOSER_VIEWS = [
'compose-keyboard', 'mentions-select', 'mentions-complete', 'mentions-message',
+15 -15
View File
@@ -49,7 +49,7 @@ import { topicDisplayName } from '../lib/utils.js';
import { acceptGroupInvitation, checkURL, confirmRiskChallenge, createChatUploader, favoriteMessages, getBootstrap,
getConversationContext, getFavorites, getFriendRequests, getGroupJoinRequests, getGroupMembers, getLatestGroupAnnouncement,
getTopicDraft, recallMessage, updateTopicDraft } from '../lib/im-api.js';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { bytesToHumanSize, relativeDateFormat, shortDateFormat } from '../lib/strformat.js';
// Run timer with this frequency (ms) for checking notification queue.
@@ -725,7 +725,7 @@ class MessagesView extends React.Component {
.then(ctrl => {
if (ctrl.code == 303) {
// Redirect to another topic requested.
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
Navigation.navigateTo(Navigation.setUrlTopic('', ctrl.params.topic));
return;
}
if (this.state.topic != ctrl.topic) {
@@ -1441,28 +1441,28 @@ class MessagesView extends React.Component {
}).catch(_ => {});
}
// Note: setUrlSidePanel only overwrites path[0] -- called with window.location.hash while a topic is
// Note: setUrlSidePanel only overwrites path[0] -- called with Navigation.currentUrl() while a topic is
// open (path[1] is the open topic), it leaves that segment in place, producing e.g.
// "#group-announcement-detail/grpXXX?topic=grpXXX". handleHashRoute then re-parses path[1] as a topic
// selector on every hashchange, fighting with the sidepanel that's trying to open and leaving the
// whole view blank. Build the target hash from an empty path instead of the current one.
// "/group-announcement-detail/grpXXX?topic=grpXXX". handleRouteChange then re-parses path[1] as a topic
// selector on every route change, fighting with the sidepanel that's trying to open and leaving the
// whole view blank. Build the target URL from an empty path instead of the current one.
handleShowAnnouncement(e) {
e.preventDefault();
// `from=chat` lets Announcement (group-management-view.jsx) tell this entry point apart from
// the one via group-settings -- both land on the identical path[0]/topic hash otherwise, so
// without it the "back" button can't know whether to return to group-settings or to this chat.
let hash = HashNavigation.addUrlParam(HashNavigation.setUrlSidePanel('', 'group-announcement-detail'), 'topic', this.state.topic);
hash = HashNavigation.addUrlParam(hash, 'from', 'chat');
HashNavigation.navigateTo(hash);
let hash = Navigation.addUrlParam(Navigation.setUrlSidePanel('', 'group-announcement-detail'), 'topic', this.state.topic);
hash = Navigation.addUrlParam(hash, 'from', 'chat');
Navigation.navigateTo(hash);
}
handleShowJoinRequests(e) {
e.preventDefault();
// `from=chat` mirrors handleShowAnnouncement above -- lets JoinRequests (group-management-view.jsx)
// return to this chat instead of always landing on group-governance.
let hash = HashNavigation.addUrlParam(HashNavigation.setUrlSidePanel('', 'group-join-requests'), 'topic', this.state.topic);
hash = HashNavigation.addUrlParam(hash, 'from', 'chat');
HashNavigation.navigateTo(hash);
let hash = Navigation.addUrlParam(Navigation.setUrlSidePanel('', 'group-join-requests'), 'topic', this.state.topic);
hash = Navigation.addUrlParam(hash, 'from', 'chat');
Navigation.navigateTo(hash);
}
handleSendFriendVerification(e) {
@@ -1471,8 +1471,8 @@ class MessagesView extends React.Component {
// current hash, since a topic is open) -- this one additionally never set `uid`, which the
// friend-verification page needs (contact-management-view.jsx's Verification reads hashParams.uid)
// to know who the verification is for; for a P2P topic that's simply the topic name itself.
HashNavigation.navigateTo(HashNavigation.addUrlParam(
HashNavigation.setUrlSidePanel('', 'friend-verification'), 'uid', this.state.topic));
Navigation.navigateTo(Navigation.addUrlParam(
Navigation.setUrlSidePanel('', 'friend-verification'), 'uid', this.state.topic));
}
// 6.3 加入群聊.png:被邀请人在跟邀请人的会话里收到一张"邀请你加入群聊……立即加入"的卡片。
@@ -2239,7 +2239,7 @@ class MessagesView extends React.Component {
if (!topic || !seq || isNaN(seq)) {
return;
}
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'jumpseq'));
Navigation.navigateTo(Navigation.removeUrlParam(Navigation.currentUrl(), 'jumpseq'));
const jump = () => window.setTimeout(() => this.handleQuoteClick(seq), 150);
if (topic.findMessage(seq)) {
jump();
+6 -6
View File
@@ -9,7 +9,7 @@ import NewTopicById from '../widgets/new-topic-by-id.jsx';
import NewTopicGroup from '../widgets/new-topic-group.jsx';
import SearchContacts from '../widgets/search-contacts.jsx';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { asEmail, asPhone, theCard } from '../lib/utils.js';
const messages = defineMessages({
@@ -34,7 +34,7 @@ class NewTopicView extends React.Component {
constructor(props) {
super(props);
const urlParams = HashNavigation.parseUrlHash(window.location.hash).params;
const urlParams = Navigation.parseUrl(Navigation.currentUrl()).params;
const initialTab = urlParams['tab'] || 'find';
const initialQuery = initialTab == 'find' ? (urlParams['q'] || null) : null;
@@ -60,7 +60,7 @@ class NewTopicView extends React.Component {
// page -- navigateTo() pushed a history entry per click purely to keep the `tab` url param in
// sync, so repeatedly switching tabs made the back button cycle through past tabs instead of
// leaving this page. navigateReplace() keeps the param in sync without that side effect.
HashNavigation.navigateReplace(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
Navigation.navigateReplace(Navigation.addUrlParam(Navigation.currentUrl(), 'tab', e.currentTarget.dataset.id));
this.setState({tabSelected: e.currentTarget.dataset.id});
}
@@ -91,19 +91,19 @@ class NewTopicView extends React.Component {
handleSearchResultSelected(topicName) {
if (this.state.tabSelected == 'find') {
HashNavigation.navigateTo(HashNavigation.removeUrlParams(window.location.hash, ['tab', 'q']));
Navigation.navigateTo(Navigation.removeUrlParams(Navigation.currentUrl(), ['tab', 'q']));
this.props.onCreateTopic(topicName);
}
}
handleNewGroupSubmit(name, description, dataUrl, priv, tags, isChannel) {
HashNavigation.navigateTo(HashNavigation.removeUrlParams(window.location.hash, ['tab', 'q']));
Navigation.navigateTo(Navigation.removeUrlParams(Navigation.currentUrl(), ['tab', 'q']));
this.props.onCreateTopic(undefined,
{public: theCard(name, dataUrl, null, description), private: priv, tags: tags}, isChannel);
}
handleGroupByID(topicName) {
HashNavigation.navigateTo(HashNavigation.removeUrlParams(window.location.hash, ['tab', 'q']));
Navigation.navigateTo(Navigation.removeUrlParams(Navigation.currentUrl(), ['tab', 'q']));
this.props.onCreateTopic(topicName);
}
+10 -10
View File
@@ -1,7 +1,7 @@
import React from 'react';
import QRCode from 'qrcodejs';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { useCachedData, readCachedData, writeCachedData } from '../lib/use-cached-data.js';
import { BASE_APP_NAME } from '../config.js';
import { PACKAGE_VERSION } from '../version.js';
@@ -12,22 +12,22 @@ import {
} from '../lib/im-api.js';
const navigate = (view, params) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? HashNavigation.addUrlParam(hash, key, value) : HashNavigation.removeUrlParam(hash, key);
hash = value ? Navigation.addUrlParam(hash, key, value) : Navigation.removeUrlParam(hash, key);
});
HashNavigation.navigateTo(hash);
Navigation.navigateTo(hash);
};
// Same as navigate() but replaces the current history entry instead of pushing a new one -- for
// steps within a single flow (e.g. youth-mode off/on/verify) so "back" leaves the flow instead of
// re-walking its already-completed steps.
const navigateReplace = (view, params) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? HashNavigation.addUrlParam(hash, key, value) : HashNavigation.removeUrlParam(hash, key);
hash = value ? Navigation.addUrlParam(hash, key, value) : Navigation.removeUrlParam(hash, key);
});
HashNavigation.navigateReplace(hash);
Navigation.navigateReplace(hash);
};
export const PERSONAL_SETTINGS_VIEWS = [
@@ -44,7 +44,7 @@ function Icon({children}) { return <i className="material-icons" aria-hidden="tr
function Header({title, back = 'personal-center', action, actionLabel, onAction, actionDisabled}) {
return <header className="personal-header">
<button type="button" onClick={() => { if (!HashNavigation.goBack()) navigate(back); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<button type="button" onClick={() => { if (!Navigation.goBack()) navigate(back); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<h1>{title}</h1>
{action ? <button type="button" className="text" onClick={onAction} disabled={actionDisabled}>{actionLabel || action}</button> : <span />}
</header>;
@@ -330,8 +330,8 @@ function SettingsList({onToast, onLogout, tinode, chatList, onDeleteAccount}) {
<MenuRow icon="verified_user" title="实名认证" value={realnameLabel} route="realname" />
{/* security-support-view.jsx 的 FraudAwareness 组件自己的注释就写着这应该是个
"always-reachable safety-info page",但此前没有任何页面真的链接过去——只能靠手改
hash 才能打开。这是 personal-settings-view.jsx 之外的一个路由,和本文件其它跳转
用的是同一套 hash 导航,可以直接指过去。 */}
地址栏才能打开。这是 personal-settings-view.jsx 之外的一个路由,和本文件其它跳转
用的是同一套导航机制,可以直接指过去。 */}
<MenuRow icon="shield" title="安全教育" route="fraud-risk-warning" />
</section>
<section><MenuRow icon="no_accounts" title="注销账号" onClick={() => setSheet('delete')} danger /></section>
+5 -5
View File
@@ -1,15 +1,15 @@
import React from 'react';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { uploadLargeFile, getAbuseReasons, submitAbuseReport, getFeedbackConfig, submitFeedback } from '../lib/im-api.js';
import { readCachedData, useCachedData } from '../lib/use-cached-data.js';
const navigate = (view, params) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? HashNavigation.addUrlParam(hash, key, value) : HashNavigation.removeUrlParam(hash, key);
hash = value ? Navigation.addUrlParam(hash, key, value) : Navigation.removeUrlParam(hash, key);
});
HashNavigation.navigateTo(hash);
Navigation.navigateTo(hash);
};
export const SECURITY_SUPPORT_VIEWS = [
@@ -20,7 +20,7 @@ export const SECURITY_SUPPORT_VIEWS = [
function Icon({children}) { return <i className="material-icons" aria-hidden="true">{children}</i>; }
function Header({title, back}) {
return <header className="support-header"><button type="button" onClick={() => { if (!HashNavigation.goBack()) navigate(back); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>{title}</h1><span /></header>;
return <header className="support-header"><button type="button" onClick={() => { if (!Navigation.goBack()) navigate(back); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button><h1>{title}</h1><span /></header>;
}
function Toast({message, onDone}) {
+71 -71
View File
@@ -31,7 +31,7 @@ import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/hos
import LocalStorageUtil from '../lib/local-storage.js';
import { getTenantContext, resolveQRCode, createGroupJoinRequest, getBootstrap } from '../lib/im-api.js';
import { readCachedData, writeCachedData } from '../lib/use-cached-data.js';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { secondsToTime } from '../lib/strformat.js'
import { defaultWallpaper, theCard, topicDisplayName, updateFavicon } from '../lib/utils.js';
@@ -101,7 +101,7 @@ const messages = defineMessages({
}
});
// 全部合法的侧栏视图名。抽成模块级常量而不是内联在 handleHashRoute 里,是因为初始化时的
// 全部合法的侧栏视图名。抽成模块级常量而不是内联在 handleRouteChange 里,是因为初始化时的
// "要不要保留 hash" 判断也需要同一份名单——那里原来写的是上游 Tinode 遗留的
// ['cred','reset','register'] 三个名字,本工程新增的上百个路由一个都不在里面,导致带任何新路由的
// 地址刷新/深链进入都会被 navigateTo('') 清空 hash 弹回首页。
@@ -231,7 +231,7 @@ class TinodeWeb extends React.Component {
this.state = this.getBlankState();
this.handleResize = this.handleResize.bind(this);
this.handleHashRoute = this.handleHashRoute.bind(this);
this.handleRouteChange = this.handleRouteChange.bind(this);
this.handleOnline = this.handleOnline.bind(this);
this.handleEscapeKey = this.handleEscapeKey.bind(this);
this.handleColorSchemeChange = this.handleColorSchemeChange.bind(this);
@@ -470,7 +470,7 @@ class TinodeWeb extends React.Component {
window.addEventListener('online', this.handleOnlineOn);
this.handleOnlineOff = _ => { this.handleOnline(false); }
window.addEventListener('offline', this.handleOnlineOff);
window.addEventListener('hashchange', this.handleHashRoute);
window.addEventListener('popstate', this.handleRouteChange);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.handleColorSchemeChange);
// Window/tab visible or invisible for pausing timers.
@@ -550,24 +550,24 @@ class TinodeWeb extends React.Component {
}
// Parse the hash navigation params.
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
const parsedNav = Navigation.parseUrl(Navigation.currentUrl());
// Maybe navigate to home screen.
if (!SIDE_PANEL_VIEWS.includes(parsedNav.path[0]) || !parsedNav.path[0]) {
// Save possible topic name.
this.setState({requestedTopic: parsedNav.path[1]});
const path = parsedNav.params && parsedNav.params.cred_done ?
HashNavigation.addUrlParam('', 'cred_done', parsedNav.params.cred_done):
Navigation.addUrlParam('', 'cred_done', parsedNav.params.cred_done):
'';
HashNavigation.navigateTo(path);
Navigation.navigateTo(path);
} else {
this.handleHashRoute();
this.handleRouteChange();
}
});
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
window.removeEventListener('hashchange', this.handleHashRoute);
window.removeEventListener('popstate', this.handleRouteChange);
window.removeEventListener('online', this.handleOnlineOn);
window.removeEventListener('offline', this.handleOnlineOff);
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleColorSchemeChange);
@@ -742,7 +742,7 @@ class TinodeWeb extends React.Component {
alertVisible: false
});
// Navigate to contacts or login view
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', null));
Navigation.navigateTo(Navigation.setUrlTopic('', null));
}
}
@@ -766,9 +766,9 @@ class TinodeWeb extends React.Component {
}
}
// Handle for hash navigation (hashchange) event: update state.
handleHashRoute() {
const hash = HashNavigation.parseUrlHash(window.location.hash);
// Handler for route navigation (popstate) event: update state.
handleRouteChange() {
const hash = Navigation.parseUrl(Navigation.currentUrl());
// Browser back navigation after a successful login re-renders one of the pre-login views
// (most commonly 'login') while the underlying Tinode connection is still authenticated --
// hash routing and the WS session's lifecycle are independent, so nothing else here would
@@ -780,7 +780,7 @@ class TinodeWeb extends React.Component {
// untranslated "already authenticated (409)" for registration. Redirecting away before any of
// that code runs removes the only way to reach it.
if (hash.path && AUTH_FLOW_VIEWS.includes(hash.path[0]) && this.tinode.isAuthenticated()) {
HashNavigation.navigateTo('');
Navigation.navigateTo('');
return;
}
// Start with panel parameters.
@@ -804,13 +804,13 @@ class TinodeWeb extends React.Component {
// A syntactically valid topic name is not enough on its own: mobilePanel='topic-view' only
// ever pairs correctly with MessagesView actually being allowed to render, which requires
// !chapterOneActive (i.e. this.tinode.isAuthenticated()) -- see tinode-web.jsx's render().
// Without this check, opening (or being redirected/deep-linked to) a #/<topic> URL while
// Without this check, opening (or being redirected/deep-linked to) a `/<topic>` URL while
// logged out set mobilePanel to 'topic-view' anyway; on mobile that made *both* the
// SidepanelView gate (`mobilePanel=='sidepanel'`) and the MessagesView gate
// (`!chapterOneActive`) false at once, so neither rendered -- a permanently blank white
// screen (app-container empty, no thrown error) that a hash change alone couldn't recover
// from, only a full reload. Reproduced live: logging out, then setting location.hash to a
// real group's `#/<topic>` URL. Falling back to the same "invalid topic" branch already used
// screen (app-container empty, no thrown error) that a route change alone couldn't recover
// from, only a full reload. Reproduced live: logging out, then navigating to a real group's
// `/<topic>` URL. Falling back to the same "invalid topic" branch already used
// for a malformed name reuses a path already proven safe with topicName=null.
if (!Tinode.topicType(topicName) || !this.tinode.isAuthenticated()) {
// Clear invalid topic name and hide messages view on mobile.
@@ -1009,7 +1009,7 @@ class TinodeWeb extends React.Component {
doLogin(login, password, tmpToken, cred) {
if (this.tinode.isAuthenticated()) {
// Already logged in. Go to default screen.
HashNavigation.navigateTo('');
Navigation.navigateTo('');
return;
}
@@ -1017,7 +1017,7 @@ class TinodeWeb extends React.Component {
if (!(login && password) && !token) {
// No login credentials provided.
// Make sure we are on the login page.
HashNavigation.navigateTo('');
Navigation.navigateTo('');
this.setState({loginDisabled: false});
return;
}
@@ -1064,7 +1064,7 @@ class TinodeWeb extends React.Component {
if (token) {
// 存的 token 失效(过期/被撤销)——这属于"会话没了",回到登录入口是对的。
this.handleLogout();
HashNavigation.navigateTo('');
Navigation.navigateTo('');
}
// 用户手输账号密码失败(401 之类)时**不要**导航。原来这里无条件 navigateTo(''),
// 会把 sidePanelSelected 清成 '',render 里 '' 又落到 'tenant',于是密码打错一次就被
@@ -1085,12 +1085,12 @@ class TinodeWeb extends React.Component {
if (!Array.isArray(params.cred) || !params.cred.length) {
return false;
}
const parsed = HashNavigation.parseUrlHash(window.location.hash);
const parsed = Navigation.parseUrl(Navigation.currentUrl());
parsed.path[0] = 'cred';
parsed.params['method'] = params.cred[0];
parsed.params['token'] = params.token;
parsed.params['code'] = params.code;
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsed.path, parsed.params));
Navigation.navigateTo(Navigation.composeUrl(parsed.path, parsed.params));
return true;
}
@@ -1120,7 +1120,7 @@ class TinodeWeb extends React.Component {
// Only the failure branch of doLogin's promise chain reset this; a success never needed to
// since it navigates away immediately -- until the same Login form could be reached again
// afterwards (browser back) with this still stuck true, permanently disabling the submit
// button. handleHashRoute's redirect above now closes off that specific path, but resetting
// button. handleRouteChange's redirect above now closes off that specific path, but resetting
// it here too means this can't get stuck regardless of how the form ends up rendered again.
loginDisabled: false,
credMethod: undefined,
@@ -1142,7 +1142,7 @@ class TinodeWeb extends React.Component {
this.tinode.disconnect();
localStorage.removeItem('auth-token');
this.handleError(err.message, 'err');
HashNavigation.navigateTo('');
Navigation.navigateTo('');
}).finally(_ => {
this.setState({loadSpinnerVisible: false});
});
@@ -1151,17 +1151,17 @@ class TinodeWeb extends React.Component {
// actually see. Land on the workbench instead (matches the "工作台优先" home screen in the mocks).
//
// 但只在"真的刚走完登录流程"时才这么跳。这个方法在页面刷新触发自动登录时也会执行,原来无条件
// 改写 hash,等于把用户刷新前所在的页面冲掉——不管停在个人中心、群设置还是收藏,刷新后一律
// 被弹到工作台。现在先看当前 hash 是不是一个有效的、非登录流程的视图,是就原样保留。
const currentView = HashNavigation.parseUrlHash(window.location.hash).path[0];
// 改写路由,等于把用户刷新前所在的页面冲掉——不管停在个人中心、群设置还是收藏,刷新后一律
// 被弹到工作台。现在先看当前路由是不是一个有效的、非登录流程的视图,是就原样保留。
const currentView = Navigation.parseUrl(Navigation.currentUrl()).path[0];
const cameFromAuthFlow = !currentView || AUTH_FLOW_VIEWS.includes(currentView) ||
!SIDE_PANEL_VIEWS.includes(currentView);
let urlHash = cameFromAuthFlow ?
HashNavigation.setUrlSidePanel(window.location.hash, 'workbench') : window.location.hash;
Navigation.setUrlSidePanel(Navigation.currentUrl(), 'workbench') : Navigation.currentUrl();
if (goToTopic) {
urlHash = HashNavigation.setUrlTopic(urlHash, goToTopic);
urlHash = Navigation.setUrlTopic(urlHash, goToTopic);
}
HashNavigation.navigateTo(urlHash);
Navigation.navigateTo(urlHash);
}
tnMeMetaDesc(desc) {
@@ -1425,7 +1425,7 @@ class TinodeWeb extends React.Component {
topicSelectedAcs: this.tinode.getTopicAccessMode(topicName),
forwardMessage: null
});
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', topicName));
Navigation.navigateTo(Navigation.setUrlTopic('', topicName));
}
} else {
// Currently selected contact deleted
@@ -1440,7 +1440,7 @@ class TinodeWeb extends React.Component {
forwardMessage: null
});
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', null));
Navigation.navigateTo(Navigation.setUrlTopic('', null));
}
}
@@ -1457,8 +1457,8 @@ class TinodeWeb extends React.Component {
this.setState({
mobilePanel: 'sidepanel'
});
if (!HashNavigation.goBack()) {
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, null));
if (!Navigation.goBack()) {
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.currentUrl(), null));
}
}
@@ -1563,7 +1563,7 @@ class TinodeWeb extends React.Component {
handleNewAccount() {
this.handleError();
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'register'));
Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(), 'register'));
}
// Actual registration of a new account.
@@ -1666,7 +1666,7 @@ class TinodeWeb extends React.Component {
handleSelectWallpapers() {
this.handleError();
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'wallpapers'));
Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(), 'wallpapers'));
}
applyWallpaperSettings(wallpaper, size, blur, effectiveSchema) {
@@ -1707,7 +1707,7 @@ class TinodeWeb extends React.Component {
handleSettings() {
this.handleError();
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash,
Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(),
this.state.myUserId ? 'edit' : 'settings'));
}
@@ -1741,7 +1741,7 @@ class TinodeWeb extends React.Component {
secureConnection: secureConnection,
});
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, ''));
Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(), ''));
}
// Tenant was resolved via POST /v1/im/tenants/resolve (see auth-onboarding-view.jsx TenantEntry).
@@ -1776,13 +1776,13 @@ class TinodeWeb extends React.Component {
// User chose 'Archived chats'.
handleShowArchive() {
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash,
Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(),
this.state.myUserId ? 'archive' : ''));
}
// User viewes 'Blocked chats'.
handleShowBlocked() {
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash,
Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(),
this.state.myUserId ? 'blocked' : ''));
}
@@ -1838,7 +1838,7 @@ class TinodeWeb extends React.Component {
// User clicked Cancel button in Setting or Sign Up panel.
handleSidepanelCancel() {
const parsed = HashNavigation.parseUrlHash(window.location.hash);
const parsed = Navigation.parseUrl(Navigation.currentUrl());
let path = '';
if (['security','support','general','notif','acc_general'].includes(parsed.path[0])) {
path = 'edit';
@@ -1859,18 +1859,18 @@ class TinodeWeb extends React.Component {
delete parsed.params.scheme;
delete parsed.params.token;
}
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsed.path, parsed.params));
Navigation.navigateTo(Navigation.composeUrl(parsed.path, parsed.params));
this.setState({errorText: '', errorLevel: null});
}
// Sidepanel navigator. No need to bind to 'this'.
basicNavigator(hash) {
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, hash));
Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(), hash));
}
// Topic info navigator. No need to bind to 'this'.
infoNavigator(hash) {
HashNavigation.navigateTo(HashNavigation.setUrlInfoPanel(window.location.hash, hash));
Navigation.navigateTo(Navigation.setUrlInfoPanel(Navigation.currentUrl(), hash));
}
// Request to start a topic, new or selected from search results, or "by ID".
@@ -1911,7 +1911,7 @@ class TinodeWeb extends React.Component {
nextState.topicSelected = newName;
}
this.setState(nextState, _ => {
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', newName));
Navigation.navigateTo(Navigation.setUrlTopic('', newName));
});
}
@@ -2033,7 +2033,7 @@ class TinodeWeb extends React.Component {
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
this.tinode.onInfoMessage = this.handleInfoMessage;
this.tinode.onDataMessage = this.handleDataMessage;
HashNavigation.navigateTo('');
Navigation.navigateTo('');
})
});
}
@@ -2053,7 +2053,7 @@ class TinodeWeb extends React.Component {
// Request to hard-delete topic.
topic.delTopic(true).then(_ => {
// Hide MessagesView and InfoView panels.
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.currentUrl(), ''));
}).catch(err => {
this.handleError(err.message, 'err');
});
@@ -2078,7 +2078,7 @@ class TinodeWeb extends React.Component {
topic.leave(true).then(_ => {
// Hide MessagesView and InfoView panels.
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.currentUrl(), ''));
}).catch(err => {
this.handleError(err.message, 'err');
});
@@ -2093,7 +2093,7 @@ class TinodeWeb extends React.Component {
topic.updateMode(null, '-JP')
.then(_ => {
// Hide MessagesView and InfoView panels.
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.currentUrl(), ''));
})
.catch(err => this.handleError(err.message, 'err'));
}
@@ -2118,10 +2118,10 @@ class TinodeWeb extends React.Component {
// Remove J and P permissions (the "block" half of "block and report").
topic.updateMode(null, '-JP')
.then(_ => {
let hash = HashNavigation.setUrlTopic(window.location.hash, '');
hash = HashNavigation.setUrlSidePanel(hash, 'abuse-report-reasons');
Object.entries(params).forEach(([key, value]) => { hash = HashNavigation.addUrlParam(hash, key, value); });
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlTopic(Navigation.currentUrl(), '');
hash = Navigation.setUrlSidePanel(hash, 'abuse-report-reasons');
Object.entries(params).forEach(([key, value]) => { hash = Navigation.addUrlParam(hash, key, value); });
Navigation.navigateTo(hash);
})
.catch(err => this.handleError(err.message, 'err'));
}
@@ -2352,10 +2352,10 @@ class TinodeWeb extends React.Component {
...data.user,
capabilities: {can_add_friend: !!data.user.can_add}
});
let hash = HashNavigation.setUrlTopic(window.location.hash, '');
hash = HashNavigation.setUrlSidePanel(hash, data.user.relationship === 'friend' ? 'friend-profile' : 'group-member-profile');
hash = HashNavigation.addUrlParam(hash, 'uid', data.user.user_id);
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlTopic(Navigation.currentUrl(), '');
hash = Navigation.setUrlSidePanel(hash, data.user.relationship === 'friend' ? 'friend-profile' : 'group-member-profile');
hash = Navigation.addUrlParam(hash, 'uid', data.user.user_id);
Navigation.navigateTo(hash);
return;
}
this.setState({scannerOpen: false});
@@ -2400,7 +2400,7 @@ class TinodeWeb extends React.Component {
// The "信息" context-menu item used to always open the old, pre-migration InfoView (info-view.jsx)
// -- for P2P/group topics that's a real, previously-unnoticed gap: the new contact-management-view.jsx
// "friend-settings" / group-management-view.jsx "group-settings" pages (built and verified across
// Ch.5/6, with real HashNavigation.goBack()-based back navigation) had no way to actually be reached
// Ch.5/6, with real Navigation.goBack()-based back navigation) had no way to actually be reached
// from an open chat's own header -- every test of them this engagement typed the hash directly.
// InfoView's own back button (handleBackNavigate in info-view.jsx) still hardcodes a single fixed
// parent per panel and never used real browser history, so routing here fixed both problems at once
@@ -2408,25 +2408,25 @@ class TinodeWeb extends React.Component {
handleShowInfoView() {
const topicName = this.state.topicSelected;
if (Tinode.isP2PTopicName(topicName)) {
// Base the new hash on '' (not window.location.hash) -- the current hash's path[1] still holds
// Base the new URL on '' (not Navigation.currentUrl()) -- the current URL's path[1] still holds
// the open chat's own topic, and setUrlSidePanel only ever touches path[0]. Reusing the current
// hash as-is here leaves that topic sitting in path[1] where it collides with group-settings'/
// friend-settings' own query params, and handleHashRoute re-interprets the leftover path[1] as a
// URL as-is here leaves that topic sitting in path[1] where it collides with group-settings'/
// friend-settings' own query params, and handleRouteChange re-interprets the leftover path[1] as a
// fresh topic selection, blanking the whole view -- the exact bug already found and fixed for
// handleShowAnnouncement/handleShowJoinRequests/handleSendFriendVerification; same fix here.
let hash = HashNavigation.setUrlSidePanel('', 'friend-settings');
hash = HashNavigation.addUrlParam(hash, 'uid', topicName);
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlSidePanel('', 'friend-settings');
hash = Navigation.addUrlParam(hash, 'uid', topicName);
Navigation.navigateTo(hash);
return;
}
if (!Tinode.isSelfTopicName(topicName)) {
let hash = HashNavigation.setUrlSidePanel('', 'group-settings');
hash = HashNavigation.addUrlParam(hash, 'topic', topicName);
HashNavigation.navigateTo(hash);
let hash = Navigation.setUrlSidePanel('', 'group-settings');
hash = Navigation.addUrlParam(hash, 'topic', topicName);
Navigation.navigateTo(hash);
return;
}
// "已保存的消息" (self topic) has no equivalent new settings page -- keep the old InfoView for it.
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'info', 'info'));
Navigation.navigateTo(Navigation.addUrlParam(Navigation.currentUrl(), 'info', 'info'));
this.setState({infoPanel: 'info'});
}
@@ -2459,7 +2459,7 @@ class TinodeWeb extends React.Component {
if (this.tinode.isAuthenticated()) {
// Adding new email or phone number in account setting.
this.tinode.getMeTopic().setMeta({cred: {meth: cred, resp: code}})
.then(_ => HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'contacts')))
.then(_ => Navigation.navigateTo(Navigation.setUrlSidePanel(Navigation.currentUrl(), 'contacts')))
.catch(err => this.handleError(err.message, 'err'));
} else {
// Credential validation on signup.
@@ -2487,7 +2487,7 @@ class TinodeWeb extends React.Component {
.then(_ => this.tinode.updateAccountBasic(null, null, newPassword, {scheme: tempAuth.scheme, secret: secret}))
.then(_ => {
this.handleError(this.props.intl.formatMessage(messages.password_reset_success), 'info');
HashNavigation.navigateTo('');
Navigation.navigateTo('');
})
.catch(err => {
this.handleError(err.message, 'err');
+2 -2
View File
@@ -1,7 +1,7 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import { WALLPAPER_DEFAULTS } from '../config.js';
import { wallpaperNameFromUrl, wallpaperTypeFromUrl } from '../lib/utils.js';
import imageIndex from '../../img/bkg/index.json';
@@ -31,7 +31,7 @@ export default class WallpapersView extends React.PureComponent {
e.preventDefault();
// Same-page tab switch (pattern / image), not a navigation to a different page -- see the same
// fix/comment in new-topic-view.jsx's handleTabClick.
HashNavigation.navigateReplace(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
Navigation.navigateReplace(Navigation.addUrlParam(Navigation.currentUrl(), 'tab', e.currentTarget.dataset.id));
this.setState({tab: e.currentTarget.dataset.id});
}
+7 -7
View File
@@ -2,7 +2,7 @@ import React from 'react';
import ContactList from '../widgets/contact-list.jsx';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
import {
uploadLargeFile, getMe, getFileUrl, getUserProfile,
getWorkbench,
@@ -15,20 +15,20 @@ import {
import { useCachedData, readCachedData, writeCachedData } from '../lib/use-cached-data.js';
const navigate = (view, params) => {
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? HashNavigation.addUrlParam(hash, key, value) : HashNavigation.removeUrlParam(hash, key);
hash = value ? Navigation.addUrlParam(hash, key, value) : Navigation.removeUrlParam(hash, key);
});
HashNavigation.navigateTo(hash);
Navigation.navigateTo(hash);
};
// Same bug/fix as content-library-view.jsx's navigateReplace: DailyReportList/MonthlyReportList's
// 我的日报|月报 <-> 团队统计 switch is a same-page tab toggle (same component, just a `team` boolean),
// not a navigation to a different page -- using navigate() here pushed a history entry per click, so
// switching back and forth a few times made the system back button (and this app's own Header back
// button, gated on HashNavigation.goBack()) cycle through past toggle states instead of leaving the
// button, gated on Navigation.goBack()) cycle through past toggle states instead of leaving the
// report page. replaceState-based navigateReplace keeps the hash in sync without that side effect.
const navigateReplace = (view) => HashNavigation.navigateReplace(HashNavigation.setUrlSidePanel(window.location.hash, view));
const navigateReplace = (view) => Navigation.navigateReplace(Navigation.setUrlSidePanel(Navigation.currentUrl(), view));
export const WORKPLACE_COLLABORATION_VIEWS = [
'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail',
@@ -41,7 +41,7 @@ function Icon({children}) { return <i className="material-icons" aria-hidden="tr
function Header({title, back = 'workbench', action, actionLabel, onAction, actionDisabled}) {
return <header className="workplace-header">
<button type="button" onClick={() => { if (!HashNavigation.goBack()) navigate(back); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<button type="button" onClick={() => { if (!Navigation.goBack()) navigate(back); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
<h1>{title}</h1>
{action ? <button type="button" onClick={onAction} aria-label={actionLabel || '操作'} disabled={actionDisabled}><Icon>{action}</Icon></button> : <span />}
</header>;
+6 -6
View File
@@ -11,7 +11,7 @@ import ReceivedMarker from './received-marker.jsx'
import { fullFormatter } from '../lib/formatters.js';
import { sanitizeUrl } from '../lib/utils.js';
import HashNavigation from '../lib/navigation.js';
import Navigation from '../lib/navigation.js';
// 4.9 语音未读红点:纯客户端概念,没有对应的后端字段——原生 Tinode 的 recv/read 只标记到
// "这条消息本身有没有被看到",跟"这条语音有没有被真的点开听过"是两回事(文字消息看一眼就算
@@ -104,17 +104,17 @@ class BaseChatMessage extends React.PureComponent {
// tinode:topic/usr123abc -> usr123abc
const pathname = new URL(e.target.dataset.val)?.pathname;
const parts = pathname.split('/').filter(Boolean);
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', parts.pop() || ''));
Navigation.navigateTo(Navigation.setUrlTopic('', parts.pop() || ''));
} catch (error) {
console.error("Invalid URL:", error);
}
break;
case 'contact_find':
e.preventDefault();
let hashUrl = HashNavigation.setUrlSidePanel(window.location.hash, 'newtpk');
hashUrl = HashNavigation.addUrlParam(hashUrl, 'q', e.target.dataset.val);
hashUrl = HashNavigation.addUrlParam(hashUrl, 'tab', 'find');
HashNavigation.navigateTo(hashUrl);
let hashUrl = Navigation.setUrlSidePanel(Navigation.currentUrl(), 'newtpk');
hashUrl = Navigation.addUrlParam(hashUrl, 'q', e.target.dataset.val);
hashUrl = Navigation.addUrlParam(hashUrl, 'tab', 'find');
Navigation.navigateTo(hashUrl);
break;
default:
// No special handling; let the browser deal with it.
+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
+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
+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
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,2 +1,2 @@
"use strict";(self.webpackChunktinode_webapp=self.webpackChunktinode_webapp||[]).push([[686],{3686:function(e,a,t){t.r(a),t.d(a,{default:function(){return c}});var s=t(1594),l=t.n(s),i=t(8181),r=t(184),n=t(3272),d=t(7849),h=t(1781),p=t(4848);class c extends l().PureComponent{constructor(e){super(e);const a=(0,d.MC)(this.props.wallpaper),t=(0,d.t7)(this.props.wallpaper);this.state={tab:a||n.S.type,selectedType:a||n.S.type,wallpaper:t||h[n.S.type][n.S.index].name,blur:0|this.props.wallpaperBlur},this.blurValues=[0,1,2,4,8,16],this.handleTabClick=this.handleTabClick.bind(this),this.handleWallpaperSelected=this.handleWallpaperSelected.bind(this),this.handleBlurChanged=this.handleBlurChanged.bind(this),this.hasChanged=this.hasChanged.bind(this)}handleTabClick(e){e.preventDefault(),r.A.navigateTo(r.A.addUrlParam(window.location.hash,"tab",e.currentTarget.dataset.id)),this.setState({tab:e.currentTarget.dataset.id})}handleWallpaperSelected(e){let a,t,s;if(e)e.preventDefault(),t=this.state.tab,a=e.currentTarget.dataset.id,s="patt"==t?0:this.state.blur;else{if(!this.hasChanged())return;t=n.S.type,a=n.S.index,s=0}const l=h[t][a].name,i="patt"==t?h[t][a].size:0;this.setState({tab:t,wallpaper:l,selectedType:t,blur:s}),this.props.onWallpaperSelected(`../${n.S.path}${l}`,i,this.blurValues[s])}handleBlurChanged(e){e.preventDefault();const a=e.currentTarget.value;this.setState({blur:a}),"img"==this.state.selectedType&&this.props.onWallpaperSelected(`../${n.S.path}${this.state.wallpaper}`,0,this.blurValues[a])}hasChanged(){return this.state.selectedType!=n.S.type||this.state.wallpaper!=h[n.S.type][n.S.index].name||0!=this.state.blur}render(){return(0,p.jsxs)("div",{className:"flex-column",children:[(0,p.jsx)("div",{className:"panel-form-row"+(this.hasChanged()?" clean-clickable":""),children:(0,p.jsxs)("a",{className:"flat-button"+(this.hasChanged()?"":" disabled"),onClick:()=>this.handleWallpaperSelected(null),children:[(0,p.jsx)("i",{className:"material-icons",children:"undo"})," ",(0,p.jsx)(i.FormattedMessage,{id:"button_restore",defaultMessage:"Restore default",description:"Reset setting to default"})]})}),(0,p.jsxs)("ul",{className:"tabbar",children:[(0,p.jsx)("li",{className:"patt"===this.state.tab?"active":null,children:(0,p.jsx)("a",{href:"#","data-id":"patt",onClick:this.handleTabClick,children:(0,p.jsx)(i.FormattedMessage,{id:"tabtitle_pattern",defaultMessage:"pattern",description:"Tab title Pattern"})})}),(0,p.jsx)("li",{className:"img"===this.state.tab?"active":null,children:(0,p.jsx)("a",{href:"#","data-id":"img",onClick:this.handleTabClick,children:(0,p.jsx)(i.FormattedMessage,{id:"tabtitle_image",defaultMessage:"image",description:"Tab title Image"})})})]}),(0,p.jsx)("div",{id:"settings-form",className:"scrollable-panel",children:"patt"===this.state.tab?(0,p.jsx)("div",{className:"image-grid",children:h.patt.map((e,a)=>{const t=this.state.wallpaper===e.name?" selected":"",s="dark"==this.props.colorSchema?"inverted":null;return(0,p.jsx)("div",{"data-id":a,className:`image-grid-cell${t}`,onClick:this.handleWallpaperSelected,children:(0,p.jsx)("img",{src:n.S.path+e.name,alt:e.name,className:s,style:{width:`${e.size}px`,height:`${e.size}px`}})},a)})}):(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)("div",{className:"panel-form-column",children:[(0,p.jsx)("div",{className:"panel-form-row",children:(0,p.jsx)("label",{className:"small",children:(0,p.jsx)(i.FormattedMessage,{id:"label_blur_wallpaper",defaultMessage:"Blur:",description:"Label adjusting blur amount"})})}),(0,p.jsx)("div",{className:"panel-form-row",children:(0,p.jsx)("input",{type:"range",id:"blur",name:"blur",min:"0",max:"5",step:"1",value:this.state.blur,onChange:this.handleBlurChanged})})]}),(0,p.jsx)("div",{className:"image-grid",children:h.img.map((e,a)=>{const t=this.state.wallpaper===e.name?" selected":"";return(0,p.jsx)("div",{"data-id":a,className:`image-grid-cell${t}`,onClick:this.handleWallpaperSelected,children:(0,p.jsx)("img",{src:n.S.path+e.pr,alt:e.name,style:{width:"100%",height:"100%"}})},a)})})]})})]})}}}}]);
"use strict";(self.webpackChunktinode_webapp=self.webpackChunktinode_webapp||[]).push([[686],{3686:function(e,a,t){t.r(a),t.d(a,{default:function(){return c}});var s=t(1594),l=t.n(s),i=t(8181),r=t(184),n=t(3272),d=t(7849),h=t(1781),p=t(4848);class c extends l().PureComponent{constructor(e){super(e);const a=(0,d.MC)(this.props.wallpaper),t=(0,d.t7)(this.props.wallpaper);this.state={tab:a||n.S.type,selectedType:a||n.S.type,wallpaper:t||h[n.S.type][n.S.index].name,blur:0|this.props.wallpaperBlur},this.blurValues=[0,1,2,4,8,16],this.handleTabClick=this.handleTabClick.bind(this),this.handleWallpaperSelected=this.handleWallpaperSelected.bind(this),this.handleBlurChanged=this.handleBlurChanged.bind(this),this.hasChanged=this.hasChanged.bind(this)}handleTabClick(e){e.preventDefault(),r.A.navigateReplace(r.A.addUrlParam(r.A.currentUrl(),"tab",e.currentTarget.dataset.id)),this.setState({tab:e.currentTarget.dataset.id})}handleWallpaperSelected(e){let a,t,s;if(e)e.preventDefault(),t=this.state.tab,a=e.currentTarget.dataset.id,s="patt"==t?0:this.state.blur;else{if(!this.hasChanged())return;t=n.S.type,a=n.S.index,s=0}const l=h[t][a].name,i="patt"==t?h[t][a].size:0;this.setState({tab:t,wallpaper:l,selectedType:t,blur:s}),this.props.onWallpaperSelected(`../${n.S.path}${l}`,i,this.blurValues[s])}handleBlurChanged(e){e.preventDefault();const a=e.currentTarget.value;this.setState({blur:a}),"img"==this.state.selectedType&&this.props.onWallpaperSelected(`../${n.S.path}${this.state.wallpaper}`,0,this.blurValues[a])}hasChanged(){return this.state.selectedType!=n.S.type||this.state.wallpaper!=h[n.S.type][n.S.index].name||0!=this.state.blur}render(){return(0,p.jsxs)("div",{className:"flex-column",children:[(0,p.jsx)("div",{className:"panel-form-row"+(this.hasChanged()?" clean-clickable":""),children:(0,p.jsxs)("a",{className:"flat-button"+(this.hasChanged()?"":" disabled"),onClick:()=>this.handleWallpaperSelected(null),children:[(0,p.jsx)("i",{className:"material-icons",children:"undo"})," ",(0,p.jsx)(i.FormattedMessage,{id:"button_restore",defaultMessage:"Restore default",description:"Reset setting to default"})]})}),(0,p.jsxs)("ul",{className:"tabbar",children:[(0,p.jsx)("li",{className:"patt"===this.state.tab?"active":null,children:(0,p.jsx)("a",{href:"#","data-id":"patt",onClick:this.handleTabClick,children:(0,p.jsx)(i.FormattedMessage,{id:"tabtitle_pattern",defaultMessage:"pattern",description:"Tab title Pattern"})})}),(0,p.jsx)("li",{className:"img"===this.state.tab?"active":null,children:(0,p.jsx)("a",{href:"#","data-id":"img",onClick:this.handleTabClick,children:(0,p.jsx)(i.FormattedMessage,{id:"tabtitle_image",defaultMessage:"image",description:"Tab title Image"})})})]}),(0,p.jsx)("div",{id:"settings-form",className:"scrollable-panel",children:"patt"===this.state.tab?(0,p.jsx)("div",{className:"image-grid",children:h.patt.map((e,a)=>{const t=this.state.wallpaper===e.name?" selected":"",s="dark"==this.props.colorSchema?"inverted":null;return(0,p.jsx)("div",{"data-id":a,className:`image-grid-cell${t}`,onClick:this.handleWallpaperSelected,children:(0,p.jsx)("img",{src:n.S.path+e.name,alt:e.name,className:s,style:{width:`${e.size}px`,height:`${e.size}px`}})},a)})}):(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)("div",{className:"panel-form-column",children:[(0,p.jsx)("div",{className:"panel-form-row",children:(0,p.jsx)("label",{className:"small",children:(0,p.jsx)(i.FormattedMessage,{id:"label_blur_wallpaper",defaultMessage:"Blur:",description:"Label adjusting blur amount"})})}),(0,p.jsx)("div",{className:"panel-form-row",children:(0,p.jsx)("input",{type:"range",id:"blur",name:"blur",min:"0",max:"5",step:"1",value:this.state.blur,onChange:this.handleBlurChanged})})]}),(0,p.jsx)("div",{className:"image-grid",children:h.img.map((e,a)=>{const t=this.state.wallpaper===e.name?" selected":"";return(0,p.jsx)("div",{"data-id":a,className:`image-grid-cell${t}`,onClick:this.handleWallpaperSelected,children:(0,p.jsx)("img",{src:n.S.path+e.pr,alt:e.name,style:{width:"100%",height:"100%"}})},a)})})]})})]})}}}}]);
//# sourceMappingURL=686.prod.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
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
+1 -1
View File
File diff suppressed because one or more lines are too long
+90 -78
View File
@@ -4557,16 +4557,20 @@ class LocalStorageUtil {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": function() { return /* binding */ HashNavigation; }
/* harmony export */ "default": function() { return /* binding */ Navigation; }
/* harmony export */ });
let navigationCount = 0;
class HashNavigation {
static parseUrlHash(hash) {
const parts = hash.split('?', 2);
const EMPTY_VIEW_PLACEHOLDER = '_';
class Navigation {
static parseUrl(url) {
const parts = url.split('?', 2);
const params = {};
let path = [];
if (parts[0]) {
path = parts[0].replace('#', '').split('/');
path = parts[0].replace(/^\//, '').split('/');
if (path.length > 1 && path[0] === EMPTY_VIEW_PLACEHOLDER) {
path[0] = '';
}
}
if (parts[1]) {
parts[1].split('&').forEach(arg => {
@@ -4581,13 +4585,17 @@ class HashNavigation {
params: params
};
}
static currentUrl() {
return window.location.pathname + window.location.search;
}
static navigateTo(url) {
navigationCount++;
window.location.hash = url;
window.history.pushState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate'));
}
static navigateReplace(url) {
window.history.replaceState(null, '', '#' + url);
window.dispatchEvent(new Event('hashchange'));
window.history.replaceState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate'));
}
static goBack() {
if (navigationCount > 0) {
@@ -4597,8 +4605,12 @@ class HashNavigation {
}
return false;
}
static composeUrlHash(path, params) {
let url = path.join('/');
static composeUrl(path, params) {
let segments = path;
if (segments.length > 1 && !segments[0]) {
segments = [EMPTY_VIEW_PLACEHOLDER, ...segments.slice(1)];
}
let url = '/' + segments.join('/');
const args = [];
for (const key in params) {
if (params.hasOwnProperty(key)) {
@@ -4612,45 +4624,45 @@ class HashNavigation {
}
return url;
}
static addUrlParam(hash, key, value) {
const parsed = HashNavigation.parseUrlHash(hash);
static addUrlParam(url, key, value) {
const parsed = Navigation.parseUrl(url);
parsed.params[key] = value;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static removeUrlParam(hash, key) {
const parsed = HashNavigation.parseUrlHash(hash);
static removeUrlParam(url, key) {
const parsed = Navigation.parseUrl(url);
delete parsed.params[key];
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static removeUrlParams(hash, keys) {
static removeUrlParams(url, keys) {
if (!Array.isArray(keys)) {
keys = [keys];
}
const parsed = HashNavigation.parseUrlHash(hash);
const parsed = Navigation.parseUrl(url);
for (const key of keys) {
delete parsed.params[key];
}
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static setUrlSidePanel(hash, sidepanel) {
const parsed = HashNavigation.parseUrlHash(hash);
static setUrlSidePanel(url, sidepanel) {
const parsed = Navigation.parseUrl(url);
parsed.path[0] = sidepanel;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static setUrlInfoPanel(hash, infopanel) {
const parsed = HashNavigation.parseUrlHash(hash);
static setUrlInfoPanel(url, infopanel) {
const parsed = Navigation.parseUrl(url);
if (infopanel) {
parsed.params.info = infopanel;
} else {
delete parsed.params.info;
}
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
static setUrlTopic(hash, topic) {
const parsed = HashNavigation.parseUrlHash(hash);
static setUrlTopic(url, topic) {
const parsed = Navigation.parseUrl(url);
parsed.path[1] = topic;
delete parsed.params.info;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
return Navigation.composeUrl(parsed.path, parsed.params);
}
}
@@ -5604,7 +5616,7 @@ const PHONE_RE = /^\d{11}$/;
const INVITE_RE = /^[A-Za-z]{5}$/;
const CREDIT_CODE_RE = /^[0-9A-Z]{18}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const navigate = (view, params) => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].composeUrlHash([view], params || {}));
const navigate = (view, params) => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].composeUrl([view], params || {}));
function Icon({
children,
className = ''
@@ -7520,7 +7532,7 @@ __webpack_require__.r(__webpack_exports__);
const navigate = (view, uid) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].currentUrl(), view);
hash = uid ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'uid', uid) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].removeUrlParam(hash, 'uid');
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(hash);
};
@@ -8499,7 +8511,7 @@ function Settings({
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("section", {
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
type: "button",
onClick: () => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, 'history-search'), 'topic', uid)),
onClick: () => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].currentUrl(), 'history-search'), 'topic', uid)),
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
children: "查看聊天记录"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
@@ -8601,7 +8613,7 @@ function Settings({
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
type: "button",
onClick: () => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, 'abuse-report-reasons');
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].currentUrl(), 'abuse-report-reasons');
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'target_type', 'user');
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'target_id', uid);
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'target_name', profile && profile.display_name || uid);
@@ -8987,13 +8999,13 @@ function resolveSourceName(topic, onDone) {
}).catch(() => {});
}
const navigate = (view, params) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, key, value) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].removeUrlParam(hash, key);
});
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(hash);
};
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, view));
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].currentUrl(), view));
const CONTENT_LIBRARY_VIEWS = ['favorites', 'favorites-mixed', 'favorites-files', 'favorites-text', 'favorites-voice', 'favorites-links', 'favorites-image', 'favorites-video', 'favorites-select', 'favorite-share', 'file-download', 'file-downloading', 'file-downloaded', 'history-search', 'history-files', 'history-links', 'history-images', 'history-videos'];
function Icon({
children
@@ -10874,7 +10886,7 @@ __webpack_require__.r(__webpack_exports__);
const navigate = (view, topic) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].currentUrl(), view);
hash = topic ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'topic', topic) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].removeUrlParam(hash, 'topic');
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(hash);
};
@@ -10895,7 +10907,7 @@ const navigateToProfile = (topic, userId, member) => {
} : {})
});
}
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].currentUrl(), 'group-member-profile'), 'uid', userId));
if (topic && userId) {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMemberProfile)(topic, userId).then(real => {
const current = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('profile-seed:' + userId) || {};
@@ -13306,11 +13318,11 @@ __webpack_require__.r(__webpack_exports__);
const navigate = (view, topic) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].currentUrl(), view);
hash = topic ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(hash, 'topic', topic) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].removeUrlParam(hash, 'topic');
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(hash);
};
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view));
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].currentUrl(), view));
function frequentContacts(chatList) {
if (!chatList || chatList.length === 0) return [];
return chatList.filter(item => item.topic && tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Tinode.isP2PTopicName(item.topic)).slice().sort((a, b) => new Date(b.touched || 0).getTime() - new Date(a.touched || 0).getTime()).slice(0, 8).map(item => ({
@@ -14990,7 +15002,7 @@ const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
class NewTopicView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
constructor(props) {
super(props);
const urlParams = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].parseUrlHash(window.location.hash).params;
const urlParams = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].parseUrl(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].currentUrl()).params;
const initialTab = urlParams['tab'] || 'find';
const initialQuery = initialTab == 'find' ? urlParams['q'] || null : null;
this.state = {
@@ -15008,7 +15020,7 @@ class NewTopicView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
}
handleTabClick(e) {
e.preventDefault();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].currentUrl(), 'tab', e.currentTarget.dataset.id));
this.setState({
tabSelected: e.currentTarget.dataset.id
});
@@ -15038,12 +15050,12 @@ class NewTopicView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
}
handleSearchResultSelected(topicName) {
if (this.state.tabSelected == 'find') {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].removeUrlParams(window.location.hash, ['tab', 'q']));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].removeUrlParams(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].currentUrl(), ['tab', 'q']));
this.props.onCreateTopic(topicName);
}
}
handleNewGroupSubmit(name, description, dataUrl, priv, tags, isChannel) {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].removeUrlParams(window.location.hash, ['tab', 'q']));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].removeUrlParams(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].currentUrl(), ['tab', 'q']));
this.props.onCreateTopic(undefined, {
public: (0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_8__.theCard)(name, dataUrl, null, description),
private: priv,
@@ -15051,7 +15063,7 @@ class NewTopicView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
}, isChannel);
}
handleGroupByID(topicName) {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].removeUrlParams(window.location.hash, ['tab', 'q']));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].removeUrlParams(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].currentUrl(), ['tab', 'q']));
this.props.onCreateTopic(topicName);
}
render() {
@@ -15166,14 +15178,14 @@ __webpack_require__.r(__webpack_exports__);
const navigate = (view, params) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(hash, key, value) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].removeUrlParam(hash, key);
});
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(hash);
};
const navigateReplace = (view, params) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(hash, key, value) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].removeUrlParam(hash, key);
});
@@ -16670,7 +16682,7 @@ __webpack_require__.r(__webpack_exports__);
const navigate = (view, params) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].addUrlParam(hash, key, value) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].removeUrlParam(hash, key);
});
@@ -18049,7 +18061,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.previewPrefetchAttempted = new Set();
this.state = this.getBlankState();
this.handleResize = this.handleResize.bind(this);
this.handleHashRoute = this.handleHashRoute.bind(this);
this.handleRouteChange = this.handleRouteChange.bind(this);
this.handleOnline = this.handleOnline.bind(this);
this.handleEscapeKey = this.handleEscapeKey.bind(this);
this.handleColorSchemeChange = this.handleColorSchemeChange.bind(this);
@@ -18242,7 +18254,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.handleOnline(false);
};
window.addEventListener('offline', this.handleOnlineOff);
window.addEventListener('hashchange', this.handleHashRoute);
window.addEventListener('popstate', this.handleRouteChange);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.handleColorSchemeChange);
document.addEventListener('visibilitychange', this.handleVisibilityEvent);
document.addEventListener('keydown', this.handleEscapeKey);
@@ -18290,7 +18302,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.handleError(err.message, 'err');
});
}
const parsedNav = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash);
const parsedNav = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrl(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl());
if (!SIDE_PANEL_VIEWS.includes(parsedNav.path[0]) || !parsedNav.path[0]) {
this.setState({
requestedTopic: parsedNav.path[1]
@@ -18298,13 +18310,13 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
const path = parsedNav.params && parsedNav.params.cred_done ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].addUrlParam('', 'cred_done', parsedNav.params.cred_done) : '';
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(path);
} else {
this.handleHashRoute();
this.handleRouteChange();
}
});
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
window.removeEventListener('hashchange', this.handleHashRoute);
window.removeEventListener('popstate', this.handleRouteChange);
window.removeEventListener('online', this.handleOnlineOn);
window.removeEventListener('offline', this.handleOnlineOff);
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleColorSchemeChange);
@@ -18493,8 +18505,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
};
};
}
handleHashRoute() {
const hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash);
handleRouteChange() {
const hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrl(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl());
if (hash.path && AUTH_FLOW_VIEWS.includes(hash.path[0]) && this.tinode.isAuthenticated()) {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo('');
return;
@@ -18744,12 +18756,12 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
if (!Array.isArray(params.cred) || !params.cred.length) {
return false;
}
const parsed = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash);
const parsed = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrl(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl());
parsed.path[0] = 'cred';
parsed.params['method'] = params.cred[0];
parsed.params['token'] = params.token;
parsed.params['code'] = params.code;
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].composeUrlHash(parsed.path, parsed.params));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].composeUrl(parsed.path, parsed.params));
return true;
}
handleLoginSuccessful() {
@@ -18784,9 +18796,9 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
loadSpinnerVisible: false
});
});
const currentView = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash).path[0];
const currentView = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrl(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl()).path[0];
const cameFromAuthFlow = !currentView || AUTH_FLOW_VIEWS.includes(currentView) || !SIDE_PANEL_VIEWS.includes(currentView);
let urlHash = cameFromAuthFlow ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, 'workbench') : window.location.hash;
let urlHash = cameFromAuthFlow ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), 'workbench') : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl();
if (goToTopic) {
urlHash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(urlHash, goToTopic);
}
@@ -18998,7 +19010,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
mobilePanel: 'sidepanel'
});
if (!_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].goBack()) {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(window.location.hash, null));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), null));
}
}
handleSendMessage(msg, uploadCompletionPromise, uploader, head) {
@@ -19083,7 +19095,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}
handleNewAccount() {
this.handleError();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, 'register'));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), 'register'));
}
handleNewAccountRequest(login_, password_, public_, cred_, tags_) {
this.handleError();
@@ -19156,7 +19168,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}
handleSelectWallpapers() {
this.handleError();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, 'wallpapers'));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), 'wallpapers'));
}
applyWallpaperSettings(wallpaper, size, blur, effectiveSchema) {
document.documentElement.style.setProperty('--wallpaper-url', `url('${wallpaper}')`);
@@ -19205,7 +19217,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}
handleSettings() {
this.handleError();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, this.state.myUserId ? 'edit' : 'settings'));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), this.state.myUserId ? 'edit' : 'settings'));
}
handleGlobalSettings(settings) {
const serverAddress = settings.serverAddress || this.state.serverAddress;
@@ -19232,7 +19244,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
transport: transport,
secureConnection: secureConnection
});
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, ''));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), ''));
}
handleTenantResolved(data, requestedCode) {
const tenant = data.tenant || {};
@@ -19259,10 +19271,10 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}
}
handleShowArchive() {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, this.state.myUserId ? 'archive' : ''));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), this.state.myUserId ? 'archive' : ''));
}
handleShowBlocked() {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, this.state.myUserId ? 'blocked' : ''));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), this.state.myUserId ? 'blocked' : ''));
}
toggleFCMToken(enabled) {
if (enabled) {
@@ -19333,7 +19345,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
});
}
handleSidepanelCancel() {
const parsed = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash);
const parsed = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrl(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl());
let path = '';
if (['security', 'support', 'general', 'notif', 'acc_general'].includes(parsed.path[0])) {
path = 'edit';
@@ -19354,17 +19366,17 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
delete parsed.params.scheme;
delete parsed.params.token;
}
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].composeUrlHash(parsed.path, parsed.params));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].composeUrl(parsed.path, parsed.params));
this.setState({
errorText: '',
errorLevel: null
});
}
basicNavigator(hash) {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, hash));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), hash));
}
infoNavigator(hash) {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlInfoPanel(window.location.hash, hash));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlInfoPanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), hash));
}
handleStartTopicRequest(topicName, newTopicParams, isChannel) {
if (topicName && this.tinode.isTopicCached(topicName)) {
@@ -19533,7 +19545,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
return;
}
topic.delTopic(true).then(_ => {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(window.location.hash, ''));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), ''));
}).catch(err => {
this.handleError(err.message, 'err');
});
@@ -19551,7 +19563,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
return;
}
topic.leave(true).then(_ => {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(window.location.hash, ''));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), ''));
}).catch(err => {
this.handleError(err.message, 'err');
});
@@ -19562,7 +19574,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
return;
}
topic.updateMode(null, '-JP').then(_ => {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(window.location.hash, ''));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), ''));
}).catch(err => this.handleError(err.message, 'err'));
}
handleReportTopic(topicName) {
@@ -19583,7 +19595,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
target_name: targetName
};
topic.updateMode(null, '-JP').then(_ => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(window.location.hash, '');
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), '');
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(hash, 'abuse-report-reasons');
Object.entries(params).forEach(([key, value]) => {
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].addUrlParam(hash, key, value);
@@ -19797,7 +19809,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
can_add_friend: !!data.user.can_add
}
});
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(window.location.hash, '');
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), '');
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(hash, data.user.relationship === 'friend' ? 'friend-profile' : 'group-member-profile');
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].addUrlParam(hash, 'uid', data.user.user_id);
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(hash);
@@ -19864,7 +19876,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(hash);
return;
}
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].addUrlParam(window.location.hash, 'info', 'info'));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), 'info', 'info'));
this.setState({
infoPanel: 'info'
});
@@ -19895,7 +19907,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
meth: cred,
resp: code
}
}).then(_ => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, 'contacts'))).catch(err => this.handleError(err.message, 'err'));
}).then(_ => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), 'contacts'))).catch(err => this.handleError(err.message, 'err'));
} else {
this.setState({
credMethod: cred,
@@ -20656,13 +20668,13 @@ __webpack_require__.r(__webpack_exports__);
const navigate = (view, params) => {
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view);
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].currentUrl(), view);
Object.entries(params || {}).forEach(([key, value]) => {
hash = value ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(hash, key, value) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].removeUrlParam(hash, key);
});
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(hash);
};
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view));
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].currentUrl(), view));
const WORKPLACE_COLLABORATION_VIEWS = ['workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail', 'monthly-report-editor', 'monthly-report-mine', 'monthly-report-team', 'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice'];
function Icon({
children
@@ -70766,7 +70778,7 @@ const messageLoader = {
const rtl = ['ar', 'fa', 'he', 'ur'];
const {
params
} = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_4__["default"].parseUrlHash(window.location.hash);
} = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_4__["default"].parseUrl(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_4__["default"].currentUrl());
const language = params && params.hl || navigator.languages && navigator.languages[0] || navigator.language || navigator.userLanguage || 'en';
const normalized = language.replace('_', '-');
const baseLanguage = normalized.split('-')[0].toLowerCase();
+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
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1828,7 +1828,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
if (!topic || !seq || isNaN(seq)) {
return;
}
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].removeUrlParam(window.location.hash, 'jumpseq'));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].removeUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].currentUrl(), 'jumpseq'));
const jump = () => window.setTimeout(() => this.handleQuoteClick(seq), 150);
if (topic.findMessage(seq)) {
jump();
@@ -2757,7 +2757,7 @@ class BaseChatMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().Pure
break;
case 'contact_find':
e.preventDefault();
let hashUrl = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_9__["default"].setUrlSidePanel(window.location.hash, 'newtpk');
let hashUrl = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_9__["default"].setUrlSidePanel(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_9__["default"].currentUrl(), 'newtpk');
hashUrl = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_9__["default"].addUrlParam(hashUrl, 'q', e.target.dataset.val);
hashUrl = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_9__["default"].addUrlParam(hashUrl, 'tab', 'find');
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_9__["default"].navigateTo(hashUrl);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -46,7 +46,7 @@ class WallpapersView extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureC
}
handleTabClick(e) {
e.preventDefault();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].currentUrl(), 'tab', e.currentTarget.dataset.id));
this.setState({
tab: e.currentTarget.dataset.id
});
File diff suppressed because one or more lines are too long