@peers-app/peers-ui 0.15.3 → 0.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,10 +13,10 @@ jobs:
13
13
 
14
14
  steps:
15
15
  - name: Checkout code
16
- uses: actions/checkout@v4
16
+ uses: actions/checkout@v6
17
17
 
18
18
  - name: Setup Node.js
19
- uses: actions/setup-node@v4
19
+ uses: actions/setup-node@v6
20
20
  with:
21
21
  node-version: '22'
22
22
  registry-url: 'https://registry.npmjs.org'
@@ -40,7 +40,7 @@ jobs:
40
40
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
41
41
 
42
42
  - name: Create GitHub Release
43
- uses: softprops/action-gh-release@v1
43
+ uses: softprops/action-gh-release@v3
44
44
  if: startsWith(github.ref, 'refs/tags/')
45
45
  with:
46
46
  generate_release_notes: true
@@ -61,6 +61,7 @@ const autolink_plugin_1 = require("./autolink-plugin");
61
61
  const markdown_plugin_1 = require("./markdown-plugin");
62
62
  const mention_node_1 = require("./mention-node");
63
63
  const mentions_plugin_1 = require("./mentions-plugin");
64
+ const move_line_plugin_1 = require("./move-line-plugin");
64
65
  const theme_1 = __importDefault(require("./theme"));
65
66
  const toolbar_1 = require("./toolbar");
66
67
  const editorConfig = {
@@ -110,7 +111,7 @@ function MarkdownEditor(props) {
110
111
  };
111
112
  }
112
113
  const _mentionConfigs = props.mentionConfigs ?? mention_configs_1.mentionConfigs;
113
- return ((0, jsx_runtime_1.jsx)(LexicalComposer_1.LexicalComposer, { initialConfig: { ...editorConfig }, children: (0, jsx_runtime_1.jsxs)("div", { className: "editor-container", children: [props.hideToolbar ? null : (0, jsx_runtime_1.jsx)(toolbar_1.ToolbarPlugin, { topRightControls: props.topRightControls }), (0, jsx_runtime_1.jsxs)("div", { className: "editor-inner", ref: editorRef, style: { maxHeight: props.maxHeight, overflowY: "auto" }, children: [(0, jsx_runtime_1.jsx)(LexicalRichTextPlugin_1.RichTextPlugin, { contentEditable: (0, jsx_runtime_1.jsx)(LexicalContentEditable_1.ContentEditable, { className: "editor-input p-2" }), placeholder: null, ErrorBoundary: LexicalErrorBoundary_1.LexicalErrorBoundary }), (0, jsx_runtime_1.jsx)(LexicalHistoryPlugin_1.HistoryPlugin, {}), props.autoFocus && (0, jsx_runtime_1.jsx)(LexicalAutoFocusPlugin_1.AutoFocusPlugin, { defaultSelection: "rootEnd" }), (0, jsx_runtime_1.jsx)(LexicalListPlugin_1.ListPlugin, {}), _mentionConfigs.length > 0 && ((0, jsx_runtime_1.jsx)(mentions_plugin_1.MentionsPlugin, { mentionConfigs: _mentionConfigs, mentionsOpen: mentionsOpen })), (0, jsx_runtime_1.jsx)(autolink_plugin_1.LexicalAutoLinkPlugin, {}), (0, jsx_runtime_1.jsx)(markdown_plugin_1.MarkdownPlugin, { markdownObs: props.value }), (0, jsx_runtime_1.jsx)(LexicalMarkdownShortcutPlugin_1.MarkdownShortcutPlugin, { transformers: markdown_plugin_1.customMarkdownTransformers }), (0, jsx_runtime_1.jsx)(LexicalCheckListPlugin_1.CheckListPlugin, {}), (0, jsx_runtime_1.jsx)(LexicalTabIndentationPlugin_1.TabIndentationPlugin, {}), (0, jsx_runtime_1.jsx)(OnKeyDownPlugin, { effects: props.effects, mentionsOpen: mentionsOpen })] })] }) }));
114
+ return ((0, jsx_runtime_1.jsx)(LexicalComposer_1.LexicalComposer, { initialConfig: { ...editorConfig }, children: (0, jsx_runtime_1.jsxs)("div", { className: "editor-container", children: [props.hideToolbar ? null : (0, jsx_runtime_1.jsx)(toolbar_1.ToolbarPlugin, { topRightControls: props.topRightControls }), (0, jsx_runtime_1.jsxs)("div", { className: "editor-inner", ref: editorRef, style: { maxHeight: props.maxHeight, overflowY: "auto" }, children: [(0, jsx_runtime_1.jsx)(LexicalRichTextPlugin_1.RichTextPlugin, { contentEditable: (0, jsx_runtime_1.jsx)(LexicalContentEditable_1.ContentEditable, { className: "editor-input p-2" }), placeholder: null, ErrorBoundary: LexicalErrorBoundary_1.LexicalErrorBoundary }), (0, jsx_runtime_1.jsx)(LexicalHistoryPlugin_1.HistoryPlugin, {}), props.autoFocus && (0, jsx_runtime_1.jsx)(LexicalAutoFocusPlugin_1.AutoFocusPlugin, { defaultSelection: "rootEnd" }), (0, jsx_runtime_1.jsx)(LexicalListPlugin_1.ListPlugin, {}), _mentionConfigs.length > 0 && ((0, jsx_runtime_1.jsx)(mentions_plugin_1.MentionsPlugin, { mentionConfigs: _mentionConfigs, mentionsOpen: mentionsOpen })), (0, jsx_runtime_1.jsx)(autolink_plugin_1.LexicalAutoLinkPlugin, {}), (0, jsx_runtime_1.jsx)(markdown_plugin_1.MarkdownPlugin, { markdownObs: props.value }), (0, jsx_runtime_1.jsx)(LexicalMarkdownShortcutPlugin_1.MarkdownShortcutPlugin, { transformers: markdown_plugin_1.customMarkdownTransformers }), (0, jsx_runtime_1.jsx)(LexicalCheckListPlugin_1.CheckListPlugin, {}), (0, jsx_runtime_1.jsx)(LexicalTabIndentationPlugin_1.TabIndentationPlugin, {}), (0, jsx_runtime_1.jsx)(move_line_plugin_1.MoveLineWithAltArrowsPlugin, { mentionsOpen: mentionsOpen }), (0, jsx_runtime_1.jsx)(OnKeyDownPlugin, { effects: props.effects, mentionsOpen: mentionsOpen })] })] }) }));
114
115
  }
115
116
  const OnKeyDownPlugin = (props) => {
116
117
  const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
@@ -0,0 +1,8 @@
1
+ import type { Observable } from "@peers-app/peers-sdk";
2
+ /**
3
+ * Registers Alt/Option + ArrowUp/ArrowDown to swap the caret's block (or list item) with an adjacent sibling.
4
+ * @param props.mentionsOpen — when true, the plugin defers so mention typeahead keeps keyboard focus.
5
+ */
6
+ export declare function MoveLineWithAltArrowsPlugin(props: {
7
+ mentionsOpen: Observable<boolean>;
8
+ }): null;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MoveLineWithAltArrowsPlugin = MoveLineWithAltArrowsPlugin;
4
+ const list_1 = require("@lexical/list");
5
+ const LexicalComposerContext_1 = require("@lexical/react/LexicalComposerContext");
6
+ const lexical_1 = require("lexical");
7
+ const react_1 = require("react");
8
+ /**
9
+ * Returns the node to reorder for Alt/Option + arrow "move line":
10
+ * a list item (among siblings in its list), or else the top-level block under the root.
11
+ */
12
+ function $getMovableBlock(node) {
13
+ let n = node;
14
+ while (n !== null) {
15
+ const parent = n.getParent();
16
+ if (parent === null) {
17
+ return null;
18
+ }
19
+ if ((0, list_1.$isListNode)(parent) && (0, list_1.$isListItemNode)(n)) {
20
+ return n;
21
+ }
22
+ if ((0, lexical_1.$isRootOrShadowRoot)(parent)) {
23
+ return n;
24
+ }
25
+ n = parent;
26
+ }
27
+ return null;
28
+ }
29
+ /**
30
+ * Registers Alt/Option + ArrowUp/ArrowDown to swap the caret's block (or list item) with an adjacent sibling.
31
+ * @param props.mentionsOpen — when true, the plugin defers so mention typeahead keeps keyboard focus.
32
+ */
33
+ function MoveLineWithAltArrowsPlugin(props) {
34
+ const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
35
+ (0, react_1.useEffect)(() => {
36
+ return editor.registerCommand(lexical_1.KEY_DOWN_COMMAND, (event) => {
37
+ if (props.mentionsOpen()) {
38
+ return false;
39
+ }
40
+ if (!event.altKey || event.metaKey || event.ctrlKey) {
41
+ return false;
42
+ }
43
+ const code = event.code;
44
+ if (code !== "ArrowUp" && code !== "ArrowDown") {
45
+ return false;
46
+ }
47
+ // KEY_DOWN_COMMAND runs inside Lexical's updateEditorSync; do not call
48
+ // editor.update() here — it would queue and run after this handler returns,
49
+ // so handled would stay false, preventDefault would never run, and the
50
+ // built-in arrow handler + browser would move the caret on top of our swap.
51
+ const selection = (0, lexical_1.$getSelection)();
52
+ if (!(0, lexical_1.$isRangeSelection)(selection) || !selection.isCollapsed()) {
53
+ return false;
54
+ }
55
+ const anchorNode = selection.anchor.getNode();
56
+ const block = $getMovableBlock(anchorNode);
57
+ if (block === null) {
58
+ return false;
59
+ }
60
+ const anchorKey = selection.anchor.key;
61
+ const anchorOffset = selection.anchor.offset;
62
+ const anchorType = selection.anchor.type;
63
+ const focusKey = selection.focus.key;
64
+ const focusOffset = selection.focus.offset;
65
+ const focusType = selection.focus.type;
66
+ if (code === "ArrowUp") {
67
+ const prev = block.getPreviousSibling();
68
+ if (prev === null) {
69
+ return false;
70
+ }
71
+ prev.insertBefore(block, false);
72
+ }
73
+ else {
74
+ const next = block.getNextSibling();
75
+ if (next === null) {
76
+ return false;
77
+ }
78
+ next.insertAfter(block, false);
79
+ }
80
+ if ((0, lexical_1.$getNodeByKey)(anchorKey) !== null) {
81
+ const restored = (0, lexical_1.$createRangeSelection)();
82
+ restored.format = selection.format;
83
+ restored.style = selection.style;
84
+ restored.anchor.set(anchorKey, anchorOffset, anchorType);
85
+ restored.focus.set(focusKey, focusOffset, focusType);
86
+ (0, lexical_1.$setSelection)(restored);
87
+ }
88
+ else {
89
+ block.selectEnd();
90
+ }
91
+ event.preventDefault();
92
+ return true;
93
+ }, lexical_1.COMMAND_PRIORITY_HIGH);
94
+ }, [editor]);
95
+ return null;
96
+ }
@@ -44,6 +44,9 @@ const globals = __importStar(require("../../globals"));
44
44
  const hooks_1 = require("../../hooks");
45
45
  const color_mode_dropdown_1 = require("./color-mode-dropdown");
46
46
  const voice_settings_1 = require("./voice-settings");
47
+ function getAppVersion() {
48
+ return globalThis.__PEERS_APP_VERSION__ || "unknown";
49
+ }
47
50
  const SettingsPage = () => {
48
51
  return ((0, jsx_runtime_1.jsx)("div", { className: "container-fluid", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: [
49
52
  {
@@ -136,7 +139,7 @@ const ProfileSection = () => {
136
139
  return null;
137
140
  }
138
141
  const hasChanges = !!me.q() || deviceName() !== savedDeviceName();
139
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mb-3", children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0 me-auto", children: "Settings" }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-primary btn-sm", onClick: handleSave, disabled: !hasChanges, children: "Save" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "form-label", children: "Display Name:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.qs.name, className: "form-control", placeholder: "Enter your name" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Device Name:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Give this device a friendly name to identify it in your network." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: deviceName, className: "form-control", placeholder: "e.g., My Laptop" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["User ID:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This uniquely identifies you to all other users." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.userId, className: "form-control", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Public Key:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This is your public key that other users will use to verify your signatures." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.publicKey, className: "form-control form-control-sm", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Public Box Key:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This is the public key that other users can use to encrypt data so that only you can open it." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.publicBoxKey, className: "form-control form-control-sm", disabled: true })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Device ID:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This uniquely identifies this device on Peers networks." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: deviceId || "", className: "form-control form-control-sm", disabled: true })] })] }));
142
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mb-3", children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0 me-auto", children: "Settings" }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-primary btn-sm", onClick: handleSave, disabled: !hasChanges, children: "Save" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "form-label", children: "Display Name:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.qs.name, className: "form-control", placeholder: "Enter your name" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Device Name:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Give this device a friendly name to identify it in your network." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: deviceName, className: "form-control", placeholder: "e.g., My Laptop" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["User ID:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This uniquely identifies you to all other users." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.userId, className: "form-control", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Public Key:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This is your public key that other users will use to verify your signatures." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.publicKey, className: "form-control form-control-sm", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Public Box Key:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This is the public key that other users can use to encrypt data so that only you can open it." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.publicBoxKey, className: "form-control form-control-sm", disabled: true })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Device ID:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This uniquely identifies this device on Peers networks." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: deviceId || "", className: "form-control form-control-sm", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "form-label", children: "App Version:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: getAppVersion(), className: "form-control form-control-sm", disabled: true })] })] }));
140
143
  };
141
144
  const PackagesRootDirectory = () => {
142
145
  return ((0, jsx_runtime_1.jsxs)("div", { className: "mt-3", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Packages Directory:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `Changing this will not effect existing packages.` })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: peers_sdk_1.packagesRootDir, className: "form-control" })] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.15.3",
3
+ "version": "0.15.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.15.3",
31
+ "@peers-app/peers-sdk": "^0.15.5",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-env": "^7.24.5",
40
40
  "@babel/preset-react": "^7.24.1",
41
41
  "@babel/preset-typescript": "^7.27.1",
42
- "@peers-app/peers-sdk": "0.15.3",
42
+ "@peers-app/peers-sdk": "0.15.5",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
@@ -21,6 +21,7 @@ import { LexicalAutoLinkPlugin } from "./autolink-plugin";
21
21
  import { customMarkdownTransformers, MarkdownPlugin } from "./markdown-plugin";
22
22
  import { MentionNode } from "./mention-node";
23
23
  import { MentionsPlugin } from "./mentions-plugin";
24
+ import { MoveLineWithAltArrowsPlugin } from "./move-line-plugin";
24
25
  import theme from "./theme";
25
26
  import { type IToolbarControl, ToolbarPlugin } from "./toolbar";
26
27
 
@@ -119,6 +120,7 @@ export function MarkdownEditor(props: IMarkdownEditorProps) {
119
120
  <MarkdownShortcutPlugin transformers={customMarkdownTransformers} />
120
121
  <CheckListPlugin />
121
122
  <TabIndentationPlugin />
123
+ <MoveLineWithAltArrowsPlugin mentionsOpen={mentionsOpen} />
122
124
  <OnKeyDownPlugin effects={props.effects} mentionsOpen={mentionsOpen} />
123
125
  </div>
124
126
  </div>
@@ -0,0 +1,116 @@
1
+ import { $isListItemNode, $isListNode } from "@lexical/list";
2
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
3
+ import type { Observable } from "@peers-app/peers-sdk";
4
+ import {
5
+ $createRangeSelection,
6
+ $getNodeByKey,
7
+ $getSelection,
8
+ $isRangeSelection,
9
+ $isRootOrShadowRoot,
10
+ $setSelection,
11
+ COMMAND_PRIORITY_HIGH,
12
+ KEY_DOWN_COMMAND,
13
+ type LexicalNode,
14
+ } from "lexical";
15
+ import { useEffect } from "react";
16
+
17
+ /**
18
+ * Returns the node to reorder for Alt/Option + arrow "move line":
19
+ * a list item (among siblings in its list), or else the top-level block under the root.
20
+ */
21
+ function $getMovableBlock(node: LexicalNode): LexicalNode | null {
22
+ let n: LexicalNode | null = node;
23
+ while (n !== null) {
24
+ const parent: LexicalNode | null = n.getParent();
25
+ if (parent === null) {
26
+ return null;
27
+ }
28
+ if ($isListNode(parent) && $isListItemNode(n)) {
29
+ return n;
30
+ }
31
+ if ($isRootOrShadowRoot(parent)) {
32
+ return n;
33
+ }
34
+ n = parent;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Registers Alt/Option + ArrowUp/ArrowDown to swap the caret's block (or list item) with an adjacent sibling.
41
+ * @param props.mentionsOpen — when true, the plugin defers so mention typeahead keeps keyboard focus.
42
+ */
43
+ export function MoveLineWithAltArrowsPlugin(props: { mentionsOpen: Observable<boolean> }) {
44
+ const [editor] = useLexicalComposerContext();
45
+
46
+ useEffect(() => {
47
+ return editor.registerCommand(
48
+ KEY_DOWN_COMMAND,
49
+ (event: KeyboardEvent) => {
50
+ if (props.mentionsOpen()) {
51
+ return false;
52
+ }
53
+ if (!event.altKey || event.metaKey || event.ctrlKey) {
54
+ return false;
55
+ }
56
+ const code = event.code;
57
+ if (code !== "ArrowUp" && code !== "ArrowDown") {
58
+ return false;
59
+ }
60
+
61
+ // KEY_DOWN_COMMAND runs inside Lexical's updateEditorSync; do not call
62
+ // editor.update() here — it would queue and run after this handler returns,
63
+ // so handled would stay false, preventDefault would never run, and the
64
+ // built-in arrow handler + browser would move the caret on top of our swap.
65
+
66
+ const selection = $getSelection();
67
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
68
+ return false;
69
+ }
70
+ const anchorNode = selection.anchor.getNode();
71
+ const block = $getMovableBlock(anchorNode);
72
+ if (block === null) {
73
+ return false;
74
+ }
75
+
76
+ const anchorKey = selection.anchor.key;
77
+ const anchorOffset = selection.anchor.offset;
78
+ const anchorType = selection.anchor.type;
79
+ const focusKey = selection.focus.key;
80
+ const focusOffset = selection.focus.offset;
81
+ const focusType = selection.focus.type;
82
+
83
+ if (code === "ArrowUp") {
84
+ const prev = block.getPreviousSibling();
85
+ if (prev === null) {
86
+ return false;
87
+ }
88
+ prev.insertBefore(block, false);
89
+ } else {
90
+ const next = block.getNextSibling();
91
+ if (next === null) {
92
+ return false;
93
+ }
94
+ next.insertAfter(block, false);
95
+ }
96
+
97
+ if ($getNodeByKey(anchorKey) !== null) {
98
+ const restored = $createRangeSelection();
99
+ restored.format = selection.format;
100
+ restored.style = selection.style;
101
+ restored.anchor.set(anchorKey, anchorOffset, anchorType);
102
+ restored.focus.set(focusKey, focusOffset, focusType);
103
+ $setSelection(restored);
104
+ } else {
105
+ block.selectEnd();
106
+ }
107
+
108
+ event.preventDefault();
109
+ return true;
110
+ },
111
+ COMMAND_PRIORITY_HIGH,
112
+ );
113
+ }, [editor]);
114
+
115
+ return null;
116
+ }
@@ -28,6 +28,14 @@ type PeersHostWindow = Window & {
28
28
  ReactNativeWebView?: unknown;
29
29
  };
30
30
 
31
+ type PeersGlobal = typeof globalThis & {
32
+ __PEERS_APP_VERSION__?: string;
33
+ };
34
+
35
+ function getAppVersion() {
36
+ return (globalThis as PeersGlobal).__PEERS_APP_VERSION__ || "unknown";
37
+ }
38
+
31
39
  export const SettingsPage: React.FC = () => {
32
40
  return (
33
41
  <div className="container-fluid">
@@ -209,6 +217,11 @@ const ProfileSection: React.FC = () => {
209
217
  </small>
210
218
  <Input value={deviceId || ""} className="form-control form-control-sm" disabled />
211
219
  </div>
220
+
221
+ <div className="mb-3">
222
+ <small className="form-label">App Version:</small>
223
+ <Input value={getAppVersion()} className="form-control form-control-sm" disabled />
224
+ </div>
212
225
  </>
213
226
  );
214
227
  };