Compare commits

...

7 Commits

Author SHA1 Message Date
github-actions[bot]
300acb69c6 chore(master): release 0.29.0 2026-03-23 00:04:19 +02:00
6257f41b58 fix: longer post highlight 2026-03-23 00:01:01 +02:00
f1140c5a4a fix: page navigation url change 2026-03-22 23:54:18 +02:00
4863610331 feat: add direct link to posts 2026-03-22 23:52:53 +02:00
Nextcloud bot
22a4e8e30e fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-22 02:14:57 +00:00
github-actions[bot]
594d036dcf chore(master): release 0.28.1 2026-03-21 23:34:19 +02:00
82376feb5d fix: add "last reply by" to all thread cards 2026-03-21 23:24:39 +02:00
29 changed files with 585 additions and 32 deletions

View File

@@ -1 +1 @@
{".":"0.28.0"}
{".":"0.29.0"}

View File

@@ -1,5 +1,26 @@
# Changelog
## [0.29.0](https://github.com/chenasraf/nextcloud-forum/compare/v0.28.1...v0.29.0) (2026-03-22)
### Features
* add direct link to posts ([4863610](https://github.com/chenasraf/nextcloud-forum/commit/48636103318a5852f50aefe437f9387e02cdd75e))
### Bug Fixes
* **l10n:** Update translations from Transifex ([22a4e8e](https://github.com/chenasraf/nextcloud-forum/commit/22a4e8e30e97f83ee727f043a360a7a20bd998a2))
* longer post highlight ([6257f41](https://github.com/chenasraf/nextcloud-forum/commit/6257f41b5805c243f6416a778152611b863bc146))
* page navigation url change ([f1140c5](https://github.com/chenasraf/nextcloud-forum/commit/f1140c5a4aaeec9b503942e17c353e06eac188ce))
## [0.28.1](https://github.com/chenasraf/nextcloud-forum/compare/v0.28.0...v0.28.1) (2026-03-21)
### Bug Fixes
* add "last reply by" to all thread cards ([82376fe](https://github.com/chenasraf/nextcloud-forum/commit/82376feb5dc1adee74ddf668e983fb8fd12a7119))
## [0.28.0](https://github.com/chenasraf/nextcloud-forum/compare/v0.27.0...v0.28.0) (2026-03-21)

View File

@@ -37,7 +37,7 @@ This app is in early stages of development. While functional, you may encounter
The forum integrates seamlessly with your Nextcloud instance, using your existing users and groups for authentication and access control.
]]></description>
<version>0.28.0</version>
<version>0.29.0</version>
<licence>agpl</licence>
<author mail="contact@casraf.dev" homepage="https://casraf.dev">Chen Asraf</author>
<namespace>Forum</namespace>

View File

@@ -3,6 +3,7 @@ OC.L10N.register(
{
"Recent Forum activity" : "Nedávná aktivita na fóru",
"More activity" : "Další aktivita",
"%1$s (Guest)" : "%1$s (host)",
"New thread by %1$s" : "Nové vlákno od %1$s",
"Reply by %1$s" : "Odpověď od %1$s",
"No recent forum activity" : "Žádná nedávná aktivita na fóru",
@@ -77,10 +78,12 @@ OC.L10N.register(
"Dashboard" : "Nástěnka",
"Forum settings" : "Nastavení fóra",
"Users" : "Uživatelé",
"Roles & Teams" : "Role a týmy",
"Categories" : "Kategorie",
"BBCodes" : "BBCodes",
"Expand" : "Rozbalit",
"Collapse" : "Sbalit",
"(Guest)" : "(host)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}Upozornění:{bEnd} Přiložené soubory budou viditelné komukoli na fóru, nezávisle na nastavení sdílení souboru.",
"Drop file here to upload" : "Soubor sem nahrajete přetažením",
"Hello world!" : "Dobrý den světe!",
@@ -120,6 +123,8 @@ OC.L10N.register(
"Uploading file …" : "Nahrávání souboru …",
"Upload failed" : "Nahrání se nezdařilo",
"Close" : "Zavřít",
"More formatting options" : "Další možnosti formátování",
"Insert template" : "Vložit šablonu",
"Pick a file to attach" : "Vyberte soubor a nasdílejte ho",
"Failed to upload file" : "Nepodařilo se nahrát soubor",
"Threads" : "Vláken",
@@ -128,8 +133,15 @@ OC.L10N.register(
"New activity" : "Nová aktivita",
"Category" : "Kategorie",
"Can view" : "Může zobrazovat",
"Can post" : "Může přidávat příspěvky",
"Can reply" : "Může odpovídat",
"Can moderate" : "Může moderovat",
"Allow" : "Umožnit",
"Allow All" : "Umožnit vše",
"{bStart}View:{bEnd} Allows seeing the category and its threads." : "{bStart}Zobrazit:{bEnd} Umožňuje zobrazení kategorie a jejích vláken.",
"{bStart}Post:{bEnd} Allows creating new threads in the category." : "{bStart}Příspěvky:{bEnd} Umožňuje vytváření nových vláken v kategorii.",
"{bStart}Reply:{bEnd} Allows replying to existing threads in the category." : "{bStart} Odpověď:{bEnd} Umožní odpovídání na existující vlákna v kategorii.",
"{bStart}Moderate:{bEnd} Allows editing and deleting posts, pinning, locking, and moving threads in the category." : "{bStart}Moderovat:{bEnd} Umožňuje upravování a mazání příspěvků, zamykání a přesouvání vláken v kategorii.",
"Pick a color" : "Zvolte barvu",
"Create category header" : "Vytvořit záhlaví kategorie",
"Edit category header" : "Upravit záhlaví kategorie",
@@ -142,6 +154,15 @@ OC.L10N.register(
"Cancel" : "Zrušit",
"Create" : "Vytvářet",
"Update" : "Aktualizovat",
"Forum setup required" : "Fórum je třeba napřed nastavit",
"Select the accounts that should have the forum admin role." : "Vyberte účty které mají mít roli správce fóra.",
"Forum admin accounts:" : "Účty pro správu fóra:",
"Select accounts …" : "Vybrat účty …",
"All other accounts will receive the default role." : "Všechny ostatní účty obdrží výchozí roli.",
"Initialize forum" : "Inicializovat fórum",
"Initializing …" : "Inicializace…",
"Forum not set up" : "Fórum nenastaveno",
"The forum has not been set up yet. Please contact an administration member to complete the setup." : "Fórum ještě nebylo nastaveno. Obraťte se na člena-správce aby nastavení dokončil.",
"Move thread to category" : "Přesunout vlákno do kategorie",
"Select the category to move this thread to:" : "Vyberte kategorii do které toto vlákno přesunout:",
"Select a category …" : "Vybrat kategorii …",
@@ -190,13 +211,24 @@ OC.L10N.register(
"Pinned thread" : "Připnuté vlákno",
"Locked thread" : "Uzamčené vlákno",
"Uncategorized" : "Nezařazeno",
"Last reply by {name}" : "Poslední odpověď od {name}",
"_%n reply_::_%n replies_" : ["%n odpověď","%n odpovědi","%n odpovědí","%n odpovědi"],
"Templates" : "Šablony",
"Add template" : "Přidat šablonu",
"Edit template" : "Upravit šablonu",
"No templates yet" : "Zatím ještě žádné šablony",
"Loading templates …" : "Načítání šablon …",
"Name" : "Název",
"Template name" : "Název šablony",
"Content" : "Obsah",
"Template content (BBCode) …" : "Obsah šablony (BBCode) …",
"Show in:" : "Zobrazit v:",
"Are you sure you want to delete this template?" : "Opravdu chcete tuto šablonu smazat?",
"Both" : "Obojí",
"Threads & replies" : "Vlákna a odpovědi",
"Neither (disabled)" : "Ani jedno (vypnuto)",
"Insert" : "Vložit",
"Failed to load templates" : "Nepodařilo se načíst šablony",
"Views" : "Zobrazení",
"Title" : "Titul",
"Enter thread title …" : "Zadejte titulek vlákna …",
@@ -233,6 +265,7 @@ OC.L10N.register(
"Thread created" : "Vlákno vytvořeno",
"Failed to create thread" : "Vlákno se nepodařilo vytvořit",
"No category specified" : "Neurčena žádná kategorie",
"You do not have permission to create threads in this category." : "Nemáte oprávnění pro vytváření vláken v této kategorii.",
"Error" : "Error",
"Created" : "Vytvořeno",
"Threads ({count})" : "Vlákna ({count})",
@@ -272,6 +305,7 @@ OC.L10N.register(
"Be the first to reply in this thread." : "Buďte první kdo odpoví v tomto vlákně.",
"by" : "od",
"This thread is locked. Only moderators can add replies." : "Toto vlákno je uzamčené. Odpovědi mohou přidávat pouze moderátoři.",
"You do not have permission to reply in this category." : "Nemáte oprávnění odpovídat v této kategorii.",
"You must be signed in to reply to this thread." : "Pokud chcete v tomto vlákně odpovědět, je třeba, abyste byli přihlášení.",
"Sign in to reply" : "Pokud chcete odpovědět, přihlaste se ke svému účtu",
"Lock thread" : "Uzamknout vlákno",
@@ -374,6 +408,18 @@ OC.L10N.register(
"Enter category description (optional)" : "Zadejte popis kategorie (volitelné)",
"New" : "Nové",
"Permissions" : "Oprávnění",
"Control which roles and teams can access and moderate this category" : "Určete které role a týmy mohou k této kategorii přistupovat a moderovat v ní",
"Select roles or teams that can view this category and its threads" : "Vyberte role nebo týmy které si mohou tuto kategorie a její vlákna zobrazovat.",
"Select roles or teams that can create new threads in this category" : "Vyberte role nebo týmy které v této kategorii mohou vytvářet nová vlákna",
"Select roles or teams that can reply to threads in this category" : "Vyberte role nebo týmy které mohou v této kategorii odpovídat na vlákna",
"Select roles or teams that can moderate (edit/delete) content in this category" : "Vyberte role nebo týmy které v této kategorii mohou moderovat (upravovat/mazat)",
"Select roles or teams …" : "Vyberte role nebo týmy …",
"Design" : "Design",
"Customize the appearance of this category" : "Přizpůsobit vzhled této kategorie",
"Category color" : "Barva kategorie",
"Text color" : "Barva textu",
"Dark text" : "Tmavý text",
"Light text" : "Světlý text",
"Preview" : "Náhled",
"Manage forum categories and organization" : "Spravovat kategorie fóra a organizování",
"Error loading categories" : "Chyba při načítání kategorií",
@@ -473,8 +519,18 @@ OC.L10N.register(
"No description" : "Bez popisu",
"Are you sure you want to delete the role \"{name}\"? This action cannot be undone." : "Opravdu chcete roli „{name}“ smazat? Tuto akci nepůjde vzít zpět.",
"System roles cannot be deleted" : "Systémové role není možné smazat",
"Team permissions" : "Oprávnění týmu",
"Manage category permissions for Nextcloud Teams" : "Spravovat oprávnění kategorie pro Nextcloud týmy",
"Loading teams …" : "Načítání týmů …",
"Error loading teams" : "Chyba při načítání týmů",
"No teams found" : "Nenalezeny žádné týmy",
"Members" : "Členové",
"No Nextcloud Teams are available" : "Nejsou k dispozici žádné Nextcloud týmy",
"Edit team" : "Upravit tým",
"Configure category permissions for this team" : "Nastavit oprávnění kategorií pro tento tým",
"Error loading team" : "Chyba při načítání týmu",
"Editing category permissions for this team. Team membership is managed via Nextcloud Teams." : "Upravování oprávnění kategorie pro tento tým. Členství v týmu je spravováno přes Nextcloud Týmy.",
"Set which categories this team can access" : "Nastavte ke kterým kategoriím může tento tým přistupovat",
"User management" : "Správa uživatelů",
"Manage forum users, roles and permissions" : "Spravovat uživatele fóra, role a oprávnění",
"Loading users …" : "Načítání uživatelů …",

View File

@@ -1,6 +1,7 @@
{ "translations": {
"Recent Forum activity" : "Nedávná aktivita na fóru",
"More activity" : "Další aktivita",
"%1$s (Guest)" : "%1$s (host)",
"New thread by %1$s" : "Nové vlákno od %1$s",
"Reply by %1$s" : "Odpověď od %1$s",
"No recent forum activity" : "Žádná nedávná aktivita na fóru",
@@ -75,10 +76,12 @@
"Dashboard" : "Nástěnka",
"Forum settings" : "Nastavení fóra",
"Users" : "Uživatelé",
"Roles & Teams" : "Role a týmy",
"Categories" : "Kategorie",
"BBCodes" : "BBCodes",
"Expand" : "Rozbalit",
"Collapse" : "Sbalit",
"(Guest)" : "(host)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}Upozornění:{bEnd} Přiložené soubory budou viditelné komukoli na fóru, nezávisle na nastavení sdílení souboru.",
"Drop file here to upload" : "Soubor sem nahrajete přetažením",
"Hello world!" : "Dobrý den světe!",
@@ -118,6 +121,8 @@
"Uploading file …" : "Nahrávání souboru …",
"Upload failed" : "Nahrání se nezdařilo",
"Close" : "Zavřít",
"More formatting options" : "Další možnosti formátování",
"Insert template" : "Vložit šablonu",
"Pick a file to attach" : "Vyberte soubor a nasdílejte ho",
"Failed to upload file" : "Nepodařilo se nahrát soubor",
"Threads" : "Vláken",
@@ -126,8 +131,15 @@
"New activity" : "Nová aktivita",
"Category" : "Kategorie",
"Can view" : "Může zobrazovat",
"Can post" : "Může přidávat příspěvky",
"Can reply" : "Může odpovídat",
"Can moderate" : "Může moderovat",
"Allow" : "Umožnit",
"Allow All" : "Umožnit vše",
"{bStart}View:{bEnd} Allows seeing the category and its threads." : "{bStart}Zobrazit:{bEnd} Umožňuje zobrazení kategorie a jejích vláken.",
"{bStart}Post:{bEnd} Allows creating new threads in the category." : "{bStart}Příspěvky:{bEnd} Umožňuje vytváření nových vláken v kategorii.",
"{bStart}Reply:{bEnd} Allows replying to existing threads in the category." : "{bStart} Odpověď:{bEnd} Umožní odpovídání na existující vlákna v kategorii.",
"{bStart}Moderate:{bEnd} Allows editing and deleting posts, pinning, locking, and moving threads in the category." : "{bStart}Moderovat:{bEnd} Umožňuje upravování a mazání příspěvků, zamykání a přesouvání vláken v kategorii.",
"Pick a color" : "Zvolte barvu",
"Create category header" : "Vytvořit záhlaví kategorie",
"Edit category header" : "Upravit záhlaví kategorie",
@@ -140,6 +152,15 @@
"Cancel" : "Zrušit",
"Create" : "Vytvářet",
"Update" : "Aktualizovat",
"Forum setup required" : "Fórum je třeba napřed nastavit",
"Select the accounts that should have the forum admin role." : "Vyberte účty které mají mít roli správce fóra.",
"Forum admin accounts:" : "Účty pro správu fóra:",
"Select accounts …" : "Vybrat účty …",
"All other accounts will receive the default role." : "Všechny ostatní účty obdrží výchozí roli.",
"Initialize forum" : "Inicializovat fórum",
"Initializing …" : "Inicializace…",
"Forum not set up" : "Fórum nenastaveno",
"The forum has not been set up yet. Please contact an administration member to complete the setup." : "Fórum ještě nebylo nastaveno. Obraťte se na člena-správce aby nastavení dokončil.",
"Move thread to category" : "Přesunout vlákno do kategorie",
"Select the category to move this thread to:" : "Vyberte kategorii do které toto vlákno přesunout:",
"Select a category …" : "Vybrat kategorii …",
@@ -188,13 +209,24 @@
"Pinned thread" : "Připnuté vlákno",
"Locked thread" : "Uzamčené vlákno",
"Uncategorized" : "Nezařazeno",
"Last reply by {name}" : "Poslední odpověď od {name}",
"_%n reply_::_%n replies_" : ["%n odpověď","%n odpovědi","%n odpovědí","%n odpovědi"],
"Templates" : "Šablony",
"Add template" : "Přidat šablonu",
"Edit template" : "Upravit šablonu",
"No templates yet" : "Zatím ještě žádné šablony",
"Loading templates …" : "Načítání šablon …",
"Name" : "Název",
"Template name" : "Název šablony",
"Content" : "Obsah",
"Template content (BBCode) …" : "Obsah šablony (BBCode) …",
"Show in:" : "Zobrazit v:",
"Are you sure you want to delete this template?" : "Opravdu chcete tuto šablonu smazat?",
"Both" : "Obojí",
"Threads & replies" : "Vlákna a odpovědi",
"Neither (disabled)" : "Ani jedno (vypnuto)",
"Insert" : "Vložit",
"Failed to load templates" : "Nepodařilo se načíst šablony",
"Views" : "Zobrazení",
"Title" : "Titul",
"Enter thread title …" : "Zadejte titulek vlákna …",
@@ -231,6 +263,7 @@
"Thread created" : "Vlákno vytvořeno",
"Failed to create thread" : "Vlákno se nepodařilo vytvořit",
"No category specified" : "Neurčena žádná kategorie",
"You do not have permission to create threads in this category." : "Nemáte oprávnění pro vytváření vláken v této kategorii.",
"Error" : "Error",
"Created" : "Vytvořeno",
"Threads ({count})" : "Vlákna ({count})",
@@ -270,6 +303,7 @@
"Be the first to reply in this thread." : "Buďte první kdo odpoví v tomto vlákně.",
"by" : "od",
"This thread is locked. Only moderators can add replies." : "Toto vlákno je uzamčené. Odpovědi mohou přidávat pouze moderátoři.",
"You do not have permission to reply in this category." : "Nemáte oprávnění odpovídat v této kategorii.",
"You must be signed in to reply to this thread." : "Pokud chcete v tomto vlákně odpovědět, je třeba, abyste byli přihlášení.",
"Sign in to reply" : "Pokud chcete odpovědět, přihlaste se ke svému účtu",
"Lock thread" : "Uzamknout vlákno",
@@ -372,6 +406,18 @@
"Enter category description (optional)" : "Zadejte popis kategorie (volitelné)",
"New" : "Nové",
"Permissions" : "Oprávnění",
"Control which roles and teams can access and moderate this category" : "Určete které role a týmy mohou k této kategorii přistupovat a moderovat v ní",
"Select roles or teams that can view this category and its threads" : "Vyberte role nebo týmy které si mohou tuto kategorie a její vlákna zobrazovat.",
"Select roles or teams that can create new threads in this category" : "Vyberte role nebo týmy které v této kategorii mohou vytvářet nová vlákna",
"Select roles or teams that can reply to threads in this category" : "Vyberte role nebo týmy které mohou v této kategorii odpovídat na vlákna",
"Select roles or teams that can moderate (edit/delete) content in this category" : "Vyberte role nebo týmy které v této kategorii mohou moderovat (upravovat/mazat)",
"Select roles or teams …" : "Vyberte role nebo týmy …",
"Design" : "Design",
"Customize the appearance of this category" : "Přizpůsobit vzhled této kategorie",
"Category color" : "Barva kategorie",
"Text color" : "Barva textu",
"Dark text" : "Tmavý text",
"Light text" : "Světlý text",
"Preview" : "Náhled",
"Manage forum categories and organization" : "Spravovat kategorie fóra a organizování",
"Error loading categories" : "Chyba při načítání kategorií",
@@ -471,8 +517,18 @@
"No description" : "Bez popisu",
"Are you sure you want to delete the role \"{name}\"? This action cannot be undone." : "Opravdu chcete roli „{name}“ smazat? Tuto akci nepůjde vzít zpět.",
"System roles cannot be deleted" : "Systémové role není možné smazat",
"Team permissions" : "Oprávnění týmu",
"Manage category permissions for Nextcloud Teams" : "Spravovat oprávnění kategorie pro Nextcloud týmy",
"Loading teams …" : "Načítání týmů …",
"Error loading teams" : "Chyba při načítání týmů",
"No teams found" : "Nenalezeny žádné týmy",
"Members" : "Členové",
"No Nextcloud Teams are available" : "Nejsou k dispozici žádné Nextcloud týmy",
"Edit team" : "Upravit tým",
"Configure category permissions for this team" : "Nastavit oprávnění kategorií pro tento tým",
"Error loading team" : "Chyba při načítání týmu",
"Editing category permissions for this team. Team membership is managed via Nextcloud Teams." : "Upravování oprávnění kategorie pro tento tým. Členství v týmu je spravováno přes Nextcloud Týmy.",
"Set which categories this team can access" : "Nastavte ke kterým kategoriím může tento tým přistupovat",
"User management" : "Správa uživatelů",
"Manage forum users, roles and permissions" : "Spravovat uživatele fóra, role a oprávnění",
"Loading users …" : "Načítání uživatelů …",

View File

@@ -212,6 +212,7 @@ OC.L10N.register(
"Pinned thread" : "Angeheftetes Thema",
"Locked thread" : "Gesperrtes Thema",
"Uncategorized" : "Ohne Kategorie",
"Last reply by {name}" : "Letzte Antwort von {name}",
"_%n reply_::_%n replies_" : ["%n Antwort","%n Antworten"],
"Templates" : "Vorlagen",
"Add template" : "Vorlage hinzufügen",

View File

@@ -210,6 +210,7 @@
"Pinned thread" : "Angeheftetes Thema",
"Locked thread" : "Gesperrtes Thema",
"Uncategorized" : "Ohne Kategorie",
"Last reply by {name}" : "Letzte Antwort von {name}",
"_%n reply_::_%n replies_" : ["%n Antwort","%n Antworten"],
"Templates" : "Vorlagen",
"Add template" : "Vorlage hinzufügen",

View File

@@ -212,6 +212,7 @@ OC.L10N.register(
"Pinned thread" : "Angeheftetes Thema",
"Locked thread" : "Gesperrtes Thema",
"Uncategorized" : "Ohne Kategorie",
"Last reply by {name}" : "Letzte Antwort von {name}",
"_%n reply_::_%n replies_" : ["%n Antwort","%n Antworten"],
"Templates" : "Vorlagen",
"Add template" : "Vorlage hinzufügen",

View File

@@ -210,6 +210,7 @@
"Pinned thread" : "Angeheftetes Thema",
"Locked thread" : "Gesperrtes Thema",
"Uncategorized" : "Ohne Kategorie",
"Last reply by {name}" : "Letzte Antwort von {name}",
"_%n reply_::_%n replies_" : ["%n Antwort","%n Antworten"],
"Templates" : "Vorlagen",
"Add template" : "Vorlage hinzufügen",

View File

@@ -3,6 +3,7 @@ OC.L10N.register(
{
"Recent Forum activity" : "Recent Forum activity",
"More activity" : "More activity",
"%1$s (Guest)" : "%1$s (Guest)",
"New thread by %1$s" : "New thread by %1$s",
"Reply by %1$s" : "Reply by %1$s",
"No recent forum activity" : "No recent forum activity",
@@ -83,6 +84,7 @@ OC.L10N.register(
"BBCodes" : "BBCodes",
"Expand" : "Expand",
"Collapse" : "Collapse",
"(Guest)" : "(Guest)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings.",
"Drop file here to upload" : "Drop file here to upload",
"Hello world!" : "Hello world!",
@@ -123,6 +125,7 @@ OC.L10N.register(
"Upload failed" : "Upload failed",
"Close" : "Close",
"More formatting options" : "More formatting options",
"Insert template" : "Insert template",
"Pick a file to attach" : "Pick a file to attach",
"Failed to upload file" : "Failed to upload file",
"Threads" : "Threads",
@@ -209,13 +212,24 @@ OC.L10N.register(
"Pinned thread" : "Pinned thread",
"Locked thread" : "Locked thread",
"Uncategorized" : "Uncategorised",
"Last reply by {name}" : "Last reply by {name}",
"_%n reply_::_%n replies_" : ["%n reply","%n replies"],
"Templates" : "Templates",
"Add template" : "Add template",
"Edit template" : "Edit template",
"No templates yet" : "No templates yet",
"Loading templates …" : "Loading templates …",
"Name" : "Surname",
"Template name" : "Template name",
"Content" : "Content",
"Template content (BBCode) …" : "Template content (BBCode) …",
"Show in:" : "Show in:",
"Are you sure you want to delete this template?" : "Are you sure you want to delete this template?",
"Both" : "Both",
"Threads & replies" : "Threads & replies",
"Neither (disabled)" : "Neither (disabled)",
"Insert" : "Insert",
"Failed to load templates" : "Failed to load templates",
"Views" : "Views",
"Title" : "Title",
"Enter thread title …" : "Enter thread title …",

View File

@@ -1,6 +1,7 @@
{ "translations": {
"Recent Forum activity" : "Recent Forum activity",
"More activity" : "More activity",
"%1$s (Guest)" : "%1$s (Guest)",
"New thread by %1$s" : "New thread by %1$s",
"Reply by %1$s" : "Reply by %1$s",
"No recent forum activity" : "No recent forum activity",
@@ -81,6 +82,7 @@
"BBCodes" : "BBCodes",
"Expand" : "Expand",
"Collapse" : "Collapse",
"(Guest)" : "(Guest)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings.",
"Drop file here to upload" : "Drop file here to upload",
"Hello world!" : "Hello world!",
@@ -121,6 +123,7 @@
"Upload failed" : "Upload failed",
"Close" : "Close",
"More formatting options" : "More formatting options",
"Insert template" : "Insert template",
"Pick a file to attach" : "Pick a file to attach",
"Failed to upload file" : "Failed to upload file",
"Threads" : "Threads",
@@ -207,13 +210,24 @@
"Pinned thread" : "Pinned thread",
"Locked thread" : "Locked thread",
"Uncategorized" : "Uncategorised",
"Last reply by {name}" : "Last reply by {name}",
"_%n reply_::_%n replies_" : ["%n reply","%n replies"],
"Templates" : "Templates",
"Add template" : "Add template",
"Edit template" : "Edit template",
"No templates yet" : "No templates yet",
"Loading templates …" : "Loading templates …",
"Name" : "Surname",
"Template name" : "Template name",
"Content" : "Content",
"Template content (BBCode) …" : "Template content (BBCode) …",
"Show in:" : "Show in:",
"Are you sure you want to delete this template?" : "Are you sure you want to delete this template?",
"Both" : "Both",
"Threads & replies" : "Threads & replies",
"Neither (disabled)" : "Neither (disabled)",
"Insert" : "Insert",
"Failed to load templates" : "Failed to load templates",
"Views" : "Views",
"Title" : "Title",
"Enter thread title …" : "Enter thread title …",

View File

@@ -7,9 +7,13 @@ OC.L10N.register(
"General" : "Général",
"Support" : "Support",
"Use BBCode for rich text formatting" : "Utilisez les BBCodes pour un formatage enrichi du texte",
"BBCode examples:" : "Exemples de BBCode :",
"Bold text" : "Texte en gras",
"Italic text" : "Texte en italique",
"Underlined text" : "Texte souligné",
"Welcome to Nextcloud Forums" : "Bienvenue dans les Forums Nextcloud",
"Forum" : "Forum",
"Welcome to the forum!" : "Bienvenue dans le forum !",
"Deleted user" : "Utilisateur supprimé",
"User ID" : "Identifiant utilisateur",
"Role" : "Rôle",

View File

@@ -5,9 +5,13 @@
"General" : "Général",
"Support" : "Support",
"Use BBCode for rich text formatting" : "Utilisez les BBCodes pour un formatage enrichi du texte",
"BBCode examples:" : "Exemples de BBCode :",
"Bold text" : "Texte en gras",
"Italic text" : "Texte en italique",
"Underlined text" : "Texte souligné",
"Welcome to Nextcloud Forums" : "Bienvenue dans les Forums Nextcloud",
"Forum" : "Forum",
"Welcome to the forum!" : "Bienvenue dans le forum !",
"Deleted user" : "Utilisateur supprimé",
"User ID" : "Identifiant utilisateur",
"Role" : "Rôle",

View File

@@ -3,6 +3,7 @@ OC.L10N.register(
{
"Recent Forum activity" : "Gníomhaíocht an Fhóraim le Déanaí",
"More activity" : "Tuilleadh gníomhaíochta",
"%1$s (Guest)" : "%1$s (Aoi)",
"New thread by %1$s" : "Snáithe nua le %1$s",
"Reply by %1$s" : "Freagra ó %1$s",
"No recent forum activity" : "Gan aon ghníomhaíocht fóram le déanaí",
@@ -83,6 +84,7 @@ OC.L10N.register(
"BBCodes" : "BBCóid",
"Expand" : "Leathnaigh",
"Collapse" : "Laghdaigh",
"(Guest)" : "(Aoi)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}Tabhair faoi deara le do thoil:{bEnd} Beidh comhaid cheangailte le feiceáil ag aon duine san fhóram, beag beann ar shocruithe comhroinnte an chomhaid.",
"Drop file here to upload" : "Scaoil comhad anseo le huaslódáil",
"Hello world!" : "Dia duit, a dhomhan!",
@@ -122,6 +124,8 @@ OC.L10N.register(
"Uploading file …" : "Ag uaslódáil comhaid …",
"Upload failed" : "Theip ar an uaslódáil",
"Close" : "Dún",
"More formatting options" : "Tuilleadh roghanna formáidithe",
"Insert template" : "Cuir teimpléad isteach",
"Pick a file to attach" : "Roghnaigh comhad le ceangal",
"Failed to upload file" : "Theip ar uaslódáil an chomhaid",
"Threads" : "Snáitheanna",
@@ -208,13 +212,24 @@ OC.L10N.register(
"Pinned thread" : "Snáithe bioráilte",
"Locked thread" : "Snáithe faoi ghlas",
"Uncategorized" : "Gan catagóir",
"Last reply by {name}" : "An freagra deireanach ó {name}",
"_%n reply_::_%n replies_" : ["%n freagra","%n freagraí","%n freagraí","%n freagraí","%n freagraí"],
"Templates" : "Teimpléid",
"Add template" : "Cuir teimpléad leis",
"Edit template" : "Cuir an teimpléad in eagar",
"No templates yet" : "Gan aon teimpléid fós",
"Loading templates …" : "Ag lódáil teimpléid …",
"Name" : "Ainm",
"Template name" : "Ainm teimpléad",
"Content" : "Ábhar",
"Template content (BBCode) …" : "Ábhar teimpléid (BBCode) …",
"Show in:" : "Taispeáin i:",
"Are you sure you want to delete this template?" : "An bhfuil tú cinnte gur mian leat an teimpléad seo a scriosadh?",
"Both" : "An dá",
"Threads & replies" : "Snáitheanna & freagraí",
"Neither (disabled)" : "Ceachtar acu (míchumasaithe)",
"Insert" : "cuir isteach",
"Failed to load templates" : "Theip ar na teimpléid a lódáil",
"Views" : "Radhairc",
"Title" : "Teideal",
"Enter thread title …" : "Cuir isteach teideal an tsnáithe …",
@@ -394,6 +409,18 @@ OC.L10N.register(
"Enter category description (optional)" : "Cuir isteach cur síos ar an gcatagóir (roghnach)",
"New" : "Nua",
"Permissions" : "Ceadanna",
"Control which roles and teams can access and moderate this category" : "Rialú cé na róil agus na foirne ar féidir leo rochtain a fháil ar an gcatagóir seo agus í a mhodhnú",
"Select roles or teams that can view this category and its threads" : "Roghnaigh róil nó foirne ar féidir leo an chatagóir seo agus a snáitheanna a fheiceáil",
"Select roles or teams that can create new threads in this category" : "Roghnaigh róil nó foirne ar féidir leo snáitheanna nua a chruthú sa chatagóir seo",
"Select roles or teams that can reply to threads in this category" : "Roghnaigh róil nó foirne ar féidir leo freagra a thabhairt ar shnáitheanna sa chatagóir seo",
"Select roles or teams that can moderate (edit/delete) content in this category" : "Roghnaigh róil nó foirne ar féidir leo ábhar sa chatagóir seo a mhodhnú (a chur in eagar/a scriosadh)",
"Select roles or teams …" : "Roghnaigh róil nó foirne …",
"Design" : "Dearadh",
"Customize the appearance of this category" : "Saincheap cuma na catagóire seo",
"Category color" : "Dath na catagóire",
"Text color" : "Dath an téacs",
"Dark text" : "Téacs dorcha",
"Light text" : "Téacs éadrom",
"Preview" : "Réamhamharc",
"Manage forum categories and organization" : "Bainistigh catagóirí agus eagrú an fhóraim",
"Error loading categories" : "Earráid ag luchtú catagóirí",

View File

@@ -1,6 +1,7 @@
{ "translations": {
"Recent Forum activity" : "Gníomhaíocht an Fhóraim le Déanaí",
"More activity" : "Tuilleadh gníomhaíochta",
"%1$s (Guest)" : "%1$s (Aoi)",
"New thread by %1$s" : "Snáithe nua le %1$s",
"Reply by %1$s" : "Freagra ó %1$s",
"No recent forum activity" : "Gan aon ghníomhaíocht fóram le déanaí",
@@ -81,6 +82,7 @@
"BBCodes" : "BBCóid",
"Expand" : "Leathnaigh",
"Collapse" : "Laghdaigh",
"(Guest)" : "(Aoi)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}Tabhair faoi deara le do thoil:{bEnd} Beidh comhaid cheangailte le feiceáil ag aon duine san fhóram, beag beann ar shocruithe comhroinnte an chomhaid.",
"Drop file here to upload" : "Scaoil comhad anseo le huaslódáil",
"Hello world!" : "Dia duit, a dhomhan!",
@@ -120,6 +122,8 @@
"Uploading file …" : "Ag uaslódáil comhaid …",
"Upload failed" : "Theip ar an uaslódáil",
"Close" : "Dún",
"More formatting options" : "Tuilleadh roghanna formáidithe",
"Insert template" : "Cuir teimpléad isteach",
"Pick a file to attach" : "Roghnaigh comhad le ceangal",
"Failed to upload file" : "Theip ar uaslódáil an chomhaid",
"Threads" : "Snáitheanna",
@@ -206,13 +210,24 @@
"Pinned thread" : "Snáithe bioráilte",
"Locked thread" : "Snáithe faoi ghlas",
"Uncategorized" : "Gan catagóir",
"Last reply by {name}" : "An freagra deireanach ó {name}",
"_%n reply_::_%n replies_" : ["%n freagra","%n freagraí","%n freagraí","%n freagraí","%n freagraí"],
"Templates" : "Teimpléid",
"Add template" : "Cuir teimpléad leis",
"Edit template" : "Cuir an teimpléad in eagar",
"No templates yet" : "Gan aon teimpléid fós",
"Loading templates …" : "Ag lódáil teimpléid …",
"Name" : "Ainm",
"Template name" : "Ainm teimpléad",
"Content" : "Ábhar",
"Template content (BBCode) …" : "Ábhar teimpléid (BBCode) …",
"Show in:" : "Taispeáin i:",
"Are you sure you want to delete this template?" : "An bhfuil tú cinnte gur mian leat an teimpléad seo a scriosadh?",
"Both" : "An dá",
"Threads & replies" : "Snáitheanna & freagraí",
"Neither (disabled)" : "Ceachtar acu (míchumasaithe)",
"Insert" : "cuir isteach",
"Failed to load templates" : "Theip ar na teimpléid a lódáil",
"Views" : "Radhairc",
"Title" : "Teideal",
"Enter thread title …" : "Cuir isteach teideal an tsnáithe …",
@@ -392,6 +407,18 @@
"Enter category description (optional)" : "Cuir isteach cur síos ar an gcatagóir (roghnach)",
"New" : "Nua",
"Permissions" : "Ceadanna",
"Control which roles and teams can access and moderate this category" : "Rialú cé na róil agus na foirne ar féidir leo rochtain a fháil ar an gcatagóir seo agus í a mhodhnú",
"Select roles or teams that can view this category and its threads" : "Roghnaigh róil nó foirne ar féidir leo an chatagóir seo agus a snáitheanna a fheiceáil",
"Select roles or teams that can create new threads in this category" : "Roghnaigh róil nó foirne ar féidir leo snáitheanna nua a chruthú sa chatagóir seo",
"Select roles or teams that can reply to threads in this category" : "Roghnaigh róil nó foirne ar féidir leo freagra a thabhairt ar shnáitheanna sa chatagóir seo",
"Select roles or teams that can moderate (edit/delete) content in this category" : "Roghnaigh róil nó foirne ar féidir leo ábhar sa chatagóir seo a mhodhnú (a chur in eagar/a scriosadh)",
"Select roles or teams …" : "Roghnaigh róil nó foirne …",
"Design" : "Dearadh",
"Customize the appearance of this category" : "Saincheap cuma na catagóire seo",
"Category color" : "Dath na catagóire",
"Text color" : "Dath an téacs",
"Dark text" : "Téacs dorcha",
"Light text" : "Téacs éadrom",
"Preview" : "Réamhamharc",
"Manage forum categories and organization" : "Bainistigh catagóirí agus eagrú an fhóraim",
"Error loading categories" : "Earráid ag luchtú catagóirí",

View File

@@ -3,6 +3,7 @@ OC.L10N.register(
{
"Recent Forum activity" : "最新論壇活動",
"More activity" : "更多活動",
"%1$s (Guest)" : "%1$s訪客",
"New thread by %1$s" : "%1$s 發佈新討論串",
"Reply by %1$s" : "%1$s 回覆",
"No recent forum activity" : "近期沒有論壇活動",
@@ -83,6 +84,7 @@ OC.L10N.register(
"BBCodes" : "BBCode",
"Expand" : "展開",
"Collapse" : "折疊",
"(Guest)" : "(訪客)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}請注意:{bEnd} 附件檔案在論壇內對所有人可見,無論該檔案本身的分享設定為何。",
"Drop file here to upload" : "將檔案拖拽到此處即可上傳",
"Hello world!" : "哈囉,世界!",
@@ -123,6 +125,7 @@ OC.L10N.register(
"Upload failed" : "上傳失敗",
"Close" : "關閉",
"More formatting options" : "更多格式設定選項",
"Insert template" : "插入模板",
"Pick a file to attach" : "選取要附加的檔案",
"Failed to upload file" : "上傳檔案失敗",
"Threads" : "討論串",
@@ -209,13 +212,24 @@ OC.L10N.register(
"Pinned thread" : "已置頂主題",
"Locked thread" : "已鎖定主題",
"Uncategorized" : "未分類",
"Last reply by {name}" : "最後由 {name} 回覆",
"_%n reply_::_%n replies_" : ["%n 個回覆"],
"Templates" : "模板",
"Add template" : "添加模板",
"Edit template" : "編輯模板",
"No templates yet" : "尚無模板",
"Loading templates …" : "模板加載中 ...",
"Name" : "名字",
"Template name" : "模板名稱",
"Content" : "內容",
"Template content (BBCode) …" : "模板內容 (BBCode) …",
"Show in:" : "顯示於:",
"Are you sure you want to delete this template?" : "您確定要刪除此模板嗎?",
"Both" : "皆是",
"Threads & replies" : "討論串及回覆",
"Neither (disabled)" : "皆非(已停用)",
"Insert" : "插入",
"Failed to load templates" : "無法加載模板",
"Views" : "視圖",
"Title" : "標題",
"Enter thread title …" : "輸入主題標題 …",

View File

@@ -1,6 +1,7 @@
{ "translations": {
"Recent Forum activity" : "最新論壇活動",
"More activity" : "更多活動",
"%1$s (Guest)" : "%1$s訪客",
"New thread by %1$s" : "%1$s 發佈新討論串",
"Reply by %1$s" : "%1$s 回覆",
"No recent forum activity" : "近期沒有論壇活動",
@@ -81,6 +82,7 @@
"BBCodes" : "BBCode",
"Expand" : "展開",
"Collapse" : "折疊",
"(Guest)" : "(訪客)",
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings." : "{bStart}請注意:{bEnd} 附件檔案在論壇內對所有人可見,無論該檔案本身的分享設定為何。",
"Drop file here to upload" : "將檔案拖拽到此處即可上傳",
"Hello world!" : "哈囉,世界!",
@@ -121,6 +123,7 @@
"Upload failed" : "上傳失敗",
"Close" : "關閉",
"More formatting options" : "更多格式設定選項",
"Insert template" : "插入模板",
"Pick a file to attach" : "選取要附加的檔案",
"Failed to upload file" : "上傳檔案失敗",
"Threads" : "討論串",
@@ -207,13 +210,24 @@
"Pinned thread" : "已置頂主題",
"Locked thread" : "已鎖定主題",
"Uncategorized" : "未分類",
"Last reply by {name}" : "最後由 {name} 回覆",
"_%n reply_::_%n replies_" : ["%n 個回覆"],
"Templates" : "模板",
"Add template" : "添加模板",
"Edit template" : "編輯模板",
"No templates yet" : "尚無模板",
"Loading templates …" : "模板加載中 ...",
"Name" : "名字",
"Template name" : "模板名稱",
"Content" : "內容",
"Template content (BBCode) …" : "模板內容 (BBCode) …",
"Show in:" : "顯示於:",
"Are you sure you want to delete this template?" : "您確定要刪除此模板嗎?",
"Both" : "皆是",
"Threads & replies" : "討論串及回覆",
"Neither (disabled)" : "皆非(已停用)",
"Insert" : "插入",
"Failed to load templates" : "無法加載模板",
"Views" : "視圖",
"Title" : "標題",
"Enter thread title …" : "輸入主題標題 …",

View File

@@ -175,17 +175,29 @@ class BookmarkController extends OCSController {
// Order threads by bookmark order and enrich them
$orderedThreads = [];
// Extract unique author IDs for batch enrichment
$authorIds = array_unique(array_map(fn ($t) => $t->getAuthorId(), $threads));
$authors = $this->userService->enrichMultipleUsers($authorIds);
// Extract unique author IDs for batch enrichment (thread authors + last reply authors)
$authorIds = array_map(fn ($t) => $t->getAuthorId(), $threads);
$lastReplyAuthorIds = array_filter(array_map(fn ($t) => $t->getLastReplyAuthorId(), $threads));
$allAuthorIds = array_unique(array_merge($authorIds, $lastReplyAuthorIds));
$authors = $this->userService->enrichMultipleUsers($allAuthorIds);
foreach ($bookmarks as $bookmark) {
$threadId = $bookmark->getEntityId();
if (isset($threadMap[$threadId])) {
$thread = $threadMap[$threadId];
$lastReply = null;
$lastReplyAuthorId = $thread->getLastReplyAuthorId();
if ($lastReplyAuthorId !== null) {
$lastReply = [
'postId' => $thread->getLastPostId(),
'author' => $authors[$lastReplyAuthorId] ?? null,
'createdAt' => $thread->getLastReplyAt(),
];
}
$enriched = $this->threadEnrichmentService->enrichThread(
$thread,
$authors[$thread->getAuthorId()] ?? null
$authors[$thread->getAuthorId()] ?? null,
$lastReply
);
// Add bookmark timestamp
$enriched['bookmarkedAt'] = $bookmark->getCreatedAt();

View File

@@ -93,10 +93,13 @@ class SearchController extends OCSController {
$offset
);
// Collect all unique author IDs from both threads and posts
// Collect all unique author IDs from threads, last reply authors, and posts
$allAuthorIds = [];
foreach ($results['threads'] as $thread) {
$allAuthorIds[] = $thread->getAuthorId();
if ($thread->getLastReplyAuthorId() !== null) {
$allAuthorIds[] = $thread->getLastReplyAuthorId();
}
}
foreach ($results['posts'] as $post) {
$allAuthorIds[] = $post->getAuthorId();
@@ -106,9 +109,18 @@ class SearchController extends OCSController {
// Batch fetch all author data once
$authors = $this->userService->enrichMultipleUsers($allAuthorIds);
// Enrich threads with pre-fetched author data
// Enrich threads with pre-fetched author data and last reply info
$enrichedThreads = array_map(function ($thread) use ($authors) {
return $this->threadEnrichmentService->enrichThread($thread, $authors[$thread->getAuthorId()]);
$lastReply = null;
$lastReplyAuthorId = $thread->getLastReplyAuthorId();
if ($lastReplyAuthorId !== null) {
$lastReply = [
'postId' => $thread->getLastPostId(),
'author' => $authors[$lastReplyAuthorId] ?? null,
'createdAt' => $thread->getLastReplyAt(),
];
}
return $this->threadEnrichmentService->enrichThread($thread, $authors[$thread->getAuthorId()], $lastReply);
}, $results['threads']);
// Enrich posts with pre-fetched author data and thread context

View File

@@ -74,15 +74,26 @@ class ThreadController extends OCSController {
try {
$threads = array_slice($this->threadMapper->findAll(), $offset, $limit);
// Extract unique author IDs
$authorIds = array_unique(array_map(fn ($t) => $t->getAuthorId(), $threads));
// Extract unique author IDs (thread authors + last reply authors)
$authorIds = array_map(fn ($t) => $t->getAuthorId(), $threads);
$lastReplyAuthorIds = array_filter(array_map(fn ($t) => $t->getLastReplyAuthorId(), $threads));
$allAuthorIds = array_unique(array_merge($authorIds, $lastReplyAuthorIds));
// Batch fetch author data (includes roles)
$authors = $this->userService->enrichMultipleUsers($authorIds);
$authors = $this->userService->enrichMultipleUsers($allAuthorIds);
// Enrich threads with pre-fetched author data
// Enrich threads with pre-fetched author data and last reply info
return new DataResponse(array_map(function ($t) use ($authors) {
return $this->threadEnrichmentService->enrichThread($t, $authors[$t->getAuthorId()]);
$lastReply = null;
$lastReplyAuthorId = $t->getLastReplyAuthorId();
if ($lastReplyAuthorId !== null) {
$lastReply = [
'postId' => $t->getLastPostId(),
'author' => $authors[$lastReplyAuthorId] ?? null,
'createdAt' => $t->getLastReplyAt(),
];
}
return $this->threadEnrichmentService->enrichThread($t, $authors[$t->getAuthorId()] ?? null, $lastReply);
}, $threads));
} catch (\Exception $e) {
$this->logger->error('Error fetching threads: ' . $e->getMessage());
@@ -108,15 +119,26 @@ class ThreadController extends OCSController {
try {
$threads = $this->threadMapper->findByCategoryId($categoryId, $limit, $offset);
// Extract unique author IDs
$authorIds = array_unique(array_map(fn ($t) => $t->getAuthorId(), $threads));
// Extract unique author IDs (thread authors + last reply authors)
$authorIds = array_map(fn ($t) => $t->getAuthorId(), $threads);
$lastReplyAuthorIds = array_filter(array_map(fn ($t) => $t->getLastReplyAuthorId(), $threads));
$allAuthorIds = array_unique(array_merge($authorIds, $lastReplyAuthorIds));
// Batch fetch author data (includes roles)
$authors = $this->userService->enrichMultipleUsers($authorIds);
$authors = $this->userService->enrichMultipleUsers($allAuthorIds);
// Enrich threads with pre-fetched author data
// Enrich threads with pre-fetched author data and last reply info
return new DataResponse(array_map(function ($t) use ($authors) {
return $this->threadEnrichmentService->enrichThread($t, $authors[$t->getAuthorId()]);
$lastReply = null;
$lastReplyAuthorId = $t->getLastReplyAuthorId();
if ($lastReplyAuthorId !== null) {
$lastReply = [
'postId' => $t->getLastPostId(),
'author' => $authors[$lastReplyAuthorId] ?? null,
'createdAt' => $t->getLastReplyAt(),
];
}
return $this->threadEnrichmentService->enrichThread($t, $authors[$t->getAuthorId()] ?? null, $lastReply);
}, $threads));
} catch (\Exception $e) {
$this->logger->error('Error fetching threads by category: ' . $e->getMessage());
@@ -205,12 +227,23 @@ class ThreadController extends OCSController {
try {
$threads = $this->threadMapper->findByAuthorId($authorId, $limit, $offset);
// For threads by a single author, we can optimize by fetching author data once
$author = $this->userService->enrichUserData($authorId);
// Collect author IDs (thread author + last reply authors)
$lastReplyAuthorIds = array_filter(array_map(fn ($t) => $t->getLastReplyAuthorId(), $threads));
$allAuthorIds = array_unique(array_merge([$authorId], $lastReplyAuthorIds));
$authors = $this->userService->enrichMultipleUsers($allAuthorIds);
// Enrich threads with pre-fetched author data
return new DataResponse(array_map(function ($t) use ($author) {
return $this->threadEnrichmentService->enrichThread($t, $author);
// Enrich threads with pre-fetched author data and last reply info
return new DataResponse(array_map(function ($t) use ($authors) {
$lastReply = null;
$lastReplyAuthorId = $t->getLastReplyAuthorId();
if ($lastReplyAuthorId !== null) {
$lastReply = [
'postId' => $t->getLastPostId(),
'author' => $authors[$lastReplyAuthorId] ?? null,
'createdAt' => $t->getLastReplyAt(),
];
}
return $this->threadEnrichmentService->enrichThread($t, $authors[$t->getAuthorId()] ?? null, $lastReply);
}, $threads));
} catch (\Exception $e) {
$this->logger->error('Error fetching threads by author: ' . $e->getMessage());

View File

@@ -9,6 +9,7 @@ vi.mock('@icons/Reply.vue', () => createIconMock('ReplyIcon'))
vi.mock('@icons/Pencil.vue', () => createIconMock('PencilIcon'))
vi.mock('@icons/Delete.vue', () => createIconMock('DeleteIcon'))
vi.mock('@icons/History.vue', () => createIconMock('HistoryIcon'))
vi.mock('@icons/LinkVariant.vue', () => createIconMock('LinkVariantIcon'))
// Mock components
vi.mock('@/components/UserInfo', () =>
@@ -39,6 +40,11 @@ vi.mock('@/components/PostHistoryDialog', () =>
}),
)
vi.mock('@nextcloud/dialogs', () => ({
showSuccess: vi.fn(),
showError: vi.fn(),
}))
// Mock NcActions and NcActionButton
vi.mock('@nextcloud/vue/components/NcActions', () =>
createComponentMock('NcActions', {
@@ -344,6 +350,137 @@ describe('PostCard', () => {
})
})
describe('direct link', () => {
it('should always show Direct link button', () => {
mockCurrentUser.mockReturnValue(null)
const post = createMockPost()
const wrapper = mount(PostCard, {
props: { post },
})
const buttons = wrapper.findAll('.nc-action-button')
expect(buttons.some((b) => b.text().includes('Direct link'))).toBe(true)
})
it('should copy permalink to clipboard and show notification when clicked', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
value: { writeText },
writable: true,
configurable: true,
})
const post = createMockPost({ id: 42 })
const wrapper = mount(PostCard, {
props: { post, currentPage: 3 },
global: {
mocks: {
$route: { path: '/t/test-thread', query: {} },
},
},
})
const directLinkButton = wrapper
.findAll('.nc-action-button')
.find((b) => b.text().includes('Direct link'))
await directLinkButton?.trigger('click')
// Should copy the absolute URL to clipboard (origin + path)
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('/apps/forum/t/test-thread?page=3&post=42'),
)
// Should show success notification
const { showSuccess } = await import('@nextcloud/dialogs')
expect(showSuccess).toHaveBeenCalledWith('Direct link copied to clipboard')
})
it('should use currentPage prop for the page number in the URL', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
value: { writeText },
writable: true,
configurable: true,
})
const post = createMockPost({ id: 10 })
const wrapper = mount(PostCard, {
props: { post, currentPage: 5 },
global: {
mocks: {
$route: { path: '/t/my-thread', query: {} },
},
},
})
const directLinkButton = wrapper
.findAll('.nc-action-button')
.find((b) => b.text().includes('Direct link'))
await directLinkButton?.trigger('click')
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('/apps/forum/t/my-thread?page=5&post=10'),
)
})
it('should default to page 1 when currentPage prop is not provided', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
value: { writeText },
writable: true,
configurable: true,
})
const post = createMockPost({ id: 10 })
const wrapper = mount(PostCard, {
props: { post },
global: {
mocks: {
$route: { path: '/t/my-thread', query: {} },
},
},
})
const directLinkButton = wrapper
.findAll('.nc-action-button')
.find((b) => b.text().includes('Direct link'))
await directLinkButton?.trigger('click')
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('/apps/forum/t/my-thread?page=1&post=10'),
)
})
it('should still show notification when clipboard API fails', async () => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: vi.fn().mockRejectedValue(new Error('denied')) },
writable: true,
configurable: true,
})
// Provide execCommand fallback for jsdom
document.execCommand = vi.fn()
const post = createMockPost({ id: 7 })
const wrapper = mount(PostCard, {
props: { post, currentPage: 2 },
global: {
mocks: {
$route: { path: '/t/fallback-thread', query: {} },
},
},
})
const directLinkButton = wrapper
.findAll('.nc-action-button')
.find((b) => b.text().includes('Direct link'))
await directLinkButton?.trigger('click')
// Should still show success notification even with fallback
const { showSuccess } = await import('@nextcloud/dialogs')
expect(showSuccess).toHaveBeenCalledWith('Direct link copied to clipboard')
})
})
describe('unauthenticated user', () => {
it('should not show edit or delete buttons when not logged in', () => {
mockCurrentUser.mockReturnValue(null)

View File

@@ -48,6 +48,12 @@
</template>
{{ strings.viewHistory }}
</NcActionButton>
<NcActionButton @click="handleDirectLink">
<template #icon>
<LinkVariantIcon :size="20" />
</template>
{{ strings.directLink }}
</NcActionButton>
</NcActions>
</div>
</div>
@@ -97,12 +103,15 @@ import ReplyIcon from '@icons/Reply.vue'
import PencilIcon from '@icons/Pencil.vue'
import DeleteIcon from '@icons/Delete.vue'
import HistoryIcon from '@icons/History.vue'
import LinkVariantIcon from '@icons/LinkVariant.vue'
import UserInfo from '@/components/UserInfo'
import PostReactions from '@/components/PostReactions'
import PostEditForm from '@/components/PostEditForm'
import PostHistoryDialog from '@/components/PostHistoryDialog'
import { t } from '@nextcloud/l10n'
import { getCurrentUser } from '@nextcloud/auth'
import { generateUrl } from '@nextcloud/router'
import { showSuccess } from '@nextcloud/dialogs'
import type { Post } from '@/types'
import type { ReactionGroup } from '@/composables/useReactions'
@@ -116,6 +125,7 @@ export default defineComponent({
PencilIcon,
DeleteIcon,
HistoryIcon,
LinkVariantIcon,
UserInfo,
PostReactions,
PostEditForm,
@@ -142,6 +152,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
currentPage: {
type: Number,
default: 1,
},
},
emits: ['reply', 'edit', 'delete', 'update'],
setup() {
@@ -162,6 +176,8 @@ export default defineComponent({
'Are you sure you want to delete this post? This action cannot be undone.',
),
unread: t('forum', 'Unread'),
directLink: t('forum', 'Direct link'),
directLinkCopied: t('forum', 'Direct link copied to clipboard'),
},
}
},
@@ -229,6 +245,33 @@ export default defineComponent({
this.showHistoryDialog = true
},
async handleDirectLink() {
this.closeActionsMenu()
// Build the direct link URL with current page and post ID
const page = this.currentPage
const routePath = this.$route.path
// Build the full absolute URL for clipboard
const path = generateUrl(`/apps/forum${routePath}?page=${page}&post=${this.post.id}`)
const absoluteUrl = window.location.origin + path
// Copy to clipboard
try {
await navigator.clipboard.writeText(absoluteUrl)
} catch {
// Fallback for older browsers
const textarea = document.createElement('textarea')
textarea.value = absoluteUrl
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
showSuccess(this.strings.directLinkCopied)
},
handleReactionsUpdate(reactions: ReactionGroup[]) {
// Update the post's reactions locally
if (this.post.reactions !== undefined) {

View File

@@ -33,6 +33,18 @@
<ClockIcon :size="16" />
<NcDateTime v-if="thread.createdAt" :timestamp="thread.createdAt * 1000" />
</span>
<a
v-if="thread.lastReply"
class="meta-item last-reply"
@click.prevent.stop="$emit('navigate-last-reply', thread)"
>
{{
t('forum', 'Last reply by {name}', {
name: thread.lastReply.author?.displayName || thread.lastReplyAuthorId || '',
})
}}
<NcDateTime :timestamp="thread.lastReply.createdAt * 1000" />
</a>
</div>
</div>
</template>
@@ -73,7 +85,7 @@ export default defineComponent({
required: true,
},
},
emits: ['click'],
emits: ['click', 'navigate-last-reply'],
data() {
return {
isDarkTheme,
@@ -244,6 +256,16 @@ export default defineComponent({
&.author {
color: var(--color-main-text);
}
&.last-reply {
text-decoration: none;
cursor: pointer;
&:hover {
color: var(--color-primary-element);
text-decoration: underline;
}
}
}
}
}

View File

@@ -94,6 +94,7 @@
:thread="thread"
:is-unread="isThreadUnread(thread)"
@click="navigateToThread(thread)"
@navigate-last-reply="navigateToLastReply(thread)"
/>
</div>
@@ -260,6 +261,14 @@ export default defineComponent({
this.$router.push(`/t/${thread.slug}`)
},
navigateToLastReply(thread: Thread) {
const query: Record<string, string> = { page: 'last' }
if (thread.lastPostId) {
query.post = String(thread.lastPostId)
}
this.$router.push({ path: `/t/${thread.slug}`, query })
},
goBack(): void {
this.$router.push('/')
},

View File

@@ -118,6 +118,7 @@
:key="thread.id"
:thread="thread"
@click="navigateToThread(thread)"
@navigate-last-reply="navigateToLastReply(thread)"
/>
</div>
</div>
@@ -355,6 +356,14 @@ export default defineComponent({
this.$router.push(`/t/${thread.slug}`)
},
navigateToLastReply(thread: Thread) {
const query: Record<string, string> = { page: 'last' }
if (thread.lastPostId) {
query.post = String(thread.lastPostId)
}
this.$router.push({ path: `/t/${thread.slug}`, query })
},
navigateToPost(post: Post) {
if (post.threadSlug) {
this.$router.push(`/t/${post.threadSlug}#post-${post.id}`)

View File

@@ -108,6 +108,7 @@
:thread="thread"
:query="currentQuery"
@click="navigateToThread(thread)"
@navigate-last-reply="navigateToLastReply(thread)"
/>
</div>
</section>
@@ -274,6 +275,16 @@ export default defineComponent({
this.$router.push(`/t/${thread.slug}`)
}
},
navigateToLastReply(thread: Thread): void {
if (thread.slug) {
const query: Record<string, string> = { page: 'last' }
if (thread.lastPostId) {
query.post = String(thread.lastPostId)
}
this.$router.push({ path: `/t/${thread.slug}`, query })
}
},
},
})
</script>

View File

@@ -205,6 +205,7 @@
:is-unread="isPostUnread(firstPost)"
:can-moderate-category="canModerate"
:can-reply="canReply"
:current-page="currentPage"
@reply="handleReply"
@update="handleUpdate"
@delete="handleDelete"
@@ -241,6 +242,7 @@
:is-unread="isPostUnread(reply)"
:can-moderate-category="canModerate"
:can-reply="canReply"
:current-page="currentPage"
@reply="handleReply"
@update="handleUpdate"
@delete="handleDelete"
@@ -674,6 +676,12 @@ export default defineComponent({
try {
this.loadingReplies = true
this.currentPage = newPage
// Update URL query param without triggering the watcher
const query = { ...this.$route.query, page: String(newPage) }
delete query.post
this.$router.replace({ query })
await this.fetchPosts(newPage)
// Scroll to the top of the replies section
@@ -1070,7 +1078,7 @@ export default defineComponent({
element.classList.add('highlight-post')
setTimeout(() => {
element.classList.remove('highlight-post')
}, 2000)
}, 3000)
})
}
},
@@ -1320,11 +1328,12 @@ export default defineComponent({
// Highlight animation for scrolled-to posts
:deep(.highlight-post) {
animation: highlightFade 2s ease-in-out;
animation: highlightFade 3s ease-in-out;
}
@keyframes highlightFade {
0% {
0%,
66% {
background-color: var(--color-primary-element-light);
box-shadow: 0 0 0 4px var(--color-primary-element-light);
}

View File

@@ -737,9 +737,10 @@ class ThreadControllerTest extends TestCase {
->willReturn($threads);
$this->userService->expects($this->once())
->method('enrichUserData')
->with($authorId)
->willReturn($enrichedAuthor);
->method('enrichMultipleUsers')
->willReturn([
$authorId => $enrichedAuthor,
]);
$this->threadEnrichmentService->expects($this->exactly(2))
->method('enrichThread')

View File

@@ -1 +1 @@
0.28.0
0.29.0