diff --git a/mattermost-plugin/webapp/src/index.tsx b/mattermost-plugin/webapp/src/index.tsx index 2eaf5326d..d02eb7095 100644 --- a/mattermost-plugin/webapp/src/index.tsx +++ b/mattermost-plugin/webapp/src/index.tsx @@ -20,6 +20,7 @@ import store from '../../../webapp/src/store' import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader' import FocalboardIcon from '../../../webapp/src/widgets/icons/logo' import {setMattermostTheme} from '../../../webapp/src/theme' + import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK} from './../../../webapp/src/wsclient' import TelemetryClient from '../../../webapp/src/telemetry/telemetryClient' @@ -37,6 +38,13 @@ import {PluginRegistry} from './types/mattermost-webapp' import './plugin.scss' +function getSubpath(siteURL: string): string { + const url = new URL(siteURL) + + // remove trailing slashes + return url.pathname.replace(/\/+$/, '') +} + const TELEMETRY_RUDDER_KEY = 'placeholder_rudder_key' const TELEMETRY_RUDDER_DATAPLANE_URL = 'placeholder_rudder_dataplane_url' const TELEMETRY_OPTIONS = { @@ -115,6 +123,11 @@ export default class Plugin { // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function async initialize(registry: PluginRegistry, mmStore: Store>>): Promise { + const siteURL = mmStore.getState().entities.general.config.SiteURL + const subpath = siteURL ? getSubpath(siteURL) : '' + windowAny.frontendBaseURL = subpath + windowAny.frontendBaseURL + windowAny.baseURL = subpath + windowAny.baseURL + this.registry = registry let theme = getTheme(mmStore.getState()) @@ -136,10 +149,10 @@ export default class Plugin { }) if (this.registry.registerProduct) { - windowAny.frontendBaseURL = '/boards' + windowAny.frontendBaseURL = subpath + '/boards' const goToFocalboardWorkspace = () => { const currentChannel = mmStore.getState().entities.channels.currentChannelId - window.open(`${window.location.origin}/boards/workspace/${currentChannel}`) + window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`) } this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(, goToFocalboardWorkspace, '', 'Boards') @@ -163,7 +176,7 @@ export default class Plugin { }) this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/plug/focalboard/go-to-current-workspace', MainApp, HeaderComponent) } else { - windowAny.frontendBaseURL = '/plug/focalboard' + windowAny.frontendBaseURL = subpath + '/plug/focalboard' this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(, () => { const currentChannel = mmStore.getState().entities.channels.currentChannelId window.open(`${window.location.origin}/plug/focalboard/workspace/${currentChannel}`) diff --git a/webapp/src/octoClient.ts b/webapp/src/octoClient.ts index a20d93ddd..497c5c006 100644 --- a/webapp/src/octoClient.ts +++ b/webapp/src/octoClient.ts @@ -12,7 +12,27 @@ import {ClientConfig} from './config/clientConfig' // OctoClient is the client interface to the server APIs // class OctoClient { - readonly serverUrl: string + readonly serverUrl: string | undefined + private logged = false + + // this need to be a function rather than a const because + // one of the global variable (`window.baseURL`) is set at runtime + // after the first instance of OctoClient is created. + // Avoiding the race condition becomes more complex than making + // the base URL dynamic though a function + private getBaseURL(): string { + const baseURL = (this.serverUrl || Utils.getBaseURL(true)).replace(/\/$/, '') + + // Logging this for debugging. + // Logging just once to avoid log noise. + if (!this.logged) { + Utils.log(`OctoClient baseURL: ${baseURL}`) + this.logged = true + } + + return baseURL + } + get token(): string { return localStorage.getItem('focalboardSessionId') || '' } @@ -27,8 +47,7 @@ class OctoClient { } constructor(serverUrl?: string, public workspaceId = '0') { - this.serverUrl = (serverUrl || Utils.getBaseURL(true)).replace(/\/$/, '') - Utils.log(`OctoClient serverUrl: ${this.serverUrl}`) + this.serverUrl = serverUrl } private async getJson(response: Response, defaultValue: any): Promise { @@ -44,7 +63,7 @@ class OctoClient { async login(username: string, password: string): Promise { const path = '/api/v1/login' const body = JSON.stringify({username, password, type: 'normal'}) - const response = await fetch(this.serverUrl + path, { + const response = await fetch(this.getBaseURL() + path, { method: 'POST', headers: this.headers(), body, @@ -82,7 +101,7 @@ class OctoClient { async register(email: string, username: string, password: string, token?: string): Promise<{code: number, json: any}> { const path = '/api/v1/register' const body = JSON.stringify({email, username, password, token}) - const response = await fetch(this.serverUrl + path, { + const response = await fetch(this.getBaseURL() + path, { method: 'POST', headers: this.headers(), body, @@ -94,7 +113,7 @@ class OctoClient { async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<{code: number, json: any}> { const path = `/api/v1/users/${encodeURIComponent(userId)}/changepassword` const body = JSON.stringify({oldPassword, newPassword}) - const response = await fetch(this.serverUrl + path, { + const response = await fetch(this.getBaseURL() + path, { method: 'POST', headers: this.headers(), body, @@ -118,7 +137,7 @@ class OctoClient { async getMe(): Promise { const path = '/api/v1/users/me' - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return undefined } @@ -128,7 +147,7 @@ class OctoClient { async getUser(userId: string): Promise { const path = `/api/v1/users/${encodeURIComponent(userId)}` - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return undefined } @@ -142,7 +161,7 @@ class OctoClient { if (readToken) { path += `&read_token=${readToken}` } - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return [] } @@ -153,7 +172,7 @@ class OctoClient { // If no boardID is provided, it will export the entire archive async exportArchive(boardID = ''): Promise { const path = `${this.workspacePath()}/blocks/export?root_id=${boardID}` - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return [] } @@ -168,7 +187,7 @@ class OctoClient { // Utils.log(`\t ${block.type}, ${block.id}`) // }) const body = JSON.stringify(blocks) - return fetch(this.serverUrl + this.workspacePath() + '/blocks/import', { + return fetch(this.getBaseURL() + this.workspacePath() + '/blocks/import', { method: 'POST', headers: this.headers(), body, @@ -196,7 +215,7 @@ class OctoClient { } private async getBlocksWithPath(path: string): Promise { - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return [] } @@ -243,7 +262,7 @@ class OctoClient { async patchBlock(blockId: string, blockPatch: BlockPatch): Promise { Utils.log(`patchBlocks: ${blockId} block`) const body = JSON.stringify(blockPatch) - return fetch(this.serverUrl + this.workspacePath() + '/blocks/' + blockId, { + return fetch(this.getBaseURL() + this.workspacePath() + '/blocks/' + blockId, { method: 'PATCH', headers: this.headers(), body, @@ -256,7 +275,7 @@ class OctoClient { async deleteBlock(blockId: string): Promise { Utils.log(`deleteBlock: ${blockId}`) - return fetch(this.serverUrl + this.workspacePath() + `/blocks/${encodeURIComponent(blockId)}`, { + return fetch(this.getBaseURL() + this.workspacePath() + `/blocks/${encodeURIComponent(blockId)}`, { method: 'DELETE', headers: this.headers(), }) @@ -272,7 +291,7 @@ class OctoClient { Utils.log(`\t ${block.type}, ${block.id}, ${block.title?.substr(0, 50) || ''}`) }) const body = JSON.stringify(blocks) - return fetch(this.serverUrl + this.workspacePath() + '/blocks', { + return fetch(this.getBaseURL() + this.workspacePath() + '/blocks', { method: 'POST', headers: this.headers(), body, @@ -283,7 +302,7 @@ class OctoClient { async getSharing(rootId: string): Promise { const path = this.workspacePath() + `/sharing/${rootId}` - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return undefined } @@ -295,7 +314,7 @@ class OctoClient { const path = this.workspacePath() + `/sharing/${sharing.id}` const body = JSON.stringify(sharing) const response = await fetch( - this.serverUrl + path, + this.getBaseURL() + path, { method: 'POST', headers: this.headers(), @@ -313,7 +332,7 @@ class OctoClient { async getWorkspace(): Promise { const path = this.workspacePath() - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return undefined } @@ -323,7 +342,7 @@ class OctoClient { async regenerateWorkspaceSignupToken(): Promise { const path = this.workspacePath() + '/regenerate_signup_token' - const response = await fetch(this.serverUrl + path, { + const response = await fetch(this.getBaseURL() + path, { method: 'POST', headers: this.headers(), }) @@ -348,7 +367,7 @@ class OctoClient { // TIPTIP: Leave out Content-Type here, it will be automatically set by the browser delete headers['Content-Type'] - const response = await fetch(this.serverUrl + this.workspacePath() + '/' + rootID + '/files', { + const response = await fetch(this.getBaseURL() + this.workspacePath() + '/' + rootID + '/files', { method: 'POST', headers, body: formData, @@ -380,7 +399,7 @@ class OctoClient { if (readToken) { path += `?read_token=${readToken}` } - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return '' } @@ -390,7 +409,7 @@ class OctoClient { async getWorkspaceUsers(): Promise { const path = this.workspacePath() + '/users' - const response = await fetch(this.serverUrl + path, {headers: this.headers()}) + const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) if (response.status !== 200) { return [] } diff --git a/webapp/src/wsclient.ts b/webapp/src/wsclient.ts index 6b8ae1529..c43040ed0 100644 --- a/webapp/src/wsclient.ts +++ b/webapp/src/wsclient.ts @@ -42,22 +42,41 @@ class WSClient { ws: WebSocket|null = null client: MMWebSocketClient|null = null clientPrefix = '' - serverUrl: string + serverUrl: string | undefined state: 'init'|'open'|'close' = 'init' onStateChange: OnStateChangeHandler[] = [] onReconnect: OnReconnectHandler[] = [] onChange: OnChangeHandler[] = [] onError: OnErrorHandler[] = [] - private mmWSMaxRetries = 10 + private mmWSMaxRetries = 100 private mmWSRetryDelay = 300 private notificationDelay = 100 private reopenDelay = 3000 private updatedBlocks: Block[] = [] private updateTimeout?: NodeJS.Timeout + private logged = false + + // this need to be a function rather than a const because + // one of the global variable (`window.baseURL`) is set at runtime + // after the first instance of OctoClient is created. + // Avoiding the race condition becomes more complex than making + // the base URL dynamic though a function + private getBaseURL(): string { + const baseURL = (this.serverUrl || Utils.getBaseURL(true)).replace(/\/$/, '') + + // Logging this for debugging. + // Logging just once to avoid log noise. + if (!this.logged) { + Utils.log(`WSClient serverUrl: ${baseURL}`) + this.logged = true + } + + return baseURL + } + constructor(serverUrl?: string) { - this.serverUrl = (serverUrl || Utils.getBaseURL(true)).replace(/\/$/, '') - Utils.log(`WSClient serverUrl: ${this.serverUrl}`) + this.serverUrl = serverUrl } initPlugin(pluginId: string, client: MMWebSocketClient): void { @@ -148,7 +167,7 @@ class WSClient { return } - const url = new URL(this.serverUrl) + const url = new URL(this.getBaseURL()) const protocol = (url.protocol === 'https:') ? 'wss:' : 'ws:' const wsServerUrl = `${protocol}//${url.host}${url.pathname.replace(/\/$/, '')}/ws` Utils.log(`WSClient open: ${wsServerUrl}`)