diff --git a/editions/multiwikiserver/tiddlywiki.info b/editions/multiwikiserver/tiddlywiki.info
index e16e3c3da..b190fa7d7 100644
--- a/editions/multiwikiserver/tiddlywiki.info
+++ b/editions/multiwikiserver/tiddlywiki.info
@@ -3,6 +3,7 @@
"plugins": [
"tiddlywiki/tiddlyweb",
"tiddlywiki/filesystem",
+ "tiddlywiki/multiwikiclient",
"tiddlywiki/multiwikiserver"
],
"themes": [
diff --git a/plugins/tiddlywiki/multiwikiclient/GettingStarted.tid b/plugins/tiddlywiki/multiwikiclient/GettingStarted.tid
new file mode 100644
index 000000000..08df08c07
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/GettingStarted.tid
@@ -0,0 +1,16 @@
+title: GettingStarted
+tags: $:/tags/GettingStarted
+caption: Step 1 Syncing
+
+Welcome to ~TiddlyWiki and the ~TiddlyWiki community
+
+Visit https://tiddlywiki.com/ to find out more about ~TiddlyWiki and what it can do.
+
+! Syncing Changes to the Server
+
+Before you can start storing important information in ~TiddlyWiki it is important to make sure that your changes are being reliably saved by the server.
+
+# Create a new tiddler using the {{$:/core/images/new-button}} button in the sidebar on the right
+# Click the {{$:/core/images/done-button}} button at the top right of the new tiddler
+# Check the ~TiddlyWiki command line for a message confirming the tiddler has been saved
+# Refresh the page in the browser to and verify that the new tiddler has been correctly saved
diff --git a/plugins/tiddlywiki/multiwikiclient/SaveWikiButtonTemplate.tid b/plugins/tiddlywiki/multiwikiclient/SaveWikiButtonTemplate.tid
new file mode 100644
index 000000000..45b8959ce
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/SaveWikiButtonTemplate.tid
@@ -0,0 +1,3 @@
+title: $:/config/SaveWikiButton/Template
+
+$:/plugins/tiddlywiki/multiwikiclient/save/offline
\ No newline at end of file
diff --git a/plugins/tiddlywiki/multiwikiclient/config-tiddlers-filter.tid b/plugins/tiddlywiki/multiwikiclient/config-tiddlers-filter.tid
new file mode 100644
index 000000000..4e5c951db
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/config-tiddlers-filter.tid
@@ -0,0 +1,2 @@
+title: $:/config/Server/ExternalFilters/[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/library/sjcl.js]] -[[$:/core]]
+text: yes
diff --git a/plugins/tiddlywiki/multiwikiclient/configOfficialPluginLibrary.tid b/plugins/tiddlywiki/multiwikiclient/configOfficialPluginLibrary.tid
new file mode 100644
index 000000000..d2a07991a
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/configOfficialPluginLibrary.tid
@@ -0,0 +1,7 @@
+title: $:/config/OfficialPluginLibrary
+tags: $:/tags/PluginLibrary
+url: https://tiddlywiki.com/library/v5.1.23/index.html
+caption: {{$:/language/OfficialPluginLibrary}}
+enabled: no
+
+The official plugin library is disabled when using the client-server configuration. Instead, plugins should be installed via the `tiddlywiki.info` file, as described [[here|https://tiddlywiki.com/#Installing%20a%20plugin%20from%20the%20plugin%20library]].
\ No newline at end of file
diff --git a/plugins/tiddlywiki/multiwikiclient/css-tiddler.tid b/plugins/tiddlywiki/multiwikiclient/css-tiddler.tid
new file mode 100644
index 000000000..821fa1186
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/css-tiddler.tid
@@ -0,0 +1,7 @@
+title: $:/core/templates/css-tiddler
+
+``
\ No newline at end of file
diff --git a/plugins/tiddlywiki/multiwikiclient/html-div-skinny-tiddler.tid b/plugins/tiddlywiki/multiwikiclient/html-div-skinny-tiddler.tid
new file mode 100644
index 000000000..010a603b5
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/html-div-skinny-tiddler.tid
@@ -0,0 +1,9 @@
+title: $:/core/templates/html-div-skinny-tiddler
+
+`
$fields>` revision="`<
>`" bag="default" _is_skinny="">
+
+ `
diff --git a/plugins/tiddlywiki/multiwikiclient/html-div-tiddler.tid b/plugins/tiddlywiki/multiwikiclient/html-div-tiddler.tid
new file mode 100644
index 000000000..ff27343fc
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/html-div-tiddler.tid
@@ -0,0 +1,9 @@
+title: $:/core/templates/html-div-tiddler
+
+`$fields>` revision="`<
>`" bag="default">
+`<$view field="text" format="htmltextencoded" />`
+ `
diff --git a/plugins/tiddlywiki/multiwikiclient/html-json-skinny-tiddler.tid b/plugins/tiddlywiki/multiwikiclient/html-json-skinny-tiddler.tid
new file mode 100644
index 000000000..ce953fbf2
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/html-json-skinny-tiddler.tid
@@ -0,0 +1,3 @@
+title: $:/core/templates/html-json-skinny-tiddler
+
+<$text text=<>/><$jsontiddler tiddler=<> exclude="text" escapeUnsafeScriptChars="yes" $revision=<> $bag="default" $_is_skinny=""/>
diff --git a/plugins/tiddlywiki/multiwikiclient/html-json-tiddler.tid b/plugins/tiddlywiki/multiwikiclient/html-json-tiddler.tid
new file mode 100644
index 000000000..f357321fb
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/html-json-tiddler.tid
@@ -0,0 +1,3 @@
+title: $:/core/templates/html-json-tiddler
+
+<$jsontiddler tiddler=<> escapeUnsafeScriptChars="yes" $revision=<> $bag="default"/>
diff --git a/plugins/tiddlywiki/multiwikiclient/icon-cloud.tid b/plugins/tiddlywiki/multiwikiclient/icon-cloud.tid
new file mode 100644
index 000000000..e448bc548
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/icon-cloud.tid
@@ -0,0 +1,4 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/icon/cloud
+tags: $:/tags/Image
+
+
\ No newline at end of file
diff --git a/plugins/tiddlywiki/multiwikiclient/javascript-tiddler.tid b/plugins/tiddlywiki/multiwikiclient/javascript-tiddler.tid
new file mode 100644
index 000000000..dc4bc4db9
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/javascript-tiddler.tid
@@ -0,0 +1,7 @@
+title: $:/core/templates/javascript-tiddler
+
+``
\ No newline at end of file
diff --git a/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js b/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js
new file mode 100644
index 000000000..d2f0a235b
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js
@@ -0,0 +1,351 @@
+/*\
+title: $:/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js
+type: application/javascript
+module-type: syncadaptor
+
+A sync adaptor module for synchronising with MultiWikiServer-compatible servers
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var CONFIG_HOST_TIDDLER = "$:/config/multiwikiclient/host",
+ DEFAULT_HOST_TIDDLER = "$protocol$//$host$/";
+
+function MultiWikiClientAdaptor(options) {
+ this.wiki = options.wiki;
+ this.host = this.getHost();
+ this.recipe = this.wiki.getTiddlerText("$:/config/multiwikiclient/recipe");
+ this.logger = new $tw.utils.Logger("MultiWikiClientAdaptor");
+ this.isLoggedIn = false;
+ this.isReadOnly = false;
+ this.logoutIsAvailable = true;
+}
+
+MultiWikiClientAdaptor.prototype.name = "multiwikiclient";
+
+MultiWikiClientAdaptor.prototype.supportsLazyLoading = true;
+
+MultiWikiClientAdaptor.prototype.setLoggerSaveBuffer = function(loggerForSaving) {
+ this.logger.setSaveBuffer(loggerForSaving);
+};
+
+MultiWikiClientAdaptor.prototype.isReady = function() {
+ return true;
+};
+
+MultiWikiClientAdaptor.prototype.getHost = function() {
+ var text = this.wiki.getTiddlerText(CONFIG_HOST_TIDDLER,DEFAULT_HOST_TIDDLER),
+ substitutions = [
+ {name: "protocol", value: document.location.protocol},
+ {name: "host", value: document.location.host},
+ {name: "pathname", value: document.location.pathname}
+ ];
+ for(var t=0; t//:
+```
+*/
+MultiWikiClientAdaptor.prototype.parseEtag = function(etag) {
+ var firstSlash = etag.indexOf("/"),
+ lastSlash = etag.lastIndexOf("/"),
+ colon = etag.lastIndexOf(":");
+ if(firstSlash === -1 || lastSlash === -1 || colon === -1) {
+ return null;
+ } else {
+ return {
+ bag: $tw.utils.decodeURIComponentSafe(etag.substring(1,firstSlash)),
+ title: $tw.utils.decodeURIComponentSafe(etag.substring(firstSlash + 1,lastSlash)),
+ revision: etag.substring(lastSlash + 1,colon)
+ };
+ }
+};
+
+if($tw.browser && document.location.protocol.substr(0,4) === "http" ) {
+ exports.adaptorClass = MultiWikiClientAdaptor;
+}
+
+})();
diff --git a/plugins/tiddlywiki/multiwikiclient/plugin.info b/plugins/tiddlywiki/multiwikiclient/plugin.info
new file mode 100644
index 000000000..a1ee7eb0d
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/plugin.info
@@ -0,0 +1,7 @@
+{
+ "title": "$:/plugins/tiddlywiki/multiwikiclient",
+ "name": "MultiWikiClient",
+ "description": "Synchronise changes from the browser to TiddlyWiki ~MultiWikiServer",
+ "list": "readme",
+ "plugin-priority": 10
+}
diff --git a/plugins/tiddlywiki/multiwikiclient/readme.tid b/plugins/tiddlywiki/multiwikiclient/readme.tid
new file mode 100644
index 000000000..8fe63b142
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/readme.tid
@@ -0,0 +1,8 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/readme
+
+This plugin runs in the browser to synchronise tiddler changes to and from a TiddlyWiki server running ~MultiWikiServer.
+
+
+This plugin is inert when run under Node.js. Disabling this plugin via the browser can not be undone via the browser since this plugin provides the mechanism to synchronize settings with the server.
+
+Changes made while offline are saved in memory and automatically synchonised with the server when the connection is re-established. However, if the browser tab is closed or another URL is loaded, the in-memory changes will be lost. The [[https://tiddlywiki.com/#BrowserStorage Plugin]] may be added to provide temporary filesystem storage of tiddler changes made while offline and enable them to be synchronised with the server the next time the wiki is loaded in the same browser.
diff --git a/plugins/tiddlywiki/multiwikiclient/readonly-styles.tid b/plugins/tiddlywiki/multiwikiclient/readonly-styles.tid
new file mode 100644
index 000000000..4269ba799
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/readonly-styles.tid
@@ -0,0 +1,27 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/readonly
+tags: [[$:/tags/Stylesheet]]
+
+\define button-selector(title)
+button.$title$, .tc-drop-down button.$title$, div.$title$
+\end
+
+\define hide-edit-controls()
+<$reveal state="$:/status/IsReadOnly" type="match" text="yes" default="yes">
+<>`,`
+<>`,`
+<>`,`
+<>`,`
+<>`,`
+<>`,`
+<>`,`
+<>`,`
+<>`,`
+<> `{
+ display: none;
+}`
+$reveal>
+\end
+
+\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline macrocallblock
+
+<>
diff --git a/plugins/tiddlywiki/multiwikiclient/save-offline.tid b/plugins/tiddlywiki/multiwikiclient/save-offline.tid
new file mode 100644
index 000000000..20c420cd8
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/save-offline.tid
@@ -0,0 +1,7 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/save/offline
+
+\import [subfilter{$:/core/config/GlobalImportFilter}]
+\define saveTiddlerFilter()
+[is[tiddler]] -[[$:/boot/boot.css]] -[prefix[$:/HistoryList]] -[status[pending]plugin-type[import]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/plugins/tiddlywiki/filesystem]] -[[$:/plugins/tiddlywiki/multiwikiclient]] -[prefix[$:/temp/]] +[sort[title]] $(publishFilter)$
+\end
+{{$:/core/templates/tiddlywiki5.html}}
diff --git a/plugins/tiddlywiki/multiwikiclient/save-wiki-button.tid b/plugins/tiddlywiki/multiwikiclient/save-wiki-button.tid
new file mode 100644
index 000000000..07e72d335
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/save-wiki-button.tid
@@ -0,0 +1,26 @@
+title: $:/core/ui/Buttons/save-wiki
+tags: $:/tags/PageControls
+caption: {{$:/plugins/tiddlywiki/multiwikiclient/icon/cloud}} Server status
+description: Status of synchronisation with server
+
+\whitespace trim
+\define config-title()
+$:/config/PageControlButtons/Visibility/$(listItem)$
+\end
+<$button popup=<> tooltip="Status of synchronisation with server" aria-label="Server status" class=<> selectedClass="tc-selected">
+
+<$list filter="[match[yes]]">
+{{$:/plugins/tiddlywiki/multiwikiclient/icon/cloud}}
+$list>
+<$list filter="[match[yes]]">
+<$text text="Server status"/>
+$list>
+
+$button>
+<$reveal state=<> type="popup" position="belowleft" animate="yes">
+
+<$list filter="[all[shadows+tiddlers]tag[$:/tags/SyncerDropdown]!has[draft.of]]" variable="listItem">
+<$transclude tiddler=<>/>
+$list>
+
+$reveal>
diff --git a/plugins/tiddlywiki/multiwikiclient/styles.tid b/plugins/tiddlywiki/multiwikiclient/styles.tid
new file mode 100644
index 000000000..8fba4ec14
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/styles.tid
@@ -0,0 +1,44 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/styles
+tags: [[$:/tags/Stylesheet]]
+
+\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline macrocallblock
+
+body.tc-dirty span.tc-dirty-indicator svg {
+ transition: fill 250ms ease-in-out;
+}
+
+body .tc-image-cloud-idle {
+ fill: <>;
+ transition: opacity 250ms ease-in-out;
+ opacity: 1;
+ display: unset;
+}
+
+body.tc-dirty .tc-image-cloud-idle {
+ opacity: 0;
+ display: none;
+}
+
+body .tc-image-cloud-progress {
+ transition: opacity 250ms ease-in-out;
+ transform-origin: 50% 50%;
+ transform: rotate(359deg);
+ animation: animation-rotate-slow 2s infinite linear;
+ fill: <>;
+ display: none;
+ opacity: 0;
+}
+
+body.tc-dirty .tc-image-cloud-progress {
+ opacity: 1;
+ display: unset;
+}
+
+@keyframes animation-rotate-slow {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: scale(359deg);
+ }
+}
diff --git a/plugins/tiddlywiki/multiwikiclient/syncer-actions-copy-logs.tid b/plugins/tiddlywiki/multiwikiclient/syncer-actions-copy-logs.tid
new file mode 100644
index 000000000..4457f7529
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/syncer-actions-copy-logs.tid
@@ -0,0 +1,6 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/copy-logs
+tags: $:/tags/SyncerDropdown
+
+<$button message="tm-copy-syncer-logs-to-clipboard" class="tc-btn-invisible">
+{{$:/core/images/copy-clipboard}} Copy syncer logs to clipboard
+$button>
diff --git a/plugins/tiddlywiki/multiwikiclient/syncer-actions-login-status.tid b/plugins/tiddlywiki/multiwikiclient/syncer-actions-login-status.tid
new file mode 100644
index 000000000..6ebf98fcd
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/syncer-actions-login-status.tid
@@ -0,0 +1,9 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/login-status
+tags: $:/tags/SyncerDropdown
+
+<$reveal state="$:/status/IsLoggedIn" type="match" text="yes">
+
+You are logged in<$reveal state="$:/status/UserName" type="nomatch" text="" default=""> as <$text text={{$:/status/UserName}}/> $reveal><$reveal state="$:/status/IsReadOnly" type="match" text="yes" default="no"> (read-only)$reveal>
+
+
+$reveal>
diff --git a/plugins/tiddlywiki/multiwikiclient/syncer-actions-login.tid b/plugins/tiddlywiki/multiwikiclient/syncer-actions-login.tid
new file mode 100644
index 000000000..02cd2b6f6
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/syncer-actions-login.tid
@@ -0,0 +1,8 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/login
+tags: $:/tags/SyncerDropdown
+
+<$reveal state="$:/status/IsLoggedIn" type="nomatch" text="yes">
+<$button message="tm-login" class="tc-btn-invisible">
+{{$:/core/images/unlocked-padlock}} Login
+$button>
+$reveal>
diff --git a/plugins/tiddlywiki/multiwikiclient/syncer-actions-logout.tid b/plugins/tiddlywiki/multiwikiclient/syncer-actions-logout.tid
new file mode 100644
index 000000000..cab5f261f
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/syncer-actions-logout.tid
@@ -0,0 +1,8 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/logout
+tags: $:/tags/SyncerDropdown
+
+<$reveal state="$:/status/IsLoggedIn" type="match" text="yes">
+<$button message="tm-logout" class="tc-btn-invisible">
+{{$:/core/images/cancel-button}} Logout
+$button>
+$reveal>
diff --git a/plugins/tiddlywiki/multiwikiclient/syncer-actions-refresh.tid b/plugins/tiddlywiki/multiwikiclient/syncer-actions-refresh.tid
new file mode 100644
index 000000000..ea95a67d6
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/syncer-actions-refresh.tid
@@ -0,0 +1,9 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/refresh
+tags: $:/tags/SyncerDropdown
+
+<$reveal state="$:/status/IsLoggedIn" type="match" text="yes">
+<$button tooltip="Get latest changes from the server" aria-label="Refresh from server" class="tc-btn-invisible">
+<$action-sendmessage $message="tm-server-refresh"/>
+{{$:/core/images/refresh-button}}<$text text="Get latest changes from the server"/>
+$button>
+$reveal>
diff --git a/plugins/tiddlywiki/multiwikiclient/syncer-actions-save-snapshot.tid b/plugins/tiddlywiki/multiwikiclient/syncer-actions-save-snapshot.tid
new file mode 100644
index 000000000..db9b16e15
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/syncer-actions-save-snapshot.tid
@@ -0,0 +1,9 @@
+title: $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/save-snapshot
+tags: $:/tags/SyncerDropdown
+
+<$button class="tc-btn-invisible">
+<$wikify name="site-title" text={{$:/config/SaveWikiButton/Filename}}>
+<$action-sendmessage $message="tm-download-file" $param={{$:/config/SaveWikiButton/Template}} filename=<>/>
+$wikify>
+{{$:/core/images/download-button}} Save snapshot for offline use
+$button>
diff --git a/plugins/tiddlywiki/multiwikiclient/tags-syncerdropdown.tid b/plugins/tiddlywiki/multiwikiclient/tags-syncerdropdown.tid
new file mode 100644
index 000000000..4b5774eb9
--- /dev/null
+++ b/plugins/tiddlywiki/multiwikiclient/tags-syncerdropdown.tid
@@ -0,0 +1,2 @@
+title: $:/tags/SyncerDropdown
+list: $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/login-status $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/login $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/refresh $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/logout $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/save-snapshot $:/plugins/tiddlywiki/multiwikiclient/syncer-actions/copy-logs
diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js
index ea7184e54..483400411 100644
--- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js
+++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js
@@ -113,14 +113,15 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
const testSpecs = [
{
- description: "Check server status",
+ description: "Check index page",
method: "GET",
- path: "/wiki/recipe-alpha/status",
+ path: "/",
headers: {
accept: "*/*"
},
- expectedResult: (jsonData,data) => {
- return jsonData.username === "Joe Bloggs";
+ expectedResult: (jsonData,data,headers) => {
+ console.log(JSON.stringify(data).slice(1,100))
+ return JSON.stringify(data).slice(1,100) === "\\n\\n\\n\\t encodeuricomponent[]] }$/bags/${ [encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico`
+ source=`/bags/${ [encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico`
class="mws-favicon-small"
width="32px"
>
@@ -43,7 +43,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-bag