Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Disallow group names longer than 32 extended graphemes
- Loading branch information
1 parent
934e0fa
commit ecc04d3
Showing
12 changed files
with
302 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright 2021 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
.module-GroupTitleInput { | ||
margin: 16px; | ||
@include font-body-1; | ||
padding: 8px 12px; | ||
border-radius: 6px; | ||
border: 2px solid $color-gray-15; | ||
background: $color-white; | ||
color: $color-black; | ||
|
||
&:focus { | ||
outline: none; | ||
|
||
@include light-theme { | ||
border-color: $ultramarine-ui-light; | ||
} | ||
@include dark-theme { | ||
border-color: $ultramarine-ui-dark; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Copyright 2021 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
declare namespace Intl { | ||
type SegmenterOptions = { | ||
granularity?: 'grapheme' | 'word' | 'sentence'; | ||
}; | ||
|
||
type SegmentData = { | ||
index: number; | ||
input: string; | ||
segment: string; | ||
}; | ||
|
||
interface Segments { | ||
containing(index: number): SegmentData; | ||
|
||
[Symbol.iterator](): Iterator<SegmentData>; | ||
} | ||
|
||
// `Intl.Segmenter` is not yet in TypeScript's type definitions, so we add it. | ||
class Segmenter { | ||
constructor(locale?: string, options?: SegmenterOptions); | ||
|
||
segment(str: string): Segments; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// Copyright 2021 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
import React, { useState } from 'react'; | ||
|
||
import { storiesOf } from '@storybook/react'; | ||
|
||
import { setup as setupI18n } from '../../js/modules/i18n'; | ||
import enMessages from '../../_locales/en/messages.json'; | ||
|
||
import { GroupTitleInput } from './GroupTitleInput'; | ||
|
||
const i18n = setupI18n('en', enMessages); | ||
|
||
const story = storiesOf('Components/GroupTitleInput', module); | ||
|
||
const Wrapper = ({ disabled }: { disabled?: boolean }) => { | ||
const [value, setValue] = useState(''); | ||
|
||
return ( | ||
<GroupTitleInput | ||
disabled={disabled} | ||
i18n={i18n} | ||
onChangeValue={setValue} | ||
value={value} | ||
/> | ||
); | ||
}; | ||
|
||
story.add('Default', () => <Wrapper />); | ||
|
||
story.add('Disabled', () => <Wrapper disabled />); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// Copyright 2021 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
import React, { forwardRef, useRef, ClipboardEvent } from 'react'; | ||
|
||
import { LocalizerType } from '../types/Util'; | ||
import { multiRef } from '../util/multiRef'; | ||
import * as grapheme from '../util/grapheme'; | ||
|
||
const MAX_GRAPHEME_COUNT = 32; | ||
|
||
type PropsType = { | ||
disabled?: boolean; | ||
i18n: LocalizerType; | ||
onChangeValue: (value: string) => void; | ||
value: string; | ||
}; | ||
|
||
/** | ||
* Group titles must have fewer than MAX_GRAPHEME_COUNT glyphs. Ideally, we'd use the | ||
* `maxLength` property on inputs, but that doesn't account for glyphs that are more than | ||
* one UTF-16 code units. For example: `'💩💩'.length === 4`. | ||
* | ||
* This component effectively implements a "max grapheme length" on an input. | ||
* | ||
* At a high level, this component handles two methods of input: | ||
* | ||
* - `onChange`. *Before* the value is changed (in `onKeyDown`), we save the value and the | ||
* cursor position. Then, in `onChange`, we see if the new value is too long. If it is, | ||
* we revert the value and selection. Otherwise, we fire `onChangeValue`. | ||
* | ||
* - `onPaste`. If you're pasting something that will fit, we fall back to normal browser | ||
* behavior, which calls `onChange`. If you're pasting something that won't fit, it's a | ||
* noop. | ||
*/ | ||
export const GroupTitleInput = forwardRef<HTMLInputElement, PropsType>( | ||
({ i18n, disabled = false, onChangeValue, value }, ref) => { | ||
const innerRef = useRef<HTMLInputElement | null>(null); | ||
const valueOnKeydownRef = useRef<string>(value); | ||
const selectionStartOnKeydownRef = useRef<number>(value.length); | ||
|
||
return ( | ||
<input | ||
disabled={disabled} | ||
className="module-GroupTitleInput" | ||
onKeyDown={() => { | ||
const inputEl = innerRef.current; | ||
if (!inputEl) { | ||
return; | ||
} | ||
|
||
valueOnKeydownRef.current = inputEl.value; | ||
selectionStartOnKeydownRef.current = inputEl.selectionStart || 0; | ||
}} | ||
onChange={() => { | ||
const inputEl = innerRef.current; | ||
if (!inputEl) { | ||
return; | ||
} | ||
|
||
const newValue = inputEl.value; | ||
if (grapheme.count(newValue) <= MAX_GRAPHEME_COUNT) { | ||
onChangeValue(newValue); | ||
} else { | ||
inputEl.value = valueOnKeydownRef.current; | ||
inputEl.selectionStart = selectionStartOnKeydownRef.current; | ||
inputEl.selectionEnd = selectionStartOnKeydownRef.current; | ||
} | ||
}} | ||
onPaste={(event: ClipboardEvent<HTMLInputElement>) => { | ||
const inputEl = innerRef.current; | ||
if (!inputEl) { | ||
return; | ||
} | ||
|
||
const selectionStart = inputEl.selectionStart || 0; | ||
const selectionEnd = | ||
inputEl.selectionEnd || inputEl.selectionStart || 0; | ||
const textBeforeSelection = value.slice(0, selectionStart); | ||
const textAfterSelection = value.slice(selectionEnd); | ||
|
||
const pastedText = event.clipboardData.getData('Text'); | ||
|
||
const newGraphemeCount = | ||
grapheme.count(textBeforeSelection) + | ||
grapheme.count(pastedText) + | ||
grapheme.count(textAfterSelection); | ||
|
||
if (newGraphemeCount > MAX_GRAPHEME_COUNT) { | ||
event.preventDefault(); | ||
} | ||
}} | ||
placeholder={i18n('setGroupMetadata__group-name-placeholder')} | ||
ref={multiRef<HTMLInputElement>(ref, innerRef)} | ||
type="text" | ||
value={value} | ||
/> | ||
); | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// Copyright 2021 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
import { assert } from 'chai'; | ||
|
||
import { count } from '../../util/grapheme'; | ||
|
||
describe('grapheme utilities', () => { | ||
describe('count', () => { | ||
it('returns the number of extended graphemes in a string (not necessarily the length)', () => { | ||
// These tests modified [from iOS][0]. | ||
// [0]: https://github.com/signalapp/Signal-iOS/blob/800930110b0386a4c351716c001940a3e8fac942/Signal/test/util/DisplayableTextFilterTest.swift#L40-L71 | ||
|
||
// Plain text | ||
assert.strictEqual(count(''), 0); | ||
assert.strictEqual(count('boring text'), 11); | ||
assert.strictEqual(count('Bokmål'), 6); | ||
|
||
// Emojis | ||
assert.strictEqual(count('💩💩💩'), 3); | ||
assert.strictEqual(count('👩❤️👩'), 1); | ||
assert.strictEqual(count('🇹🇹🌼🇹🇹🌼🇹🇹'), 5); | ||
assert.strictEqual(count('🇹🇹'), 1); | ||
assert.strictEqual(count('🇹🇹 '), 2); | ||
assert.strictEqual(count('👌🏽👌🏾👌🏿'), 3); | ||
assert.strictEqual(count('😍'), 1); | ||
assert.strictEqual(count('👩🏽'), 1); | ||
assert.strictEqual(count('👾🙇💁🙅🙆🙋🙎🙍'), 8); | ||
assert.strictEqual(count('🐵🙈🙉🙊'), 4); | ||
assert.strictEqual(count('❤️💔💌💕💞💓💗💖💘💝💟💜💛💚💙'), 15); | ||
assert.strictEqual(count('✋🏿💪🏿👐🏿🙌🏿👏🏿🙏🏿'), 6); | ||
assert.strictEqual(count('🚾🆒🆓🆕🆖🆗🆙🏧'), 8); | ||
assert.strictEqual(count('0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣🔟'), 11); | ||
assert.strictEqual(count('🇺🇸🇷🇺🇦🇫🇦🇲'), 4); | ||
assert.strictEqual(count('🇺🇸🇷🇺🇸 🇦🇫🇦🇲🇸'), 7); | ||
assert.strictEqual(count('🇺🇸🇷🇺🇸🇦🇫🇦🇲'), 5); | ||
assert.strictEqual(count('🇺🇸🇷🇺🇸🇦'), 3); | ||
assert.strictEqual(count('123'), 3); | ||
|
||
// Normal diacritic usage | ||
assert.strictEqual(count('Příliš žluťoučký kůň úpěl ďábelské ódy.'), 39); | ||
|
||
// Excessive diacritics | ||
assert.strictEqual(count('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘'), 5); | ||
assert.strictEqual(count('H҉̸̧͘͠A͢͞V̛̛I̴̸N͏̕͏G҉̵͜͏͢ ̧̧́T̶̛͘͡R̸̵̨̢̀O̷̡U͡҉B̶̛͢͞L̸̸͘͢͟É̸ ̸̛͘͏R͟È͠͞A̸͝Ḑ̕͘͜I̵͘҉͜͞N̷̡̢͠G̴͘͠ ͟͞T͏̢́͡È̀X̕҉̢̀T̢͠?̕͏̢͘͢'), 28); | ||
assert.strictEqual(count('L̷̳͔̲͝Ģ̵̮̯̤̩̙͍̬̟͉̹̘̹͍͈̮̦̰̣͟͝O̶̴̮̻̮̗͘͡!̴̷̟͓͓'), 4); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// Copyright 2021 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
export function count(str: string): number { | ||
const segments = new Intl.Segmenter().segment(str); | ||
const iterator = segments[Symbol.iterator](); | ||
|
||
let result = -1; | ||
for (let done = false; !done; result += 1) { | ||
done = Boolean(iterator.next().done); | ||
} | ||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.