[Разработка веб-сайтов, Программирование, Haskell, Функциональное программирование] Создаем веб-приложение на Haskell с использованием Reflex. Часть 3
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Часть 1.
Часть 2.
Всем привет! В этой части мы рассмотрим использование класса EventWriter и библиотеки ghcjs-dom.
Использование EventWriter
Сейчас, для того, чтобы прокинуть события с более глубоких уровней, мы передаем их в качестве возвращемых значений. Это не всегда удобно, особенно, когда надо возвращать что-то, помимо события (например, форма ввода может возвращать одновременно и событие нажатия кнопки, и данные из формы). Гораздо удобнее было бы использовать механизм, который может "прокинуть" события наверх автоматически, не задумываясь о том, что надо их постоянно возвращать. И такой механизм есть — EventWriter. Этот класс позволяет записывать события, наподобие стандартной монады Writer. Перепишем наше приложение с использованием EventWriter.
Для начала рассмотрим сам класс EventWriter:
class (Monad m, Semigroup w) => EventWriter t w m | m -> t w where
tellEvent :: Event t w -> m ()
Тип w как раз и есть тип нашего события, и этот тип является экземпляром класса Semigroup, т.е. значения этого типа можно комбинировать друг с другом. В том случае, если два разных события записываются с помощью tellEvent, и они в какой-то один и тот же момент срабатывают, то они должны быть как-то объединены в одно событие того же типа, чтобы результатом выполнения монады было одно событие.
Существует трансформер, являющийся экземпляром этого класса — EventWriterT, для его запуска используется функция runEventWriterT.
Далее переходим к изменению функций. Наибольшие изменения ожидают функцию rootWidget.
rootWidget :: MonadWidget t m => m ()
rootWidget =
divClass "container" $ mdo
elClass "h2" "text-center mt-3" $ text "Todos"
(_, ev) <- runEventWriterT $ do
todosDyn <- foldDyn appEndo mempty ev
newTodoForm
delimiter
todoListWidget todosDyn
blank
Мы добавили запуск трансформера и избавились от всех возвращаемых событий.
Изменения в newTodoForm не такие большие, но все же, стоит их отметить:
newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()
newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo
iEl <- inputElement $ def
& initialAttributes .~
( "type" =: "text"
<> "class" =: "form-control"
<> "placeholder" =: "Todo" )
& inputElementConfig_setValue .~ ("" <$ btnEv)
let
addNewTodo = \todo -> Endo $ \todos ->
insert (nextKey todos) (newTodo todo) todos
newTodoDyn = addNewTodo <$> value iEl
btnAttr = "class" =: "btn btn-outline-secondary"
<> "type" =: "button"
(btnEl, _) <- divClass "input-group-append" $
elAttr' "button" btnAttr $ text "Add new entry"
let btnEv = domEvent Click btnEl
tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl
Мы видим, что обновился тип функции, она теперь ничего не возвращает, и добавлено необходимое ограничение EventWriter. В теле функции, соответственно, мы избавились от возвращаемого значения и теперь используем функцию tellEvent.
Функция todoListWidget сильно упростилась.
todoListWidget
:: (EventWriter t (Endo Todos) m, MonadWidget t m)
=> Dynamic t Todos -> m ()
todoListWidget todosDyn = rowWrapper $
void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget
Нас теперь вообще не интересует возвращаемое событие, и, соответственно, отпала необходимость в извлечении Event из Dynamic.
В функции todoWidget также произошли заметные изменения. Больше нет необходимости работать с возвращаемым типом — преобразовывать Event t (Event t TodoEvent). Отличие функции dyn_ от функции dyn, в том, что она игнорирует возвращаемое значение.
todoWidget
:: (EventWriter t (Endo Todos) m, MonadWidget t m)
=> Int -> Dynamic t Todo -> m ()
todoWidget ix todoDyn' = do
todoDyn <- holdUniqDyn todoDyn'
dyn_ $ ffor todoDyn $ \td@Todo{..} -> case todoState of
TodoDone -> todoDone ix todoText
TodoActive False -> todoActive ix todoText
TodoActive True -> todoEditable ix todoText
Единственное изменение в функциях todoDone, todoActive и todoEditable это новый тип и запись события вместо его возврата.
todoActive
:: (EventWriter t (Endo Todos) m, MonadWidget t m)
=> Int -> Text -> m ()
todoActive ix todoText = divClass "d-flex border-bottom" $ do
divClass "p-2 flex-grow-1 my-auto" $
text todoText
divClass "p-2 btn-group" $ do
(doneEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Done"
(editEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Edit"
(delEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Drop"
tellEvent $ Endo <$> leftmost
[ update (Just . toggleTodo) ix <$ domEvent Click doneEl
, update (Just . startEdit) ix <$ domEvent Click editEl
, delete ix <$ domEvent Click delEl
]
todoDone
:: (EventWriter t (Endo Todos) m, MonadWidget t m)
=> Int -> Text -> m ()
todoDone ix todoText = divClass "d-flex border-bottom" $ do
divClass "p-2 flex-grow-1 my-auto" $
el "del" $ text todoText
divClass "p-2 btn-group" $ do
(doneEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Undo"
(delEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Drop"
tellEvent $ Endo <$> leftmost
[ update (Just . toggleTodo) ix <$ domEvent Click doneEl
, delete ix <$ domEvent Click delEl
]
todoEditable
:: (EventWriter t (Endo Todos) m, MonadWidget t m)
=> Int -> Text -> m ()
todoEditable ix todoText = divClass "d-flex border-bottom" $ do
updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $
editTodoForm todoText
divClass "p-2 btn-group" $ do
(doneEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Finish edit"
let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix
tellEvent $
tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)
Применение класса EventWriter упростило код и сделало его более читаемым.
ghcjs-dom
reflex позволяет нам только модифицировать DOM, но зачастую от JS-приложений требуется больше. Например, если требуется копировать текст по нажатию на кнопку, то reflex не предоставляет нужных нам для этого средств. На помощь приходит библиотека ghcjs-dom. По сути, это реализация JS API на Haskell. В ней можно найти все те же самые типы и функции, которые есть в JS.
На чистом JS, без использования сторонних библиотек, функция копирования текста может выглядеть следующим образом:
function toClipboard(txt){
var inpEl = document.createElement("textarea");
document.body.appendChild(inpEl);
inpEl.value = txt
inpEl.focus();
inpEl.select();
document.execCommand('copy');
document.body.removeChild(inpEl);
}
В привычном использовании мы вешаем этот обработчик, например, на кнопку.
Как это будет выглядеть на Haskell? В первую очередь, создадим новый модуль GHCJS для работы с ghcjs и определим соответствующую функцию.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MonoLocalBinds #-}
module GHCJS where
import Control.Monad
import Data.Functor (($>))
import Data.Text (Text)
import GHCJS.DOM
import GHCJS.DOM.Document
(createElement, execCommand, getBodyUnchecked)
import GHCJS.DOM.Element as Element hiding (scroll)
import GHCJS.DOM.HTMLElement as HE (focus)
import GHCJS.DOM.HTMLInputElement as HIE (select, setValue)
import GHCJS.DOM.Node (appendChild, removeChild)
import GHCJS.DOM.Types hiding (Event, Text)
import Reflex.Dom as R
toClipboard :: MonadJSM m => Text -> m ()
toClipboard txt = do
doc <- currentDocumentUnchecked
body <- getBodyUnchecked doc
inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc
("textarea" :: Text)
void $ appendChild body inpEl
HE.focus inpEl
HIE.setValue inpEl txt
HIE.select inpEl
void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text)
void $ removeChild body inpEl
Почти каждой строке из haskell функции toClipboard есть соответствие из JS функции. Стоит отметить, что здесь нет привычного класса MonadWidget, а используется MonadJSM — это та монада, в которой производятся вся работы с помощью ghcjs-dom. Класс MonadWidget наследует класс MonadJSM. Рассмотрим, как осуществляется привязка обработчика к событию:
copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()
copyByEvent txt ev =
void $ performEvent $ ev $> toClipboard txt
Здесь мы видим новую для нас функцию performEvent, и с помощью нее осуществляется привязка обработчика к событию. Она является методом класса PerformEvent:
class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where
type Performable m :: * -> *
performEvent :: Event t (Performable m a) -> m (Event t a)
performEvent_ :: Event t (Performable m ()) -> m ()
Теперь изменим виджет невыполненного задания, предварительно не забыв добавить импорт import GHCJS:
todoActive
:: (EventWriter t TodoEvent m, MonadWidget t m) => Int -> Todo -> m ()
todoActive ix Todo{..} =
divClass "d-flex border-bottom" $ do
divClass "p-2 flex-grow-1 my-auto" $
text todoText
divClass "p-2 btn-group" $ do
(copyEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Copy"
(doneEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Done"
(editEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Edit"
(delEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Drop"
copyByEvent todoText $ domEvent Click copyEl
tellEvent $ leftmost
[ ToggleTodo ix <$ domEvent Click doneEl
, StartEditTodo ix <$ domEvent Click editEl
, DeleteTodo ix <$ domEvent Click delEl
]
Была добавлена новая кнопка Copy и вызов определенной функции copyByEvent. Эти же самые действия можно проделать с виджетами для других состояний задания.
Полученный результат, как всегда, можно посмотреть в нашем репозитории.
В следующей части рассмотрим использование JSFFI (JS Foreign Function Interface).
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Программирование] Функция property() в Python (перевод)
- [Программирование, Java] Отправка электронных писем с помощью Spring (перевод)
- [Тестирование игр] 7 методов тестирования игр (перевод)
- [Программирование, Разработка робототехники, Бизнес-модели, Робототехника] Кружок робототехники VS технический центр
- [Программирование, Совершенный код, Управление разработкой] Почему в мире так много отстойного ПО (перевод)
- [Oracle, Программирование, SQL, DevOps] Настройка CI/CD скриптов миграции БД с нуля с использованием GitLab и Liquibase
- [Разработка веб-сайтов, JavaScript, TypeScript] TypeScript: Раскладываем tsconfig по полочкам. Часть 2
- [Программирование микроконтроллеров, Производство и разработка электроники, Компьютерное железо] Новые цены на микроконтроллеры STM32 из свежих поставок
- [Perl, Ruby, Python, Программирование, Node.JS] Сервер в одну строку на 17 языках (перевод)
- [Программирование, Учебный процесс в IT, Карьера в IT-индустрии] Личный опыт: подготовка к магистратуре JetBrains в Университете ИТМО и первые впечатления
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_programmirovanie (Программирование), #_haskell, #_funktsionalnoe_programmirovanie (Функциональное программирование), #_haskell, #_frp, #_reflex, #_reflexplatform, #_reflexfrp, #_vebrazrabotka (веб-разработка), #_funktsionalnoe (функциональное), #_programmirovanie (программирование), #_fp (фп), #_frp (фрп), #_fp, #_blog_kompanii_typeable (
Блог компании Typeable
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_programmirovanie (
Программирование
), #_haskell, #_funktsionalnoe_programmirovanie (
Функциональное программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:20
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Часть 1. Часть 2. Всем привет! В этой части мы рассмотрим использование класса EventWriter и библиотеки ghcjs-dom. Использование EventWriter Сейчас, для того, чтобы прокинуть события с более глубоких уровней, мы передаем их в качестве возвращемых значений. Это не всегда удобно, особенно, когда надо возвращать что-то, помимо события (например, форма ввода может возвращать одновременно и событие нажатия кнопки, и данные из формы). Гораздо удобнее было бы использовать механизм, который может "прокинуть" события наверх автоматически, не задумываясь о том, что надо их постоянно возвращать. И такой механизм есть — EventWriter. Этот класс позволяет записывать события, наподобие стандартной монады Writer. Перепишем наше приложение с использованием EventWriter. Для начала рассмотрим сам класс EventWriter: class (Monad m, Semigroup w) => EventWriter t w m | m -> t w where
tellEvent :: Event t w -> m () Тип w как раз и есть тип нашего события, и этот тип является экземпляром класса Semigroup, т.е. значения этого типа можно комбинировать друг с другом. В том случае, если два разных события записываются с помощью tellEvent, и они в какой-то один и тот же момент срабатывают, то они должны быть как-то объединены в одно событие того же типа, чтобы результатом выполнения монады было одно событие. Существует трансформер, являющийся экземпляром этого класса — EventWriterT, для его запуска используется функция runEventWriterT. Далее переходим к изменению функций. Наибольшие изменения ожидают функцию rootWidget. rootWidget :: MonadWidget t m => m ()
rootWidget = divClass "container" $ mdo elClass "h2" "text-center mt-3" $ text "Todos" (_, ev) <- runEventWriterT $ do todosDyn <- foldDyn appEndo mempty ev newTodoForm delimiter todoListWidget todosDyn blank Мы добавили запуск трансформера и избавились от всех возвращаемых событий. Изменения в newTodoForm не такие большие, но все же, стоит их отметить: newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()
newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo iEl <- inputElement $ def & initialAttributes .~ ( "type" =: "text" <> "class" =: "form-control" <> "placeholder" =: "Todo" ) & inputElementConfig_setValue .~ ("" <$ btnEv) let addNewTodo = \todo -> Endo $ \todos -> insert (nextKey todos) (newTodo todo) todos newTodoDyn = addNewTodo <$> value iEl btnAttr = "class" =: "btn btn-outline-secondary" <> "type" =: "button" (btnEl, _) <- divClass "input-group-append" $ elAttr' "button" btnAttr $ text "Add new entry" let btnEv = domEvent Click btnEl tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl Мы видим, что обновился тип функции, она теперь ничего не возвращает, и добавлено необходимое ограничение EventWriter. В теле функции, соответственно, мы избавились от возвращаемого значения и теперь используем функцию tellEvent. Функция todoListWidget сильно упростилась. todoListWidget
:: (EventWriter t (Endo Todos) m, MonadWidget t m) => Dynamic t Todos -> m () todoListWidget todosDyn = rowWrapper $ void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget Нас теперь вообще не интересует возвращаемое событие, и, соответственно, отпала необходимость в извлечении Event из Dynamic. В функции todoWidget также произошли заметные изменения. Больше нет необходимости работать с возвращаемым типом — преобразовывать Event t (Event t TodoEvent). Отличие функции dyn_ от функции dyn, в том, что она игнорирует возвращаемое значение. todoWidget
:: (EventWriter t (Endo Todos) m, MonadWidget t m) => Int -> Dynamic t Todo -> m () todoWidget ix todoDyn' = do todoDyn <- holdUniqDyn todoDyn' dyn_ $ ffor todoDyn $ \td@Todo{..} -> case todoState of TodoDone -> todoDone ix todoText TodoActive False -> todoActive ix todoText TodoActive True -> todoEditable ix todoText Единственное изменение в функциях todoDone, todoActive и todoEditable это новый тип и запись события вместо его возврата. todoActive
:: (EventWriter t (Endo Todos) m, MonadWidget t m) => Int -> Text -> m () todoActive ix todoText = divClass "d-flex border-bottom" $ do divClass "p-2 flex-grow-1 my-auto" $ text todoText divClass "p-2 btn-group" $ do (doneEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Done" (editEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Edit" (delEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Drop" tellEvent $ Endo <$> leftmost [ update (Just . toggleTodo) ix <$ domEvent Click doneEl , update (Just . startEdit) ix <$ domEvent Click editEl , delete ix <$ domEvent Click delEl ] todoDone :: (EventWriter t (Endo Todos) m, MonadWidget t m) => Int -> Text -> m () todoDone ix todoText = divClass "d-flex border-bottom" $ do divClass "p-2 flex-grow-1 my-auto" $ el "del" $ text todoText divClass "p-2 btn-group" $ do (doneEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Undo" (delEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Drop" tellEvent $ Endo <$> leftmost [ update (Just . toggleTodo) ix <$ domEvent Click doneEl , delete ix <$ domEvent Click delEl ] todoEditable :: (EventWriter t (Endo Todos) m, MonadWidget t m) => Int -> Text -> m () todoEditable ix todoText = divClass "d-flex border-bottom" $ do updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $ editTodoForm todoText divClass "p-2 btn-group" $ do (doneEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Finish edit" let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix tellEvent $ tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl) Применение класса EventWriter упростило код и сделало его более читаемым. ghcjs-dom reflex позволяет нам только модифицировать DOM, но зачастую от JS-приложений требуется больше. Например, если требуется копировать текст по нажатию на кнопку, то reflex не предоставляет нужных нам для этого средств. На помощь приходит библиотека ghcjs-dom. По сути, это реализация JS API на Haskell. В ней можно найти все те же самые типы и функции, которые есть в JS. На чистом JS, без использования сторонних библиотек, функция копирования текста может выглядеть следующим образом: function toClipboard(txt){
var inpEl = document.createElement("textarea"); document.body.appendChild(inpEl); inpEl.value = txt inpEl.focus(); inpEl.select(); document.execCommand('copy'); document.body.removeChild(inpEl); } В привычном использовании мы вешаем этот обработчик, например, на кнопку. Как это будет выглядеть на Haskell? В первую очередь, создадим новый модуль GHCJS для работы с ghcjs и определим соответствующую функцию. {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MonoLocalBinds #-} module GHCJS where import Control.Monad import Data.Functor (($>)) import Data.Text (Text) import GHCJS.DOM import GHCJS.DOM.Document (createElement, execCommand, getBodyUnchecked) import GHCJS.DOM.Element as Element hiding (scroll) import GHCJS.DOM.HTMLElement as HE (focus) import GHCJS.DOM.HTMLInputElement as HIE (select, setValue) import GHCJS.DOM.Node (appendChild, removeChild) import GHCJS.DOM.Types hiding (Event, Text) import Reflex.Dom as R toClipboard :: MonadJSM m => Text -> m () toClipboard txt = do doc <- currentDocumentUnchecked body <- getBodyUnchecked doc inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc ("textarea" :: Text) void $ appendChild body inpEl HE.focus inpEl HIE.setValue inpEl txt HIE.select inpEl void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text) void $ removeChild body inpEl Почти каждой строке из haskell функции toClipboard есть соответствие из JS функции. Стоит отметить, что здесь нет привычного класса MonadWidget, а используется MonadJSM — это та монада, в которой производятся вся работы с помощью ghcjs-dom. Класс MonadWidget наследует класс MonadJSM. Рассмотрим, как осуществляется привязка обработчика к событию: copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()
copyByEvent txt ev = void $ performEvent $ ev $> toClipboard txt Здесь мы видим новую для нас функцию performEvent, и с помощью нее осуществляется привязка обработчика к событию. Она является методом класса PerformEvent: class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where
type Performable m :: * -> * performEvent :: Event t (Performable m a) -> m (Event t a) performEvent_ :: Event t (Performable m ()) -> m () Теперь изменим виджет невыполненного задания, предварительно не забыв добавить импорт import GHCJS: todoActive
:: (EventWriter t TodoEvent m, MonadWidget t m) => Int -> Todo -> m () todoActive ix Todo{..} = divClass "d-flex border-bottom" $ do divClass "p-2 flex-grow-1 my-auto" $ text todoText divClass "p-2 btn-group" $ do (copyEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Copy" (doneEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Done" (editEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Edit" (delEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Drop" copyByEvent todoText $ domEvent Click copyEl tellEvent $ leftmost [ ToggleTodo ix <$ domEvent Click doneEl , StartEditTodo ix <$ domEvent Click editEl , DeleteTodo ix <$ domEvent Click delEl ] Была добавлена новая кнопка Copy и вызов определенной функции copyByEvent. Эти же самые действия можно проделать с виджетами для других состояний задания. Полученный результат, как всегда, можно посмотреть в нашем репозитории. В следующей части рассмотрим использование JSFFI (JS Foreign Function Interface). =========== Источник: habr.com =========== Похожие новости:
Блог компании Typeable ), #_razrabotka_vebsajtov ( Разработка веб-сайтов ), #_programmirovanie ( Программирование ), #_haskell, #_funktsionalnoe_programmirovanie ( Функциональное программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:20
Часовой пояс: UTC + 5