Commit 84f42d561913f861b364caecc35050feadc23744
scroll to reply box on reply, context message on full thread
fixes #615Matt McKegg committed on 9/30/2017, 5:14:29 AM
Parent: 0c2767265b52527a46422d9f034af38bc974b503
Files changed
lib/catch-links.js | changed |
lib/anchor-hook.js | added |
locales/en.json | changed |
main-window.js | changed |
modules/app/views.js | changed |
modules/feed/html/rollup.js | changed |
modules/message/html/compose.js | changed |
modules/message/sheet/likes.js | changed |
modules/page/html/render/message.js | changed |
overrides/patchcore/message/html/action/reply.js | added |
lib/catch-links.js | ||
---|---|---|
@@ -21,9 +21,9 @@ | ||
21 | 21 | var url = Url.parse(href) |
22 | 22 | if (url.host) { |
23 | 23 | cb(href, true) |
24 | 24 | } else if (href !== '#') { |
25 | - cb(href, false) | |
25 | + cb(href, false, anchor.anchor) | |
26 | 26 | } |
27 | 27 | } |
28 | 28 | |
29 | 29 | ev.preventDefault() |
lib/anchor-hook.js | ||
---|---|---|
@@ -1,0 +1,16 @@ | ||
1 | +var watch = require('mutant/watch') | |
2 | + | |
3 | +module.exports = AnchorHook | |
4 | + | |
5 | +function AnchorHook (name, current, cb) { | |
6 | + return function (element) { | |
7 | + return watch(current, (current) => { | |
8 | + if (current === name) { | |
9 | + window.requestAnimationFrame(() => { | |
10 | + element.scrollIntoView() | |
11 | + if (typeof cb === 'function') cb(element) | |
12 | + }) | |
13 | + } | |
14 | + }) | |
15 | + } | |
16 | +} |
locales/en.json | ||
---|---|---|
@@ -135,6 +135,7 @@ | ||
135 | 135 | "OK": "OK", |
136 | 136 | "Close": "Close", |
137 | 137 | "New Message": "New Message", |
138 | 138 | "unsubscribed from ": "unsubscribed from ", |
139 | - "en": "en" | |
139 | + "en": "en", | |
140 | + "on ": "on " | |
140 | 141 | } |
main-window.js | ||
---|---|---|
@@ -44,15 +44,15 @@ | ||
44 | 44 | 'intl.sync.i18n': 'first', |
45 | 45 | })) |
46 | 46 | |
47 | 47 | setupContextMenuAndSpellCheck(api.config.sync.load()) |
48 | - | |
48 | + | |
49 | 49 | const i18n = api.intl.sync.i18n |
50 | 50 | |
51 | 51 | var id = api.keys.sync.id() |
52 | 52 | var latestUpdate = LatestUpdate() |
53 | 53 | var subscribedChannels = api.channel.obs.subscribed(id) |
54 | - | |
54 | + | |
55 | 55 | // prompt to setup profile on first use |
56 | 56 | onceTrue(api.sbot.obs.connection, (sbot) => { |
57 | 57 | sbot.latestSequence(sbot.id, (_, key) => { |
58 | 58 | if (key == null) { |
@@ -125,9 +125,9 @@ | ||
125 | 125 | ), |
126 | 126 | views.html |
127 | 127 | ]) |
128 | 128 | |
129 | - catchLinks(container, (href, external) => { | |
129 | + catchLinks(container, (href, external, anchor) => { | |
130 | 130 | if (external) { |
131 | 131 | electron.shell.openExternal(href) |
132 | 132 | } else if (ref.isBlob(href)) { |
133 | 133 | electron.shell.openExternal(api.blob.sync.url(href)) |
@@ -136,13 +136,13 @@ | ||
136 | 136 | if (err) throw err |
137 | 137 | if (handler) { |
138 | 138 | handler(href) |
139 | 139 | } else { |
140 | - api.app.navigate(href) | |
140 | + api.app.navigate(href, anchor) | |
141 | 141 | } |
142 | 142 | }) |
143 | 143 | } else { |
144 | - api.app.navigate(href) | |
144 | + api.app.navigate(href, anchor) | |
145 | 145 | } |
146 | 146 | }) |
147 | 147 | |
148 | 148 | return container |
@@ -210,10 +210,10 @@ | ||
210 | 210 | }, title) |
211 | 211 | return element |
212 | 212 | } |
213 | 213 | |
214 | - function setView (href) { | |
215 | - views.setView(href) | |
214 | + function setView (href, anchor) { | |
215 | + views.setView(href, anchor) | |
216 | 216 | } |
217 | 217 | |
218 | 218 | function getExternalHandler (key, cb) { |
219 | 219 | api.sbot.async.get(key, function (err, value) { |
modules/app/views.js | ||
---|---|---|
@@ -98,9 +98,9 @@ | ||
98 | 98 | } |
99 | 99 | } |
100 | 100 | } |
101 | 101 | |
102 | - function setView (view) { | |
102 | + function setView (view, anchor) { | |
103 | 103 | loadView(view) |
104 | 104 | |
105 | 105 | if (views.has(view)) { |
106 | 106 | if (lastViewed[view] !== true) { |
@@ -110,8 +110,14 @@ | ||
110 | 110 | if (currentView() && lastViewed[currentView()] !== true) { |
111 | 111 | lastViewed[currentView()] = Date.now() |
112 | 112 | } |
113 | 113 | |
114 | + var viewElement = views.get(view) | |
115 | + | |
116 | + if (viewElement && typeof viewElement.setAnchor === 'function') { | |
117 | + viewElement.setAnchor(anchor) | |
118 | + } | |
119 | + | |
114 | 120 | if (view !== currentView()) { |
115 | 121 | canGoForward.set(false) |
116 | 122 | canGoBack.set(true) |
117 | 123 | forwardHistory.length = 0 |
modules/feed/html/rollup.js | ||
---|---|---|
@@ -208,16 +208,22 @@ | ||
208 | 208 | meta, |
209 | 209 | renderedMessage, |
210 | 210 | when(replyElements.length, [ |
211 | 211 | when(replies.length > replyElements.length || partial, |
212 | - h('a.full', {href: item.key}, [i18n('View full thread') +' (', replies.length, ')']) | |
212 | + h('a.full', {href: item.key, anchor: getFirstId(replyElements)}, [i18n('View full thread') + ' (', replies.length, ')']) | |
213 | 213 | ), |
214 | 214 | h('div.replies', replyElements) |
215 | 215 | ]) |
216 | 216 | ]) |
217 | 217 | } |
218 | 218 | }) |
219 | 219 | |
220 | + function getFirstId (elements) { | |
221 | + if (Array.isArray(elements) && elements.length) { | |
222 | + return elements[0].dataset.id | |
223 | + } | |
224 | + } | |
225 | + | |
220 | 226 | function names (ids) { |
221 | 227 | var items = map(Array.from(ids), api.about.obs.name) |
222 | 228 | return computed([items], (names) => names.map((n) => `- ${n}`).join('\n')) |
223 | 229 | } |
modules/message/html/compose.js | ||
---|---|---|
@@ -22,9 +22,9 @@ | ||
22 | 22 | exports.gives = nest('message.html.compose') |
23 | 23 | |
24 | 24 | exports.create = function (api) { |
25 | 25 | const i18n = api.intl.sync.i18n |
26 | - return nest('message.html.compose', function ({shrink = true, meta, prepublish, placeholder = 'Write a message'}, cb) { | |
26 | + return nest('message.html.compose', function ({shrink = true, meta, hooks, prepublish, placeholder = 'Write a message'}, cb) { | |
27 | 27 | var files = [] |
28 | 28 | var filesById = {} |
29 | 29 | var focused = Value(false) |
30 | 30 | var hasContent = Value(false) |
@@ -105,8 +105,9 @@ | ||
105 | 105 | publishBtn |
106 | 106 | ]) |
107 | 107 | |
108 | 108 | var composer = h('Compose', { |
109 | + hooks, | |
109 | 110 | classList: [ |
110 | 111 | when(expanded, '-expanded', '-contracted') |
111 | 112 | ] |
112 | 113 | }, [ |
@@ -114,8 +115,12 @@ | ||
114 | 115 | warning, |
115 | 116 | actions |
116 | 117 | ]) |
117 | 118 | |
119 | + composer.focus = function () { | |
120 | + textArea.focus() | |
121 | + } | |
122 | + | |
118 | 123 | addSuggest(textArea, (inputText, cb) => { |
119 | 124 | if (inputText[0] === '@') { |
120 | 125 | cb(null, getProfileSuggestions(inputText.slice(1))) |
121 | 126 | } else if (inputText[0] === '#') { |
modules/message/sheet/likes.js | ||
---|---|---|
@@ -27,11 +27,11 @@ | ||
27 | 27 | }, [i18n('Liked by')]), |
28 | 28 | renderContactBlock(ids) |
29 | 29 | ]) |
30 | 30 | |
31 | - catchLinks(content, (href, external) => { | |
31 | + catchLinks(content, (href, external, anchor) => { | |
32 | 32 | if (!external) { |
33 | - api.app.navigate(href) | |
33 | + api.app.navigate(href, anchor) | |
34 | 34 | close() |
35 | 35 | } |
36 | 36 | }) |
37 | 37 |
modules/page/html/render/message.js | ||
---|---|---|
@@ -1,7 +1,8 @@ | ||
1 | 1 | var { h, when, map, Proxy, Struct, Value, computed } = require('mutant') |
2 | 2 | var nest = require('depnest') |
3 | 3 | var ref = require('ssb-ref') |
4 | +var AnchorHook = require('../../../../lib/anchor-hook') | |
4 | 5 | |
5 | 6 | exports.needs = nest({ |
6 | 7 | 'keys.sync.id': 'first', |
7 | 8 | 'feed.obs.thread': 'first', |
@@ -10,9 +11,9 @@ | ||
10 | 11 | render: 'first', |
11 | 12 | compose: 'first' |
12 | 13 | }, |
13 | 14 | 'sbot.async.get': 'first', |
14 | - 'intl.sync.i18n':'first', | |
15 | + 'intl.sync.i18n': 'first' | |
15 | 16 | }) |
16 | 17 | |
17 | 18 | exports.gives = nest('page.html.render') |
18 | 19 | |
@@ -22,8 +23,9 @@ | ||
22 | 23 | if (!ref.isMsg(id)) return |
23 | 24 | var loader = h('div', {className: 'Loading -large'}) |
24 | 25 | |
25 | 26 | var result = Proxy(loader) |
27 | + var anchor = Value() | |
26 | 28 | |
27 | 29 | var meta = Struct({ |
28 | 30 | type: 'post', |
29 | 31 | root: Proxy(id), |
@@ -34,8 +36,11 @@ | ||
34 | 36 | |
35 | 37 | var compose = api.message.html.compose({ |
36 | 38 | meta, |
37 | 39 | shrink: false, |
40 | + hooks: [ | |
41 | + AnchorHook('reply', anchor, (el) => el.focus()) | |
42 | + ], | |
38 | 43 | placeholder: when(meta.recps, i18n('Write a private reply'), i18n('Write a public reply')) |
39 | 44 | }) |
40 | 45 | |
41 | 46 | api.sbot.async.get(id, (err, value) => { |
@@ -68,12 +73,20 @@ | ||
68 | 73 | meta.branch.set(isReply ? thread.branchId : thread.lastId) |
69 | 74 | |
70 | 75 | var container = h('Thread', [ |
71 | 76 | h('div.messages', [ |
72 | - when(thread.branchId, h('a.full', {href: thread.rootId}, [i18n('View full thread')])), | |
77 | + when(thread.branchId, h('a.full', {href: thread.rootId, anchor: id}, [i18n('View full thread')])), | |
73 | 78 | map(thread.messages, (msg) => { |
74 | 79 | return computed([msg, thread.previousKey(msg)], (msg, previousId) => { |
75 | - return api.message.html.render(msg, {pageId: id, previousId, includeReferences: true}) | |
80 | + return h('div', { | |
81 | + hooks: [AnchorHook(msg.key, anchor, showContext)] | |
82 | + }, [ | |
83 | + api.message.html.render(msg, { | |
84 | + pageId: id, | |
85 | + previousId, | |
86 | + includeReferences: true | |
87 | + }) | |
88 | + ]) | |
76 | 89 | }) |
77 | 90 | }, { |
78 | 91 | maxTime: 5, |
79 | 92 | idle: true |
@@ -83,11 +96,35 @@ | ||
83 | 96 | ]) |
84 | 97 | result.set(when(thread.sync, container, loader)) |
85 | 98 | }) |
86 | 99 | |
87 | - return h('div', {className: 'SplitView'}, [ | |
100 | + var view = h('div', {className: 'SplitView'}, [ | |
88 | 101 | h('div.main', [ |
89 | 102 | result |
90 | 103 | ]) |
91 | 104 | ]) |
105 | + | |
106 | + view.setAnchor = function (value) { | |
107 | + anchor.set(value) | |
108 | + } | |
109 | + | |
110 | + return view | |
92 | 111 | }) |
93 | 112 | } |
113 | + | |
114 | +function showContext (element) { | |
115 | + var scrollParent = getScrollParent(element) | |
116 | + if (scrollParent) { | |
117 | + // ensure context is visible | |
118 | + scrollParent.scrollTop = Math.max(0, scrollParent.scrollTop - 100) | |
119 | + } | |
120 | +} | |
121 | + | |
122 | +function getScrollParent (element) { | |
123 | + while (element.parentNode) { | |
124 | + if (element.parentNode.scrollTop) { | |
125 | + return element.parentNode | |
126 | + } else { | |
127 | + element = element.parentNode | |
128 | + } | |
129 | + } | |
130 | +} |
overrides/patchcore/message/html/action/reply.js | ||
---|---|---|
@@ -1,0 +1,10 @@ | ||
1 | +var h = require('mutant/h') | |
2 | +var nest = require('depnest') | |
3 | + | |
4 | +exports.gives = nest('message.html.action') | |
5 | + | |
6 | +exports.create = (api) => { | |
7 | + return nest('message.html.action', function reply (msg) { | |
8 | + return h('a', { href: msg.key, anchor: 'reply' }, 'Reply') | |
9 | + }) | |
10 | +} |
Built with git-ssb-web