diff options
author | Julius Härtl <jus@bitgrid.net> | 2019-06-15 00:26:12 +0300 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2019-06-15 11:14:01 +0300 |
commit | d392585407cfd5abfd6b9f1880c96feeacc5447a (patch) | |
tree | 2ea57b1a86aeac4e07d3a0630fdaa99db0432dc8 | |
parent | 1428971a593764d829007b30ffe9d13d3ffec8ab (diff) |
Implement guest name change
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | lib/Controller/PublicSessionController.php | 8 | ||||
-rw-r--r-- | lib/Db/SessionMapper.php | 4 | ||||
-rw-r--r-- | lib/Service/ApiService.php | 20 | ||||
-rw-r--r-- | lib/Service/SessionService.php | 22 | ||||
-rw-r--r-- | src/components/EditorWrapper.vue | 49 | ||||
-rw-r--r-- | src/components/GuestNameDialog.vue | 52 | ||||
-rw-r--r-- | src/helpers.js | 8 | ||||
-rw-r--r-- | src/services/SyncService.js | 25 |
9 files changed, 125 insertions, 64 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 6dc7cfefb..4243816c2 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -14,6 +14,7 @@ return [ ['name' => 'Session#close', 'url' => '/session/close', 'verb' => 'GET'], ['name' => 'PublicSession#create', 'url' => '/public/session/create', 'verb' => 'GET'], + ['name' => 'PublicSession#updateSession', 'url' => '/public/session', 'verb' => 'POST'], ['name' => 'PublicSession#fetch', 'url' => '/public/session/fetch', 'verb' => 'GET'], ['name' => 'PublicSession#sync', 'url' => '/public/session/sync', 'verb' => 'POST'], ['name' => 'PublicSession#push', 'url' => '/public/session/push', 'verb' => 'POST'], diff --git a/lib/Controller/PublicSessionController.php b/lib/Controller/PublicSessionController.php index c5e430eab..4aff8d83b 100644 --- a/lib/Controller/PublicSessionController.php +++ b/lib/Controller/PublicSessionController.php @@ -110,4 +110,12 @@ class PublicSessionController extends PublicShareController { return $this->apiService->sync($documentId, $sessionId, $sessionToken, $version, $autosaveContent, $force, $manualSave, $token); } + /** + * @NoAdminRequired + * @PublicPage + */ + public function updateSession(int $documentId, int $sessionId, string $sessionToken, string $guestName) { + return $this->apiService->updateSession($documentId, $sessionId, $sessionToken, $guestName); + } + } diff --git a/lib/Db/SessionMapper.php b/lib/Db/SessionMapper.php index 87e305cff..c162d82ae 100644 --- a/lib/Db/SessionMapper.php +++ b/lib/Db/SessionMapper.php @@ -30,6 +30,10 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +/** + * @method Session update(Session $session) + * @method Session insert(Session $session) + */ class SessionMapper extends QBMapper { public function __construct(IDBConnection $db) { diff --git a/lib/Service/ApiService.php b/lib/Service/ApiService.php index a98d957ee..2b0280ece 100644 --- a/lib/Service/ApiService.php +++ b/lib/Service/ApiService.php @@ -61,7 +61,8 @@ class ApiService { try { $this->documentService->checkSharePermissions($token, Constants::PERMISSION_UPDATE); $readOnly = false; - } catch (NotFoundException $e) {} + } catch (NotFoundException $e) { + } } else if ($fileId) { $file = $this->documentService->getFileById($fileId); $readOnly = !$file->isUpdateable(); @@ -77,7 +78,8 @@ class ApiService { if (count($activeSessions) === 0 || $forceRecreate) { try { $this->documentService->resetDocument($file->getId(), $forceRecreate); - } catch (DocumentHasUnsavedChangesException $e) {} + } catch (DocumentHasUnsavedChangesException $e) { + } } $document = $this->documentService->createDocument($file); @@ -113,7 +115,8 @@ class ApiService { if (count($activeSessions) === 0) { try { $this->documentService->resetDocument($documentId); - } catch (DocumentHasUnsavedChangesException $e) {} + } catch (DocumentHasUnsavedChangesException $e) { + } } return new DataResponse([]); } @@ -167,4 +170,15 @@ class ApiService { 'document' => $document ]); } + + public function updateSession(int $documentId, int $sessionId, string $sessionToken, string $guestName) { + if (!$this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { + return new DataResponse([], 500); + } + + if ($guestName === '') { + return new DataResponse([ 'message' => 'A guest name needs to be provided'], 500); + } + return $this->sessionService->updateSession($documentId, $sessionId, $sessionToken, $guestName); + } } diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index fe796955c..b4eb393dd 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -60,7 +60,9 @@ class SessionService { $color = $avatarGenerator->getGuestAvatar($userName)->avatarBackgroundColor($userName); $color = sprintf("#%02x%02x%02x", $color->r, $color->g, $color->b); $session->setColor($color); - $session->setGuestName($guestName); + if ($this->userId === null) { + $session->setGuestName($guestName); + } $session->setLastContact($this->timeFactory->getTime()); return $this->sessionMapper->insert($session); } @@ -102,8 +104,20 @@ class SessionService { return true; } - public function cleanupSession() { - // find expired sessions - // remove them + /** + * @param $documentId + * @param $sessionId + * @param $sessionToken + * @param $guestName + * @return Session + * @throws DoesNotExistException + */ + public function updateSession(int $documentId, int $sessionId, string $sessionToken, string $guestName): Session { + if ($this->userId !== null) { + throw new \Exception('Logged in users cannot set a guest name'); + } + $session = $this->sessionMapper->find($documentId, $sessionId, $sessionToken); + $session->setGuestName($guestName); + return $this->sessionMapper->update($session); } } diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue index 91ec5531d..3f2d28c25 100644 --- a/src/components/EditorWrapper.vue +++ b/src/components/EditorWrapper.vue @@ -31,6 +31,7 @@ <div id="editor"> <menu-bar v-if="!syncError && !readOnly" ref="menubar" :editor="tiptap"> <div v-if="currentSession && active" id="editor-session-list"> + <guest-name-dialog v-if="isPublic && currentSession.guestName" :sync-service="syncService" /> <div v-tooltip="lastSavedStatusTooltip" class="save-status" :class="lastSavedStatusClass"> {{ lastSavedStatus }} </div> @@ -47,8 +48,6 @@ </div> <collision-resolve-dialog v-if="hasSyncCollission && !readOnly" @resolveUseThisVersion="resolveUseThisVersion" @resolveUseServerVersion="resolveUseServerVersion" /> - - <guest-name-dialog v-if="isPublic && !guestNameConfirmed" :value="guestName" @input="setGuestName($event)" /> </div> </template> @@ -56,7 +55,7 @@ import Vue from 'vue' import { SyncService, ERROR_TYPE } from './../services/SyncService' -import { endpointUrl } from './../helpers' +import { endpointUrl, getRandomGuestName } from './../helpers' import { createEditor, markdownit, createMarkdownSerializer } from './../EditorFactory' import { EditorContent } from 'tiptap' @@ -129,10 +128,7 @@ export default { lastSavedString: '', syncError: null, readOnly: true, - forceRecreate: false, - - guestName: '', - guestNameConfirmed: false + forceRecreate: false } }, computed: { @@ -191,14 +187,8 @@ export default { watch: { lastSavedStatus: function() { this.$refs.menubar.redrawMenuBar() } }, - beforeMount() { - const guestName = localStorage.getItem('text-guestName') - if (guestName !== null) { - this.guestName = guestName - } - }, mounted() { - if (this.active && (this.hasDocumentParameters) && !this.isPublic) { + if (this.active && (this.hasDocumentParameters)) { this.initSession() } setInterval(() => { this.updateLastSavedStatus() }, 2000) @@ -211,12 +201,6 @@ export default { } }, methods: { - setGuestName(guestName) { - this.guestName = guestName - this.guestNameConfirmed = true - localStorage.setItem('text-guestName', this.guestName) - this.initSession() - }, updateLastSavedStatus() { if (this.document) { this.lastSavedString = window.moment(this.document.lastSavedVersionTime * 1000).fromNow() @@ -227,9 +211,10 @@ export default { this.$emit('error', 'No valid file provided') return } + const guestName = localStorage.getItem('text-guestName') ? localStorage.getItem('text-guestName') : getRandomGuestName() this.syncService = new SyncService({ shareToken: this.shareToken, - guestName: this.guestName, + guestName, forceRecreate: this.forceRecreate, serialize: (document) => { const markdown = (createMarkdownSerializer(this.tiptap.nodes, this.tiptap.marks)).serialize(document) @@ -241,6 +226,7 @@ export default { this.currentSession = session this.document = document this.readOnly = document.readOnly + localStorage.setItem('text-guestName', this.currentSession.guestName) }) .on('change', ({ document, sessions }) => { if (this.document.baseVersionEtag !== '' && document.baseVersionEtag !== this.document.baseVersionEtag) { @@ -254,7 +240,7 @@ export default { this.tiptap.setOptions({ editable: !this.readOnly }) }) - .on('loaded', ({ document, session, documentSource }) => { + .on('loaded', ({ documentSource }) => { this.tiptap = createEditor({ content: markdownit.render(documentSource), onUpdate: ({ state }) => { @@ -337,26 +323,25 @@ export default { updateSessions(sessions) { this.sessions = sessions.sort((a, b) => b.lastContact - a.lastContact) let currentSessionIds = this.sessions.map((session) => session.userId) + let currentGuestIds = this.sessions.map((session) => session.guestId) + const removedSessions = Object.keys(this.filteredSessions) - .filter(sessionId => !currentSessionIds.includes(sessionId)) + .filter(sessionId => !currentSessionIds.includes(sessionId) && !currentGuestIds.includes(sessionId)) - // remove sessions + // remove sessions TODO: remove guest sessions for (let index in removedSessions) { Vue.delete(this.filteredSessions, removedSessions[index]) } for (let index in this.sessions) { let session = this.sessions[index] - if (!session.userId) { - session.userId = session.id - } - - if (this.filteredSessions[session.userId]) { + const sessionKey = session.displayName ? session.userId : session.id + if (this.filteredSessions[sessionKey]) { // update timestamp if relevant - if (this.filteredSessions[session.userId].lastContact < session.lastContact) { - Vue.set(this.filteredSessions[session.userId], 'lastContact', session.lastContact) + if (this.filteredSessions[sessionKey].lastContact < session.lastContact) { + Vue.set(this.filteredSessions[sessionKey], 'lastContact', session.lastContact) } } else { - Vue.set(this.filteredSessions, session.userId, session) + Vue.set(this.filteredSessions, sessionKey, session) } } }, diff --git a/src/components/GuestNameDialog.vue b/src/components/GuestNameDialog.vue index e774f7333..fe82328f1 100644 --- a/src/components/GuestNameDialog.vue +++ b/src/components/GuestNameDialog.vue @@ -21,47 +21,51 @@ --> <template> - <div class="guest-name-dialog"> - <p>{{ t('text', 'Please enter a name to identify you as a public editor:') }}</p> - <form @submit.prevent="setGuestName()"> - <input ref="guestNameField" type="text" :value="value"> - <input type="submit" class="icon-confirm" value=""> - </form> - </div> + <form v-tooltip="t('text', 'Please enter a name to identify you as a public editor:')" class="guest-name-dialog" @submit.prevent="setGuestName()"> + <input v-model="guestName" type="text"> + <input type="submit" class="icon-confirm" value=""> + </form> </template> <script> export default { name: 'GuestNameDialog', props: { - value: { - type: String, - default: '' + syncService: { + type: Object, + default: null } }, + data() { + return { + guestName: '' + } + }, + beforeMount() { + this.guestName = this.syncService.session.guestName + }, methods: { setGuestName() { - this.$emit('input', this.$refs.guestNameField.value) + const previousGuestName = this.syncService.session.guestName + this.syncService.updateSession(this.guestName).then(() => { + localStorage.setItem('text-guestName', this.guestName) + }).catch((e) => { + this.guestName = previousGuestName + }) } } } </script> <style scoped lang="scss"> - .guest-name-dialog { - padding: 30px; - text-align: center; - - form { - display: flex; - width: 100%; - max-width: 200px; - margin: auto; - margin-top: 30px; + form.guest-name-dialog { + display: flex; + width: 100%; + max-width: 200px; + margin: auto; - input[type=text] { - flex-grow: 1; - } + input[type=text] { + flex-grow: 1; } } </style> diff --git a/src/helpers.js b/src/helpers.js index f68d18d46..77dfc5532 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -41,7 +41,13 @@ const endpointUrl = (endpoint, isPublic = false) => { return `${_baseUrl}/${endpoint}` } +const randomGuestNames = ['Artichoke', 'Arugula', 'Asparagus', 'Avocado', 'Bamboo Shoot', 'Bean Sprout', 'Bean', 'Beet', 'Belgian Endive', 'Bell Pepper', 'Bitter Melon', 'Bitter Gourd', 'Bok Choy', 'Broccoli', 'Brussels Sprout', 'Burdock Root', 'Cabbage', 'Calabash', 'Caper', 'Carrot', 'Cassava', 'Cauliflower', 'Celery', 'Celery Root', 'Celtuce', 'Chayote', 'Chinese Broccoli', 'Corn', 'Baby Corn', 'Cucumber', 'English Cucumber', 'Gherkin', 'Pickling Cucumber', 'Daikon Radish', 'Edamame', 'Eggplant', 'Elephant Garlic', 'Endive', 'Curly', 'Escarole', 'Fennel', 'Fiddlehead', 'Galangal', 'Garlic', 'Ginger', 'Grape Leave', 'Green Bean', 'Wax Bean', 'Green', 'Amaranth Leave', 'Beet Green', 'Collard Green', 'Dandelion Green', 'Kale', 'Kohlrabi Green', 'Mustard Green', 'Rapini', 'Spinach', 'Swiss Chard', 'Turnip Green', 'Hearts of Palm', 'Horseradish', 'Jerusalem Artichoke', 'Jícama', 'Kale', 'Curly', 'Lacinato', 'Ornamental', 'Kohlrabi', 'Leeks', 'Lemongrass', 'Lettuce', 'Butterhead', 'Iceberg', 'Leaf', 'Romaine', 'Lotus Root', 'Lotus Seed', 'Mushroom', 'Napa Cabbage', 'Nopales', 'Okra', 'Olive', 'Onion', 'Green Onion', 'Parsley', 'Parsley Root', 'Parsnip', 'Pepper', 'Plantain', 'Potato', 'Pumpkin', 'Purslane', 'Radicchio', 'Radish', 'Rutabaga', 'Shallots', 'Spinach', 'Squash', 'Sweet Potato', 'Swiss Chard', 'Taro', 'Tomatillo', 'Tomato', 'Turnip', 'Water Chestnut', 'Water Spinach', 'Watercress', 'Winter Melon', 'Yams', 'Zucchini'] +const getRandomGuestName = () => { + return randomGuestNames[Math.floor(Math.random() * randomGuestNames.length)] +} + export { documentReady, - endpointUrl + endpointUrl, + getRandomGuestName } diff --git a/src/services/SyncService.js b/src/services/SyncService.js index 976c7b5b6..15bce1901 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -134,6 +134,27 @@ class SyncService { ) } + updateSession(guestName) { + if (!this.isPublic()) { + return + } + return axios.post( + endpointUrl('session', !!this.options.shareToken), { + documentId: this.document.id, + sessionId: this.session.id, + sessionToken: this.session.token, + token: this.options.shareToken, + guestName + } + ).then(({ data }) => { + this.session = data + return data + }).catch((error) => { + console.error('Failed to update the session', error) + return Promise.reject(error) + }) + } + sendSteps(_sendable) { let sendable = _sendable || sendableSteps(this.state) if (!sendable) { @@ -228,6 +249,10 @@ class SyncService { } } + isPublic() { + return !!this.options.shareToken + } + } export default SyncService |