Dopiero piszę te słowa, zaczynając artykuł, a już wiem, że szykuje się długi tekst. Na tyle długi, że zostanie podzielony na dwie części, aby dać Wam (oraz sobie) wytchnienie, a jednocześnie pozwolić omówić każdy aspekt dokładniej. Będzie to też artykuł dosyć techniczny, skierowany przede wszystkim do twórców stron WWW, ale nie tylko. Pełni on bowiem rolę "proof of concept" - prezentacji tego, jak można podejść do systemu komentarzy na blogu lub w innym serwisie internetowym w taki sposób, aby oszczędzić sobie trochę pracy, zrealizować ciekawą ideę, a jednocześnie ściślej powiązać wiadomości ze światem społecznościowym. A to może podobać się redakcjom czy twórcom portalu, zwłaszcza kiedy liczą koszt rozwoju oprogramowania.
Oczywiście, ktoś mógłby powiedzieć, że zewnętrzny system komentarzy nie jest niczym nowym w świecie blogowym, gdyż istnieje choćby Disqus, a na niektórych stronach można znaleźć wstrzyknięte komentarze z Facebooka. Tym niemniej, nie wszystkim uśmiecha się płacenie lub ograniczenia dotyczące darmowych wersji (w tym szczególnie wyświetlanie reklam) lub powiązanie z "dużym graczem". Co więcej, to nadal umieszczanie kont i wiadomości na zewnętrznym serwerze jakiejś firmy, do czego jesteśmy przyzwyczajeni, ale nie zawsze może nam się to podobać. A co jeśli wykorzystalibyśmy istniejące mechanizmy mniejszych, otwartych społecznościówek i po prostu wyświetlali już i tak publiczne posty w formie komentarzy? Dodatkowo, te wiadomości mogłyby być dodawane zarówno z poziomu blogu, jak i samego serwisu social media, ale traktowane byłoby to jako jeden, wspólny wątek? I to wszystko za darmo oraz na serwerach należących zwykle do społeczności, ludzi ją tworzących? Właśnie na tym polega pomysł z wykorzystaniem Mastodona.
Tak, jak wspomniałem, artykuł został podzielony na dwie części. W pierwszej (którą właśnie czytacie) zajmiemy się wyjaśnieniem lub przypomnieniem:
- jaki w ogóle mamy cel,
- czym jest właściwie Mastodon i dlaczego jest dobrym kandydatem do systemu komentarzy,
- jak pobierać komentarze i wyświetlać je w formie wątku,
- gdzie można znaleźć przykład.
Z kolei w drugiej części zajmiemy się omówieniem bezpośredniego odpowiadania na posty z poziomu blogu, co jest bardziej złożonym tematem. Jednocześnie nie ograniczymy się do surowego omówienia metody działania - będziemy też zwracać uwagę na miejsca, gdzie może nastąpić optymalizacja, przy czym nie mówimy tylko o czasowej, ale też np. transferowej. Dodatkowo omówimy parę ogólnych mechanizmów stosowanych szczególnie przy korzystaniu z API. Przemycimy także parę uwag dotyczących standardu kodowania.
Artykuł jest z jednej strony techniczny, ale z drugiej - pokazuje ogólne możliwości, jakie drzemią w tym środowisku, a które mogą spodobać się np. specjalistom od social media. Na pewno skorzystają na nim programiści lub osoby, które opiekują się swoim blogiem i nie boją się czasem wejść do kodu. Jednak, musimy mieć świadomość, że to nie jest opis gotowej biblioteki do wykorzystania czy jedyne słuszne rozwiązanie - to raczej pokazanie mechanizmu działania wraz z przykładem kodu, który pozwala osiągnąć zamierzony cel. Demonstracja odbędzie się w języku PHP, ale to, oczywiście, nie oznacza, że tylko w tej technologii można stworzyć taki system komentarzy - to, co będę opisywał, pasuje także do innych języków czy frameworków. Choć nie da się ukryć, że większość blogów jednak korzysta właśnie z Personal Home Page.
Gotowy przykład
W tekście będę omawiał także podstawy "teoretyczne", wyjaśniając pewne podstawowe pojęcia i pokazując ich plusy oraz minusy. Jednak zdaję sobie sprawę, że nie wszystkich to interesuje - niektórzy chcą dostać od razu odpowiedź na swoje pytanie "jak zaimplementować system komentarzy". Z tego powodu przygotowaliśmy przykład, który zresztą będzie bazą do omówienia w dalszej części tekstu. Oto on:
https://github.com/WildaSoftware/MastodonCommentSystem
Jest to aplikacja w Laravelu stworzona na podstawie tego blogu, w którym znajduje się jeden przykładowy artykuł oraz źródło komentarzy do niego. Jeśli wolicie uczyć się poprzez samodzielną analizę kodu i próbowanie rozwiązań w praktyce, to zapraszam do klonowania repozytorium. Instrukcja konfiguracji aplikacji jest zawarta w pliku README.md
. Powodzenia! Mimo wszystko mam nadzieję, że choćby z ciekawości wrócicie potem do tego tekstu lub będziecie przerabiać te materiały równocześnie :)
Jedna uwaga - doskonale wiem, że kod nie jest idealnym przykładem aplikacji webowej napisanej w Laravelu. To jest jedynie przykład, który pokazuje, w jaki sposób praktycznie można poradzić sobie z pewnymi kwestiami i do którego można zerknąć podczas implementowania systemu komentarzy u siebie. Natomiast nie znaczy to, że nie zajmujemy się aplikacjami w tym frameworku - również mamy w nim doświadczenie, mimo że naszą podstawową technologią jest Yii 2.0. W razie czego - wiecie, jak się z nami skontaktować.
Co chcemy osiągnąć?
Na początku należy określić, co w ogóle chcemy zrobić. Naszym celem jest to, aby pod artykułami na blogu wyświetlał się wątek z komentarzami. Technicznie będą to posty na portalu społecznościowym (tutaj: dowolna instancja Mastodona, do czego zaraz przejdziemy), które są odpowiedziami na pewien źródłowy post. Wykorzystamy do tego fakt, że zwykle ogłaszamy publikację nowego artykułu właśnie za pomocą wpisu w social media, na który ludzie mogą odpowiadać. I dokładnie to będziemy tutaj wyświetlać w nieco bardziej dostosowanej formie.
Jako przykład możemy przywołać nasz artykuł o znaczeniu szachów w rozwoju programisty - po przewinięciu strony na dół trafimy do sekcji komentarzy, gdzie wyświetli nam się wątek, mający źródło m.in. w moim toocie zapraszającym do przeczytania tekstu ("toot" to wpis w serwisie Mastodon). Wszystkie odpowiedzi (będące czasem odpowiedziami do innych odpowiedzi) są wyświetlane w wątku i są faktycznymi tootami napisanymi przez innych użytkowników Fediverse (do tego też dojdziemy) lub przeze mnie. Gdy klikniemy na datę lub login danego rozmówcy, to przejdziemy do konkretnych wiadomości lub profilu już w portalu społecznościowym. Jednocześnie, bierzemy pod uwagę tylko publiczne wpisy - jeśli któryś z użytkowników napisze toota niepublicznego lub wręcz bezpośredniego, to ta wiadomość nie pojawi się w wątku na blogu ze względu na prywatność.
Dodatkowo, dobrze by było móc nie tylko odczytywać i wyświetlać tooty jako komentarze, ale także odpowiadać na nie. Z uwagi na fakt, że blog jest zewnętrzną stroną, nie jest to takie trywialne - użytkownik musi użyć swojego konta na Mastodonie, ale dobrze by było, aby miał poczucie, że pisze komentarz z poziomu strony. Dlatego po kliknięciu na ikonkę odpowiedzi zostanie przekierowany do Mastodona w celu potwierdzenia swojej osoby (bardzo podobnie, jak to ma miejsce przy logowaniu się kontem Google'a lub Facebooka), a następnie zawrócony do blogu w celu napisania treści wiadomości, która i tak przechodzi przez Mastodona.
To wszystko daje nam system komentarzy, który pokazuje dyskusję pod artykułem, ale jednocześnie nie angażuje zasobów bloga i co więcej - nie wymaga przechowywania danych kont w naszej bazie danych, co odciąża nas pod kątem wielu mechanizmów uwierzytelniających i autoryzacyjnych.
Czym jest wewnętrzny i zewnętrzny system komentarzy?
W wielu przypadkach komentarze pod artykułami są w całości obsługiwane w ramach danego serwisu. W dawnych czasach było to bardzo "frywolne" - nierzadko możliwość dodania wpisu nie wymagała zalogowania się ani jakiegokolwiek potwierdzenia swojej tożsamości. Objawiało się to wygodą, natomiast skutkowało spamem, postami nienadającymi się do cytowania i ogólnym śmietnikiem, który należało czyścić lub wyłączyć. Z tego powodu coraz częściej zaczęto stosować mechanizm CAPTCHA lub wymagać rejestracji konta, co przybiera różne formy. Jeśli portalowi zależy na dużym ruchu (np. serwisy informacyjne), to proces uwierzytelnienia jest bardzo prosty, natomiast w innych przypadkach przypomina normalną rejestrację, z aktywacją konta itd. Coraz częściej serwisy bazują głównie na kontach dużych serwisów (tzw. SSO, Single Sign-On), np. Google, Facebook czy Apple, które z jednej strony są wygodne dla użytkowników, a z drugiej - ściągają część obowiązków z głowy twórców portalu.
Opisany wyżej mechanizm nazywamy "wewnętrznym", gdyż całość jest realizowana po stronie serwisu. Zarówno sama mechanika wyświetlania oraz odpowiadania na komentarze, jak i obsługa kont (nawet w przypadku użycia SSO) jest do zaimplementowania przez twórców portalu. Z jednej strony to kuszące, gdyż mamy nad wszystkim całkowitą kontrolę, także pod kątem estetyki - łatwiej taki system komentarzy zintegrować graficznie z portalem. Z drugiej strony - jest to pracochłonne, być może nawet bardziej niż wszystkim się wydaje. Wymaga implementacji każdego elementu i zadbania o proces uwierzytelniania (staje się to łatwiejsze, gdy system i tak wymaga zalogowania się w celu korzystania z innych funkcji). Dodatkowo, dochodzą dwie kwestie, które należy rozważyć. Jedną z nich jest moderowanie treści dodanej przez użytkowników, co w przypadku dużych serwisów może być trudne, ale też konieczne ze względu na spam i nieodpowiednią zawartość, jaką złośliwi użytkownicy są w stanie zamieścić. Drugim kłopotem jest fakt, że internauci coraz bardziej wzbraniają się przed zakładaniem masy kont w różnych serwisach, gdyż potem i tak nie pamiętają haseł lub w ogóle o fakcie, że dane konto posiadają. Ten problem w pewien sposób rozwiązuje właśnie SSO.
Na przeciwnym biegunie mamy zewnętrzne systemy komentarzy w postaci Disqusa, OpenWeba czy wielu innych. Działają one na podobnej zasadzie jak wtyczka Feedybacky - system znajduje się na zewnętrznym serwerze i jest wstrzykiwany na blog lub portal za pomocą tagu iframe
albo poprzez użycie odpowiedniego skryptu JavaScript. Wymaga to założenia konta u zewnętrznego dostawcy, skonfigurowania parametrów (np. maksymalna długość komentarza), a następnie użycia odpowiednich danych na właściwej stronie. W takim układzie programista nie musi implementować tego mechanizmu u siebie ani tym bardziej zamartwiać się obsługą kont - to wszystko jest po stronie dostawcy. Wygląda to na bardzo wygodne rozwiązanie i takie rzeczywiście jest, a dodatkowo jest ustandaryzowane - użytkownik przeglądający blogi i napotykający np. Disqusa rozpoznaje go, wie, czego może się po nim spodziewać i być może nawet ma jedno konto, który wykorzystuje do komentowania wpisów na wszystkich ulubionych blogach.
Natomiast, oczywiście, istnieje parę wad takiego podejścia. Po pierwsze, są to często rozwiązania płatne lub ograniczone w pewien sposób, co też trzeba zrozumieć - zewnętrzny dostawca musi chronić swoje zasoby przed "testowaniem" systemu komentarzy ponad jego możliwości. Dodatkowo, musi w jakiś sposób na tym zarabiać i opłacić swoich programistów oraz serwery, co wymaga wprowadzenia abonamentu lub reklam. Po drugie, za cenę braku konieczności własnoręcznej implementacji, twórcy stron mają mniejsze możliwości dopasowania systemu komentarzy do własnego layoutu. Istnieją na to sposoby lub twórcy zapewniają wprowadzanie własnych ustawień (choćby poprzez pliki CSS czy po prostu "wyklikanie" poszczególnych opcji), ale na pewno nie jest to tak "customizowalne" jak natywny system. Po trzecie, jesteśmy uzależnieni od zewnętrznej firmy - jeśli ta zmieni warunki usługi, cenę, limity, sposób działania komentarzy lub zwyczajnie zamkną usługę, to pozostaje nam się z tym pogodzić i/lub zmienić dostawcę.
Gdzie plasuje się nasz system komentarzy, który zaimplementujemy? Pomiędzy - można powiedzieć, że to rozwiązanie hybrydowe. Zewnętrzna natura polega na tym, że źródłem komentarzy jest zewnętrzny serwis, który w dodatku nie jest jednym portalem (o czym za chwilę), co w pewien sposób pomaga w radzeniu sobie z utratą dostępu do usługi. Także konta użytkowników są po stronie Mastodona i nas nie interesują hasła ani inne wrażliwe dane konkretnych profilów. Natomiast "wewnętrzność" objawia się w potrzebie przygotowania mechanizmu wyświetlania oraz wprowadzania treści komentarzy, co z jednej strony jest wadą, a z drugiej zaletą, gdyż możemy wiele dostosować do naszych potrzeb. Takie podejście będziemy omawiać dzisiaj oraz w następnej części. Co więcej, prezentowane rozwiązanie można również zawrzeć w bibliotece, tworząc z tego de factu zewnętrzny system komentarzy, łatwiejszy do "zaimplementowania" przez inne osoby. To już stanie się "zadaniem dla chętnych".
Czym jest Mastodon i Fediverse?
Warto również przypomnieć i jeszcze raz omówić, czym właściwie jest Mastodon oraz wiążący się z tym Fediverse - jeszcze raz, gdyż już kiedyś to zrobiliśmy. Tylko wtedy skupiliśmy się na jego społecznościowym aspekcie, natomiast tutaj omówimy to zagadnienie od bardziej technicznej strony, ale także filozofii działania. Jeśli jesteście ciekawi, przeczytajcie kolejne akapity, a jeśli chcecie przejść do "mięsa" - możecie śmiało ominąć tę sekcję, choć polecam zajrzeć przynajmniej do końcówki tego rozdziału.
Wyobraźmy sobie dowolny serwis społecznościowy, zapewniający tworzenie treści - zapewne w tym momencie do głowy przyszły Wam takie tuzy, jak Facebook, Twitter, Instagram czy YouTube. To są tak zwane systemy scentralizowane - gdy rejestrujemy konto np. na Twitterze, trafiamy do tej samej bazy, co miliony innych użytkowników i mamy łatwy wgląd do każdej publicznej wiadomości. Najważniejsze jest to, że dostęp do całego serwisu jest pod jednym, konkretnym adresem, który odnosi się do jednego serwera (a raczej farmy serwerów, ale nie bądźmy drobiazgowi). Jest to wygodne, natomiast istnieją też różne wady oraz niekiedy wątpliwości moralne. Taka "społecznościówka" jest zarządzana przez konkretną firmę, gdyż nie jest to już prosta strona dla miłośników jakiegoś tematu - to globalny moloch, którego rozwój i utrzymanie wymagają ogromnych nakładów, co z kolei sprawia, że firmy chcą dużo zarobić. I pół biedy, gdyby odbywało się to w sposób niebudzący wątpliwości etycznych, ale tutaj często dane darmowych (i nie tylko) kont są wykorzystywane do lepszego personalizowania reklam, które stanowią główne źródło utrzymania serwisów. Reklamy mogą nie przeszkadzać (a personalizacja w pewnych przypadkach może nawet pomagać), ale czasem to robią, dopełniając obraz serwisu jako de facto systemu "billboardowego", w którym każdy się promuje i żyje ponad stan (tak często kojarzy się Instagram lub LinkedIn). Dodatkowo, w serwisie obowiązuje pewien regulamin i zasady zachowania, co jest dobre. Jednak z uwagi na tak ogromną bazę twórców oraz treści, właściciele portalu nie mogą moderować postów ręcznie - robią to boty, które czasem się mylą i blokują dostęp użytkownikom, którzy niczego złego nie zrobili. Radzenie sobie z takimi sytuacjami wiąże się ze stresem, pisaniem do pomocy technicznej i utratą zaufania do takiego portalu, a czasem przejściem na tzw. media własne. Warto też wspomnieć o tym, że jako że jest to system scentralizowany, to staje się bardziej podatny na awarie. Oczywiście, przy kilkuset (lub nawet więcej) serwerach obsługujących dany portal, problemy z jedną maszyną nie są kłopotliwe, ale jeśli jakaś zmiana ujawni się na wielu jednostkach, to cały system może przestać działać, czego przykłady mieliśmy w przyszłości.
Wielu osobom nie pasowały te warunki - niektórym te związane z problemami technicznymi, części łatwość dostania blokady, a innym kwestie prywatności i te bardziej ideologiczne. Po początkowym zachwycie gigantami, cząsta społeczności internetowej zapragnęła stworzyć własne, otwarte rozwiązanie. W ten sposób opracowano np. Mastodona (za chwilę wrócimy do innych aplikacji, bo nie jest to jedyna) - platformę mikroblogową bez reklam, która przypomina Twittera, choć nim nie jest i istnieją pewne funkcjonalne różnice. One jednak nie są interesujące w kontekście tego artykułu. Teraz wyobraźmy sobie, że możemy postawić takiego Mastodona u siebie, na swoim serwerze i to bez pytania autora. Możemy nawet na ten serwer samodzielnie przyjąć użytkowników, choć wiadomo, że pewnie stosunkowo niewielu, gdyż nie dysponujemy takimi zasobami jak duże firmy. To przypomina trochę czasy forów internetowych i mniejszych społeczności skupionych wokół konkretnego tematu. Natomiast, wracając do posiadania serwera, zwróćmy uwagę, że nie jesteśmy jedyni - nasz sąsiad również może skonfigurować swoją maszynę z Mastodonem. Inne osoby również. Każdy z nich staje się administratorem swojej instancji i może przyjąć do siebie pewną liczbę użytkowników lub mieć serwer tylko dla siebie. I teraz następuje moment kulminacyjny - te instancje mogą się komunikować ze sobą. Są na różnych serwerach, mają osobne bazy danych, ale użytkownik z instancji A może obserwować użytkownika z instancji B i odpowiadać mu. To nie jest system forów, które nie są ze sobą połączone - tutaj mówimy o "federacji serwerów" i tworzeniu jednego wielkiego systemu złożonego z autonomicznych jednostek zarządzanych przez lokalne (lub mniej lokalne) społeczności. Przypomina to trochę świat i komunikację obywateli różnych krajów, którzy stanowią jedną globalną wioskę. Oczywiście, tak jak w dyplomacji, tak tutaj może się zdarzyć, że dwie instancje ze sobą "nie rozmawiają", ale są to marginalne przypadki.
Kwestia instancji i komunikacji pomiędzy nimi będzie niezmiernie kluczowa w drugiej części naszej przygody, a więc podczas komentowania artykułu z poziomu blogu. Przy okazji - federacja to wspaniały przykład jednego z rodzajów systemów rozproszonych.
No dobrze, a czym jest zatem Fediverse? Pośrednio odpowiedzieliśmy na to wyżej - jest to możliwość grupowania się instancji w federacje i tworzenia wrażenia jednego wielkiego systemu, ale istnieje jeszcze jeden aspekt. Wróćmy na chwilę do Twittera. Mając w nim konto, widzimy posty innych użytkowników tego portalu, możemy im odpowiadać, dodawać do ulubionych itd. Jednak nie możemy z poziomu tego samego konta obserwować kogoś na Instagramie lub Facebooku. Nie możemy też skomentować filmu znajomego na YouTubie korzystając z LinkedIna. Aby to zrobić, musimy mieć profil na każdym z tych portali i korzystać z nich osobno. Tymczasem, w świecie federacyjnym Mastodon nie jest jedyną możliwą aplikacją. Ba - nie jest nawet jedynym portalem mikroblogowym, gdyż istnieje choćby Pleroma, Soapbox, Misskey czy obiecujący Calckey (o którym wiadomo, że zmieni nazwę, ale w chwili pisania tego tekstu nie jest ona znana). Również inne "centralki" mają swoje odpowiedniki. Fani Instagrama odnajdą się w Pixelfedzie, widzowie na YouTubie zainteresują się PeerTubem, a redditowcy lub wykopowcy powitają Lemmy'ego lub polski /kbin. Nawet miłośnicy książek zamiast Goodreads posiadają alternatywę w postaci BookWyrma. Dlaczego o tym wszystkim piszę? Dlatego, że te wszystkie portale korzystają z jednego bazowego systemu (nie wchodząc w szczegóły - implementują jego interfejs) zwanego ActivityPub, dzięki czemu mając konto na Mastodonie (a raczej jednej instancji Mastodona) możemy skomentować zdjęcie kolegi z Pixelfeda (i znowu - raczej jednej instancji Pixelfeda), a zaraz zostać zaobserwowanym przez użytkownika z instancji Soapboxa, który wspomni o koleżance z instancji Calckeya. Oczywiście, są wyjątki lub pewne specyficzne funkcje dostępne tylko w określonych aplikacjach, ale wszystkie portale mogą być ze sobą połączone. A są ich tysiące.
Inną, ciekawą metaforę zaproponował Tomasz Dunia na swoim blogu, porównując Fediverse do całego Wszechświata, w którym są galaktyki (Mastodon, Pixelfed, Friendica itd.), składające się z planet (konkretne instancje). Z kolei ActivityPub to prądy, którymi poruszamy się swoim pojazdem garażującym na konkretnej planecie (naszym kontem na jednej z instancji) z jednej planety do drugiej lub nawet między galaktykami. Oczywiście, nic nie stoi na przeszkodzie, aby mieć wiele kont na różnych portalach i jest to nawet powszechne. Tym niemniej, z powodzeniem można używać jednego profilu.
Tak przedstawia się ta idea i jest ona dostępna za darmo (opiera się na datkach społeczności, ew. dodatkowych usługach), co idealnie wpisuje się w bazę dla systemu komentarzy. Czy scentralizowane systemy społecznościowe to faktyczne niebezpieczeństwo to kwestia sporna i zależy od punktu widzenia oraz często filozofii życiowej, w tym podejścia do korporacji. Wszystko ma swoje zalety i wady, także Fediverse, dlatego warto znać tę koncepcję, aby poszerzać swoje horyzonty. Warto również wspomnieć o tym, że duże firmy też zainteresowały się federacjami i zapewniły (lub zapewniają) integrację z nimi - mam tutaj na myśli np. Wordpressa, Tumblra oraz Flipboarda. Nie jest to jedyna próba "odcięcia" się od tzw. "big techu" - na horyzoncie pojawiły się rozwiązania alternatywne w postaci Nostr czy tworu byłego twórcy Twittera Jacka Dorseya, Bluesky. Należy też pamiętać o protokole do komunikacji bezpośredniej, a mianowicie Matrix.
System komentarzy opisywany w tym artykule polega w całości na Mastodonie, gdyż wykorzystujemy nie tyle Fediverse, co API samej aplikacji. To akurat różni się w zależności od portalu, natomiast jeśli np. Calckey miałby API kompatybilne z Mastodonem (a są na to pewne szanse), to przedstawione rozwiązanie działałoby również z nim. Natomiast dlaczego blog nie może po prostu implementować ActivityPub? Otóż, mógłby to zrobić, ale z rozmysłem nie podążyliśmy tą drogą ze względu na to, żeby uniknąć obsługi kont użytkowników. Jednak, jeśli ktoś zainteresowany tym tematem, to tą drogą podąża Wordpress.
Wyświetlenie wątku komentarzy
Czas na konkrety, gdzie przyda się wcześniej przywoływany gotowy przykład, do którego często będziemy się odnosić. Tak, jak pisałem, w tej części skupimy się na samym odczycie komentarzy. Wymaga to wykonania następujących kroków:
- Określenie pewnych tootów (postów) jako "źródłowe".
- Pobranie z API treści zarówno tootów źródłowych, jak i odpowiedzi na nie.
- Sformatowanie treści, aby można było je bezpiecznie wyświetlić na blogu.
- Wyświetlenie komentarzy w postaci wątku.
Samo podejście do odczytu nie jest bardzo oryginalne, gdyż opierałem się na implementacjach zaproponowanych przez Carla Schwana, Daniela Pecosa Martineza oraz Patricka Curry'ego, dostosowując je do PHP-a i poprawiając w paru miejscach, choćby przy wcięciach czy obsłudze przypadków wyjątkowych.
Określenie pewnych tootów (postów) jako "źródłowe"
Wróćmy do przywoływanego wcześniej artykułu o szachach. Gdy przewiniemy do systemu komentarzy, zobaczymy dwa tooty w kolorze żółtym, które są wyraźnie wyróżnione - są to tzw. tooty źródłowe. Stanowią one bazę dla odpowiedzi, które z kolei będą traktowane przez nasz system jako komentarze. Są więc takim "punktem zaczepienia". Tutaj uwaga - moglibyśmy zrezygnować z ich wyświetlania i skupić się na samych odpowiedziach, natomiast warto wziąć pod uwagę to, że tooty źródłowe również mają jakiś tekst i to do nich zwykle odnoszą się komentujący użytkownicy. Może być też ich wiele, co oznacza, że trzeba przygotować się na więcej niż jeden wątek odpowiedzi. W niczym nam to jednak nie przeszkadza, a zapewnia wręcz większą elastyczność, jeżeli inna osoba tootowałaby o naszym artykule i chcielibyśmy umieścić również odpowiedzi pod jego wpisem (oczywiście, warto zapytać najpierw o zgodę).
W bazie danych, gdzie znajdują się same artykuły (w tabelkach post
oraz post_translation
, gdyż dbamy o wielojęzyczność), istnieje tabela post_comment_source
, która zawiera informację o tootach źródłowych:
CREATE TABLE post_comment_source ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, language_id BIGINT UNSIGNED NOT NULL, mastodon_instance VARCHAR(128) NOT NULL, mastodon_user VARCHAR(128) NOT NULL, mastodon_toot_id VARCHAR(128) NOT NULL, data_received MEDIUMTEXT NULL COLLATE, date_data_received TIMESTAMP NULL DEFAULT NULL, PRIMARY KEY (id), CONSTRAINT post_comment_source_language_id_foreign FOREIGN KEY (language_id) REFERENCES language (id), CONSTRAINT post_comment_source_post_id_foreign FOREIGN KEY (post_id) REFERENCES post (id) ); Przykład danych: { id: 1, post_id: 1, language_id: 1, mastodon_instance: "social.wildasoftware.pl", mastodon_user: "wilda", mastodon_toot_id: "110435606334801439" }
Jak widać, nie dość, że do każdego artykułu możemy dołączyć wiele tootów źródłowych (tutaj akurat jest przykład jednego), to dodatkowo rozróżnić je w zależności od języka. W naszym przykładzie pod "jedynką" w language
figuruje język angielski. Dane toota prowadzą do tego wpisu, gdzie w linku możecie zobaczyć, że zgadzają się wszystkie informacje. Pola data_received
oraz date_data_received
omówimy później - są związane z cache'owaniem wartości.
Warto wspomnieć o tym, że pewną wadą tego rozwiązania jest konieczność ręcznego dodania linków do tootów, co ma miejsce już po opublikowania tych wpisów, a więc będzie istniało tę parę minut, przez które na stronie nie będą wyświetlane komentarze. Nie jest to duży problem, natomiast jego ominięcie może się wiązać z większą pracą i przygotowaniem infrastruktury do jednoczesnego umieszczenia toota i dodawania go do bazy danych, czego tutaj nie będziemy omawiać.
Pobranie z API treści zarówno tootów źródłowych, jak i odpowiedzi na nie
Jeśli uruchomicie przykładowy projekt lub przyjrzycie się uważnie komentarzom na naszym blogu, to zauważycie, że nie są one ładowane od razu - pojawiają się dopiero po chwili od momentu przewinięcia strony do sekcji z wpisami. O takim sposobie mówimy, że są ładowane na żądanie lub że jest to lazy loading - dane są wczytywane dopiero wówczas, kiedy rzeczywiście są potrzebne. W tym przypadku ma to dużą zaletę - nie obciążamy ani naszego bloga, ani instancji z tootami źródłowymi, gdy użytkownik nie jest zainteresowany komentarzami, czyli kiedy w ogóle nie chce ich wyświetlić. Zapewnienie tego nie jest takie trudne - zerknijmy na pliki w przykładzie. Gdy wchodzimy na np. pierwszy artykuł poprzez URL /post/1
, przechodzimy przez BlogController
, w którym pobieramy informacje o samym artykule i tootach źródłowych, przekazując dane na widok blog-post.blade.php
(jak widać, korzystamy z silnika szablonów Blade), w którym wywołujemy skrypt post.js
:
// blog-post.blade.php ... <div> ... @if (count($commentSourceIds) > 0) <div class="row pb-3"> <div id="comment-container" class="col-12"> <h2 class="comments-header pb-3">{{ __('comments') }}</h2> <div id="comment-container-internal">{{ __('loadingComments') }} </div> </div> </div> @endif ... </div> <script src="<?= URL::to('/') ?>/js/post.js"></script> <script> (new PostScript( "<?= URL::to('/') ?>", <?= $post->id ?>, <?= json_encode($commentSourceIds) ?>, <?= json_encode([ 'enterMastodonDomain' => __('enterMastodonDomain'), ]) ?>, <?= !empty($mastodonCode) ? '"'.$mastodonCode.'"' : '' ?> )).init(); </script> // post.js class PostScript { constructor(baseUrl, postId, commentSourceIds, translations, mastodonCode) { ... this.isCommentContainerLoaded = false; } init() { ... if(this.commentSourceIds.length > 0 && !this.mastodonCode) { const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if(entry.intersectionRatio > 0) { this.loadComments(); } }); }, { root: null }); observer.observe(document.getElementById('comment-container')); } ... } loadComments(withFormInitiating = false) { if(this.isCommentContainerLoaded) { return; } $.get(this.baseUrl + 'post/comment', { sourceIds: this.commentSourceIds }, (page) => { $('#comment-container-internal').html(page); this.isCommentContainerLoaded = true; if(withFormInitiating) { // later in the second part: initializing the answer form } }); } }
Tworzymy kontener, do którego będziemy wczytywać komentarze. Zauważmy, że jeśli przy artykule nie ma podanych żadnych tootów źródłowych, to nie będzie żadnych komentarzy, więc ten div
nie będzie potrzebny - stąd instrukcja warunkowa. Jedna uwaga ogólna - nie będziemy tutaj szczegółowo omawiać stylowania (w czym pomaga Bootstrap) oraz internacjonalizacji (tłumaczenia poszczególnych tekstów znajdują w tym pliku). Nie poruszymy również tematu samego Laravela czy MVC, natomiast liczę na to, że przykłady będą w stanie pokazać odpowiedni kierunek prac.
W funkcji init
widzimy fragment, który odpowiada za reakcję w przypadku, kiedy użytkownik przewinie stronę do miejsca z komentarzami i istnieją jakieś tooty źródłowe. Technicznie opiera się to na klasie IntersectionObserver
, która reaguje w sytuacji, kiedy użytkownik wyświetli fragment strony zawierający dany element HTML, czyli nasz kontener. Wtedy uruchamia się pobieranie komentarzy z naszego kontrolera na podstawie ID-ków wpisów z tootami źródłowymi w bazie danych. Na razie nie przejmujmy się zmiennymi mastodonCode
oraz withFormInitiating
- one będą wykorzystywane dopiero przy samym komentowaniu.
Pozostała nam do omówienia najważniejsza część - wykorzystanie API Mastodona do pobrania treści tootów. To już jednak ma miejsce w kontrolerze BlogController
, gdzie znajduje się metoda comments
, na którą wskazuje routing.
Kod metody w kontrolerze omówimy partiami.
public function comments(Request $request) { $sourceIds = $request->get('sourceIds', []); if(empty($sourceIds)) { return response('Missing comment source IDs', 400); } $commentSources = $this->postCommentSourceRepository->getByIds($sourceIds); $mastodonToots = []; $now = new \DateTime(); foreach($commentSources as $source) { //... } return view('inc/blog-post-comments', $this->presenter->formatComments(['mastodonToots' => $mastodonToots])); }
Przede wszystkim warto zadbać o to, żeby wywołując tę metodę, sprawdzić, czy na pewno nikt nie zrobił tego "złośliwie", wysyłając pustą listę tootów źródłowych. W takim przypadku nie ma sensu wykonywać dalszych operacji - zwracamy kod HTTP 400 (Bad Request). Upewniamy się przy tym, że w normalnym trybie nigdzie w aplikacji taka sytuacja nie wystąpi - jeśli prześledzimy od początku kod podany wyżej i ten, to zauważymy, że comments
zostanie wywołane tylko w momencie, kiedy istnieją jakiekolwiek tooty źródłowe. Te zabezpieczenia zostały wprowadzone na wypadek prób ataków i są swego rodzaju standardem, związanym także z optymalizacją wykonywania się kodu.
Następnie pobieramy resztę informacji o tootach źródłowych, czyli wcześniej przywoływaną instancję Mastodona, login użytkownika i ID toota. I tutaj ktoś może rzec, że zrobiliśmy to nieoptymalnie - mogliśmy pobrać te informacje za pierwszym razem, przy wyświetlaniu posta, w ten sposób oszczędzając na jednym zapytaniu do bazy (można to prześledzić czytając cały kod klasy BlogController
). Tylko że wówczas musielibyśmy te wszystkie informacje przekazywać z kontrolera na widok, z widoku do skryptu JavaScript, a później ze skryptu do metody w kontrolerze, sztucznie wydłużając żądanie HTTP. Stąd taka optymalizacja nie tyle czasowa, co estetyczna (choć w przypadku zbyt wielu wpisów źródłowych i zbyt długich informacji, mogłoby się zdarzyć, że w którymś momencie serwer odmówiłby przyjęcia zbyt długiego URL-a, w którym byłyby zawarte te wszystkie parametry). Warto też zwrócić uwagę na zmienną mastodonToots
, która będzie przechowywała pobrane tooty - dlaczego w ogóle piszemy wszędzie (także w bazie danych) przedrostek "mastodon"? Przecież nie dość, że wydłuża to nazwy pól i zmiennych, to jeszcze "ogranicza nas", gdybyśmy chcieli zmienić źródło komentarzy. Tak, to prawda - ale zauważmy, że kod jest przygotowany także na sytuację, w której komentarze będziemy pobierać z innych aplikacji i każda z nich będzie wymagała nieco innego sposobu. Gdy zerkniemy na wcześniej opisywaną tabelę post_comment_source
, to zobaczymy, że równie dobrze można tam dodać kolejne pola dotyczące np. Facebooka czy Pixelfeda i nadal nie trzeba będzie zmieniać nazwy tabeli. Tak samo w metodzie comments
mogą istnieć kolejne części funkcji, które będą zbierać wpisy z innych portali i tworzyć większy konglomerat niż tylko mastodonToots
. Stąd taki drobny niuans dot. nazewnictwa zmiennych, który został zastosowany świadomie.
Widzimy też, że pobieramy aktualny czas (przydatny zaraz przy cache'owaniu) oraz na końcu będziemy zwracać widok HTML z przetworzonymi komentarzami. Do tego zaraz dojdziemy. Przejdźmy teraz do wnętrza pętli foreach
.
foreach($commentSources as $source) { if(!empty($source->data_received)) { $cacheDate = new \DateTime($source->date_data_received); $diff = $now->getTimestamp() - $cacheDate->getTimestamp(); if($diff <= 600) { $mastodonToots[] = json_decode($source->data_received, true); continue; } } try { $url = 'http://'.$source->mastodon_instance.'/api/v1/statuses/'.$source->mastodon_toot_id; $response = Http::get($url); if($response->successful()) { $toot = json_decode($response->body(), true); $url .= '/context'; $commentsResponse = Http::get($url); if($commentsResponse->successful()) { $comments = json_decode($commentsResponse->body(), true)['descendants']; $toot['comments'] = $comments; $mastodonToots[] = $toot; } else { Log::error('Error during calling '.$url); Log::error($commentsResponse->status().': '.print_r($commentsResponse->body(), true)); } PostCommentSource::where('id', $source->id)->update([ 'data_received' => json_encode($toot), 'date_data_received' => $now->format('Y-m-d H:i:s'), ]); } else { Log::error('Error during calling '.$url); Log::error($response->status().': '.print_r($response->body(), true)); } } catch(\Exception $e) { Log::error($e->getMessage()); Log::error($e->getTraceAsString()); } }
Pomińmy na razie pierwszego ifa i skupmy się na zawartości bloku try
- dla każdego toota źródłowego musimy wywołać dwa żądania:
- pobranie informacji o samym toocie źródłowym, aby go wyświetlić:
/api/v1/statuses/:id
, - pobranie informacji o komentarzach pod tym tootem:
/api/v1/statuses/:id/context
.
Warto wspomnieć, że termin "toot" związany z Mastodonem jest raczej stosowany wśród normalnych użytkowników - od strony technicznej są to statusy. Stąd to słowo pojawia się w wywołaniach API (czynionych u nas za pomocą biblioteki Guzzle) do konkretnej instancji. Nie wdając się w szczegóły - najpierw pobieramy samego toota, konwertujemy JSON-a na tablicę PHP-ową, a następnie pobieramy wszystkie komentarze, doisując je do oryginalnego toota. Potem dodajemy wpis na tablicę i przechodzimy do kolejnego źródła. Jeśli jakaś operacja się nie powiedzie, zapisujemy ten fakt w logach oraz - co istotne - sprawdzamy kolejne źródła lub prawidłowo zwracamy wynik, choćby był pusty. Nie może wystąpić sytuacja, w której użytkownik czeka w nieskończoność na pobranie komentarzy, nie mając świadomości, że po drodze np. jedna instancja nie odpowiada i blokuje całą operację.
Wróćmy teraz do miejsc, w których wykorzystywane są pola data_received
oraz date_data_received
. Po co w ogóle są nam potrzebne? Rozważmy sytuację, w której umieszczamy artykuł na stronie, publikujemy toota i w ciągu minuty wchodzi na niego 100 osób, którzy czytają w miarę równym tempie i docierają do sekcji komentarzy. To oznacza, że dla każdej z tych osób musimy wykonać po 2 żądania do API Mastodona na każdy toot źródłowy. Co więcej, w tak krótkim czasie te żądania zwrócą te same dwa wyniki, gdyż nie zdążą się pojawić nowe wpisy w międzyczasie. Widzimy zatem, że jest tutaj pole na optymalizację i cache'owanie wyników, co można osiągnąć zapisywaniem pobranych danych wraz ze znacznikiem czasowym. Do tego służą właśnie omawiane pola. Następnie, przy innych żądaniach (np. druga osoba "dojechała" do sekcji komentarzy), sprawdzamy czy zapisane dane są "dostatecznie nowe" (w naszym przypadku czasem granicznym będzie 600 sekund, czyli 10 minut; nawiasem mówiąc, ta wartość powinna być w parametrach i możliwie łatwa w modyfikacji). I rzeczywiście - jeśli wejdziemy na artykuł z komentarzami za pierwszym razem, wywołując pobranie komentarzy, a następnie odświeżymy stronę, to prawdopodobnie wpisy załadują się nam dużo szybciej, gdyż są pobierane z lokalnej bazy danych, a nie z innego serwera (instancji Mastodona). Aż do kolejnych 10 minut.
Jest to optymalizacja nie tylko czasowa i to dobry moment, aby wrócić do federacyjnej natury Mastodona. Pamiętajmy, że nasz kod odpytuje API realnej instancji, która jest zarządzana przez pewnego człowieka, dysponującego serwerem, na jaki go stać. W niektórych usługach istnieją limity na przesyłaną liczbę bajtów, a same serwery mają swoje restrykcje dotyczące liczby przetwarzanych żądań. To oznacza, że nasz bardzo popularny artykuł, który ma dużo komentarzy, może czasowo obciążyć serwer instancji, na czym ucierpią zwykli użytkownicy oraz administrator, jeśli płaci np. za wykorzystany transfer w chmurze. Dlatego już po stronie samego Mastodona istnieją domyślne ustawienia blokujące zbyt częste zapytania z jednego źródła. Jednak, aby nie nadwyrężać instancji oraz nie narażać administratora i naszego bloga na straty, warto zastosować cache'owanie.
Nie są to duże liczby - jednorazowe pobranie komentarzy do artykułu o szachach to ok. 14 KB transferu i 350 ms czasu oczekiwania na odpowiedź. Przy 50 takich podwójnych żądaniach jest to 700 KB - to nadal niewiele. Ale przy większej liczbie lub popularności utrzymującej się przez dłuższy czas oraz przy chęci zachowania zwykłej przyzwoitości warto skusić się na przechowywanie części danych u siebie.
Sformatowanie treści, aby można było je bezpiecznie wyświetlić na blogu
W porządku, mamy dane. Teraz można je wyświetlić. Ale zanim to zrobimy, musimy je jeszcze przetworzyć.
public function comments(Request $request) { ... return view('inc/blog-post-comments', $this->presenter->formatComments(['mastodonToots' => $mastodonToots])); }
Widzimy, że przed wysłaniem danych na widok, przekształcamy je w tzw. prezenterze, a więc klasie, która "czyści" dane przed obróbką HTML-ową. Zgodnie bowiem z podejściem MVC i dobrymi obyczajami, widok powinien zadbać tylko o wyświetlenie danych, a nie ich dodatkowe formatowanie. Jednocześnie wiadomo, że nie powinien się tym zajmować też kontroler czy model. Stąd czasem pomiędzy tymi dwiema warstwami umieszcza się prezentery, które wykonują "brudną robotę". W przypadku aplikacji angularowych często stosuje się do tego tzw. pipe'y.
A mówimy tutaj nie tylko o ewentualnej zmianie struktury, ale też usunięciu pewnych informacji w trosce o bezpieczeństwo. Zwróćcie uwagę, że użytkownicy Mastodona mają dość dużą dowolność w uzupełnianiu informacji o sobie, mogąc np. dokładać do nicka różne emoji. Dodatkowo, w samych tootach wypada sprawdzić, czy nie zostały tam umieszczone złośliwe skrypty JS, narażające nas na tzw. atak XSS (Cross-Site Scripting).
Dodatkowo zachodzą też operacje stricte estetyczne, jak choćby odpowiednie sformatowanie daty wpisu. Nie ma sensu omawiać całego kodu - poniżej przedstawiam treść klasy BlogPresenter
. Warto natomiast zwrócić uwagę na obliczanie głębokości (ang. depth) wpisu, gdyż zaraz będzie nam to potrzebne przy wyświetlaniu.
class BlogPresenter { public function formatComments(array $input): array { $output = $input; $replyCount = []; foreach($output['mastodonToots'] as &$toot) { $toot = $this->presentComment($toot); if(!empty($toot['in_reply_to_id'])) { if(!array_key_exists($toot['in_reply_to_id'], $replyCount)) { $replyCount[$toot['in_reply_to_id']] = 0; } $replyCount[$toot['in_reply_to_id']]++; } foreach($toot['comments'] as &$reply) { $reply = $this->presentComment($reply); if(!empty($reply['in_reply_to_id'])) { if(!array_key_exists($reply['in_reply_to_id'], $replyCount)) { $replyCount[$reply['in_reply_to_id']] = 0; } $replyCount[$reply['in_reply_to_id']]++; } } } $depths = []; $isIncreasingChildrenDepth = []; // setting an appropriate toot's depth in terms of responded post foreach($output['mastodonToots'] as &$toot) { $isIncreasingChildrenDepth[$toot['id']] = false; $depth = 0; $toot['depth'] = $depth; $depths[$toot['id']] = $depth; foreach($toot['comments'] as &$reply) { $isIncreasingChildrenDepth[$reply['id']] = false; $depth = ($depths[$reply['in_reply_to_id']] ?: 1) + (!empty($isIncreasingChildrenDepth[$reply['in_reply_to_id']]) ? 1 : 0); $reply['depth'] = $depth; $depths[$reply['id']] = $depth; if($replyCount[$reply['in_reply_to_id']] > 1) { $isIncreasingChildrenDepth[$reply['id']] = true; } } } return $output; } private function presentComment(array $comment) { $output = $comment; $output['account']['display_name'] = htmlspecialchars($output['account']['display_name']); $output['account']['avatar_static'] = htmlspecialchars($output['account']['avatar_static']); foreach($output['account']['emojis'] as $emoji) { $code = ':'.$emoji['shortcode']; $staticUrl = htmlspecialchars($emoji['static_url']); $output['account']['display_name'] = str_replace($code, '', $comment['account']['display_name']); } if(!strpos($output['account']['acct'], '@')) { $parsedUrl = parse_url($output['account']['url']); if(!empty($parsedUrl)) { $output['account']['acct'] .= '@'.$parsedUrl['host']; } } $output['created_at'] = (new DateTime($output['created_at']))->format('d.m.Y H:i:s'); return $output; } }
Wyświetlenie komentarzy w postaci wątku
W tym, ostatnim już, etapie, czekają na nas dwa wyzwania. Pierwsze z nich to samo wyświetlenie bloczka z komentarzem. Drugie to obliczenie odpowiedniego wcięcia. I na tych ostatnich na chwilę się zatrzymamy.
Gdy wejdziemy do komentarzy artykułu o szachach, to zobaczymy, że niektóre komentarze są wcięte, poczynając, oczywiście, od odpowiedzi bezpośrednio odnoszących się do toota źródłowego. Jednak, jeśli ktoś odpowiada na odpowiedź, to teoretycznie również powinien zostać dodany odstęp, ale nie zawsze tak się dzieje. Sytuacja zmienia się, gdy powstaną dwa niezależne wątki odpowiedzi na jednego toota - wówczas dodatkowy margines jest konieczny lub przynajmniej przydatny. Cóż, nie jest to łatwy problem.
Dlatego już w powyżej przedstawionym prezenterze, pod koniec metody formatComments
, następuje obliczenie głębokości za pomocą następującego algorytmu, który najpierw przedstawimy stricte technicznie.
- Przygotowujemy tablicę
replyCount
zawierającą liczbę odpowiedzi na danego toota. - Przygotowujemy tablicę
isIncreasingChildrenDepth
(w skrócieiicd
) orazdepths
dla każdego toota. - Dla toota źródłowego głębokość wynosi 0 a
iicd
wynosifalse
. - Dla każdego komentarza głębokość wynosi głębokość toota, na którego jest to odpowiedź lub 1.
- Jeśli odpowiedziany toot ma ustawione
true
wiicd
, głębokość komentarza jest zwiększana o 1. - Jeśli liczba odpowiedzi odpowiedzanego toota jest większa niż 1, wartość
iicd
dla tego toota zmienia się natrue
.
Przyznaję, że prześledzenie tego wymaga rozpisania sobie przykładu. Dlatego może przełóżmy ten opis na bardziej ludzki.
- Tooty źródłowe zawsze mają głębokość 0.
- Jeśli toot odpowiada bezpośrednio na toot źródłowy, zawsze ma głębokość 1.
- Jeśli toot X jest odpowiedzą na toota Y, który nie ma więcej odpowiedzi, to głębokość toota X jest równa głębokości toota Y (następują tuż po sobie).
- Jeśli toot X jest odpowiedzią na toota Y, który ma więcej odpowiedzi niż 1, to głębokość toota Y jest równa głębokości toota Y plus 1.
Ta głębokość jest dopisywana do każdego toota w prezenterze, a następnie w taki sposób przekazywana do widoku:
// blog-post-comments.blade.php @if (count($mastodonToots) > 0) @foreach($mastodonToots as $toot) @include('inc/blog-post-comment-box', ['comment' => $toot, 'isReply' => false]) @foreach($toot['comments'] as $reply) @include('inc/blog-post-comment-box', ['comment' => $reply, 'isReply' => true]) @endforeach @endforeach @else {{ __('noComments') }} @endif // blog-post-comment-box.php <div class="col-12 comment {{ $isReply ? 'reply' : '' }}" style=" margin-left: calc(var(--comment-base-indent) * {{ $comment['depth'] }}); max-width: calc(100% - (var(--comment-base-indent) * {{ $comment['depth'] }})) "> <div class="comment-author row"> <div class="comment-author-avatar col-3 col-md-2 col-lg-1"> <img src="{{ $comment['account']['avatar_static'] }}" alt=""> </div> <div class="comment-author-details col-9 col-md-7 col-lg-8"> <div class="row"> <a class="comment-author-details-name col-12" href="{{ $comment['account']['url'] }}" target="_blank" rel="nofollow">{!! $comment['account']['display_name'] !!}</a> <div class="comment-author-details-user col-12">{{ $comment['account']['acct'] }}</div> </div> </div> <div class="comment-date col-12 col-md-3 text-right"> <a href="{{ $comment['url'] }}" target="_blank" rel="nofollow">{{ $comment['created_at'] }}</a> </div> </div> <div class="comment-content row"> <div class="col-12"> {!! $comment['content'] !!} </div> </div> <div class="comment-reply-to row"> <div class="col-12 text-right"> <i class="comment-reply-link fa-solid fa-reply fa-lg" data-src="{{ $comment['url'] }}" title="{{ __('replyToThisCommentWithMastodon') }}"></i> </div> </div> </div>
Tak, jak pisałem, nie będziemy wgłębiać się w stylowanie (odpowiednie dyrektywy znajdziemy w pliku SCSS), natomiast warto zwrócić uwagę na odstępy. Istnieje bazowy margines wynoszący 1.5rem, jednak jest to tylko zmienna CSS-owa, na podstawie której ustalamy jest faktyczne odstawienie bloczka. Ten to wynik mnożenia bazowego marginesu i obliczonej głębokości dla komentarza. Co istotne, nie jest to koniec prac w tym względzie - jeśli poprzestaniemy na odsunięcia bloczka, to zachowa on tę samą długość i przy bardzo złożonych wątkach prawe krawędzie będą wychodziły poza ekran. Dlatego musimy zadbać o analogicznie liczenie stylu max-width
, który pozwala obliczyć prawidłową szerokość komentarza.
Inne fragmenty HTML-a są standardowe i można zobaczyć ich działanie w praktyce u nas na blogu lub w projekcie przykładowym.
Podsumowanie
To była część związana z wytłumaczeniem fundamentów, na których opieramy się budując nasz system komentarzy oraz objaśnieniem mechaniki wyświetlania istniejących komentarzy. W tym miejscu robimy pauzę i zapraszamy do zadawania pytań oraz przeczytania drugiej części artykułu, która wyjaśni sposób odpowiadania na tooty bezpośrednio z bloga, co pozwoli nam bardziej zapoznać się z API Mastodona, procesem autoryzacji OAuth oraz mechanizmem SSO.
Jeśli uważacie, że można ten mechanizm poprawić lub znaleźliście w nim jakieś nieścisłości - nie wahajcie się pisać. Im więcej opinii różnych osób i dyskusji, tym więcej wiedzy i przemyśleń mogą wynieść z tego programiści oraz inne osoby zainteresowane tematem.
Pozdrawiam i dziękuję - Jakub Rojek.