Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

browser: add last modification and document status at the bottom #4531

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions browser/css/toolbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -1806,3 +1806,12 @@ menu-entry-with-icon.padding-left + menu-entry-icon.width */
font-size: var(--default-font-size);
color: var(--color-main-text);
}

#saved-status-label {
background-image: url("data:image/svg+xml,%3Csvg id='Icons' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%235cb82a;%7D.cls-2%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3E_%3C/title%3E%3Cg id='Funktions-Icons_24' data-name='Funktions-Icons 24'%3E%3Ccircle class='cls-1' cx='10' cy='10' r='10'/%3E%3Crect class='cls-2' x='3.36' y='10.52' width='6.75' height='2.5' rx='0.5' ry='0.5' transform='translate(10.29 -1.31) rotate(45)'/%3E%3Crect class='cls-2' x='5.63' y='8.75' width='11.75' height='2.5' rx='0.5' ry='0.5' transform='translate(-3.7 11.06) rotate(-45)'/%3E%3C/g%3E%3C/svg%3E");
background-position: left center;
background-repeat: no-repeat;
padding-left: 18px;
margin-left: 15px;
white-space: nowrap;
}
1 change: 1 addition & 0 deletions browser/html/cool.html.m4
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ m4_ifelse(MOBILEAPP, [true],
data-enable-accessibility = "%ENABLE_ACCESSIBILITY%"
data-out-of-focus-timeout-secs = "%OUT_OF_FOCUS_TIMEOUT_SECS%"
data-idle-timeout-secs = "%IDLE_TIMEOUT_SECS%"
data-min-saved-message-timeout-secs = %MIN_SAVED_MESSAGE_TIMEOUT_SECS%;
data-protocol-debug = "%PROTOCOL_DEBUG%"
data-enable-debug = "%ENABLE_DEBUG%"
data-frame-ancestors = "%FRAME_ANCESTORS%"
Expand Down
37 changes: 37 additions & 0 deletions browser/src/control/Control.Menubar.js
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,8 @@ L.Control.Menubar = L.Control.extend({
map.on('addmenu', this._addMenu, this);
map.on('languagesupdated', this._onInitLanguagesMenu, this);
map.on('updatetoolbarcommandvalues', this._onStyleMenu, this);
map.on('initmodificationindicator', this._onInitModificationIndicator, this);
map.on('updatemodificationindicator', this._onUpdateModificationIndicator, this);
},

onRemove: function() {
Expand All @@ -1385,6 +1387,8 @@ L.Control.Menubar = L.Control.extend({
this._map.off('addmenu', this._addMenu, this);
this._map.off('languagesupdated', this._onInitLanguagesMenu, this);
this._map.off('updatetoolbarcommandvalues', this._onStyleMenu, this);
this._map.off('initmodificationindicator', this._onInitModificationIndicator, this);
this._map.off('updatemodificationindicator', this._onUpdateModificationIndicator, this);

this._menubarCont.remove();
this._menubarCont = null;
Expand Down Expand Up @@ -2532,6 +2536,39 @@ L.Control.Menubar = L.Control.extend({
}
return null;
},

_onInitModificationIndicator: function(lastmodtime) {
var lastModButton = L.DomUtil.get('menu-last-mod');
if (lastModButton !== null && lastModButton !== undefined
&& lastModButton.firstChild.innerHTML !== null
&& lastModButton.firstChild.childElementCount == 0) {
if (lastmodtime == null) {
// No modification time -> hide the indicator
L.DomUtil.setStyle(lastModButton, 'display', 'none');
return;
}
var mainSpan = document.createElement('span');
this.lastModIndicator = document.createElement('span');
mainSpan.appendChild(this.lastModIndicator);

//this._map.updateModificationIndicator(this._lastmodtime);

// Replace menu button body with new content
lastModButton.firstChild.replaceChildren();
lastModButton.firstChild.appendChild(mainSpan);

if (L.Params.revHistoryEnabled) {
L.DomUtil.setStyle(lastModButton, 'cursor', 'pointer');
}

this._map.fire('modificationindicatorinitialized');
}
},
_onUpdateModificationIndicator: function(e) {
if (this.lastModIndicator !== null && this.lastModIndicator !== undefined) {
this.lastModIndicator.innerHTML = e.lastSaved;
}
}
});

L.control.menubar = function (options) {
Expand Down
34 changes: 34 additions & 0 deletions browser/src/control/Control.StatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class StatusBar extends JSDialog.Toolbar {
map.on('updatestatepagenumber', this.onPageChange, this);
map.on('search', this.onSearch, this);
map.on('zoomend', this.onZoomEnd, this);
map.on('initmodificationindicator', this.onInitModificationIndicator, this);
map.on('updatemodificationindicator', this.onUpdateModificationIndicator, this);
}

localizeStateTableCell(text) {
Expand Down Expand Up @@ -242,6 +244,7 @@ class StatusBar extends JSDialog.Toolbar {
this._generateHtmlItem('permissionmode'), // spreadsheet, text, presentation
{type: 'toolitem', id: 'signstatus', command: '.uno:Signature', w2icon: '', text: _UNO('.uno:Signature'), visible: false},
{type: 'spacer', id: 'permissionspacer'},
this._generateHtmlItem('documentstatus'), // spreadsheet, text, presentation, drawing
{type: 'customtoolitem', id: 'prev', command: 'prev', text: _UNO('.uno:PageUp', 'text'), pressAndHold: true},
{type: 'customtoolitem', id: 'next', command: 'next', text: _UNO('.uno:PageDown', 'text'), pressAndHold: true},
{type: 'separator', id: 'prevnextbreak', orientation: 'vertical'},
Expand Down Expand Up @@ -292,6 +295,7 @@ class StatusBar extends JSDialog.Toolbar {
this.showItem('StateTableCellMenu', !app.map.isReadOnlyMode());
this.showItem('statetablebreak', !app.map.isReadOnlyMode());
this.showItem('permissionmode-container', true);
this.showItem('documentstatus-container', true);
}
break;

Expand All @@ -304,6 +308,7 @@ class StatusBar extends JSDialog.Toolbar {
this.showItem('languagestatus', !app.map.isReadOnlyMode());
this.showItem('languagestatusbreak', !app.map.isReadOnlyMode());
this.showItem('permissionmode-container', true);
this.showItem('documentstatus-container', true);
}
break;

Expand All @@ -313,6 +318,7 @@ class StatusBar extends JSDialog.Toolbar {
this.showItem('languagestatus', !app.map.isReadOnlyMode());
this.showItem('languagestatusbreak', !app.map.isReadOnlyMode());
this.showItem('permissionmode-container', true);
this.showItem('documentstatus-container', true);
}
break;
case 'drawing':
Expand All @@ -321,6 +327,7 @@ class StatusBar extends JSDialog.Toolbar {
this.showItem('languagestatus', !app.map.isReadOnlyMode());
this.showItem('languagestatusbreak', !app.map.isReadOnlyMode());
this.showItem('permissionmode-container', true);
this.showItem('documentstatus-container', true);
}
break;
}
Expand Down Expand Up @@ -505,6 +512,33 @@ class StatusBar extends JSDialog.Toolbar {

JSDialog.MenuDefinitions.set('LanguageStatusMenu', menuEntries);
}

onInitModificationIndicator(lastmodtime) {
const docstatcontainer = document.getElementById('documentstatus-container');
if (lastmodtime == null) {
if (docstatcontainer !== null && docstatcontainer !== undefined) {
docstatcontainer.classList.add('hidden');
}
return;
}
docstatcontainer.classList.remove('hidden');

this.map.fire('modificationindicatorinitialized');
}

// status can be '', 'SAVING', 'MODIFIED' or 'SAVED'
onUpdateModificationIndicator(e) {
if (this._lastModstatus !== e.status) {
this.updateHtmlItem('DocumentStatus', e.status);
this._lastModStatus = e.status;
}
if (e.lastSaved !== null && e.lastSaved !== undefined) {
const lastSaved = document.getElementById('last-saved');
if (lastSaved !== null && lastSaved !== undefined) {
lastSaved.textContent = e.lastSaved;
}
}
}
}

JSDialog.StatusBar = function (map) {
Expand Down
2 changes: 2 additions & 0 deletions browser/src/control/Toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ L.Map.include({
},

save: function(dontTerminateEdit, dontSaveIfUnmodified, extendedData) {
this.fire('updatemodificationindicator', { status: 'SAVING' });

var msg = 'save' +
' dontTerminateEdit=' + (dontTerminateEdit ? 1 : 0) +
' dontSaveIfUnmodified=' + (dontSaveIfUnmodified ? 1 : 0);
Expand Down
25 changes: 25 additions & 0 deletions browser/src/control/jsdialog/Widget.HTMLContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,30 @@ function getPageStatusElements(text: string) {
return getStatusbarItemElements('PageStatus', _('Number of Pages'), text);
}

function getDocumentStatusElements(text: string) {
const docstat = getStatusbarItemElements(
'DocumentStatus',
_('Your changes have been saved'),
'',
);

if (text === 'SAVING') docstat.textContent = _('Saving...');
else if (text === 'SAVED') {
const lastSaved = document.createElement('span');
lastSaved.id = 'last-saved';
lastSaved.title = _('Your changes have been saved') + '.';
lastSaved.textContent = '';
docstat.appendChild(lastSaved);

const savedStatus = document.createElement('span');
savedStatus.id = 'saved-status-label';
savedStatus.textContent = _('Document saved');
docstat.appendChild(savedStatus);
}

return docstat;
}

var getElementsFromId = function (
id: string,
closeCallback: EventListenerOrEventListenerObject,
Expand Down Expand Up @@ -159,6 +183,7 @@ var getElementsFromId = function (
else if (id === 'statetablecell') return getStateTableCellElements(data.text);
else if (id === 'slidestatus') return getSlideStatusElements(data.text);
else if (id === 'pagestatus') return getPageStatusElements(data.text);
else if (id === 'documentstatus') return getDocumentStatusElements(data.text);
};

function htmlContent(
Expand Down
42 changes: 16 additions & 26 deletions browser/src/map/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ L.Map = L.Evented.extend({
}
});

this.on('modificationindicatorinitialized', function() {
this._modIndicatorInitialized = true;
});

// When all these conditions are met, fire statusindicator:initializationcomplete
this.initConditions = {
'doclayerinit': false,
Expand Down Expand Up @@ -245,6 +249,10 @@ L.Map = L.Evented.extend({

// Fire an event to let the client know whether the document needs saving or not.
this.fire('postMessage', {msgId: 'Doc_ModifiedStatus', args: { Modified: e.state === 'true' }});

if (this._everModified) {
this.fire('updatemodificationindicator', { status: e.state === 'true' ? 'MODIFIED' : 'SAVED' });
}
}
}, this);

Expand Down Expand Up @@ -424,29 +432,8 @@ L.Map = L.Evented.extend({
},

initializeModificationIndicator: function() {
var lastModButton = L.DomUtil.get('menu-last-mod');
if (lastModButton !== null && lastModButton !== undefined
&& lastModButton.firstChild.innerHTML !== null
&& lastModButton.firstChild.childElementCount == 0) {
if (this._lastmodtime == null) {
// No modification time -> hide the indicator
L.DomUtil.setStyle(lastModButton, 'display', 'none');
return;
}
var mainSpan = document.createElement('span');
this.lastModIndicator = document.createElement('span');
mainSpan.appendChild(this.lastModIndicator);

this.updateModificationIndicator(this._lastmodtime);

// Replace menu button body with new content
lastModButton.firstChild.replaceChildren();
lastModButton.firstChild.appendChild(mainSpan);

if (L.Params.revHistoryEnabled) {
L.DomUtil.setStyle(lastModButton, 'cursor', 'pointer');
}
}
this.fire('initmodificationindicator', this._lastmodtime);
this.updateModificationIndicator(this._lastmodtime);
},

updateModificationIndicator: function(newModificationTime) {
Expand All @@ -458,13 +445,16 @@ L.Map = L.Evented.extend({

clearTimeout(this._modTimeout);

if (this.lastModIndicator !== null && this.lastModIndicator !== undefined) {
if (this._modIndicatorInitialized) {
var dateTime = new Date(this._lastmodtime.replace(/,.*/, 'Z'));
var dateValue;

var elapsed = Date.now() - dateTime;
var rtf1 = new Intl.RelativeTimeFormat(String.locale, { style: 'narrow' });
if (elapsed < 60000) {
if (('minSavedMessageTimeoutSecs' in window) && (elapsed < (window.minSavedMessageTimeoutSecs * 1000))) {
timeout = window.minSavedMessageTimeoutSecs * 1000;
dateValue = '';
} else if (elapsed < 60000) {
dateValue = _('Last saved:') + ' ' + rtf1.format(-Math.round(elapsed / 1000), 'second');
timeout = 6000;
} else if (elapsed < 3600000) {
Expand All @@ -479,7 +469,7 @@ L.Map = L.Evented.extend({
timeout = 60000;
}

this.lastModIndicator.innerHTML = dateValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also could do this.fire('lastmodificationupdate', some data) - then we can handle it in many places by:
map.on('lastmodificationupdate', this.someHandlerFunction, this); so someHandlerFunction will do the job - without tight coupling the code

this.fire('updatemodificationindicator', {lastSaved: dateValue});

if (timeout) {
this._modTimeout = setTimeout(L.bind(this.updateModificationIndicator, this, -1), timeout);
Expand Down
1 change: 1 addition & 0 deletions coolwsd.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<out_of_focus_timeout_secs desc="The maximum number of seconds before dimming and stopping updates when the browser tab is no longer in focus. Defaults to 300 seconds." type="uint" default="300">300</out_of_focus_timeout_secs>
<idle_timeout_secs desc="The maximum number of seconds before dimming and stopping updates when the user is no longer active (even if the browser is in focus). Defaults to 15 minutes." type="uint" default="900">900</idle_timeout_secs>
<custom_os_info desc="Custom string shown as OS version in About dialog, get from system if empty." type="string" default=""></custom_os_info>
<min_saved_message_timeout_secs type="uint" desc="The minimum number of seconds before the last modified message is being displayed." default="6">6</min_saved_message_timeout_secs>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow that's nice!

</per_view>

<ver_suffix desc="Appended to etags to allow easy refresh of changed files during development" type="string" default=""></ver_suffix>
Expand Down
1 change: 1 addition & 0 deletions wsd/COOLWSD.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,7 @@ void COOLWSD::innerInitialize(Application& self)
{ "per_view.idle_timeout_secs", "900" },
{ "per_view.out_of_focus_timeout_secs", "120" },
{ "per_view.custom_os_info", "" },
{ "per_view.min_saved_message_timeout_secs", "0"},
{ "security.capabilities", "true" },
{ "security.seccomp", "true" },
{ "security.jwt_expiry_secs", "1800" },
Expand Down
2 changes: 2 additions & 0 deletions wsd/FileServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,8 @@ FileServerRequestHandler::ResourceAccessDetails FileServerRequestHandler::prepro
Poco::replaceInPlace(preprocess, std::string("%OUT_OF_FOCUS_TIMEOUT_SECS%"), std::to_string(outOfFocusTimeoutSecs));
const unsigned int idleTimeoutSecs = config.getUInt("per_view.idle_timeout_secs", 900);
Poco::replaceInPlace(preprocess, std::string("%IDLE_TIMEOUT_SECS%"), std::to_string(idleTimeoutSecs));
const unsigned int minSavedMessTimeoutSecs = config.getUInt("per_view.min_saved_message_timeout_secs", 0);
Poco::replaceInPlace(preprocess, std::string("%MIN_SAVED_MESSAGE_TIMEOUT_SECS%"), std::to_string(minSavedMessTimeoutSecs));

#if ENABLE_WELCOME_MESSAGE
std::string enableWelcomeMessage = "true";
Expand Down
Loading