diff --git a/client/components/Windows/Help.vue b/client/components/Windows/Help.vue
index acbd11b7..b7558c02 100644
--- a/client/components/Windows/Help.vue
+++ b/client/components/Windows/Help.vue
@@ -87,6 +87,36 @@
+ Gestures
+
+
+
Single-Finger Swipe Left
+
+
+
+
+
Single-Finger Swipe Right
+
+
+
+
+
Two-Finger Swipe Left
+
+
Switch to the next window in the channel list.
+
+
+
+
+
Two-Finger Swipe Right
+
+
Switch to the previous window in the channel list.
+
+
+
Keyboard Shortcuts
@@ -774,6 +804,7 @@ export default {
data() {
return {
isApple: navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) || false,
+ isTouch: navigator.maxTouchPoints > 0,
};
},
};
diff --git a/client/css/style.css b/client/css/style.css
index 9fe17344..edf84fa5 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -2038,6 +2038,10 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
padding-right: 15px;
}
+#help .help-item .subject.gesture {
+ font-weight: bold;
+}
+
#help .help-item .description p {
margin-bottom: 0;
}
diff --git a/client/js/helpers/listenForTwoFingerSwipes.js b/client/js/helpers/listenForTwoFingerSwipes.js
new file mode 100644
index 00000000..7be48e87
--- /dev/null
+++ b/client/js/helpers/listenForTwoFingerSwipes.js
@@ -0,0 +1,108 @@
+"use strict";
+
+// onTwoFingerSwipe will be called with a cardinal direction ("n", "e", "s" or
+// "w") as its only argument.
+function listenForTwoFingerSwipes(onTwoFingerSwipe) {
+ let history = [];
+
+ document.body.addEventListener(
+ "touchmove",
+ function (event) {
+ if (event.touches.length !== 2) {
+ return;
+ }
+
+ const a = event.touches.item(0);
+ const b = event.touches.item(1);
+
+ const timestamp = window.performance.now();
+ const center = [(a.screenX + b.screenX) / 2, (a.screenY + b.screenY) / 2];
+
+ if (history.length > 0) {
+ const last = history[history.length - 1];
+ const centersAreEqual =
+ last.center[0] === center[0] && last.center[1] === center[1];
+
+ if (last.timestamp === timestamp || centersAreEqual) {
+ // Touches with the same timestamps or center don't help us
+ // see the speed of movement. Ignore them.
+ return;
+ }
+ }
+
+ history.push({timestamp, center});
+ },
+ {passive: true}
+ );
+
+ document.body.addEventListener(
+ "touchend",
+ function () {
+ if (event.touches.length >= 2) {
+ return;
+ }
+
+ try {
+ const direction = getSwipe(history);
+
+ if (direction) {
+ onTwoFingerSwipe(direction);
+ }
+ } finally {
+ history = [];
+ }
+ },
+ {passive: true}
+ );
+
+ document.body.addEventListener(
+ "touchcancel",
+ function () {
+ history = [];
+ },
+ {passive: true}
+ );
+}
+
+// Returns the cardinal direction of the swipe or null if there is no swipe.
+function getSwipe(hist) {
+ // Speed is in pixels/millisecond. Must be maintained throughout swipe.
+ const MIN_SWIPE_SPEED = 0.2;
+
+ if (hist.length < 2) {
+ return null;
+ }
+
+ for (let i = 1; i < hist.length; ++i) {
+ const previous = hist[i - 1];
+ const current = hist[i];
+
+ const speed =
+ distance(previous.center, current.center) /
+ Math.abs(previous.timestamp - current.timestamp);
+
+ if (speed < MIN_SWIPE_SPEED) {
+ return null;
+ }
+ }
+
+ return getCardinalDirection(hist[0].center, hist[hist.length - 1].center);
+}
+
+function distance([x1, y1], [x2, y2]) {
+ return Math.hypot(x1 - x2, y1 - y2);
+}
+
+function getCardinalDirection([x1, y1], [x2, y2]) {
+ // If θ is the angle of the vector then this is tan(θ)
+ const tangent = (y2 - y1) / (x2 - x1);
+
+ // All values of |tan(-45° to 45°)| are less than 1, same for 145° to 225°
+ if (Math.abs(tangent) < 1) {
+ return x1 < x2 ? "e" : "w";
+ }
+
+ return y1 < y2 ? "s" : "n";
+}
+
+export default listenForTwoFingerSwipes;
diff --git a/client/js/keybinds.js b/client/js/keybinds.js
index 00874857..2fbb3311 100644
--- a/client/js/keybinds.js
+++ b/client/js/keybinds.js
@@ -6,6 +6,7 @@ import store from "./store";
import {switchToChannel, router, navigate} from "./router";
import isChannelCollapsed from "./helpers/isChannelCollapsed";
import isIgnoredKeybind from "./helpers/isIgnoredKeybind";
+import listenForTwoFingerSwipes from "./helpers/listenForTwoFingerSwipes";
// Switch to the next/previous window in the channel list.
Mousetrap.bind(["alt+up", "alt+down"], function (e, keys) {
@@ -13,11 +14,22 @@ Mousetrap.bind(["alt+up", "alt+down"], function (e, keys) {
return true;
}
+ navigateWindow(keys.split("+").pop() === "up" ? -1 : 1);
+
+ return false;
+});
+
+listenForTwoFingerSwipes(function (cardinalDirection) {
+ if (cardinalDirection === "e" || cardinalDirection === "w") {
+ navigateWindow(cardinalDirection === "e" ? -1 : 1);
+ }
+});
+
+function navigateWindow(direction) {
if (store.state.networks.length === 0) {
- return false;
+ return;
}
- const direction = keys.split("+").pop() === "up" ? -1 : 1;
const flatChannels = [];
let index = -1;
@@ -44,9 +56,7 @@ Mousetrap.bind(["alt+up", "alt+down"], function (e, keys) {
index = (((index + direction) % length) + length) % length;
jumpToChannel(flatChannels[index]);
-
- return false;
-});
+}
// Switch to the next/previous lobby in the channel list
Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function (e, keys) {