Flutter. Много
2.78K subscribers
313 photos
23 videos
250 links
Заказать мобильную разработку: https://amiga.agency/?utm_source=tg
Заказать рекламу в канале @amiga_agency_bot

Новости Flutter-разработки, дайджесты мероприятий, личный опыт.
Download Telegram
Как выбрать лучший инструмент для отслеживания крашей?

Hola Amigos! Crashlytics помогает собирать, анализировать и систематизировать отчеты о сбоях приложений. Полезный инструмент для оперативного устранения неполадок. Мы используем Firebase Crashlytics.

Преимущества Firebase Crashlytics

⚙️ Группирует сбои в удобный список, выделяя ключевые проблемы и предоставляя необходимую информацию для быстрого определения и решения основных причин.

⚙️ Предлагает рекомендации по устранению типичных проблем со стабильностью, облегчая процесс отладки и решения проблем.

⚙️ Ошибки приложения отображаются как события в аналитической системе, обеспечивая полный контекст и возможность отслеживания связанных событий.

⚙️ Временные уведомления о новых, критических или возрастающих проблемах обеспечивают быструю реакцию и устранение проблемы в реальном времени.

Как интегрировать Firebase Crashlytics во Flutter-проект описано в документации.

Есть и другие инструменты:

🔵Если вам нужны гибкие возможности конфигурации и поддержка множества языков, рассмотрите Sentry.io.
🔵Для тех, кто ищет расширенные функции отчетности и поддержку множества платформ — Bugsnag.
🔵Если ваш приоритет — взаимодействие с пользователями и сбор обратной связи, то Instabug.

Поделитесь своим опытом использования Crashlytics в чате! Какой инструмент выбрали для себя и почему?
Please open Telegram to view this post
VIEW IN TELEGRAM
Управление зависимостями в Flutter

Hola, Amigos! Сегодня мы поговорим о pubspec.yaml, в котором прописываются зависимости пакетов с помощью символа «^». Данный символ позволяет сделать «гибкую» зависимость, при которой мы охватываем сразу диапазон всех версий пакета, которые гарантированно обратно совместимы с указанной версией.

Преимущества «гибких» зависимостей:
1️⃣ Автоматически используем самую последнюю и лучшую версию пакета, если у нас всего одна зависимость.
2️⃣ Даем Dart'у возможность умно выбирать совместимую версию пакета среди других зависимостей.

Pubspec.lock — почему он важен?
Гарантирует, что каждый разработчик, заходящий в проект, использует те же версии библиотек, что и первый разработчик. Даже с символом «^», мы избегаем разночтений версий между разными устройствами.

Метаданные в pubspec.lock:
– Название пакета: для идентификации.
– Dependency: описывается, является ли сама эта зависимость прямой или транзитивной (зависимость для другой зависимости).
– Description: дополнительная информация о типе зависимости.
– Source: как зависимость была добавлена в проект.
– Version: конкретная версия пакета.

На этом всё! Делитесь в чате своими историями о работе с pubspec.yaml.
Please open Telegram to view this post
VIEW IN TELEGRAM
Интеграция видеоплеера YouTube во Flutter

Hola, Amigos! На связи Вова Зевеке, Flutter dev Amiga. Я долгое время работаю с проектом NL — международной торговой маркой. Больше всего она известна своими протеиновыми коктейлями и снеками.

Одной из моих задач было — интегрировать видеоплеер во Flutter- приложение, с которого можно было бы смотреть видео с YouTube. Казалось бы, нет ничего особенного, просто подключаем пакет, и все готово. Но я столкнулся с рядом проблем:

➡️ долгая загрузка видео,
➡️ некорректная перемотка видео,
➡️ аудио быстрее видео.

Обо всём подробно рассказал в статье. Делитесь в чате, был ли у вас подобный опыт, когда в простой задаче, на первый взгляд, оказалось много подводных камней.
Please open Telegram to view this post
VIEW IN TELEGRAM
Обзор adaptive_dialog

Hola, Amigos! Сегодня расскажем про #пп adaptive_dialog. Инструмент упрощает создание диалогов, предлагая заготовленные пресеты, которые автоматически стилизуются под Android и iOS.

С adaptive_dialog в вашем арсенале 6 удобных диалогов:

1️⃣ showOkAlertDialog (окно с кнопкой "OK");
2️⃣ showOkCancelAlertDialog (окно с кнопками "OK" и "Cancel");
3️⃣ showConfirmationDialog (окно с выбором итема из списка);
4️⃣ showModalActionSheet (окно-выплывашка снизу);
5️⃣ showTextInputDialog (окно с текстовыми полями);
6️⃣ showTextAnswerDialog (окно с текстовым полем формы "вопрос-ответ");

У всех видов диалоговых окон есть группа общих полей:

– title ([String] заголовок диалога);
– style ([AdaptiveStyle] стиль отображения диалога);
– builder ([Widget Function(BuildContext, Widget)] собственно, функция-билдер виджета);
– onWillPop ([Future<bool> Function()] функция, действующая при закрытии диалога);
– message ([String] текст диалога).

Также есть уникальные поля, расширяющие кастомизацию их функционала.

Переходите в чат и рассказывайте, пользовались ли вы пакетом adaptive_dialog и в каких проектах?
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Записки мобильного разработчика

Hola, Amigos! На связи Вова Зевеке, Flutter dev Amiga. Сегодня расскажем, как можно сделать запуск приложения с анимированным логотипом.

В основном файле main.dart устанавливаем SplashPage в качестве домашней страницы, чтобы приложение стартовало с анимированного логотипа.

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
),
home: const SplashPage(),
);
}
}


Далее создаем SplashPage с использованием TickerProviderStateMixin для управления анимациями. Инициализируем анимации в методе initState().

Future<void> playAnimation() async {
await Future.delayed(const Duration(milliseconds: 1000));
for (int i = 0; i < controllerColorOpacityList.length; i++) {
await controllerColorOpacityList[i].animateTo(1);
}
if (mounted) {
Navigator.of(context).pushReplacement(_createRoute());
}
}


И, наконец, реализуем анимированный переход между SplashPage и MainBottomNavigationBar с использованием PageRouteBuilder.

Route _createRoute() {
return PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 700),
pageBuilder: (context, animation, secondaryAnimation) => const MainBottomNavigationBar(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const Offset begin = Offset(0.0, 1.0);
const Offset end = Offset.zero;

final Tween<Offset> tween = Tween(begin: begin, end: end);
final Animation<double> curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.linear,
);

return FadeTransition(
opacity: curvedAnimation,
child: child,
);
},
);
}


На этом всё!

Ссылка github на код.

Делитесь в чате своими историями с анимациями, будет интересно почитать!
This media is not supported in your browser
VIEW IN TELEGRAM
AppLifecycle

Hola, Amigos! Сегодня с вами я, Саша Чаплыгин, Flutter dev Amiga. Предлагаю обсудить одну интересную тему — отслеживание состояния приложения: в фоне, закрыто или не активно.

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

Читайте на Habr и делитесь своим опытом работы с жизненным циклом приложения в чате ⚙️
Please open Telegram to view this post
VIEW IN TELEGRAM
Полезные посты и статьи января, которые вы могли пропустить

Hola, Amigos! Вот и дайджест января. Уже целый месяц нового года прошел!

🔵Видео-лекции с DevFest 2023
🔵Flutter 3.16 (часть 1)
🔵Flutter 3.16 (часть 2)
🔵Анимация и Flutter
🔵Интеграция видеоплеера во Flutter
🔵Жизненный цикл приложения

А еще напоминаем, что обзоры полезных пакетов вы можете найти по тэгу #пп.

Выбирайте, что вам интересно и переходите по ссылкам.

Рассказывайте в чате, как начался ваш 2024-ый? И какими проектами вы сейчас увлечены?
Please open Telegram to view this post
VIEW IN TELEGRAM
Советы от Team Lead для подготовки к первому собеседованию

Hola, Amigos! На связи Сережа Климович, Mobile Team Lead Amiga. Сегодня вернемся к теме собеседований, хочу вам дать несколько советов от себя и напомнить о рекомендациях нашей Group Lead HR, Кати. Берите на вооружение!

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

Оценивайте свои знания и умения трезво, не стыдно чего-то не знать. А некоторые компании, и мы в том числе, готовы обучать и «выращивать» сотрудников.

Что должен знать даже стажер?

🟡Минимально в Dart: типы данных и переменные, функции, классы, Control flow statement, примитивные структуры, литералы, print, понимание null safety.
🟡В Flutter: runApp, MaterialApp, Scaffold, Align, Container, Flex, Expanded, Spacer, ListView, Text, простая навигация между двумя экранами, разница между Stateless и Stateful виджетами.
🟡Уметь устанавливать и настраивать IDE, dart и flutter для разработки.
🟡Понимать суть работы удаленных репозиториев Git (GitHub / GitLab).

Junior должен знать всё, о чем я написал выше. Плюс:

⭕️Иметь практический опыт основ объектно-ориентированного и асинхронного программирования.
⭕️Понимать механизм управления локальным состоянием. Знание виджетов для верстки экранов, способов взаимодействия с пользователем (кнопки, жесты и т.п.).
⭕️Базовое управление проектами. Обязательно: Pub и pub.dev, pubspec.yaml.
⭕️Уметь взаимодействовать с git через IDE.
⭕️Понимать HTTP запросы, например get/post.
⭕️Знать форматы запросов и ответов, например JSON и пакет json_serializable.

С таким набором скиллов, я думаю, у вас точно есть все шансы получить свою первую работу! И не забывайте, что личные качества тоже имеют большое значение. Будьте честными и открытыми. Успехов!

Если вам интересна эта тема и есть вопросы, то пишите в чат. Пообщаемся 🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Анимация и Flutter

Hola, Amigos! Меня зовут Сергей Климович, Mobile Team Lead агентства заказной разработки Amiga. В мире мобильной разработки Flutter выделяется своей гибкостью и простотой в создании красивых пользовательских интерфейсов.

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

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

В новой статье рассказываю, как использовать шейдеры в приложениях Flutter, открыв новые горизонты для креативной реализации дизайнерских идей ⚙️

Всем хорошего кода! Делитесь в чате своим опытом работы с анимацией в Flutter.
Please open Telegram to view this post
VIEW IN TELEGRAM
Home Widget для Android

Hola, Amigos! На связи Вова Зевеке, Flutter dev Amiga. Сегодня расскажем, как сделать виджет HomeScreen с помощью пакета home_widget.

Для приложения на Android создадим макет виджета внутри android/app/src/main/res/layout. Нажимаем правой кнопкой мыши на папку android, выбираем Flutter/Open Android module in Android Studio. В новом окне нажимаем правой кнопкой мыши на app, выбираем New/Widget.

Сгенерируется несколько файлов для редактирования. Один из них — файл конфигурации виджета (расположен в android/app/src/main/res/xml). Ещё один файл – сам макет виджета (расположен в android/app/src/main/res/layout).

Обратите особое внимание в коде ниже на строчку android:id="@+id/firstString". Тут мы выдали тексту id-шник, который нам понадобится позже.

</RelativeLayout
<TextView
android:id="@+id/firstString"
… />
</RelativeLayout>


Также в AndroidManifest.xml у нас появился ресивер.

<receiver
android:name=".HomeWidgetTextTable"…
</receiver>


Нам осталось настроить передачу данных между нашим Flutter-приложением и HomeWidget. Для этого в файле android\app\src\main\java\com\example\sandbox\HomeWidgetTextTable.kt импортируем плагин и пишем код обновления HomeWidget.

class HomeWidgetTextTable : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
}

internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val widgetData = HomeWidgetPlugin.getData(context)
val firstString = widgetData.getString("firstString", null)
val views = RemoteViews(context.packageName, R.layout.home_widget_text_table)
views.setTextViewText(R.id.firstString, firstString)

appWidgetManager.updateAppWidget(appWidgetId, views)
}


Теперь создаем TextField и набираем текст, отображаемый в HomeWidget.

  @override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
HomeWidget.initiallyLaunchedFromHomeWidget().then((value) async {
updateUI();
});
});
super.initState();
}

void updateUI() {
var text = controller.text;
HomeWidget.saveWidgetData<String>('firstString', text);
HomeWidget.updateWidget(
//name: 'TextTable',
androidName: 'HomeWidgetTextTable',
iOSName: 'HomeWidgetTextTable',
);
}

@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: controller,
style: const TextStyle(
color: Colors.white,
),
onChanged: (value) {
updateUI();
},
),
const SizedBox(height: 16),
],
);
}
}


Как мы видим, созданный нами метод updateUI отвечает за передачу новых данных HomeWidget.

Ставьте реакцию, если было полезно!
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Записки мобильного разработчика

Hola, Amigos! На связи Вова Зевеке, Flutter dev Amiga. Сегодня я поведаю о своём опыте интеграции штрих-кодов во Flutter-приложение.

Задача: сделать страницу с вертикальным штрих-кодом на весь экран. Слева от него надписи «номер карты» и сам номер карты.

Дополнительным усложнением был контроль яркости устройства. Если он меньше 80% на этой странице, то нужно повысить его искусственно.

Сначала создаю штрих-код с помощью пакета barcode_widget.

BarcodeWidget(
barcode: Barcode.ean13(drawEndChar: true),
data: data,
drawText: false,
errorBuilder: (context, error) => Center(
child: Text(error),
),
)


Теперь разворачиваю этот виджет на 90 градусов, чтоб он принял вертикальное положение на весь экран. К сожалению, настройки ширины ограничены и пришлось всё делать через RotatedBox.

RotatedBox(
quarterTurns: 1,
child: Column(
children: [
Expanded(
flex: 8,
child: Padding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
child: BarcodeWidget(
<...>
),
),
),
],
),
)


Штрих-код готов, осталось добавить записи и тоже вертикально.

RotatedBox(
quarterTurns: 1,
child: Column(
children: [
Expanded(
flex: 8,
child: Padding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
child: BarcodeWidget(
<...>
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.current.barCodePageCardNumber,
style: textStyle,
),
Text(
widget.cardCode,
style: textStyle,
),
],
),
),
)
],
),
)


Перепробовал множество способов вёрстки, и это оптимальное решение, которое мне удалось найти. Если у вас был более удачный опыт, пишите в чате!

Осталось сделать регулирование яркости устройства. Я использовал пакет screen_brightness. Для работы с ним необходимо разрешение для Android: <uses-permission android:name="android.permission.WAKE_LOCK" />. Пакет позволяет менять яркость устройства через класс ScreenBrightness.

@override
void initState() {
initBrightness();
super.initState();
}

Future<void> initBrightness() async {
final isRealDevice = await SafeDevice.isRealDevice;
if (isRealDevice) {
brightness = await ScreenBrightness().current;
if (brightness < 0.8) {
await ScreenBrightness().setScreenBrightness(0.8);
}
}
}


Однако, яркость нужно вернуть обратно, если пользователь покинет страницу. Для этого нам и пригодится сохранённое значение яркости.

Material(
child: PopScope(
onPopInvoked: (value) async {
await ScreenBrightness().setScreenBrightness(brightness);
},
child: Scaffold(
appBar: AppBar(
backgroundColor: const Color(0xFF1A1A18),
leading: InkWell(
onTap: () async {
await await ScreenBrightness().setScreenBrightness(brightness);
if (mounted) {
Navigator.of(context).pop();
}
},
<…>


Был ли у вас подобные задачи? ➡️ Чат.
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Записки мобильного разработчика

Hola, Amigos! На связи Саша Чаплыгин, Flutter-dev Amiga. Мы уже касались темы Sliver'ов, а сегодня я предлагаю погрузиться в практику.

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

Будет много кода, поэтому переходите на Habr 🙂

Делитесь в чате своим опытом с Slivers!
Please open Telegram to view this post
VIEW IN TELEGRAM
Настройка локализации

Hola, Amigos! На связи Вова, Flutter dev Amiga. Сегодня расскажем, как можно настроить локализацию в приложении.

Сначала поставим необходимые пакеты в pubspec.yaml:

flutter_localizations:
sdk: flutter
intl: ^0.18.1
intl_utils: ^2.8.7


Flutter localizations – пакет локализации, использующий остальные пакеты ниже.
Intl – пакет для перевода текстов приложения, предоставляет возможности интернационализации и локализации.
Intl utils – вспомогательный пакет для Intl, он генерирует шаблонный код для библиотеки Intl и добавляет автозаполнение ключей в коде Dart.

Далее необходимо у настроек Flutter разрешить генерирование локализованных строк в arb-файлах (generate: true):

flutter:
uses-material-design: true
generate: true


Последнее в pubspec.yaml – настроить intl:

flutter_intl:
enabled: true
main_locale: ru


Теперь создаём папку l10n, в ней .arb-файлы для языков, на которые будет переведено приложение. В нашем случае – это русский и английский.

В intl_ru.arb:

{
"@@locale": "ru",
"title": "Енот"
}


В intl_en.arb:

{
"@@locale": "en",
"title": "Racoon"
}


Подключаем локализацию к приложению в main.dart:

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
///тут подключаем локализацию
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
DefaultCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
///
home: const MainScreen(),
);
}


Для генерирования кода локализации выполняем команду flutter pub run intl_utils:generate. Или упростите интеграцию .arb-переводов в Flutter с помощью плагина для VS Code.

Возникает вопрос, как использовать локализованные тексты? Тут нам на помощь приходит константа S:

Text(
S.current.racoon,
),


Расскажите в чате, в каком проекте вы использовали данный пакет?
Обзор Scaffold

Hola, Amigos! Сегодня расскажем, как виджет Scaffold может сделать нашу жизнь проще.

Что такое Scaffold?

Scaffold предоставляет базовую структуру для построения пользовательского интерфейса согласно дизайн-системе Material Design.

Элементы Scaffold

appBar: Панель приложения наверху экрана.
body: Основной контент вашего приложения.
bottomNavigationBar: Нижняя панель навигации.

Как настроить?

Просто добавьте их в соответствующие поля виджета.

Виджеты Drawer и endDrawer

drawer: Панель сбоку, которая появляется при свайпе или нажатии на значок гамбургера.
endDrawer: То же самое, но для правой стороны.

Настройки:

– drawerDragStartBehavior: Как начать перетаскивание.
– drawerScrimColor: Цвет фона, когда drawer открыт.
– drawerEdgeDragWidth: Ширина зоны для открытия drawer свайпом.
o– nDrawerChanged, onEndDrawerChanged: Коллбэки при открытии/закрытии.

Инструменты для кнопок

– persistentFooterButtons: Кнопки внизу.
– floatingActionButton: Плавающая кнопка в правом нижнем углу.

Еще несколько полезных штук:

– bottomSheet: Дополнительная информация внизу.
– backgroundColor: Цвет заднего фона.
– resizeToAvoidBottomInset: Подгон размеров при открытой клавиатуре.
– extendBody, extendBodyBehindAppBar: Управление размерами тела.
– primary: Отображается ли Scaffold в верхней части экрана.

Практика

Попробуйте добавить свои AppBar, создать навигацию с кнопками, исследовать возможности расположения виджетов. Экспериментируйте и делитесь своим опытом в чате!
Дайджест февраля

Hola, Amigos! Собрали для вас в одну подборку все полезные посты и статьи февраля, которые вы могли пропустить. Выбирайте, что вам интересно, и переходите по ссылкам.

⚙️ Советы от Team Lead для подготовки к первому собеседованию

⚙️ Анимация и Flutter

⚙️ Home Widget для Android

⚙️ Splash screen

⚙️ Кастомные иконки запуска приложения

⚙️ «Записки мобильного разработчика» про штрих-код

⚙️ Настройки локализации

⚙️ «Записки мобильного разработчика» о Slivers

Напоминаем, что обзоры полезных пакетов вы можете найти по тэгу #пп.

Всем хорошего кода! 🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
Обновления Flutter 3.19

Hola, Amigos! На связи Саша Чаплыгин, Flutter-dev агентства продуктовой разработки Amiga. Мы с командой подготовили для вас перевод статьи о новинках Flutter в версии 3.19.

Этот выпуск ориентирован на улучшение UX. Представлено множество функций, которые удовлетворяют разнообразные потребности создателей приложений.

В статье вас ждет:

🟡 Интеграция AI с Gemini API
🟡 Модернизация движка
🟡 Оптимизация производительности
🟡 Обновления DevTools
🟡 Разработка Desktop
🟡 Экосистемный прогресс
🟡 Устаревшие версии и критические изменения

Переходите на Habr и знакомьтесь с новыми возможностями Flutter!
Please open Telegram to view this post
VIEW IN TELEGRAM
Hola, Amigos! В прошлом году мы мощнейне ворвались в APW’23.

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

Взгляните на фото выше, помните наши яркие футболки?🙂

APW больше не будет...

Теперь все идём на BOOST! Меняется не только название конференции, но и формат.

5 потоков вместо трёх:

- управление агентством;
- управление разработкой;
- маркетинг и продажи;
- диджитал-маркетинг;
- дизайн и креатив.

150 спикеров из крупнейших диджитал-агентств и 1000+ участников. От нашей команды также будет несколько спикеров в разные потоки, но об этом расскажем чуть позже
🅰️

Две вечеринки, нетворкинг, рейтинги Partners' Club и AGIMA.Outsource, результаты ротации 2024, концерт и супермерч.

Встречаемся 29–30 августа 2024 в «Красном Октябре», Москва.

До 1 апреля действует специальная цена на билет!

Переходите по ссылке и занимайте места ➡️ http://boostconf.ru!
Please open Telegram to view this post
VIEW IN TELEGRAM