Skip to content

Commit da5ed49

Browse files
feat: Allow to edit a Mastodon thread when sharing a link
1 parent b4a6799 commit da5ed49

35 files changed

+1519
-448
lines changed

locales/fr_FR/LC_MESSAGES/main.mo

538 Bytes
Binary file not shown.

locales/fr_FR/LC_MESSAGES/main.po

Lines changed: 133 additions & 110 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 61 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
},
2525
"dependencies": {
2626
"@hotwired/stimulus": "^3.1.0",
27-
"@hotwired/turbo": "^8"
27+
"@hotwired/turbo": "^8",
28+
"stringz": "^2.1.0",
29+
"twitter-text": "^3.1.0"
2830
}
2931
}

src/Router.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ public static function load(): \Minz\Router
163163
$router->addRoute('POST', '/links/:id/read/later', 'links/Read#later', 'read link later');
164164
$router->addRoute('POST', '/links/:id/read/never', 'links/Read#never', 'mark link to never read');
165165
$router->addRoute('POST', '/links/:id/read/delete', 'links/Read#delete', 'mark link as unread');
166+
$router->addRoute(
167+
'GET',
168+
'/links/:id/shares/mastodon',
169+
'links/MastodonThreads#new',
170+
'new link mastodon thread',
171+
);
172+
$router->addRoute(
173+
'POST',
174+
'/links/:id/shares/mastodon',
175+
'links/MastodonThreads#create',
176+
'create link mastodon thread',
177+
);
178+
$router->addRoute(
179+
'GET',
180+
'/links/:id/shares/mastodon/created',
181+
'links/MastodonThreads#created',
182+
'link mastodon thread created',
183+
);
166184

167185
// Link collections
168186
$router->addRoute('GET', '/links/:id/collections', 'links/Collections#index', 'link collections');

src/assets/javascripts/application.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import AutosubmitController from './controllers/autosubmit_controller.js';
55
import AutosaveController from './controllers/autosave_controller.js';
66
import BackButtonController from './controllers/back_button_controller.js';
77
import CaptionSwitcherController from './controllers/caption_switcher_controller.js';
8+
import CharactersCounterController from './controllers/characters_counter_controller.js';
89
import CollectionsSelectorController from './controllers/collections_selector_controller.js';
910
import CopyToClipboardController from './controllers/copy_to_clipboard_controller.js';
1011
import CsrfController from './controllers/csrf_controller.js';
1112
import FormFileController from './controllers/form_file_controller.js';
1213
import GroupSelectorController from './controllers/group_selector_controller.js';
1314
import InputPasswordController from './controllers/input_password_controller.js';
15+
import ItemsController from './controllers/items_controller.js';
1416
import LinkSuggestionController from './controllers/link_suggestion_controller.js';
1517
import ModalController from './controllers/modal_controller.js';
1618
import ModalOpenerController from './controllers/modal_opener_controller.js';
@@ -26,12 +28,14 @@ application.register('autosubmit', AutosubmitController);
2628
application.register('autosave', AutosaveController);
2729
application.register('back-button', BackButtonController);
2830
application.register('caption-switcher', CaptionSwitcherController);
31+
application.register('characters-counter', CharactersCounterController);
2932
application.register('collections-selector', CollectionsSelectorController);
3033
application.register('copy-to-clipboard', CopyToClipboardController);
3134
application.register('csrf', CsrfController);
3235
application.register('form-file', FormFileController);
3336
application.register('group-selector', GroupSelectorController);
3437
application.register('input-password', InputPasswordController);
38+
application.register('items', ItemsController);
3539
application.register('link-suggestion', LinkSuggestionController);
3640
application.register('modal', ModalController);
3741
application.register('modal-opener', ModalOpenerController);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import { length } from 'stringz';
3+
import regexSupplant from 'twitter-text/dist/lib/regexSupplant';
4+
import validDomain from 'twitter-text/dist/regexp/validDomain';
5+
import validPortNumber from 'twitter-text/dist/regexp/validPortNumber';
6+
import validUrlPath from 'twitter-text/dist/regexp/validUrlPath';
7+
import validUrlPrecedingChars from 'twitter-text/dist/regexp/validUrlPrecedingChars';
8+
import validUrlQueryChars from 'twitter-text/dist/regexp/validUrlQueryChars';
9+
import validUrlQueryEndingChars from 'twitter-text/dist/regexp/validUrlQueryEndingChars';
10+
11+
import icon from '../icon.js';
12+
import _ from '../l10n.js';
13+
14+
// Code from Mastodon (AGPL)
15+
// @see https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/features/compose/util/url_regex.js
16+
const urlRegex = regexSupplant(
17+
'(' + // $1 URL
18+
'(#{validUrlPrecedingChars})' + // $2
19+
'(https?:\\/\\/)' + // $3 Protocol
20+
'(#{validDomain})' + // $4 Domain(s)
21+
'(?::(#{validPortNumber}))?' + // $5 Port number (optional)
22+
'(\\/#{validUrlPath}*)?' + // $6 URL Path
23+
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String
24+
')',
25+
{
26+
validUrlPrecedingChars,
27+
validDomain,
28+
validPortNumber,
29+
validUrlPath,
30+
validUrlQueryChars,
31+
validUrlQueryEndingChars,
32+
},
33+
'gi',
34+
);
35+
36+
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
37+
38+
export default class extends Controller {
39+
static targets = ['source', 'counter']
40+
41+
static values = {
42+
max: Number,
43+
}
44+
45+
connect () {
46+
this.updateCounter();
47+
}
48+
49+
updateCounter () {
50+
const count = this.countCharacters(this.sourceTarget.value);
51+
52+
let label = _('{{count}} characters out of a maximum of {{max}}');
53+
label = label.replace('{{count}}', count);
54+
label = label.replace('{{max}}', this.maxValue);
55+
this.counterTarget.ariaLabel = label;
56+
57+
if (count > this.maxValue) {
58+
this.counterTarget.innerHTML = `${count} / ${this.maxValue} ${icon('error')}`;
59+
60+
this.counterTarget.classList.add('counter--over');
61+
62+
this.sourceTarget.setCustomValidity(_('The post is too long.'));
63+
this.sourceTarget.ariaInvalid = 'true';
64+
} else {
65+
this.counterTarget.innerHTML = `${count} / ${this.maxValue}`;
66+
67+
this.counterTarget.classList.remove('counter--over');
68+
69+
this.sourceTarget.setCustomValidity('');
70+
this.sourceTarget.ariaInvalid = null;
71+
}
72+
}
73+
74+
countCharacters(text) {
75+
// Code from Mastodon (AGPL)
76+
// @see https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/features/compose/util/counter.js
77+
let countableText = text;
78+
countableText = countableText.replace(urlRegex, urlPlaceholder);
79+
countableText = countableText.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, '$1@$3');
80+
81+
return length(countableText);
82+
}
83+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
export default class extends Controller {
4+
static targets = ['container', 'prototype']
5+
6+
static values = {
7+
index: Number,
8+
}
9+
10+
connect () {
11+
this.refreshLabels();
12+
}
13+
14+
addItem () {
15+
const element = this.prototypeTarget.content.firstElementChild.cloneNode(true);
16+
element.innerHTML = element.innerHTML.replace(/__index__/g, this.indexValue);
17+
18+
this.containerTarget.appendChild(element);
19+
20+
this.indexValue++;
21+
22+
this.refreshLabels();
23+
24+
const focusableElements = Array.from(element.querySelectorAll('textarea'));
25+
if (focusableElements.length >= 1) {
26+
focusableElements[0].focus();
27+
}
28+
}
29+
30+
removeItem (event) {
31+
const target = event.target;
32+
const element = target.closest('[data-items-target="item"]');
33+
34+
element.remove();
35+
36+
this.refreshLabels();
37+
}
38+
39+
refreshLabels () {
40+
const elementsWithArialLabel = this.containerTarget.querySelectorAll('[aria-label]');
41+
elementsWithArialLabel.forEach((element, index) => {
42+
let labelPattern = element.dataset.labelPattern;
43+
44+
if (!labelPattern) {
45+
// First time we refresh the labels, we save the content of
46+
// labels as patterns.
47+
labelPattern = element.ariaLabel;
48+
element.dataset.labelPattern = labelPattern;
49+
}
50+
51+
// Update the labels with the correct number.
52+
element.ariaLabel = labelPattern.replace(/__number__/, index + 1);
53+
});
54+
}
55+
}

src/assets/stylesheets/application.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
@import './components/popup.css';
3232
@import './components/sections.css';
3333
@import './components/tags.css';
34+
@import './components/threads.css';
3435
@import './components/titles.css';
3536
@import './custom/collections.css';
3637
@import './custom/collections-selector.css';

src/assets/stylesheets/components/anchors.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ h1 a:focus {
4444
line-height: 1.5;
4545
text-decoration: none;
4646

47-
border: 0.1em solid currentcolor;
47+
border: 1px solid currentcolor;
4848
border-radius: var(--border-radius);
4949
}
5050

0 commit comments

Comments
 (0)