Что означает кирпич в дискорде
Перейти к содержимому

Что означает кирпич в дискорде

  • автор:

Что значит красный минус в Дискорде?

У многих участников в «Дискорде» есть различные значки, конечно те, кто давно пользуется этой платформой давным-давно в курсе, что каждый из значков обозначает.

Я значка красного минуса не нашла, но нашла вот такой значок — минус в красном значке.

Такой значок выдается лишь тем участникам, которые прошли тест. В общем, такой значок имеется далеко не у всех тех, кто пользуется дискордом.

Еще есть такое: если человек приобрел «невидимку», то для всех он будет не в сети, хотя на самом деле этот человек может быть онлайн, но другие участники об этом знать не будут. Вот как раз у таких участников и есть значок с красным сигналом.

FrachlitzStudio / DiscordMarkdownRUS.md

Save FrachlitzStudio/375dd6bcca3d20ff65e7728bb1fc416b to your computer and use it in GitHub Desktop.

Discord Markdown Guide RUS

Discord Markdown [ENG] [RUS]

Как использовать markdown в Discord’e, чтобы сделать текст более красивым?

Последнее обновление: 17.10.20

Шрифт

Пример Маркировка
Наклонённый *Наклонённый* или _Наклонённый_
Жирный **Жирный**
Зачёркнутый ~~Зачёркнутый~~
П͟о͟д͟ч͟ё͟р͟к͟н͟у͟т͟ы͟й͟ __Подчёркнутый__

Всё это можно комбинировать друг с другом:

Наклонённый, Жирный и Зачёркнутый : ~~_**Наклонённый, Жирный и Зачёркнутый**_~~

Спойлеры

Чтобы спрятать какой-то текст перед нажатием на него, следует записать нужную фразу между ||

Spoiler

Цитаты

Чтобы процитировать чьё-либо сообщение, нужно нажать на него ПКМ и кликнуть на соответствующую кнопку. Или же можно это сделать самому:

Quote

Блоки Кода

Можно писать блоки кода прямо в Discord’e.

Однострочный Блок Кода

Нужно записать текст между «`»:

OneLineCodeBlock

Однострочные блоки кода можно комбинировать со шрифтами:

OneLineCodeBlockEmphasis

Многострочный Блок Кода

Чтобы сделать многострочный блок кода, нужно поставить ««`» в начале и конце:

MultilineCodeBlock

Многострочные блоки кода также можно комбинировать со шрифтами:

MultiLineCodeBlockEmphasis

«Цветной» Многострочный Блок Кода

Используя синтаксис различных языков программирования, можно делать разноцветный текст:

CSS

Другие примеры

Открыть список

JS

Python

Diff

ini

html

php

sql

asciidoc

autohotkey

bash

fix

md

tex

arm

excel

Существует намного больше примеров, но, как мне кажется, никто не будет их использовать, т. к. они выглядят не очень красиво.

Убрать Markdown

Если нужно убрать markdown, следует поставить \ перед символами:

  • \*Не наклонённый\*
  • \> Не цитата
  • \|\|Не спойлер\|\| и т. д.

Чтобы убрать embed часть сообщения (видео-плеер от YouTube, заголовок и часть статьи с блога и т. п.), нужно писать ссылку между < >:

embed-removing

«Призрачные» пинги

Существуют способы, как принудительно упомянуть канал/роль/пользователя. И не важно, что юзер не присутствует на сервере или канал находится на другом.

Для всех методов следует включить Режим разработчика в настройках Внешнего вида.

Роль

Чтобы упомянуть роль, не имея на это прав, следует:

  1. Копировать её ID. Как это сделать? CopyRoleID
  2. Вставить его в конструкцию . Как это выглядит в Discord’e RolePingУчастники с этой ролью не получат уведомление!Можно упомянуть только на том сервере, где эта роль есть.

Пользователь

Чтобы упомянуть пользователя, которого нет на сервере/у которого нет прав перебывать в канале, следует:

  1. Копировать его ID. Как это сделать? CopyUserID
  2. Вставить его в конструкцию . Как это выглядит в Discord’e UserPingРаботает также в ЛС.

Канал

Чтобы принудительно упомянуть канал (если нет доступа или нужно перенаправить в канал на другом сервере), следует:

  1. Копировать его ID.

ChannelPing1 ChannelPing2

Эмодзи

Чтобы вставить эмодзи где-угодно (например, в заголовок канала), следует:

  1. Копировать его ID. Как это сделать? CopyEmojiID
  2. Вставить его в конструкцию или (для gif-эмодзи). Как это выглядит в Discord’e PastedEmoji

Дата и время

Чтобы указать какую-то дату и время (это может быть полезно для ботов), следует:

PastedEmoji

  1. Узнать количество секунд, пройденных с 01.01.1970 до нужной даты (так званое, эпохальное время).
  2. Вставить его в конструкцию или . Как это выглядит в Discord’e Также можно для удобства воспользоваться сторонним ресурсом. Например, https://hammertime.djdavid98.art/ru

Быстрая реакция

Чтобы быстро добавить реакцию, нужно поставить «+» и написать эмодзи: +:emoji: . Текст мгновенно пропадёт, а реакция появиться на последнем сообщении. Если этого не произошло, следует нажать Enter.

Как Discord хранит миллиарды сообщений

Discord продолжает расти быстрее, чем мы ожидали, как и пользовательский контент. Чем больше пользователей — тем больше сообщений в чате. В июле мы объявили о 40 млн сообщений в день, в декабре объявили о 100 млн, а в середине января преодолели 120 млн. Мы сразу решили хранить историю чатов вечно, так что пользователи могут вернуться в любой момент и получить доступ к своим данным с любого устройства. Это много данных, поток и объём которых нарастает, и все они должны быть доступными. Как мы это делаем? Cassandra!

Что мы делали

Изначальную версию Discord написали быстрее чем за два месяца в начале 2015 года. Возможно, одной из лучших СУБД для быстрого выполнения итераций является MongoDB. Всё в Discord специально хранилось в едином реплисете (replica set) MongoDB, но мы также готовили всё для простой миграции в новую СУБД (мы знали, что не собираемся использовать шардинг MongoDB из-за его сложности и неизвестной стабильности). На самом деле это часть нашей корпоративной культуры: разрабатывай быстро, чтобы испытать новую функцию продукта, но всегда с курсом на более надёжное решение.

Сообщения хранились в коллекции MongoDB с единым составным индексом на channel_id и created_at . Примерно в ноябре 2015 года мы вышли на рубеж 100 млн сообщений в базе, и тогда начали понимать проблемы, которые нас ждут: данные и индекс больше не помещаются в ОЗУ, а задержки становятся непредсказуемыми. Пришло время мигрировать в более подходящую СУБД.

Выбор правильной СУБД

Перед выбором новой СУБД нам требовалось понять имеющиеся шаблоны чтения/записи и почему возникли проблемы с текущим решением.

  • Быстро стало понятно, что операции чтения исключительно случайны, а соотношения чтение/запись примерно 50/50.
  • Тяжёлые серверы голосовых чатов Discord практически не присылали сообщений. То есть они присылали одно или два сообщения каждые несколько дней. За год сервер такого типа вряд ли достигнет рубежа в 1000 сообщений. Проблема в том, что даже несмотря на такое малое количество сообщений, эти данные труднее доставлять пользователям. Просто возвращение пользователю 50-ти сообщений может привести к многим случайным операциям поиска на диске, что приводит к вытеснению дискового кэша.
  • Тяжёлые серверы приватных текстовых чатов Discord отправляют приличное количество сообщений, легко попадая в диапазон между 100 тыс. и 1 млн сообщений в год. Запрашивают они обычно только самые последние данные. Проблема в том, что на этих серверах обычно менее 100 участников, так что скорость запроса данных низкая и вряд ли они будут в дисковом кэше.
  • Большие публичные серверы Discord отправляют очень много сообщений. Там тысячи участников, отправляющих тысячи сообщений в день. Легко набираются миллионы сообщений в год. Они почти всегда запрашивают сообщения, отправленные в последний час, и это происходит часто. Поэтому данные обычно находятся в дисковом кэше.
  • Мы знали, что в наступающем году у пользователей появится ещё больше способов генерировать случайные чтения: это возможность просматривать свои упоминания за последние 30 дней и затем перескакивать в тот момент истории, просмотр и переход к прикреплённым сообщениям и полнотекстовый поиск. Всё это означает ещё больше случайных чтений!
  • Линейная масштабируемость — Мы не хотим пересматривать решение позже или вручную переносить данные в другой шард.
  • Автоматическая отказоустойчивость — Нам нравится спать по ночам и делать Discord настолько самоисцеляющимся, насколько это возможно.
  • Небольшая поддержка — Она должна работать сразу же, как мы её установим. От нас требуется только добавлять больше нод по мере увеличения данных.
  • Доказано в работе — Мы любим пробовать новые технологии, но не слишком новые.
  • Предсказуемая производительность — Нам отправляются сообщения, если время отклика API в 95% случаев превышает 80 мс. Мы также не хотим сталкиваться с необходимостью кэшировать сообщения в Redis или Memcached.
  • Не хранилище блобов — Запись тысяч сообщений в секунду не будет отлично работать, если нам придётся непрерывно десериализировать блобы и присоединять к ним данные.
  • Open source — Мы верим, что управляем собственной судьбой, и не хотим зависеть от сторонней компании.

Сделав выбор, нужно было доказать, что он действительно оправдан.

Моделирование данных

Лучший способ описать новичку Cassandra — это аббревиатура KKV. Две буквы “K” содержат в себе первичный ключ. Первая “K” — это ключ раздела. Он помогает определить, в какой ноде живут данные и где их найти на диске. Внутри раздела множество строк, и конкретную строку внутри раздела определяет вторая “K” — ключ кластеризации. Он работает как первичный ключ внутри раздела и определяет способ сортировки строк. Можете представить раздел как упорядоченный словарь. Все эти качества вместе взятые позволяют очень мощное моделирование данных.

Помните, что сообщения в MongoDB индексировались с использованием channel_id и created_at ? channel_id стал ключом раздела, поскольку все сообщения работают в канале, но created_at не даёт хорошего ключа кластеризации, потому что два сообщения могут быть созданы в одно время. К счастью, каждый ID в Discord на самом деле создан в Snowflake, то есть хронологически сортируется. Так что можно было использовать именно их. Первичный ключ превратился в (channel_id, message_id) , где message_id — это Snowflake. Это значит, что при загрузке канала мы можем сказать Cassandra точный диапазон, где искать сообщения.

Вот упрощённая схема для нашей таблицы сообщений (она пропускает примерно 10 колонок).

CREATE TABLE messages ( channel_id bigint, message_id bigint, author_id bigint, content text, PRIMARY KEY (channel_id, message_id) ) WITH CLUSTERING ORDER BY (message_id DESC);

Хотя схемы у Cassandra и похожи на схемы реляционных БД, их легко изменять, что не оказывает какого-либо временного влияния на производительность. Мы взяли лучшее от хранилища блобов и реляционного хранилища.

Как только начался импорт существующих сообщений в Cassandra, мы сразу увидели в логах предупреждения, что найдены разделы размером более 100 МБ. Да ну?! Ведь Cassandra заявляет о поддержке разделов 2 ГБ! По всей видимости, сама возможность не означает, что так нужно делать. Большие разделы накладывают сильную нагрузку на сборщик мусора в Cassandra при уплотнении, расширении кластера и т.д. Наличие большого раздела также означает, что данные в нём нельзя распределить по кластеру. Стало ясно, что нам придётся как-то ограничить размеры разделов, потому что некоторые каналы Discord могут существовать годами и постоянно увеличиваться в размере.

Мы решили распределить наши сообщения блоками (buckets) по времени. Мы посмотрели на самые большие каналы в Discord и определили, что если хранить сообщения блоками примерно по 10 дней, то комфортно вложимся в лимит 100 МБ. Блоки нужно получать из message_id или метки времени.

DISCORD_EPOCH = 1420070400000 BUCKET_SIZE = 1000 * 60 * 60 * 24 * 10 def make_bucket(snowflake): if snowflake is None: timestamp = int(time.time() * 1000) - DISCORD_EPOCH else: # When a Snowflake is created it contains the number of # seconds since the DISCORD_EPOCH. timestamp = snowflake_id >> 22 return int(timestamp / BUCKET_SIZE) def make_buckets(start_id, end_id=None): return range(make_bucket(start_id), make_bucket(end_id) + 1)

Ключи разделов Cassandra могут быть составными, так что нашим новым первичным ключом стал ((channel_id, bucket), message_id) .

CREATE TABLE messages ( channel_id bigint, bucket int, message_id bigint, author_id bigint, content text, PRIMARY KEY ((channel_id, bucket), message_id) ) WITH CLUSTERING ORDER BY (message_id DESC);

Для запроса недавних сообщений в канале мы сгенерировали диапазон блоков от текущего времени до channel_id (он тоже хронологически сортируется как Snowflake и должен быть старше, чем первое сообщение). Затем мы последовательно опрашиваем разделы до тех пор, пока не соберём достаточно сообщений. Обратная сторона такого метода в том, что изредка активным инстансам Discord придётся опрашивать много разных блоков, чтобы собрать достаточно сообщений со временем. На практике оказалось, что всё в порядке, потому что для активного инстанса Discord обычно находится достаточно сообщений в первом разделе, и таких большинство.

Импорт сообщений в Cassandra прошёл без помех, и мы были готовы опробовать её в производстве.

Тяжёлый запуск

Выводить новую систему в производство всегда страшно, так что хорошей идеей будет проверить её, не затрагивая пользователей. Мы настроили систему на дублирование операций чтения/записи в MongoDB и Cassandra.

Немедленно после запуска в баг-трекере появились ошибки, что author_id равен нулю. Как он может быть нулевым? Это обязательное поле!

Согласованность в конечном счёте

Cassandra — система типа AP, то есть гарантированная целостность здесь приносится в жертву доступности, что мы и хотели, в общем. В Cassandra противопоказано чтение перед записью (операции чтения более дорогие) и поэтому всё, что делает Cassandra, — это обновление и вставку (upsert), даже если предоставить только определённые колонки. Вы также можете писать в любую ноду, и она автоматически разрешит конфликты, используя семантику «последняя запись выигрывает» по каждой колонке. Так как это нас коснулось?

Пример состояния гонки редактирование/удаление

В случае, если пользователь редактировал сообщение, в то время как другой пользователь удалял то же самое сообщение, у нас появлялась строка с полностью отсутствующими данными, за исключением первичного ключа и текста, потому что Cassandra записывает только обновления и вставки. Для этой проблемы есть два возможных решения:

  1. Записывать обратно целое сообщение во время редактирования сообщения. Тогда есть возможность воскрешения удалённых сообщений и добавляются шансы конфликтов для одновременных записей в другие колонки.
  2. Выявить повреждённое сообщение и удалить его из базы.

Решая эту проблему, мы заметили, что были весьма неэффективны с операциями записи. Поскольку Cassandra согласована в конечном счёте, то она не может вот так взять и немедленно удалить данные. Ей нужно реплицировать удаления на другие ноды, и это следует сделать даже если ноды временно недоступны. Cassandra справляется с этим, приравнивая удаление к своеобразной форме записи под названием “tombstone” («надгробие»). Во время операции чтения она просто проскакивает через «надгробия», которые встречаются по пути. Время жизни «надгробий» настраивается (по умолчанию, 10 дней), и они навсегда удаляются во время уплотнения базы, если срок вышел.

Удаление колонки и запись нуля в колонку — это абсолютно одно и то же. В обоих случаях создаётся «надгробие». Поскольку все записи в Cassandra являются обновлениями и вставками, то вы создаёте «надгробие» даже если изначально записываете нуль. На практике, наша полная схема сообщения состояла из 16 колонок, но среднее сообщение имело только 4 установленных значения. Мы записывали 12 «надгробий» в Cassandra, обычно без всякой причины. Решение проблемы было простым: записывать в базу только ненулевые значения.

Производительность

Известно, что Cassandra быстрее выполняет операции записи, чем чтения, и мы наблюдали в точности это. Операции записи происходили в интервале менее миллисекунды, а операции чтения — менее 5 миллисекунд. Такие показатели наблюдались независимо от типа данных, к которым осуществлялся доступ. Производительность сохранялась неизменной в течение недели тестирования. Ничего удивительного, мы получили в точности то, чего ожидали.

Задержка чтения/записи, по данным из лога

В соответствии с быстрой, надёжной производительностью чтения, вот пример перехода к сообщению годичной давности в канале с миллионами сообщений:

Большой сюрприз

Всё прошло гладко, так что мы выкатили Cassandra как нашу основную базу данных и вывели из строя MongoDB в течение недели. Она продолжала безукоризненно работать… примерно 6 месяцев, пока однажды не перестала реагировать.

Мы заметили, что Cassandra непрерывно останавливается на 10 секунд во время сборки мусора, но совершенно не могли понять, почему. Начали копать — и нашли канал Discord, который требовал 20 секунд для загрузки. Виновником был публичный Discord-сервер подреддита Puzzles & Dragons. Поскольку он публичный, мы присоединились посмотреть. К нашему удивлению, на канале было только одно сообщение. В тот момент стало очевидно, что они удалили миллионы сообщений через наши API, оставив только одно сообщение на канале.

Если вы внимательно читали, то помните, как Cassandra обрабатывает удаления при помощи «надгробий» (упомянуто в главе «Согласованность в конечном счёте»). Когда пользователь загружает этот канал, хоть там одно сообщение, Cassandra приходится эффективно сканировать миллионы «надгробий» сообщений. Тогда она генерирует мусор быстрее, чем JVM может собрать его.

Мы решили эту проблему следующим образом:

  • Уменьшили время жизни надгробий с 10 дней до 2 дней, потому что мы каждый вечер запускаем починку Cassandra (противоэнтропийный процесс) на нашем кластере сообщений.
  • Изменили код запросов, чтобы отслеживать пустые блоки на канале и избегать их в будущем. Это значит, что если пользователь снова инициировал этот запрос, то в худшем случае Cassandra будет сканировать только самый последний блок.

Будущее

В данный момент у нас работает кластер из 12 нодов с коэффициентом репликации 3, и мы продолжим добавлять новые ноды Cassandra по мере надобности. Мы верим, что этот подход работоспособен в долговременной перспективе, но по мере роста Discord просматривается отдалённое будущее, когда придётся сохранять миллиарды сообщений в день. У Netflix и Apple работают кластеры с сотнями нодов, поэтому пока что нам не о чем волноваться. Однако хочется иметь пару идей про запас.

Ближайшее будущее

  • Обновить наш кластер сообщений с Cassandra 2 на Cassandra 3. Новый формат хранения в Cassandra 3 может сократить объём хранения более чем на 50%.
  • Более новые версии Cassandra лучше справляются с обработкой большего количества данных в каждом ноде. Мы сейчас храним примерно 1 ТБ сжатых данных в каждом из них. Думаем, что можно безопасно сократить количество нодов в кластере, увеличив этот лимит до 2 ТБ.

Отдалённое будущее

  • Изучить Scylla — это СУБД, совместимая с Cassandra и написанная на C++. В нормальной работе наши ноды Cassandra в реальности потребляют немного ресурсов CPU, однако в непиковые часы во время починки Cassandra (противоэнтропийный процесс) они довольно сильно зависят от CPU, а время починки возрастает в зависимости от количества данных, записанных с момента прошлой починки. Scylla обещает значительно увеличить скорость починки.
  • Создать систему для архивации неиспользуемых каналов в Google Cloud Storage и загрузки их обратно по требованию. Мы хотим избежать этого и не думаем, что такое придётся делать.

Заключение

Прошло уже больше года с момента перехода на Cassandra, и несмотря на «большой сюрприз», это было спокойное плавание. Мы вышли с более 100 миллионов общего количества сообщений на более чем 120 миллионов сообщений в день, сохранив производительность и стабильность.

Благодаря успеху этого проекта, с тех пор мы перенесли все остальные наши данные в производстве на Cassandra, и тоже успешно.

В продолжении этой статьи мы исследуем, как мы осуществляем полнотекстовый поиск по миллиардам сообщений.

У нас до сих пор нет специализированных инженеров DevOps (только четыре инженера бэкенда), так что очень классно иметь систему, о которой не приходится волноваться. Мы набираем сотрудников, так что обращайтесь, если подобные задачки щекочут ваше воображение.

  • Высокая производительность
  • Мессенджеры
  • MongoDB
  • Тестирование веб-сервисов

Как Discord реализовал навигацию клавиатурой по всему приложению

Мы делаем Discord доступным для каждого. В 2020 году мы многое сделали, чтобы значительно улучшить ассебилити в приложении. Остаётся ещё много работы, но мы уже сейчас можем рассказать об одном из наших последних проектов — Навигация по клавиатуре.

Она может быть полезна всем пользователям. Ей пользуются те, кто не может нормально использовать мышь или те, кто используют скрин-ридеры. Она круче, чем шорткаты и дает возможность работать максимально эффективно. Для нашей относительно небольшой команды подобные проекты дают значимый результат и в одночасье делают нас лучшими.

Продвинутые пользователи знают, что Discord уже несколько лет немного поддерживает использование клавиатуры. Шорткаты позволяют перемещаться между каналами, перемещаться по полям форм, быстро открывать поиск или Быстрый переключатель. Нашей целью стало пройти путь от обычных шорткатов до «вы можете делать всё что угодно обходясь клавиатурой» и при этом сделать UX первоклассным.

Четыре части, которые вы видите в Discord в любой момент времени: список серверов, список каналов, сообщения и список пользователей.

На нашем пути был ряд дизайнерских и технических сложностей. Но самым сложным и казалось нерешаемым как для современных браузеров, так и для других проектов в Интернете был вопрос: как мы можем постоянно подсвечивать на каком элементе страницы находится фокус?

С первого взгляда звучит просто и кажется, что это уже давно решенная задача. Мы тоже так думали! Однако непреодолимые проблемы и ограничения быстро перечеркнули все варианты, которые мы смогли придумать.

Проблема

Если вы когда-либо работали с фокусными стилями в CSS, то вы наверное знакомы с двумя главными опциями которые есть в браузерах на текущий момент: псевдо-класс :focus и свойство outline.

Браузеры как правило задают свои дефолтные стили фокуса этими двумя опциями, но если вы хотите, чтобы стили фокуса были одинаковыми в разных браузерах или у вас есть макет, которого вам нужно придерживаться, то скорее всего вы добавите стили подобно этим

:focus

Этого должно быть достаточно, чтобы выделить обводкой любой элемент получивший фокус. Но если так сделать в большом проекте всё сломается.

Вот примеры обрезания обводки в контейнере со стилем overflow-hidden, несовпадения радиуса обводки и скругленных элементов, а также странные границы элемента заданные padding’ами и margin’ами.

Кажется это всё можно относительно просто исправить. Просто задайте padding вокруг, используйте box-shadow или будьте аккуратнее с overflow: hidden. Тогда всё будет хорошо, верно?

На самом деле это не масштабируемый подход. Ограничение использования CSS-свойств создаст дополнительные сложности при разработке, которые непросто разруливать с помощью девтулзов. Кроме того, мы хотели сделать для наших разработчиков решение, которое будет «работать из коробки».

Конечный результат, который мы хотим получить — клавиатурная навигация, позволяющая любому человеку перемещаться по Discord.

Мы потратили неделю на обдумывание того, как мы можем это реализовать. Все компоненты нашей дизайн-системы уже имеют стили для состояния фокуса. Также глобально навешан лисенер следящий за изменением document.activeElement и кое-что перерендеривается, когда он меняется. В общем, анализ всех проблем мог бы занять целый пост, нам нужно было искать компромиссы.

Исследуя проблемы с дефолтными стилями фокуса мы поняли, что нужно отделить рендер обводки от рендера целевого элемента. Также мы поняли, что нельзя сделать просто один верхнеуровневый компонент, потому что новые проблемы, такие как рендеринг поверх скрытых слоев или следование за скроллом делали обводку «плавающей» и оторванными от контента на странице.

В итоге мы получили мощную и гибкую систему, отвечающую всем нашим требованиям, а также с возможностями покрыть граничные кейсы, с которыми мы столкнёмся при проектировании UI.

Вся система построена из двух компонентов: FocusRing для объявления, где обводка должна быть и FocusRingScope для объявления корневого элемента для её рендера.

API

FocusRing

Основной компонент нашего решения — это FocusRing. Сам он ничего не рендерит, однако является обработчиком всего, начиная от определения, когда целевой элемент получает или теряет фокус, заканчивая взаимодействием с FocusRingScope и передачей нескольких пропсов.

Простейший пример использования — нужно просто обернуть целевой элемент в FocusRing.

function Button(props) < return ( > /> ); >

Пример рендеринга FocusRing вокруг кнопки. Код приведён выше.

По умолчанию FocusRing берет дочерний компонент и переопределяет для них onBlur и onFocus при помощи React.cloneElement. Кроме того, пропсы FocusRing позволяют задать должно ли кольцо эмулировать :focus или :focus-within , передать CSS-классы для состояния фокуса дочернего компонента и даже передать другой целевой компонент для захвата фокуса или позиционирования обводки.

Более сложный пример с использованиям некоторых пропсов будет выглядеть так:

function SearchBar() < const containerRef = React.useRef(null); const inputRef = React.useRef(null); return ( ringTarget=> 
ref=> placeholder="Search" />
>
); >

В этом примере рефы focusTarget и ringTarget говорят FocusRing перехватывать фокус инпута, но рендерить кольцо вокруг всего контейнера.

Такое поведение полезно, когда фокусное состояние может получить только часть компонента. Например, SearchBar, о котором мы говорили выше или кастомный чекбокс, у которого лейбл входит в область клика, но фокус получает только инпут.

Вот пример SearchBar с отрендеренным FocusRing вокруг всего контейнера

FocusRingScope

FocusRingScope — это обертка для рендера обводки. Как было упомянуто ранее, обводка не может быть отрендерена в одном руте DOM-дерева и вместо этого она должна быть на более мелких элементах страницы (таких как прокручиваемые блоки или абсолютно-спозиционрованные контейнеры). FocusRingScope является корневым (родительским) для таких блоков. За рендеринг FocusRing будет отвечать ближайший родительский FocusRingScope .

Это позволяет покрывать ситуации, когда некоторые элементы могут быть частично закрыты или прокручены за пределы вьюпорта и рендеринг обводки должен то учитывать. Всё что требуется для FocusRingScope — это ссылка на контейнер, который он использует для вычисления позиции обводки.

function Scroller() < const containerRef = React.useRef(null); return ( 
> ref=>
); >

Под капотом компоненты взаимодействуют через Context. FocusRing предоставляет контекст с элементом, вокруг которого отрендерена обводка. Затем он вычисляет положение и стили. В конце FocusRingScope делает подписку на целевой компонент, чтобы обновляться при каждом его изменении.

Единственное, что нужно помнить разработчику — это включить FocusRingScope в корень приложения. В противном случае обводке негде будет рендериться по умолчанию.

Остающиеся проблемы

Даже при таком подходе остаются проблемы, которые не имеют хороших красивых решений. Нам удалось их обойти, но есть места, где даже кастомная реализация сталкивается с ограничениями браузера.

К счастью, для некоторых из этих проблем уже предложены нативные решения. Остается лишь вопросом времени получение официальной поддержки в браузерах, и тогда мы сможем переписать наш код.

Не сломать анимации

Переходы, трансформации и анимации — это большая часть современного веб-дизайна. Они помогают пользователям ориентироваться на сайте и привлечь внимание к важным частям страницы. Да и просто приятно по посмотреть на качественную красивую анимацию.

Но когда элемент двигается по странице очень сложно понять, где он находится в текущий момент и продолжает ли он двигаться.

Пример того, как обводка не успевает за положением движущегося целевого элемента.

Поскольку мы не выполняем рендеринг непосредственно в этих элементах, мы не можем ожидать, что анимация перенесет нашу обводку вместе с ними. Большинство из анимаций работают напрямую с отдельными элементами. Это означает, что изменения будут влиять только на этот элемент и его потомков. Нам придется получать из DOM новое положение элемента на каждом кадре и соответствующим образом перерисовывать обводку.

Делать что-либо на каждом кадре — не идеальный вариант. Можно, конечно, использовать хотя бы requestAnimationFrame , чтобы избежать повторного рендера, когда положение элементов не изменилось, но конечным результатом неизбежно будет постоянный цикл, который обращается к DOM на каждом кадре. Мы исследовали возможность снижения оверхеда, повесили лисенеры на события animationstart и animationend и запускали цикл только между этими событиями, но из-за всплытия событий в DOM, не всегда гарантируется правильное обновление.

На самом деле от браузера нужен способ определять, когда обвязка элемента изменилась и уметь выполнять коллбэк в этот момент. requestAnimationFrame позволяет вам понять что-то типа «я собираюсь изменить что-то на следующем кадре», но вы не можете понять обратного — «сообщи, когда что-то изменится на следующем кадре», чтобы иметь возможность перехватывать эти изменения нативно. Что-то вроде window.onAnimationFrame было бы здесь прекрасным дополнением. Однако в настоящее время не похоже, что есть какие-либо пропозалы по такому хуку, что означает, что реальное решение вряд ли появится в ближайшее время.

Наша реализация сейчас работает довольно хорошо, но все же иногда появляются запаздывающие кадры, от которых было бы неплохо избавиться. Но на данный момент мы довольны результатом и не думаем, что можно сделать сильно лучше.

Пример кольца фокусировки, которое почти не отстает от анимации.

Адаптивные формы

Одна из первых проблем, вызванных использованием стандартного CSS заключается в том, что он крайне ограничен в возможностях его кастомизации. Несмотря на то, что обводка очень похожа на border, вы не можете задать outline-radius или что-либо еще для регулировки смещения обводки относительно элемента.

К счастью, здесь мы вообще не полагаемся на эти свойства. Поскольку мы явно рендерим каждый элемент, чтобы показать кольцо, мы можем делать все, что захотим. Но это также означает, что мы не наследуем ни одно из этих свойств от целевого элемента. В частности, точный радиус границы — это один из ключевых моментов, благодаря которому обводка ощущается плотной и правильно интегрированной с элементами на странице. Например, обводка с радиусом 4px будет выглядеть плохо вокруг кнопки с более закругленными углами:

Сравнение обводки с константным радиусом границы с обводкой с адаптивным радиусом для соответствия целевому элементу.

Кастомизировать радиус обводки оказалось на удивление просто. У нас уже есть целевой элемент в контексте, поэтому при запросе его положения, мы также получаем стиль элемента и в частности его border-radius. После этого можно адаптировать border-radius обводки везде автоматически через кастомные CSS-свойства.

function AdaptiveRadiusRing() < const = React.useContext(FocusRingContext); const radius = React.useMemo( () => window.getComputedStyle(ringTarget).borderRadius, [ringTarget] ); const style = < '__adaptive-ring-radius': radius, >; return  style= />; > // In CSS // .ring < // border-radius: var(--__adaptive-ring-radius, 4px); // >view raw

Круто то, что getComputedStyle , будет возвращать нужное значение в случае, если радиус разный для разных сторон элемента, поэтому то, что может показаться сложным, на самом деле будет обрабатываться просто по умолчанию без сложностей для разработчиков. Мы можем легко расширить эту функциональность на другие свойства в будущем. Например, для z-index для случаев, когда элемент абсолютно-спозиционирован или для чего-либо другого.

Здесь нет лучшего решения. Было бы неплохо, если бы свойство outline имело дочернее свойство outline-radius или outline-radius наследовался бы от border-radius. Но в данном случае это не помогло бы из-за множества других проблем с псевдо-классом :focus и свойством outline. А сейчас мы имеем контроль и любое количество дополнительного контроля всегда приветствуется.

Адаптивные цвета

Одна из последних вещей, которую мы хотели сделать — это убедиться, что обводка будет достаточно заметна поверх любого фона, на котором находится целевой элемент.

Например, Discord часто использует различные цветные фоны на профилях пользователей для представления различных статусов, таких как игра, стриминг или прослушивание музыки. Эти цвета не всегда хорошо сочетаются с синим цветом, который мы используем для обводки по умолчанию.

Пример полупрозрачного белого контура обводки на ярко-зеленом фоне

На первый взгляд, это кажется достаточно простым для реализации: как и в случае с border-radius и z-index, мы можем просто получить элемент и установить нужный цвет в зависимости от данных, верно? Но не существует прямого способа получить цвет фона, на котором расположен элемент (по крайней мере в современных браузерах). Вы можете получить background-color элемента, но это не одно и то же. Если просто посмотреть на фон контейнера FocusRingScope , то можно не покрыть случаи, когда кто-то разместит div с бэкграундом где-то между обводкой и FocusRingScope .

К счастью, решение существует, хотя оно не является ни красивым, ни эффективным. Единственный доступный на данный момент способ точно определить цвет, который находится за элементом — это вручную пройтись по всем элементам-родителям и проверить их фоновый цвет. Но даже в этом случае вам придется прибегнуть к математике, чтобы скомпоновать цвета вместе, если в них есть какая-либо прозрачность. Эффективность этого подхода зависит от того, насколько сложным и глубоким является DOM-поддерево целевого элемента.

В зависимости от того, насколько сложна структура DOM, в которой находится элемент, это может быть довольно просто сделать, а может потребоваться несколько десятков элементов и относительно медленный запрос DOM для каждого из них.

Хотя это вполне работоспособное решение, в ближайшем будущем может появиться лучший способ. Обсуждается вопрос о включении в CSS значения currentBackgroundColor . В идеале это значение также можно было бы запрашивать и пропустить весь ручной обход предков, который требуется в настоящее время. И еще одна функция, предложенная в проекте CSS Color Level 5 — это функция color-contrast, которая позволит авторам указывать список цветов, а браузеру автоматически выбирать наиболее заметный цвет.

Будущее

Здесь мы рассмотрели только один небольшой (но самый не тривиальный) аспект всего нашего проекта «Клавиатурная навигация». Хотя нашей основной целью было просто интегрировать клавиатурную навигацию в существующее приложение, мы также хотели убедиться, что сможем поддерживать её в будущем и избежать большинства регрессий по мере роста и развития приложения.

В общем, нам пришлось решать еще целый ряд задач, каждая из которых имела свои сложности. Вот лишь несколько примеров: создание новых утилит для автоматического управления фокусом во всем приложении, редизайн старых фич для обеспечения поддержки правильной семантики, а также рефакторинг многих основных UI-компонентов для обеспечения работы с клавиатурой по умолчанию.

Хотя многие ситуации, с которыми мы столкнулись, были сложными, они, как правило, имели решения, которые появлялись естественным образом. Отображение обводки представляло собой уникальную проблему, когда любое простое решение не отвечало нашим требованиям к качеству и эффективности. Мы исчерпали всю функциональность современных браузеров, и в итоге нам пришлось создавать собственное решение с нуля.

Однако в ходе всей этой работы мы узнали много нового о проблемах состояния фокуса и о потенциальных способах, с помощью которых браузеры могли бы облегчить эту задачу в будущем. Мы уже вскользь обсудили некоторые из этих проблем с разработчиками браузеров и надеемся на совместную работу вендоров и авторов спецификаций, чтобы превратить эти идеи в текущий стандарт.

Мы заопенсорсили весь код! Поэтому, если вы хотите узнать, как мы решали, стоящие перед нами проблемы или вы хотите сделать такую же фичу в вашем приложения, то вы можете ознакомиться с кодом на GitHub.

Навигация по клавиатуре является основополагающей частью создания доступного интерфейса, которым может наслаждаться каждый. Мы рады, что наконец-то внедрили в Discord полную, высококачественную реализацию. Это проект, который затрагивает каждую часть приложения и создает совершенно новый способ взаимодействия с ним.

Нам не терпится увидеть, как пользователи смогут воспользоваться этим новым способом навигации, и мы с нетерпением ждем будущих проектов, которые помогут нам сделать Discord более доступным, более инклюзивным и более доступным местом для общения для всех, независимо от их положения или способностей.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *