gui: Add copy to clipboard, share by email, and share by SMS buttons to device IDs (fixes #2771, ref #3868) (#7984)

gui: Add copy to clipboard, share by email, and share by SMS buttons to device IDs (fixes #2771, ref #3868)

Add buttons to allow for simpler sharing device IDs with others. The
first one copies the ID to clipboard (trying to use three different
methods, depending on the browser). The second one triggers a mailto
link with prefilled subject and body. The third one triggers an sms link
with prefilled body. The short description of Syncthing included in the
latter part of the body is a direct copy from the description at the
official website https://syncthing.net.

Issue #3868 is referred here, because the copy to clipboard button
offers an alternative method for IE11 users to actually be able to copy
device IDs without having to select it manually (which doesn't work).

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
This commit is contained in:
tomasz1986 2022-11-07 20:11:12 +01:00 committed by GitHub
parent a1cc293c21
commit 5e384c9185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 217 additions and 6 deletions

View File

@ -524,6 +524,11 @@ ul.three-columns li, ul.two-columns li {
* columns. */
white-space: normal;
}
/* Move share buttons below device ID on small screens. */
#shareDeviceIdButtons {
display: inline-block;
}
}
.form-horizontal .form-group {
@ -553,6 +558,13 @@ html[lang|="ko"] i {
font-style: normal;
}
/* Prevent buttons from jumping up and down
when a tooltip is shown for one of them. */
.btn-group-vertical > .tooltip + .btn,
.btn-group-vertical > .tooltip + .btn-group {
margin-top: -1px;
}
.select-on-click {
-webkit-user-select: all;
user-select: all;

View File

@ -45,6 +45,7 @@
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
"Available debug logging facilities:": "Available debug logging facilities:",
"Be careful!": "Be careful!",
"Body:": "Body:",
"Bugs": "Bugs",
"Cancel": "Cancel",
"Changelog": "Changelog",
@ -67,6 +68,9 @@
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Copied from original",
"Copied!": "Copied!",
"Copy": "Copy",
"Copy failed! Try to select and copy manually.": "Copy failed! Try to select and copy manually.",
"Currently Shared With Devices": "Currently Shared With Devices",
"Custom Range": "Custom Range",
"Danger!": "Danger!",
@ -207,6 +211,7 @@
"Last seen": "Last seen",
"Latest Change": "Latest Change",
"Learn more": "Learn more",
"Learn more at {%url%}": "Learn more at {{url}}",
"Limit": "Limit",
"Listener Failures": "Listener Failures",
"Listener Status": "Listener Status",
@ -326,6 +331,8 @@
"Settings": "Settings",
"Share": "Share",
"Share Folder": "Share Folder",
"Share by Email": "Share by Email",
"Share by SMS": "Share by SMS",
"Share this folder?": "Share this folder?",
"Shared Folders": "Shared Folders",
"Shared With": "Shared With",
@ -357,6 +364,7 @@
"Statistics": "Statistics",
"Stopped": "Stopped",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{{receiveEncrypted}}\" too.",
"Subject:": "Subject:",
"Support": "Support",
"Support Bundle": "Support Bundle",
"Sync Extended Attributes": "Sync Extended Attributes",
@ -364,9 +372,11 @@
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
"Sync Status": "Sync Status",
"Syncing": "Syncing",
"Syncthing device ID for \"{%devicename%}\"": "Syncthing device ID for \"{{devicename}}\"",
"Syncthing has been shut down.": "Syncthing has been shut down.",
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.": "Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing is listening on the following network addresses for connection attempts from other devices:",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.",
"Syncthing is restarting.": "Syncthing is restarting.",
@ -396,6 +406,7 @@
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The following items were changed locally.": "The following items were changed locally.",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "The following methods are used to discover other devices on the network and announce this device to be found by others:",
"The following text will automatically be inserted into a new message.": "The following text will automatically be inserted into a new message.",
"The following unexpected items were found.": "The following unexpected items were found.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
@ -422,6 +433,7 @@
"This setting controls the free space required on the home (i.e., index database) disk.": "This setting controls the free space required on the home (i.e., index database) disk.",
"Time": "Time",
"Time the item was last modified": "Time the item was last modified",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "To connect with the Syncthing device named \"{{devicename}}\", add a new remote device on your end with this ID:",
"Today": "Today",
"Trash Can": "Trash Can",
"Trash Can File Versioning": "Trash Can File Versioning",
@ -473,6 +485,7 @@
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.",
"Yes": "Yes",
"Yesterday": "Yesterday",
"You can also copy and paste the text into a new message manually.": "You can also copy and paste the text into a new message manually.",
"You can also select one of these nearby devices:": "You can also select one of these nearby devices:",
"You can change your choice at any time in the Settings dialog.": "You can change your choice at any time in the Settings dialog.",
"You can read more about the two release channels at the link below.": "You can read more about the two release channels at the link below.",
@ -481,6 +494,8 @@
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
"You must keep at least one version.": "You must keep at least one version.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "You should never add or change anything locally in a \"{{receiveEncrypted}}\" folder.",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Your SMS app should open to let you choose the recipient and send it from your own number.",
"Your email app should open to let you choose the recipient and send it from your own address.": "Your email app should open to let you choose the recipient and send it from your own address.",
"days": "days",
"directories": "directories",
"files": "files",

View File

@ -990,6 +990,7 @@
<ng-include src="'syncthing/folder/revertOverrideView.html'"></ng-include>
<ng-include src="'syncthing/device/removeDeviceDialogView.html'"></ng-include>
<ng-include src="'syncthing/core/logViewerModalView.html'"></ng-include>
<ng-include src="'syncthing/device/shareDeviceIdDialogView.html'"></ng-include>
<!-- vendor scripts -->
<script type="text/javascript" src="vendor/jquery/jquery-2.2.2.js"></script>

View File

@ -3178,6 +3178,132 @@ angular.module('syncthing.core')
address.indexOf('unix://') == 0 ||
address.indexOf('unixs://') == 0);
};
$scope.shareDeviceIdDialog = function (method) {
// This function can be used to share both user's own and remote
// device IDs. Three sharing methods are used - copy to clipboard,
// send by email, and send by SMS.
var params = {
method: method,
};
var deviceID = $scope.currentDevice.deviceID;
var deviceName = $scope.deviceName($scope.currentDevice);
// Title and footer can be reused between different sharing
// methods, hence we define them separately before the body.
var title = $translate.instant('Syncthing device ID for "{%devicename%}"', {devicename: deviceName});
var footer = $translate.instant("Learn more at {%url%}", {url: "https://syncthing.net"});
switch (method) {
case "email":
params.heading = $translate.instant("Share by Email");
params.icon = "fa fa-envelope-o";
// Email message format requires using CRLF for line breaks.
// Ref: https://datatracker.ietf.org/doc/html/rfc5322
params.subject = title;
params.body = [
$translate.instant('To connect with the Syncthing device named "{%devicename%}", add a new remote device on your end with this ID:', {devicename: deviceName}),
deviceID,
$translate.instant("Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet."),
footer
].join('\r\n\r\n');
break;
case "sms":
params.heading = $translate.instant("Share by SMS");
params.icon = "fa fa-comments-o";
// SMS is limited to 160 characters (non-Unicode), so we keep
// it as short as possible, e.g. by stripping hyphens from
// device ID. The current minimum length is around 140 chars,
// but some room is required for longer sharing device names.
params.body = [
title,
deviceID.replace(/-/g, ''),
footer
].join('\n');
break;
}
$scope.shareDeviceIdParams = params;
$('#share-device-id-dialog').modal('show');
};
$scope.shareDeviceId = function () {
switch ($scope.shareDeviceIdParams.method) {
case 'email':
location.href = 'mailto:?subject=' + encodeURIComponent($scope.shareDeviceIdParams.subject) + '&body=' + encodeURIComponent($scope.shareDeviceIdParams.body);
break;
case 'sms':
// Ref1: https://rfc-editor.org/rfc/rfc5724
// Ref2: https://stackoverflow.com/questions/6480462/how-to-pre-populate-the-sms-body-text-via-an-html-link
location.href = 'sms:?&body=' + encodeURIComponent($scope.shareDeviceIdParams.body);
break;
}
}
$scope.showTemporaryTooltip = function (event, tooltip) {
// This function can be used to display a temporary tooltip above
// the current element. This way, we can dynamically add a tooltip
// with explanatory text after the user performs an interactive
// operation, e.g. clicks a button. If the element already has a
// tooltip, it will be saved first and then restored once the user
// moves focus to a different element.
var e = event.currentTarget;
var oldTooltip = e.getAttribute('data-original-title');
e.setAttribute('data-original-title', tooltip);
$(e).tooltip('show');
if (oldTooltip) {
e.setAttribute('data-original-title', oldTooltip);
} else {
e.removeAttribute('data-original-title');
}
};
$scope.copyToClipboard = function (event, content) {
var success = $translate.instant("Copied!");
var failure = $translate.instant("Copy failed! Try to select and copy manually.");
var message = success;
if (navigator.clipboard && navigator.clipboard.writeText) {
// Default for modern browsers on localhost or HTTPS. Doesn't
// work on unencrypted HTTP for security reasons.
navigator.clipboard.writeText(content);
} else if (window.clipboardData && window.clipboardData.setData) {
// Fallback for Internet Explorer. Needs to go second before
// "document.queryCommandSupported", as the browser supports the
// other method too, yet it can often be disabled for security
// reasons, causing the copy to fail. The IE-specific method is
// more reliable.
window.clipboardData.setData('Text', content);
} else if (document.queryCommandSupported) {
// Fallback for modern browsers on HTTP and non-IE old browsers.
// Check for document.queryCommandSupported("copy") support is
// omitted on purpose, as old Chrome versions reported "false"
// despite supporting the feature. The position and opacity
// hacks are needed to work inside Bootstrap modals.
var e = event.currentTarget;
var textarea = document.createElement("textarea");
e.appendChild(textarea);
textarea.style.position = "fixed";
textarea.style.opacity = "0";
textarea.textContent = content;
textarea.select();
try {
document.execCommand("copy");
} catch (ex) {
message = failure;
} finally {
e.removeChild(textarea);
}
} else {
message = failure;
}
$scope.showTemporaryTooltip(event, message);
};
})
.directive('shareTemplate', function () {
return {

View File

@ -13,9 +13,18 @@
<div class="input-group">
<input ng-if="editingDeviceNew()" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required="" valid-deviceid list="discovery-list" aria-required="true" />
<div ng-if="!editingDeviceNew()" class="well well-sm form-control text-monospace" style="height: auto;" select-on-click>{{currentDevice.deviceID}}</div>
<div class="input-group-btn">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#idqr" ng-disabled="editingDeviceNew() && !deviceEditor.deviceID.$valid">
<span class="fas fa-qrcode"></span>&nbsp;<span translate>Show QR</span>
<div id="shareDeviceIdButtons" class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="copyToClipboard($event, currentDevice.deviceID)" ng-disabled="editingDeviceNew() && !deviceEditor.deviceID.$valid" tooltip data-original-title="{{ 'Copy' | translate }}">
<span class="fa fa-lg fa-clone"></span>
</button>
<button type="button" class="btn btn-default" ng-click="shareDeviceIdDialog('email')" ng-disabled="editingDeviceNew() && !deviceEditor.deviceID.$valid" tooltip data-original-title="{{ 'Share by Email' | translate }}">
<span class="fa fa-lg fa-envelope-o"></span>
</button>
<button type="button" class="btn btn-default" ng-click="shareDeviceIdDialog('sms')" ng-disabled="editingDeviceNew() && !deviceEditor.deviceID.$valid" tooltip data-original-title="{{ 'Share by SMS' | translate }}">
<span class="fa fa-lg fa-comments-o"></span>
</button>
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#idqr" ng-disabled="editingDeviceNew() && !deviceEditor.deviceID.$valid" tooltip data-original-title="{{ 'Show QR' | translate }}">
<span class="fa fa-lg fa-qrcode"></span>
</button>
</div>
</div>

View File

@ -1,7 +1,20 @@
<modal id="idqr" status="info" icon="fas fa-qrcode" heading="{{'Device Identification' | translate}} - {{deviceName(currentDevice)}}" large="yes" closeable="yes">
<div class="modal-body">
<div class="well well-sm text-monospace text-center select-on-click">{{currentDevice.deviceID}}</div>
<img ng-if="currentDevice.deviceID" class="center-block img-thumbnail" ng-src="qr/?text={{currentDevice.deviceID}}" height="328" width="328" alt="{{'QR code' | translate}}" />
<div class="modal-body text-center">
<div class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
<div ng-if="currentDevice.deviceID">
<img class="img-thumbnail" ng-src="qr/?text={{currentDevice.deviceID}}" height="328" width="328" alt="{{'QR code' | translate}}" />
<div class="btn-group-vertical" style="vertical-align: top;">
<button type="button" class="btn btn-default" ng-click="copyToClipboard($event, currentDevice.deviceID)">
<span class="fa fa-lg fa-clone text-left"></span>&nbsp;<span translate>Copy</span>
</button>
<button type="button" class="btn btn-default" ng-click="shareDeviceIdDialog('email')">
<span class="fa fa-lg fa-envelope-o"></span>&nbsp;<span translate>Share by Email</span>
</button>
<button type="button" class="btn btn-default" ng-click="shareDeviceIdDialog('sms')">
<span class="fa fa-lg fa-comments-o"></span>&nbsp;<span translate>Share by SMS</span>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">

View File

@ -0,0 +1,35 @@
<modal id="share-device-id-dialog" status="warning" icon="{{shareDeviceIdParams.icon}}" heading="{{shareDeviceIdParams.heading}}" large="{{ shareDeviceIdParams.method == 'email' ? 'yes' : 'no' }}" closeable="yes">
<div class="modal-body" ng-switch="shareDeviceIdParams.method">
<p>
<span translate>The following text will automatically be inserted into a new message.</span>
<span>
<span ng-switch-when="email">
<span translate>Your email app should open to let you choose the recipient and send it from your own address.</span>
</span>
<span ng-switch-when="sms">
<span translate>Your SMS app should open to let you choose the recipient and send it from your own number.</span>
</span>
</span>
<span translate>You can also copy and paste the text into a new message manually.</span>
</p>
<div ng-switch-when="email">
<hr>
<h5 translate>Subject:</h5>
<pre style="word-break: normal; white-space: pre-wrap;">{{shareDeviceIdParams.subject}}<button type="button" class="btn btn-default pull-right" ng-click="copyToClipboard($event, shareDeviceIdParams.subject)" tooltip data-original-title="{{ 'Copy' | translate }}">
<span class="fa fa-clone"></span>
</button></pre>
<h5 translate>Body:</h5>
</div>
<pre style="word-break: normal; white-space: pre-wrap;">{{shareDeviceIdParams.body}}<button type="button" class="btn btn-default pull-right" ng-click="copyToClipboard($event, shareDeviceIdParams.body)" tooltip data-original-title="{{ 'Copy' | translate }}">
<span class="fa fa-clone"></span>
</button></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" ng-click="shareDeviceId()">
<span class="{{shareDeviceIdParams.icon}}"></span>&nbsp;<span translate>Share</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Cancel</span>
</button>
</div>
</modal>