В рознице «Перекрёстка» работает порядка 30 тыс. сотрудников без закрепленного рабочего места и персонального компьютера. Чтобы они могли активнее участвовать в жизни компании и коммуницировать с коллегами, мы разработали «Перчатку». Это приложение с чатом и корпоративными сервисами: графиком смен и отпусков, информацией о выплатах и другими возможностями вроде ведения блогов и комментирования публикаций коллег.
Инструментарий «Перчатки» также включает чат-бота «Василису», которая помогает новичкам влиться в коллектив: сопровождает в первые дни, находит корпоративные онлайн-курсы. Отличительной особенностью проекта являются элементы геймификации — за активность в «Перчатке» пользователи получают специальные баллы («клеверы»), на которые можно приобретать «сувенирку».
Помимо младших сотрудников торговой сети, «Перчаткой» пользуются технические специалисты и сами разработчики. Им приложение помогает собирать фидбек о функциях, а также краудсорсить новые идеи.
Как мы подошли к проекту
Мы отошли от существующих решений в силу их недостаточной гибкости для наших задач. Так, обычные мессенджеры не подошли нам с точки зрения идентификации личности — в них собеседникам сложно удостовериться, что с ними действительно коммуницирует сотрудник сети, увидеть его должность или реальное имя. Если вы не знаете друг друга лично, установить доверительные отношения и обсуждать рабочие вопросы в таком контексте достаточно сложно.
Если говорить о кастомизации, то на рынке, конечно же, есть неплохие готовые платформы вроде rocket.chat. Но, с нашей точки зрения, они бы потребовали времени на изучение существующей кодовой базы, и — с учетом планов по интеграции корпоративных сервисов в качестве дополнения к чату — «на берегу» нам было сложно рассчитать, сколько ресурсов уйдет на доработку.
Именно поэтому мы начали разработку проекта с нуля. Благодаря этому подходу — получили возможность добавить множество полезных мелочей, улучшающих пользовательский опыт. Например, поддержку хештегов, внутренних и внешних ссылок и упоминания участников беседы. Так, клик по хештегу позволяет посмотреть связанные с ним публикации, а обработка ссылок делает навигацию более плавной и позволяет взаимодействовать с контентом внутри приложения.
Всю коммуникацию «Перчатки» мы построили на связке REST + WebSocket, а в основу разработки положили React Native, поскольку он позволяет проектировать интерфейсы и анимации под кроссплатформенные приложения. В сети можно встретить мнение, что React — медленный инструмент, который плохо подходит для чего-либо кроме SPA, и действительно, можно найти сайты на нем, которые долго грузятся и медленно исполняются. Но в то же время есть огромное количество контрпримеров вроде Instagram. Существует специальный свод правил [тут и тут], следуя которым не составит труда сделать приложения на React Native производительными. Есть и множество других способов оптимизации — например, анимации исключительно в UI-треде с помощью Reanimated.
Особенности реализации
React Native поддерживает библиотеки, автоматизирующие отдельные компоненты пайплайна разработки. В частности, react-native-image-crop-picker упрощает работу с мультимедиа, а react-native-sqlite-storage помог нам с кешированием сообщений в базе данных. Сейчас мы храним сообщения на стороне клиента — так проще загружать историю переписки пользователя.
Обычно для решения этой задачи используют AsyncStorage, который парсит сообщения при запуске приложения. Нам такой вариант не подходил, поскольку он замедляет работу системы, а большинство пользователей из целевой аудитории и так владеет не самыми мощными устройствами. Чтобы снизить нагрузку на «железо», мы использовали компактную SQLite и подготовили спец. запросы — например, вот так выглядит запрос на создание базы:
const db = await SQLite.openDatabase({ name: DATABASE_NAME, location: "default" })
await db.executeSql(`CREATE TABLE IF NOT EXISTS messages (
id VARCHAT(32) PRIMARY KEY NOT NULL,
user TEXT,
keyboard TEXT,
canEdit BOOLEAN,
deleted BOOLEAN,
forwardedMessage TEXT,
type VARCHAT(32),
mentions TEXT,
status TINYINT,
text TEXT,
emotions TEXT,
time BIGINT,
attachments TEXT,
chatId VARCHAT(32)
)`)
А вот так — запрос на получение сообщений:
const db = await this.getInstance()
const dbMessages = await db.executeSql(
"SELECT * FROM messages m WHERE m.chatId = ? AND m.time <= ? ORDER BY time DESC LIMIT 16",
[chatId, date],
)
Если говорить о других технологиях, которые мы задействовали на разных этапах разработки, это Nest.js и MongoDB и Socket.io. На них построен наш чат-бот «Василиса». Из интересных библиотек можно выделить Reanimated и Redux Saga. Первая помогает анимировать UI-элементы, вторая — отвечает за бизнес-логику.
Что пошло не так
Сложности с обработкой скроллинга. За отображение сообщений отвечает компонент FlatList. Он заточен под работу с длинными списками и рендерит только видимые на экране элементы, однако не позволяет задавать начальную позицию скролла, если высота списка заранее неизвестна. Чтобы обойти это ограничение, мы сперва отображаем старые сообщения и только потом сохраняем позицию скролла. Но если на iOS это можно сделать с помощью компонента maintainVisibleContentPosition, то на Android такой функциональности нет.
Изначально мы решили проблему добавлением «невидимого списка», куда по умолчанию попадали все новые сообщения. Мы замеряли его высоту после рендера, использовали это значение для прокрутки основного списка и просто меняли их местами. Такой подход эмулировал загрузку без скролла, но работал медленно. В итоге мы обратились к нативному модулю React, который использует UIManagerModuleListener и запоминает позицию скролла при вызове willDispatchViewUpdates. Теперь на каждую команду onLayoutUpdated список прокручивается до запомненного значения. К слову, на аналогичном подходе построены библиотеки для работы со списками вроде flat-list-mvcp.
Проблемы с реплаями. Эту функциональность реализует метод scrollToIndex, но он не работает, если сообщение, на которое нужно ответить, еще не было отображено (как мы уже говорили, FlatList не рендерит все элементы сразу). Проблему решили просто — если scrollToIndex завершился с ошибкой, вызываем его еще раз, так как к этому моменту система отображает больше сообщений.
handleOnScrollToIndexFailed(info: {index: number; averageItemLength: number; highestMeasuredFrameIndex: number }) {
const { index } = info
this.scroll.current?.scrollToOffset({ offset: info.averageItemLength * info.index, animated: true })
setTimeout(() => {
this.scroll.current?.scrollToIndex({ index, animated: true })
}, 100)
}
Да, нам приходится обрабатывать ошибку каждый раз, когда React Native не может найти элемент в списке, но такой подход хорошо показал себя в боевых условиях.
Перегрузка бэкенда. С этой проблемой мы столкнулись уже в процессе эксплуатации «Перчатки». Сотрудники начали использовать приложение для проведения марафонов. Так мы называем чаты, в которых коллеги получают задания и отписываются об их выполнении. В какой-то момент число участников в них стало исчисляться сотнями, и бэкенд перестал обрабатывать запросы.
Оказалось, что при посещении главного экрана система подтягивала список пользователей для всех чатов. Фикс достаточно прост — мы начали подгружать информацию только при открытии конкретной «разговорной комнаты». Теперь кратковременно видны системные имена пользователей на латинице, прежде чем они отображаются на кириллице, но это не мешает работе в приложении.
Что дальше?
За две недели после запуска в «Перчатке» зарегистрировалась четверть сотрудников «Перекрестка», каждый день в приложение заходит 20% из них, и эти показатели растут. Мы продолжим наблюдать за работой проекта в боевом режиме и будем исправлять возникающие баги, оттачивать функциональность и улучшать внутренние процессы. Уже сейчас мы прорабатываем инструментарий для взаимодействия с аудиторией на конференциях, тестируем опросы для оценки настроений персонала и обдумываем дополнительные возможности.
Комментарии (16)
nukler
14.09.2021 14:46Боже, какой кошмар то. "ведения блогов и комментирования публикаций коллег". "Чтобы они могли активнее участвовать в жизни компании"
Зачем все это??? Зачем активно участвовать в жизни компании? И так понятно что ТОПы будут нести ересь, писать всякую ахинею типа "мы команда, мы должны напрячься, затянуть пояса" и все в таком же духе, а все остальные ниже рангом будут восхвалять, анонимные опросы в корп.чате с вопросом что вас не устраивает в компании?
И вот эта фраза "есть неплохие готовые платформы вроде rocket.chat." наверное должна заканчиваться "но у них всех есть фатальный недостаток".
>>Им приложение помогает собирать фидбек о функциях, а также краудсорсить новые идеи.
Рукалицоджипег, почему идеи а не "фичи"?
X5RetailGroup Автор
15.09.2021 10:55Здравствуйте!
Ответим так) Мы в любом случае большую часть своей жизни проводим на рабочем месте и для многих людей с развитыми социальными функциями (не для всех конечно) важно ощущать себя частью команды, делиться мнениями и общаться с коллегами. В приложении негативом сотрудники тоже очень активно, кстати, делятся. В "Перчатке" можно предложить какое-то нововведение напрямую топ-менеджеру, и если оно эффективное и экономически обоснованное, оно будет реализовано, есть такие кейсы.
В компаниях с развитой корпоративной культурой всегда активные блоги, иногда даже два — анонимный и открытый — а называются они по-разному (Перчатка, Этушка или как-то ещё).
nukler
15.09.2021 11:08Я понял, спасибо. Можете личное мнение написать в "Личку"?
Второй вопрос. Почему от одной компании ПО в маркете разное. Имеется в виду, как Пятерочка/Перекресток/Около и так далее? Просто странно как то. Пилить одно приложение проще, же. Я понимаю набор товаров разный и бонусы разные, ну так как то начать в эту сторону что то делать.
P.S. И да, у жены так и не получилось заказать ни через одно. В приложении "Перекрёсток" бонусная карта не прикрепилась, потому что у неё нет CVV (оператор предложил сходить и купить новую карту, зачем тогда заказывать доставку если я могу сходить и сам?). В Пятерочке нельзя было выбрать место доставки, так как ранее выбирали другое местоположение (в другом городе делали заказ) и оно видимо как то закрепилось за номером (оператор так и не смог помочь и не понимал почему не работает) и так далее.
Ulrih
14.09.2021 19:26Лучше бы вы принудили сотрудников следить за ценниками, каждый день косяки на кассах и как нестрано все в пользу магазина, ниразу не ошиблись в меньшую сторону.
X5RetailGroup Автор
15.09.2021 10:42добрый день!
у нас есть решение электронных ценников, которое исключает возможность человеческого фактора ошибки, но пока что оно пилотируется и работает не во всех магазинах сетей.
Ulrih
15.09.2021 11:02так ценник то правильный, а на кассе другая цена! которая всегда больше того что на прилавке.
X5RetailGroup Автор
15.09.2021 11:27а на кассах самообслуживания или экспресс-сканом (прямо в телефоне) вы пробовали оплачивать? это очень удобно
Ulrih
15.09.2021 12:08пробовал, но там тоже самое - кривая цена, кассиру можно претензию пръедьявить хотябы и заставить пробить по цене с прилавка.
Mox
15.09.2021 16:13А вы использовали react-navigation или react-native-navigation от wix? И что использовали для BottomSheets (если использовали)?
А что вы думаете про перспективы redux-saga? Автор похоже что забросил проект, я думаю про перевод всей асинхронной логики на хуки.
X5RetailGroup Автор
20.09.2021 11:54Мы используем react-navigation - эта самая популярная библиотека + она на 100% закрывает все наши потребности в плане навигации по приложению. Можно было бы поэкспериментировать с react-native-navigation, но при разработки MVP даже не вставал вопрос о выборе библиотеке навигации. Для различных вариацией BottomSheets мы использовали три разных библиотеки react-native-scroll-bottom-sheet, reanimated-bottom-sheet, reanimated-bottom-sheet. У каждой библиотеки имеются свои возможности и ограничения - функционала очень много и не везде удалось использовать одну и ту же библиотеку. Возможно, в будущем, при рефакторинге, нам удасться сократить число библиотек для работы с BottomSheet в проекте.Идея с переносом асинхронной логике в хуки сама по себе интересная. Нам не нравится, что redux-saga обязывает результат запроса сначала положить в глобальное состояние приложения, а только потом как-то реагировать в компоненте/скрине. Иногда (довольно часто) нужно просто async/await функцию вызвать прямо из компонента. Мы использовали в проекте redux-saga по причине того, что этот подход достаточно давно известен и хорошо себя зарекомендовал. Однако, в последнее время, мы присматриваемся к переносу части логики связанной с чатами на MobX, чтобы уменьшить количество бойлерплейта и снизить сложность кода бизнес логики. За лето мы написали на MobX веб версию чатов и подумали, что будет хорошей идеей унифицировать эту часть логики для всех платформ, чтобы не писать ее дважды
Mox
21.09.2021 23:51По поводу boilerplate - я решил это использование redux-toolkit, код получается кратким и выразительным, + там внутри immer.js, который заботиться об иммутабельности данных сам
DMGarikk
20.09.2021 12:20За две недели после запуска в «Перчатке» зарегистрировалась четверть сотрудников «Перекрестка», каждый день в приложение заходит 20% из них, и эти показатели растут.
дайте угадаю (я не сотрудник x5 просто видел как это в других делается конторах)
'мы зарегистрировали в приложении всех сотрудников автоматически'
'заставляем всех туда заходить потому что в томже приложении рассылаем важную инфу, например квиточки по ЗП или объявления которые раньше обязаны прочитать все но теперь в старом месте они недоступны'
радуемся-удивляемся-хлопаем, у нас за неделю 80% персонала начало пользоваться!!! 20% каждый день пользуются!!! ничёсе удача 11!!! какой удобный инструмент!
==
ох…
вообще меня удивляет что сейчас на дворе 21 год, а с первой такой системой я столкнулся в российской дочке забугорной корпорации в 14 году… и вообще по мере работы наблюдал как в разных конторах 'мы разработали инновационный портал для сотрудников с соцсетями'… чот вы долго до этого шли.
Mox
06.11.2021 12:38А еще вопрос - как вы связываете SQLLite и React? Через контекст? Или просто через какой-то сервис читаете нужные данные в state нужного компонента?
Mox
По поводу FlatList - может быть лучше с дизайнерами как-то решать такие ситуации, попросить их придумать элемент списка фиксированной высоты, чтобы работал getItemLayout и можно было делать скролл к нужной позиции?
Просто сложновато выглядит ваше решение.
X5RetailGroup Автор
Если бы можно было сделать элемент в чате фиксированной высоты, то мы бы использовали предложенный вами метод. Однако, к чатам это не применимо как бы дизайнер не старался - все элементы всегда будут иметь разные размеры в зависимости от длины текста. Так же, нужно учитывать, что существуют еще такие уникальные по длине элементы как голосовые сообщения, изображения и вложения.
Mox
Да, в чате никак, согласен (