From 46adb0ece787adcde17f7531aaacf36329e2ae8d Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Tue, 14 Apr 2026 17:41:06 +0200 Subject: [PATCH] feat(push): panel preferencji /konto/prywatnosc + triggery B2B (D.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migracja 101 dodaje 8 nowych kolumn notify_push_* na users (wszystkie default TRUE). Panel preferencji rozszerzony o kartę "Powiadomienia push (na urządzeniu)" z 3 podsekcjami (interakcje dot. mnie, aktualności Izby, wydarzenia) — 9 przełączników. "Nowa wiadomość prywatna" świadomie jest w obu kartach (e-mail + push) — userzy mogą niezależnie wybrać oba kanały. Triggery B2B: - zainteresowanie ogłoszeniem (ClassifiedInterest) → push do autora z notify_push_classified_interest - pytanie do ogłoszenia (ClassifiedQuestion) → push do autora z notify_push_classified_question Fazy D.2 (forum + broadcast) i D.3 (wydarzenia + cron) w kolejnych PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- blueprints/auth/routes.py | 12 ++ blueprints/community/classifieds/routes.py | 31 ++++- database.py | 12 +- .../migrations/101_add_push_preferences.sql | 15 +++ templates/konto/prywatnosc.html | 121 ++++++++++++++++++ 5 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 database/migrations/101_add_push_preferences.sql diff --git a/blueprints/auth/routes.py b/blueprints/auth/routes.py index 6bb5bb5..b996900 100644 --- a/blueprints/auth/routes.py +++ b/blueprints/auth/routes.py @@ -914,6 +914,18 @@ def konto_prywatnosc(): user.contact_prefer_phone = request.form.get('prefer_phone') == 'on' user.contact_prefer_portal = request.form.get('prefer_portal') == 'on' user.notify_email_messages = request.form.get('notify_email_messages') == 'on' + + # Web Push preferences per event type + user.notify_push_messages = request.form.get('notify_push_messages') == 'on' + user.notify_push_classified_interest = request.form.get('notify_push_classified_interest') == 'on' + user.notify_push_classified_question = request.form.get('notify_push_classified_question') == 'on' + user.notify_push_forum_reply = request.form.get('notify_push_forum_reply') == 'on' + user.notify_push_forum_quote = request.form.get('notify_push_forum_quote') == 'on' + user.notify_push_announcements = request.form.get('notify_push_announcements') == 'on' + user.notify_push_board_meetings = request.form.get('notify_push_board_meetings') == 'on' + user.notify_push_event_invites = request.form.get('notify_push_event_invites') == 'on' + user.notify_push_event_reminders = request.form.get('notify_push_event_reminders') == 'on' + db.commit() logger.info(f"Privacy settings updated for user: {user.email}") diff --git a/blueprints/community/classifieds/routes.py b/blueprints/community/classifieds/routes.py index a203f07..44e301e 100644 --- a/blueprints/community/classifieds/routes.py +++ b/blueprints/community/classifieds/routes.py @@ -521,6 +521,22 @@ def toggle_interest(classified_id): except Exception as e: logger.warning(f"Failed to send classified interest notification: {e}") + # Web Push to classified author (opt-in via notify_push_classified_interest) + try: + author = db.query(User).filter(User.id == classified.author_id).first() + if author and author.notify_push_classified_interest is not False: + from blueprints.push.push_service import send_push + interested_name = current_user.name or current_user.email.split('@')[0] + send_push( + user_id=author.id, + title='Zainteresowanie Twoim ogłoszeniem', + body=f'{interested_name}: „{classified.title}"', + url=f'/tablica/{classified_id}', + tag=f'classified-interest-{classified_id}', + ) + except Exception as e: + logger.warning(f"Failed to send classified interest push: {e}") + return jsonify({ 'success': True, 'interested': True, @@ -612,7 +628,7 @@ def ask_question(classified_id): classified.updated_at = datetime.now() db.commit() - # Notify classified author (in-app + email) + # Notify classified author (in-app + email + push) if classified.author_id != current_user.id: questioner_name = current_user.name or current_user.email.split('@')[0] try: @@ -623,6 +639,19 @@ def ask_question(classified_id): send_classified_question_email( classified_id, classified.title, questioner_name, content, author.email, author.name or author.email.split('@')[0]) + # Web Push (opt-in via notify_push_classified_question) + if author and author.notify_push_classified_question is not False: + from blueprints.push.push_service import send_push + import re as _re + preview_plain = _re.sub(r'<[^>]+>', '', content or '').strip() + preview = (preview_plain[:80] + '…') if len(preview_plain) > 80 else preview_plain + send_push( + user_id=author.id, + title=f'Nowe pytanie: {classified.title[:60]}', + body=f'{questioner_name}: {preview}' if preview else f'{questioner_name} zadał pytanie', + url=f'/tablica/{classified_id}', + tag=f'classified-question-{classified_id}', + ) except Exception as e: logger.warning(f"Failed to send classified question notification: {e}") diff --git a/database.py b/database.py index fab6363..8dfef7b 100644 --- a/database.py +++ b/database.py @@ -339,7 +339,17 @@ class User(Base, UserMixin): # Email notification preferences notify_email_messages = Column(Boolean, default=True) # Email when receiving private message - notify_push_messages = Column(Boolean, default=True) # Web Push when receiving private message + + # Web Push notification preferences (per event type) + notify_push_messages = Column(Boolean, default=True) # Prywatna wiadomość + notify_push_classified_interest = Column(Boolean, default=True) # Zainteresowanie ogłoszeniem B2B + notify_push_classified_question = Column(Boolean, default=True) # Pytanie pod ogłoszeniem B2B + notify_push_forum_reply = Column(Boolean, default=True) # Odpowiedź w moim wątku forum + notify_push_forum_quote = Column(Boolean, default=True) # Cytat mojego wpisu forum + notify_push_announcements = Column(Boolean, default=True) # Nowa aktualność Izby + notify_push_board_meetings = Column(Boolean, default=True) # Posiedzenia Rady (utw./program/protokół) + notify_push_event_invites = Column(Boolean, default=True) # Zaproszenie na wydarzenie + notify_push_event_reminders = Column(Boolean, default=True) # Przypomnienie 24h przed wydarzeniem # Relationships conversations = relationship('AIChatConversation', back_populates='user', cascade='all, delete-orphan') diff --git a/database/migrations/101_add_push_preferences.sql b/database/migrations/101_add_push_preferences.sql new file mode 100644 index 0000000..59c8b50 --- /dev/null +++ b/database/migrations/101_add_push_preferences.sql @@ -0,0 +1,15 @@ +-- Migration 101: per-event push notification preferences for users +-- +-- 8 nowych kolumn notify_push_* na users (obok istniejących notify_email_messages +-- i notify_push_messages z migracji 100). Wszystkie default TRUE — nowi i istniejący +-- userzy domyślnie otrzymują wszystkie typy. Kontrola w panelu /konto/prywatnosc. + +ALTER TABLE users + ADD COLUMN IF NOT EXISTS notify_push_classified_interest BOOLEAN DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS notify_push_classified_question BOOLEAN DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS notify_push_forum_reply BOOLEAN DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS notify_push_forum_quote BOOLEAN DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS notify_push_announcements BOOLEAN DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS notify_push_board_meetings BOOLEAN DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS notify_push_event_invites BOOLEAN DEFAULT TRUE, + ADD COLUMN IF NOT EXISTS notify_push_event_reminders BOOLEAN DEFAULT TRUE; diff --git a/templates/konto/prywatnosc.html b/templates/konto/prywatnosc.html index 30f7af4..51c499b 100644 --- a/templates/konto/prywatnosc.html +++ b/templates/konto/prywatnosc.html @@ -369,6 +369,127 @@ +
+

+ + + + + + + Powiadomienia push (na urządzeniu) +

+ +
+ Jak to działa: aby otrzymywać powiadomienia, w nagłówku strony kliknij ikonę telefonu z falami i zezwól — na każdym urządzeniu (komputer, Android, iPhone) trzeba to wykonać osobno. Poniższe przełączniki decydują, za co chcesz dostawać powiadomienia — niezależnie od urządzenia. +
+ +
Interakcje dotyczące mnie
+ +
+
+
Nowa wiadomość prywatna
+
Ktoś napisał do Ciebie w rozmowie prywatnej na portalu
+
+ +
+ +
+
+
Zainteresowanie Twoim ogłoszeniem B2B
+
Ktoś kliknął „Jestem zainteresowany" przy Twoim ogłoszeniu na tablicy B2B
+
+ +
+ +
+
+
Pytanie do Twojego ogłoszenia B2B
+
Ktoś zadał publiczne pytanie pod Twoim ogłoszeniem
+
+ +
+ +
+
+
Odpowiedź w Twoim temacie na forum
+
Ktoś odpisał w wątku, który założyłeś
+
+ +
+ +
+
+
Odpowiedź na Twoją wiadomość (cytat)
+
Ktoś cytuje lub odpowiada bezpośrednio pod Twoim wpisem na forum
+
+ +
+ +
Aktualności Izby
+ +
+
+
Nowa aktualność / ogłoszenie Izby
+
Biuro Izby opublikowało nową aktualność dla wszystkich członków
+
+ +
+ +
+
+
Posiedzenia Rady Izby
+
Utworzenie nowego posiedzenia, publikacja programu oraz publikacja protokołu
+
+ +
+ +
Wydarzenia
+ +
+
+
Zaproszenie na wydarzenie
+
Zostałeś dodany do listy uczestników nowego wydarzenia w kalendarzu Izby
+
+ +
+ +
+
+
Przypomnienie 24h przed wydarzeniem
+
Automatyczne przypomnienie dzień przed wydarzeniami, na które jesteś zapisany
+
+ +
+
+